• abp-vnext-ZeroToOne系列


    目录

    前言

    本系列参照官方文档BookStore,创建一个BookStore应用程序。

    旨在从零开始(ZeroToOne, zto)不使用模板,

    从创建一个空的解决方案开始,一步一步地去了解如何使用Abp.vNext去构建一个应用程序。

    1.初步构建项目架构

    创建一个空的解决方案Zto.BookStore,然后依次添加如下项目,

    注意:以下创建的项目,都的以Zto.BookStore.为前缀,为了叙述的简单,故省略之,比如:

    *.Domain指的是项目Zto.BookStore.Domain

    模块化架构最佳实践 & 约定

    应用程序启动模板

    1.1 *.Domain.Shared

    基本设置

    • 修改默认命名空间为Zto.BookStore

    • 新建文件Localization

    依赖包

    • Volo.Abp.Core

    知识点: Abp模块化

    参考资料:

    创建AbpModule

    根目录下创建AbpModule:

    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore
    {
        public class BookStoreDomainSharedModule : AbpModule
        {
        }
    }
    

    创建BookType

    创建文件夹Books,在该文件夹下新建BookType.cs:

    namespace Zto.BookStore.Books
    {
        public enum BookType
        {
            Undefined, //未定义的
            Adventure, //冒险
            Biography, //传记
            Dystopia,  //地狱
            Fantastic, //神奇的
            Horror,    //恐怖,
            Science,   //科学
            ScienceFiction, //科幻小说
            Poetry     //诗歌
        }
    }
    
    

    Book相关常量

    Books文件夹下新建一个BookConsts.cs类,用于存储Book相关常量值

    namespace Zto.BookStore.Books
    {
        public static class BookConsts
        {
            public const int MaxNameLength = 256; //名字最大长度
        }
    }
    

    本地化

    官方文档

    创建本地化资源

    开始的UI开发之前,我们首先要准备本地化的文本(这是通常在开发应用程序时需要做的).

    本地化资源用于将相关的本地化字符串组合在一起,并将它们与应用程序的其他本地化字符串分开,

    通常一个模块会定义自己的本地化资源. 本地化资源就是一个普通的类. 例如:

    • 在文件夹Localization下,新建BookStoreResource.cs
        [LocalizationResourceName("BookStore")]
        public class BookStoreResource
        {
    
        }
    

    [LocalizationResourceName("BookStore")]标记资源名

    • 在文件夹Localization/BookStore,添加两个语言资源json文件,

      • en.json

        {
          "Culture": "en",
          "Texts": {
            "Menu:Home": "Home",
            "Welcome": "Welcome",
            "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
            "Menu:BookStore": "Book Store",
            "Menu:Books": "Books",
            "Actions": "Actions",
            "Edit": "Edit",
            "PublishDate": "Publish date",
            "NewBook": "New book",
            "Name": "Name",
            "Type": "Type",
            "Price": "Price",
            "CreationTime": "Creation time",
            "AreYouSureToDelete": "Are you sure you want to delete this item?",
            "Enum:BookType:0": "Undefined",
            "Enum:BookType:1": "Adventure",
            "Enum:BookType:2": "Biography",
            "Enum:BookType:3": "Dystopia",
            "Enum:BookType:4": "Fantastic",
            "Enum:BookType:5": "Horror",
            "Enum:BookType:6": "Science",
            "Enum:BookType:7": "Science fiction",
            "Enum:BookType:8": "Poetry",
            "BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?",
            "SuccessfullyDeleted": "Successfully deleted!",
            "Permission:BookStore": "Book Store",
            "Permission:Books": "Book Management",
            "Permission:Books.Create": "Creating new books",
            "Permission:Books.Edit": "Editing the books",
            "Permission:Books.Delete": "Deleting the books",
            "BookStore:00001": "There is already an author with the same name: {name}",
            "Permission:Authors": "Author Management",
            "Permission:Authors.Create": "Creating new authors",
            "Permission:Authors.Edit": "Editing the authors",
            "Permission:Authors.Delete": "Deleting the authors",
            "Menu:Authors": "Authors",
            "Authors": "Authors",
            "AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
            "BirthDate": "Birth date",
            "NewAuthor": "New author"
          }
        }
        
        
      • zh-Hans.json

        {
          "culture": "zh-Hans",
          "texts": {
            "Menu:Home": "首页",
            "Welcome": "欢迎",
            "LongWelcomeMessage": "欢迎来到该应用程序. 这是一个基于ABP框架的启动项目. 有关更多信息, 请访问 abp.io.",
        
            "Enum:BookType:0": "未知",
            "Enum:BookType:1": "冒险",
            "Enum:BookType:2": "传记",
            "Enum:BookType:3": "地狱",
            "Enum:BookType:4": "神奇的",
            "Enum:BookType:5": "恐怖",
            "Enum:BookType:6": "科学",
            "Enum:BookType:7": "科幻小说 ",
            "Enum:BookType:8": "诗歌"
          }
        }
        
        • 每个本地化文件都需要定义 culture (文化) 代码 (例如 "en" 或 "en-US").

        • texts 部分只包含本地化字符串的键值集合 (键也可能有空格).

    特别注意

    必须将语言资源文件的属性设置为

    1. 复制到输出目录:不复制
    2. 生成操作:嵌入的资源

    1.2 *.Domain

    基本设置

    • 修改默认命名空间为Zto.BookStore

    项目引用

    • *.Domain.Shared

    依赖包

    • Volo.Abp.Core

    创建AbpModule

    根目录下创建AbpModule:

    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore
    {
        [DependsOn(typeof(BookStoreDomainSharedModule))]
        public class BookStoreDomainModule : AbpModule
        {
        }
    }
    

    创建Book领域模型

    创建文件夹Books,在该文件夹下新建Book.cs

    using Volo.Abp.Domain.Entities.Auditing;
    using System;
    
    namespace Zto.BookStore.Books
    {
        public class Book : AuditedAggregateRoot<Guid>
        {
            public Guid AuthorId { get; set; }
            public String Name { get; set; }
            public BookType Type { get; set; }
            public DateTime PublishDate { get; set; }
            public float Price { get; set; }
        }
    }
    

    项目常量值类BookStoreConsts

    在根目录下创建BookStoreConsts.cs,用于保存项目中常量数据值

    namespace Zto.BookStore
    {
        public static class BookStoreConsts
        {
            public const string DbTablePrefix = "Bks"; //常量值:表前缀
            public const string DbSchema = null; //常量值:表的架构
        }
    }
    

    1.3 *.EntityFrameworkCore

    • 修改默认命名空间为Zto.BookStore

    • 创建文件夹EntityFrameworkCore

    项目引用

    • *.Domain

    依赖包

    • Volo.Abp.EntityFrameworkCore

    • Volo.Abp.EntityFrameworkCore.SqlServer:使用MsSqlServer数据库

    创建AbpModule

    在文件夹EntityFrameworkCore下创建AbpModule:

    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        [DependsOn(typeof(BookStoreDomainModule))]
        public class BookStoreEntityFrameworkCoreModule : AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
                {
                    /* Remove "includeAllEntities: true" to create
                     * default repositories only for aggregate roots */
                    options.AddDefaultRepositories(includeAllEntities: true);
                });
    
                Configure<AbpDbContextOptions>(options =>
                {
                    /* The main point to change your DBMS.
                     * See also BookStoreMigrationsDbContextFactory for EF Core tooling. */
                    options.UseSqlServer();
                });
            }
        }
    }
    

    代码解析:

    • AddDefaultRepositories(includeAllEntities: true)

      添加默认Repository实现,includeAllEntities: true表示为所以实体类实现仓储(Repository)类

    • options.UseSqlServer();使用MsSqlServer数据库

    创建DbContext

    在文件夹EntityFrameworkCore中创建BookStoreDbContext.cs

    using Microsoft.EntityFrameworkCore;
    using Volo.Abp.Data;
    using Volo.Abp.EntityFrameworkCore;
    using Zto.BookStore.Books;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        [ConnectionStringName("BookStoreConnString")]
        public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
        {
            public DbSet<Book> Books { get; set; }
            public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
                : base(options)
            {
            }
    
            protected override void OnModelCreating(ModelBuilder builder)
            {
                base.OnModelCreating(builder);
                
                /* Configure the shared tables (with included modules) here */
                // 配置从其它modules引入的模型
    
    
                /* Configure your own tables/entities inside the ConfigureBookStore method */
                // 配置本项目自己的表和实体模型
                builder.ConfigureBookStore();
            }
        }
    }
    

    代码解析:

    • [ConnectionStringName("BookStoreConnString")]:表示要使用的数据库连接字符串

    BookStore的EFcore 实体模型映射

    创建/EntityFrameworkCore/BookStoreDbContextModelCreatingExtensions.cs:

    该类用于配置本项目(即:BookStore项目)自己的表和实体模型

    using Microsoft.EntityFrameworkCore;
    using Volo.Abp.EntityFrameworkCore.Modeling;
    using Zto.BookStore.Books;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        public static class BookStoreDbContextModelCreatingExtensions
        {
            public static void ConfigureBookStore(this ModelBuilder builder)
            {
                Check.NotNull(builder, nameof(builder));
    
                /* Configure your own tables/entities inside here */
                builder.Entity<Book>(e =>
                {
                    e.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
                    e.ConfigureByConvention(); //auto configure for the base class props ,优雅的配置和映射继承的属性,应始终对你所有的实体使用它.
                    e.Property(p => p.Name).HasMaxLength(BookConsts.MaxNameLength);
    
                });
            }
        }
    }
    

    其中:

    • e.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);

    配置表的前缀和表的架构

    • e.ConfigureByConvention();优雅的配置和映射继承的属性,应始终对你所有的实体使用它

    命令行中执行数据库迁移

    如果严格按上述顺序依次创建项目,并添加代码

    这时,我们可以随便创建一个控制台程序,并添加配置文件appsettings.json

    {
      "ConnectionStrings": {
        "BookStoreConnString": "Server=.;Database=BookStore_Zto;Trusted_Connection=True;MultipleActiveResultSets=true"
      }
    }
    
    1. 设置控制台程序为默认启动项目,

    2. 打开程【序包管理器控制台】,并将【默认项目】设置为项目:.EntityFrameworkCore.DbMigrations ,

    3. 执行EF数据库迁移命令

    add-migration initDb
    

    会抛出如下错误:

    Unable to create an object of type 'BookStoreDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
    

    这是因为:我们没有为BookStoreDbContext提供无参数构造函数,但是``BookStoreDbContext必须得继承 AbpDbContext,其不提供无参数构造函数,故在项目*.EntityFrameworkCore.DbMigrations中是无法执行数据库迁移的,如何解决数据库迁移呢?请看章节【**设计时创建DbContext`**】。

    1.4 *.EntityFrameworkCore.DbMigrations

    • Q1:为什么要创建这个工程呢?

    ​ **A: **用于EF的数据库迁移,因为如果项目是使用其它的 O/R框架 ,迁移的方式就不一样,所以数据库的迁移,也使用接口方式,这样就可以替换。

    基本设置

    • 修改默认命名空间为Zto.BookStore

    • 创建文件夹EntityFrameworkCore

    项目引用

    • *.EntityFrameworkCore

    依赖包

    • Microsoft.EntityFrameworkCore.Design:设计时创建DbContex,用于命令行执行数据库迁移

    创建AbpModule

    在文件夹EntityFrameworkCore下创建AbpModule:

    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        [DependsOn(
            typeof(BookStoreEntityFrameworkCoreModule)
            )]
        public class BookStoreEntityFrameworkCoreDbMigrationsModule : AbpModule
        {
            context.Services.AddAbpDbContext<BookStoreMigrationsDbContext>();
        }
    }
    

    迁移DbContexnt

    在文件夹EntityFrameworkCore下创建BookStoreMigrationsDbContext.cs

    DbContext仅仅用于数据库迁移

    using Microsoft.EntityFrameworkCore;
    using Volo.Abp.EntityFrameworkCore;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        /// <summary>
        /// This DbContext is only used for database migrations.
        /// It is not used on runtime. See BookStoreDbContext for the runtime DbContext.
        /// It is a unified model that includes configuration for
        /// all used modules and your application.
        /// 
        /// 这个DbContext只用于数据库迁移。
        /// 它不在运行时使用。有关运行时DbContext,请参阅BookStoreDbContext。
        /// 它是一个统一配置所有使用的模块和您的应用程序的模型
        /// </summary>
        [ConnectionStringName("BookStoreConnString")]
        public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbContext>
        {
            public BookStoreMigrationsDbContext(DbContextOptions<BookStoreMigrationsDbContext> options)
                : base(options)
            {
                
            }
    
            protected override void OnModelCreating(ModelBuilder builder)
            {
                base.OnModelCreating(builder);
    
                /* Configure the shared tables (with included modules) here */
                // 配置从其它modules引入的模型
    
    
    
                /* Configure your own tables/entities inside the ConfigureBookStore method */
                // 配置本项目自己的表和实体模型
                builder.ConfigureBookStore();
            }
    
        }
    }
    
    

    注意:在此处我们就通过特性[ConnectionStringName("BookStoreConnString")]指定其连接字符串

    设计时创建DbContext

    在章节【 *.EntityFrameworkCore -- > 命令行中执行数据库迁移】中,看到那时使用ef命令是执行数据库迁移的时,会抛出如下异常:

    Unable to create an object of type 'BookStoreDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
    

    解决方案就是设计时创建DbContext

    什么是设计时创建DbContext

    参考资料:
    https://docs.microsoft.com/zh-cn/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli

    从设计时工厂创建DbContext
    你还可以通过实现接口来告诉工具如何创建 DbContext IDesignTimeDbContextFactory<TContext>
    如果实现此接口的类在与派生的项目相同的项目中 DbContext
    或在应用程序的启动项目中找到,
    则这些工具将绕过创建 DbContext 的其他方法,并改用设计时工厂。

    如果需要以不同于运行时的方式配置 DbContext 的设计时,则设计时工厂特别有用 DbContext 。如果构造函数采用其他参数,
    但未在 di 中注册,如果根本不使用 di,
    或者出于某种原因而不是使用 CreateHostBuilder ASP.NET Core 应用程序的类中的方法 Main

    总之一句话:
    实现了IDesignTimeDbContextFactory<BookStoreMigrationsDbContext>
    就可以使用命令行执行数据库迁移,例如:

    (1).在 NET Core CLI中执行: dotnet ef database update
    (2).在 Visual Studio中执行:Update-Database

    实现IDesignTimeDbContextFactory<>

    综上,

    1. 确保已入如下Nuget包:

      • Microsoft.EntityFrameworkCore.Design

      • Volo.Abp.EntityFrameworkCore.SqlServer

        如果使用的是MySql数据库,已入的包是Volo.Abp.EntityFrameworkCore.MySQL

    2. 在文件夹EntityFrameworkCore下创建BookStoreMigrationsDbContextFactory,

    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Design;
    using Microsoft.Extensions.Configuration;
    using System.IO;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        /// <summary>
        ///   This class is needed for EF Core console commands
        ///   (like Add-Migration and Update-Database commands) 
        ///   
        ///   参考资料:
        ///   https://docs.microsoft.com/zh-cn/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli
        ///   从设计时工厂创建DbContext:
        ///   你还可以通过实现接口来告诉工具如何创建 DbContext IDesignTimeDbContextFactory<TContext> :
        ///   如果实现此接口的类在与派生的项目相同的项目中 DbContext 
        ///   或在应用程序的启动项目中找到,
        ///   则这些工具将绕过创建 DbContext 的其他方法,并改用设计时工厂。
        /// 
        ///   如果需要以不同于运行时的方式配置 DbContext 的设计时,则设计时工厂特别有用 DbContext 。如果构造函数采用其他参数,
        ///   但未在 di 中注册,如果根本不使用 di,
        ///   或者出于某种原因而不是使用 CreateHostBuilder ASP.NET Core 应用程序的类中的方法 Main 。
        /// 
        /// 
        ///   总之一句话:
        ///   实现了IDesignTimeDbContextFactory<BookStoreMigrationsDbContext>,
        ///   就可以使用命令行执行数据库迁移,
        ///      (1).在 NET Core CLI中执行: dotnet ef database update
        ///      (2).在 Visual Studio中执行:Update-Database 
        /// </summary>
        public class BookStoreMigrationsDbContextFactory : IDesignTimeDbContextFactory<BookStoreMigrationsDbContext>
        {
            public BookStoreMigrationsDbContext CreateDbContext(string[] args)
            {
                var configuration = BuildConfiguration();
                var builder = new DbContextOptionsBuilder<BookStoreMigrationsDbContext>()
                     .UseSqlServer(configuration.GetConnectionString("BookStoreConnString")); //SqlServer数据库
                    //.UseMySql(configuration.GetConnectionString("BookStoreConnString"), ServerVersion.); //MySql数据库
    
                return new BookStoreMigrationsDbContext(builder.Options);
            }
    
            private static IConfigurationRoot BuildConfiguration()
            {
                var builder = new ConfigurationBuilder()
                    //项目Zto.BookStore.DbMigrator的根目录
                    .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../Zto.BookStore.DbMigrator/"))
                    .AddJsonFile("appsettings.json", optional: false);
    
                return builder.Build();
    
                return builder.Build();
            }
        }
    }
    

    这样就可以在NET Core CLIVisual Studio中使用命令诸如如下命令执行数据库迁移

    Add-Migration
    dotnet ef database update
    

    ef命名会自动找到类BookStoreMigrationsDbContextFactory

    public class BookStoreMigrationsDbContextFactory : IDesignTimeDbContextFactory<BookStoreMigrationsDbContext>
    

    这时,我们可以随便创建一个控制台程序(本例为项目Zto.BookStore.DbMigrator),并添加配置文件appsettings.json

    {
      "ConnectionStrings": {
        "BookStoreConnString": "Server=.;Database=BookStore_Zto;Trusted_Connection=True;MultipleActiveResultSets=true"
      }
    }
    
    1. 设置控制台程序为默认启动项目,

      不过,如果现在已经通过以下代码在BookStoreMigrationsDbContextFactory中明确指明了配置文件的地址:

              private static IConfigurationRoot BuildConfiguration()
              {
                  var builder = new ConfigurationBuilder()
                      .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../Zto.BookStore.DbMigrator/"))
                      .AddJsonFile("appsettings.json", optional: false);
      
                  return builder.Build();
              }
      

      即,如下代码

        .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../Zto.BookStore.DbMigrator/"))
      

      指明了配置文件位于项目Zto.BookStore.DbMigrator的根目中,所以这时可以不用将设置控制台程序为默认启动项目

    2. 打开程【程序包管理器控制台】,并将【默认项目】设置为项目:*.EntityFrameworkCore.DbMigrations ,

    3. 执行EF数据库迁移命令

      add-migration initDb
      

      这时,命令行提示:

      PM> add-migration initDb
      Build started...
      Build succeeded.
      To undo this action, use Remove-Migration.
      
    4. 把挂起的migration更新到数据库

      update-database
      

      这时,命令行提示:

      PM> update-database
      Build started...
      Build succeeded.
      Security Warning: The negotiated TLS 1.0 is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.
      Security Warning: The negotiated TLS 1.0 is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.
      Security Warning: The negotiated TLS 1.0 is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.
      Applying migration '20201207183001_initDb'.
      Done.
      PM> 
      

      同时在项目.EntityFrameworkCore.DbMigrations的根目录下,会自动生成文件夹Migrations,其中包含两个文件

      • 20201207183001_initDb.cs

        using System;
        using Microsoft.EntityFrameworkCore.Migrations;
        
        namespace Zto.BookStore.Migrations
        {
            public partial class initDb : Migration
            {
                protected override void Up(MigrationBuilder migrationBuilder)
                {
                    migrationBuilder.CreateTable(
                        name: "BksBooks",
                        columns: table => new
                        {
                            Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                            AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
                            Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
                            Type = table.Column<int>(type: "int", nullable: false),
                            PublishDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                            Price = table.Column<float>(type: "real", nullable: false),
                            ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: true),
                            ConcurrencyStamp = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true),
                            CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
                            CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
                            LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
                            LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
                        },
                        constraints: table =>
                        {
                            table.PrimaryKey("PK_BksBooks", x => x.Id);
                        });
                }
        
                protected override void Down(MigrationBuilder migrationBuilder)
                {
                    migrationBuilder.DropTable(
                        name: "BksBooks");
                }
            }
        }
        
        
      • BookStoreMigrationsDbContextModelSnapshot.cs:迁移快照

    5. 数据库也自动生成了数据库及其相关表

      image-20201208191539533

    在项目*.EntityFrameworkCore.DbMigrations中数据库迁移的局限性

    直接在项目*.EntityFrameworkCore.DbMigrations中使用命令行执行数据库迁移有如下局限性:

    • 不能支持多租户(如果开发的系统要求支持多租户的话)的数据库迁移

    • 不能执行种子数据:

      使用EF Core执行标准的 Update-Database 命令,但是它不会初始化种子数据.

    鉴于以上局限性,我们把数据库迁移的工作全部集中到控制台项目.DbMigrator中,以下两节所创建的类

    • EntityFrameworkCoreBookStoreDbSchemaMigrator

    • BookStoreDbMigrationService

    就是为了这个目标而提前准备的。

    迁移接口:IBookStoreDbSchemaMigrator

    项目*.Domain/Data文件夹下,创建接口:IBookStoreDbSchemaMigrator,如下所示:

    public interface IBookStoreDbSchemaMigrator
    {
        Task MigrateAsync();
    }
    

    创建其实现类EntityFrameworkCoreBookStoreDbSchemaMigrator,主要是通过代码

    dbContext.database.MigrateAsync();
    

    更新migration到数据库:

    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Zto.BookStore.Data;
    
    namespace Zto.BookStore.EntityFrameworkCore
    {
        public class EntityFrameworkCoreBookStoreDbSchemaMigrator : IBookStoreDbSchemaMigrator, ITransientDependency
        {
            private readonly IServiceProvider _serviceProvider;
    
            public EntityFrameworkCoreBookStoreDbSchemaMigrator(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }
    
            public async Task MigrationAsync()
            {
                /*
                * 我们有意从IServiceProvider解析BookStoreMigrationsDbContext(而不是直接注入它),
                * 是为了能正确获取当前的范围、当前租户的连接字符串
                */
                var dbContext = _serviceProvider.GetRequiredService<BookStoreMigrationsDbContext>();
                var database = dbContext.Database;
                //var connString = database.GetConnectionString();
    
                /*
                 * Asynchronously applies any pending migrations for the context to the database.
                 * Will create the database if it does not already exist.
                 */
                await database.MigrateAsync();
            }
        }
    }
    

    特别注意:

    database.MigrateAsync();只是相当于update-database`,故:在该方法执行前,

    确保已经手动执行命令add-migration xxx创建migration

    数据库迁移服务

    创建一个数据库迁移服务BookStoreDbMigrationService,使用代码(而不是EFCore命令行)统一管理所有数据库迁移任务,比如:

    • 调用实现了上节所定义的接口IBookStoreDbSchemaMigrator的实现类,
    • 若系统执行多租户,为租户执行数据库迁移
    • 执行种子数

    其中,关键性代码如下:

    • 更新migration到数据库

      await database.MigrateAsync();
      
    • 执行种子数据

       _dataSeeder.SeedAsync(tenant?.Id);
      

    完整代码如下:

    BookStoreDbMigrationService.cs

    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.Abstractions;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.TenantManagement;
    
    namespace Zto.BookStore.Data
    {
        public class BookStoreDbMigrationService : ITransientDependency
        {
            public ILogger<BookStoreDbMigrationService> Logger { get; set; }
    
            private readonly IDataSeeder _dataSeeder;
            private readonly IEnumerable<IBookStoreDbSchemaMigrator> _dbSchemaMigrators;
            private readonly ITenantRepository _tenantRepository;
            private readonly ICurrentTenant _currentTenant;
    
            public BookStoreDbMigrationService(
                IDataSeeder dataSeeder,
                IEnumerable<IBookStoreDbSchemaMigrator> dbSchemaMigrators,
                ITenantRepository tenantRepository,
                ICurrentTenant currentTenant)
            {
                _dataSeeder = dataSeeder;
                _dbSchemaMigrators = dbSchemaMigrators;
                _tenantRepository = tenantRepository;
                _currentTenant = currentTenant;
    
                Logger = NullLogger<BookStoreDbMigrationService>.Instance;
            }
    
            public async Task MigrateAsync()
            {
                Logger.LogInformation("Started database migrations...");
    
                await MigrateDatabaseSchemaAsync(); //执行数据库迁移
                await SeedDataAsync();  //执行种子数据
                Logger.LogInformation($"Successfully completed host database migrations.");
    
                /*-----------------------------------------------------------------
                 * 以下为多租户执行的数据库迁移
                 -----------------------------------------------------------------*/
                var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
                var migratedDatabaseSchemas = new HashSet<string>();
                foreach (var tenant in tenants)
                {
                    if (!tenant.ConnectionStrings.Any())
                    {
                        continue;
                    }
    
                    using (_currentTenant.Change(tenant.Id))
                    {
                        var tenantConnectionStrings = tenant.ConnectionStrings
                            .Select(x => x.Value)
                            .ToList();
    
                        if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings))
                        {
                            await MigrateDatabaseSchemaAsync(tenant);
    
                            migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings);
                        }
    
                        await SeedDataAsync(tenant);
                    }
    
                    Logger.LogInformation($"Successfully completed {tenant.Name} tenant database migrations.");
                }
    
                Logger.LogInformation("Successfully completed database migrations.");
            }
    
            /// <summary>
            /// 执行数据库迁移
            /// </summary>
            /// <param name="tenant"></param>
            /// <returns></returns>
            private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
            {
                Logger.LogInformation(
                    $"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
    
                foreach (var migrator in _dbSchemaMigrators)
                {
                    await migrator.MigrateAsync();
                }
            }
    
            /// <summary>
            /// 执行种子数据
            /// </summary>
            /// <param name="tenant"></param>
            /// <returns></returns>
            private async Task SeedDataAsync(Tenant tenant = null)
            {
                Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
    
                await _dataSeeder.SeedAsync(tenant?.Id);
            }
        }
    }
    
    

    代码解析:

    • MigrateDatabaseSchemaAsync()循环执行所有数据库迁移接口实例

    • SeedDataAsync()执行种子数据

    • MigrateAsync()方法将被下一节的创建的迁移控制台程序项目.DbMigrator使用,用于统一执行数据库迁移操作

    注意

    因为这里我们使用到了多租户数据库迁移的判定,需要额外已入以下包:

    • Volo.Abp.TenantManagement.Domain

    简化BookStoreDbMigrationService

    由于目前缺乏对

    的了解,所以把跟它们相关的功能代码注释掉,简化后的``BookStoreDbMigrationService`如下:

    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.Abstractions;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.TenantManagement;
    
    namespace Zto.BookStore.Data
    {
        public class BookStoreDbMigrationService : ITransientDependency
        {
            public ILogger<BookStoreDbMigrationService> Logger { get; set; }
            
            private readonly IEnumerable<IBookStoreDbSchemaMigrator> _dbSchemaMigrators;
    
            public BookStoreDbMigrationService(
                IEnumerable<IBookStoreDbSchemaMigrator> dbSchemaMigrators)
            {
                _dbSchemaMigrators = dbSchemaMigrators;
                Logger = NullLogger<BookStoreDbMigrationService>.Instance;
            }
    
            public async Task MigrateAsync()
            {
                Logger.LogInformation("Started database migrations...");
                await MigrateDatabaseSchemaAsync(); //执行数据库迁移
                Logger.LogInformation("Successfully completed database migrations.");
            }
    
            /// <summary>
            /// 执行数据库迁移
            /// </summary>
            /// <param name="tenant"></param>
            /// <returns></returns>
            private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
            {
                Logger.LogInformation(
                    $"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
    
                foreach (var migrator in _dbSchemaMigrators)
                {
                    await migrator.MigrateAsync();
                }
            }
    
        }
    }
    
    

    1.5 *.DbMigrator

    新建控制台项目*.DbMigrator,以后所有的数据库迁移都推荐使这个控制台项目进行

    可以在开发生产环境迁移数据库架构初始化种子数据.

    基本设置

    • 创建配置文件appsettings.json:

      {
        "ConnectionStrings": {
          "BookStoreConnString": "Server=.;Database=BookStore_Zto;Trusted_Connection=True;MultipleActiveResultSets=true"
        }
      }
      

    特别注意

    一定要把配置文件的属性设置为:

    • 复制到输出目录:始终复制
    • 生成操作:内容

    项目引用

    • *.EntityFrameworkCore.DbMigrations

    依赖包

    • Microsoft.EntityFrameworkCore.Tools:数据库迁移
    • Volo.Abp.Autofac:依赖注入
    • Serilog日志:
      • Serilog.Sinks.File
      • Serilog.Sinks.Console
      • Serilog.Extensions.Logging
    • Microsoft.Extensions.Hosting:控制台宿主程序

    创建AbpModule

    在根目录下创建AbpModule:

    using Volo.Abp.Autofac;
    using Zto.BookStore.EntityFrameworkCore;
    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore.DbMigrator
    {
        [DependsOn(
            typeof(AbpAutofacModule),
            typeof(BookStoreEntityFrameworkCoreDbMigrationsModule)
            )]
        public class BookStoreDbMigratorModule : AbpModule
        {
        }
    }
    

    创建HostServer

    知识点:IHostedService

    当注册 IHostedService 时,.NET Core 会在应用程序启动和停止期间分别调用 IHostedService 类型的 StartAsync()StopAsync() 方法。

    此外,如果我们想控制我们自己的服务程序的生命周期,那么可以使用IHostApplicationLifetime

    IHostSerice定义如下:

    
    namespace Microsoft.Extensions.Hosting
    {
        //
        // 摘要:
        //     Defines methods for objects that are managed by the host.
        public interface IHostedService
        {
            Task StartAsync(CancellationToken cancellationToken);
            Task StopAsync(CancellationToken cancellationToken);
        }
    }
    

    数据库迁移HostedService

    创建一个名为DbMigratorHostedService的类,继承IHostedService接口

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Serilog;
    using System.Threading;
    using System.Threading.Tasks;
    using Volo.Abp;
    using Zto.BookStore.Data;
    
    namespace Zto.BookStore.DbMigrator
    {
        public class DbMigratorHostedService : IHostedService
        {
            //自己控制的服务程序的生命周期
            private readonly IHostApplicationLifetime _hostApplicationLifetime;
    
            public DbMigratorHostedService(IHostApplicationLifetime hostApplicationLifetime)
            {
                _hostApplicationLifetime = hostApplicationLifetime;
            }
            public async Task StartAsync(CancellationToken cancellationToken)
            {
                using (var application = AbpApplicationFactory.Create<BookStoreDbMigratorModule>(options =>
                {
                    options.UseAutofac();
                    options.Services.AddLogging(c => c.AddSerilog());
                }))
                {
                    application.Initialize();
    
                    await application
                        .ServiceProvider
                        .GetRequiredService<BookStoreDbMigrationService>()
                        .MigrateAsync();
    
                    application.Shutdown();
    
                    _hostApplicationLifetime.StopApplication();
                }
            }
    
            public Task StopAsync(CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
    }
    
    

    其中,核心代码只是:

    BookStoreDbMigrationService.MigrateAsync()
    

    执行数据库的迁移,包括:更新migration和种子数据

    依赖注入HostedService

    知识点:Serilog

    在控制台项目中使用Serilog

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using Serilog;
    using Serilog.Events;
    using System.IO;
    using System.Threading.Tasks;
    
    namespace Zto.BookStore.DbMigrator
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                Log.Logger = new LoggerConfiguration()
                    .MinimumLevel.Information() //设置最低等级
                    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) //根据命名空间或类型重置日志最小级别
                    .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning)
    #if DEBUG
                    .MinimumLevel.Override("Zto.BookStore", LogEventLevel.Debug)
    #else
                    .MinimumLevel.Override("Zto.BookStore", LogEventLevel.Information)
    #endif
                    .Enrich.FromLogContext()
                    .WriteTo.File(Path.Combine(Directory.GetCurrentDirectory(), "Logs/logs.txt")) //将日志写到文件
                    .WriteTo.Console()//将日志写到控制台
                    .CreateLogger();
    
                await CreateHostBuilder(args).RunConsoleAsync();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) => 
                Host.CreateDefaultBuilder(args)
                    .ConfigureLogging((context, logging) => logging.ClearProviders()) //Removes all logger providers from builder.
                    .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<DbMigratorHostedService>();
            });
        }
    }
    
    

    代码解析:

    ​ 依赖注入DbMigratorHostedService服务,控制台程序自动将执行HostServiceStartAsync()方法

    执行数据库迁移

    设置控制台程序为启动项目,并运行,执行数据库迁移。

    控制台输出日志:

    [13:54:12 INF] Started database migrations...
    [13:54:12 INF] Migrating schema for host database...
    Security Warning: The negotiated TLS 1.0 is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.
    [13:54:14 INF] Successfully completed host database migrations.
    

    执行完成后,自动生成数据库及其相关表:

    image-20201208191539533

    特别注意:

    ​ 这个控制台程序最终的本质是执行dbContext.database.MigrateAsync();只是相当于update-database

    故:在该方法执行前,确保在项目*.EntityFrameworkCore.DbMigrations中已经手动执行命令add-migration xxx创建migration

    种子数据

    在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的数据种子系统. 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。

    IDataSeedContributor:种子数贡献者

    *.Domain 项目下创建派生 IDataSeedContributor 的类,并且拷贝以下代码:

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Repositories;
    using Zto.BookStore.Books;
    
    namespace Zto.BookStore
    {
        public class BookStoreDataSeederContributor
          : IDataSeedContributor, ITransientDependency
        {
            private readonly IRepository<Book, Guid> _bookRepository;
    
            public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
            {
                _bookRepository = bookRepository;
            }
    
            public async Task SeedAsync(DataSeedContext context)
            {
                if (await _bookRepository.GetCountAsync() <= 0)
                {
                    await _bookRepository.InsertAsync(
                        new Book
                        {
                            Name = "1984",
                            Type = BookType.Dystopia,
                            PublishDate = new DateTime(1949, 6, 8),
                            Price = 19.84f
                        },
                        autoSave: true
                    );
    
                    await _bookRepository.InsertAsync(
                        new Book
                        {
                            Name = "The Hitchhiker's Guide to the Galaxy",
                            Type = BookType.ScienceFiction,
                            PublishDate = new DateTime(1995, 9, 27),
                            Price = 42.0f
                        },
                        autoSave: true
                    );
                }
            }
        }
    }
    
    

    如果数据库中当前没有图书,则此代码使用 IRepository<Book, Guid>(默认为repository)将两本书插入数据库

    其中,IDataSeedContributor接口如下:

    namespace Volo.Abp.Data
    {
        public interface IDataSeedContributor
        {
            Task SeedAsync(DataSeedContext context);
        }
    }
    
    • IDataSeedContributor 定义了 SeedAsync 方法用于执行 数据种子逻辑.

    • 通常检查数据库是否已经存在种子数据.

    • 你可以注入服务,检查数据播种所需的任何逻辑.

    IDataSeeder服务:执行种子数据

    数据种子贡献者由ABP框架自动发现,并作为数据播种过程的一部分执行.

    如何自动执行种子数据呢?答案是:IDataSeeder服务

    你可以通过依赖注入 IDataSeeder 并且在你需要时使用它初始化种子数据. 它内部调用 IDataSeedContributor 的实现去完成数据播种

    修改项目 *.Domain中的BookStoreDbMigrationService,依赖注入

     private readonly IDataSeeder _dataSeeder;
    

    并如下使用执行种子数据

     await _dataSeeder.SeedAsync(tenant?.Id);
    

    下面是修改后的完整代码如下:

    public class BookStoreDbMigrationService : ITransientDependency
        {
            public ILogger<BookStoreDbMigrationService> Logger { get; set; }
    
            private readonly IDataSeeder _dataSeeder;
            private readonly IEnumerable<IBookStoreDbSchemaMigrator> _dbSchemaMigrators;
    
            public BookStoreDbMigrationService(
                IDataSeeder dataSeeder,
                IEnumerable<IBookStoreDbSchemaMigrator> dbSchemaMigrators
                )
            {
                _dataSeeder = dataSeeder;
                _dbSchemaMigrators = dbSchemaMigrators;
    
                Logger = NullLogger<BookStoreDbMigrationService>.Instance;
            }
    
            public async Task MigrateAsync()
            {
                Logger.LogInformation("Started database migrations...");
    
                await MigrateDatabaseSchemaAsync(); //执行数据库迁移
                await SeedDataAsync();  //执行种子数据
    
                Logger.LogInformation("Successfully completed database migrations.");
            }
    
            /// <summary>
            /// 执行数据库迁移
            /// </summary>
            /// <param name="tenant"></param>
            /// <returns></returns>
            private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
            {
                Logger.LogInformation(
                    $"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
    
                foreach (var migrator in _dbSchemaMigrators)
                {
                    await migrator.MigrateAsync();
                }
            }
    
            /// <summary>
            /// 执行种子数据
            /// </summary>
            /// <param name = "tenant" ></ param >
            /// < returns ></ returns >
            private async Task SeedDataAsync(Tenant tenant = null)
            {
                Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
                await _dataSeeder.SeedAsync(tenant?.Id);
    
            }
        }
    

    设置控制台程序*.DbMigrator为启动项目,并运行,执行数据库迁移。

    这时查看Book表,多了两条种子数据:

    image-20201208210723459

    dataSeeder.SeedAsync(tenant?.Id)干了啥?

    _dataSeeder是个什么呢?

    image-20201208192119822

    相关源码如下:

    DataSeederExtensions

    using System;
    using System.Threading.Tasks;
    
    namespace Volo.Abp.Data
    {
        public static class DataSeederExtensions
        {
            public static Task SeedAsync(this IDataSeeder seeder, Guid? tenantId = null)
            {
                return seeder.SeedAsync(new DataSeedContext(tenantId));
            }
        }
    }
    

    DataSeedContext

    using System;
    using System.Collections.Generic;
    using JetBrains.Annotations;
    
    namespace Volo.Abp.Data
    {
        public class DataSeedContext
        {
            public Guid? TenantId { get; set; }
    
            /// <summary>
            /// Gets/sets a key-value on the <see cref="Properties"/>.
            /// </summary>
            /// <param name="name">Name of the property</param>
            /// <returns>
            /// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
            /// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
            /// </returns>
            [CanBeNull]
            public object this[string name]
            {
                get => Properties.GetOrDefault(name);
                set => Properties[name] = value;
            }
    
            /// <summary>
            /// Can be used to get/set custom properties.
            /// </summary>
            [NotNull]
            public Dictionary<string, object> Properties { get; }
    
            public DataSeedContext(Guid? tenantId = null)
            {
                TenantId = tenantId;
                Properties = new Dictionary<string, object>();
            }
    
            /// <summary>
            /// Sets a property in the <see cref="Properties"/> dictionary.
            /// This is a shortcut for nested calls on this object.
            /// </summary>
            public virtual DataSeedContext WithProperty(string key, object value)
            {
                Properties[key] = value;
                return this;
            }
        }
    }
    

    DataSeeder

    using System.Threading.Tasks;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Uow;
    
    namespace Volo.Abp.Data
    {
        //TODO: Create a Volo.Abp.Data.Seeding namespace?
        public class DataSeeder : IDataSeeder, ITransientDependency
        {
            protected IServiceScopeFactory ServiceScopeFactory { get; }
            protected AbpDataSeedOptions Options { get; }
    
            public DataSeeder(
                IOptions<AbpDataSeedOptions> options,
                IServiceScopeFactory serviceScopeFactory)
            {
                ServiceScopeFactory = serviceScopeFactory;
                Options = options.Value;
            }
    
            [UnitOfWork]
            public virtual async Task SeedAsync(DataSeedContext context)
            {
                using (var scope = ServiceScopeFactory.CreateScope())
                {
                    foreach (var contributorType in Options.Contributors)
                    {
                        var contributor = (IDataSeedContributor) scope
                            .ServiceProvider
                            .GetRequiredService(contributorType);
    
                        await contributor.SeedAsync(context);
                    }
                }
            }
        }
    }
    

    综上可知:

    IDataSeeder它内部调用 IDataSeedContributorSeedAsync方法去完成数据播种

    1.6 *.Application.Contracts

    应用服务层

    应用服务实现应用程序的用例, 将领域层逻辑公开给表示层.

    从表示层(可选)调用应用服务,DTO (数据传对象) 作为参数. 返回(可选)DTO给表示层.

    基本设置

    • 修改默认命名空间为Zto.BookStore

    • 创建文件夹Books

    项目引用

    • *.Domain.Shared

    依赖包

    • *.Volo.Abp.Ddd.Application.Contracts

    创建AbpModule

    在文件夹Books下创建AbpModule:

    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore
    {
        [DependsOn(
         typeof(BookStoreDomainSharedModule)
            )]
        public class BookStoreApplicationContractsModule : AbpModule
        {
    
        }
    }
    

    DTO

    在文件夹Books下创建Dto:

    BooksDto

    using System;
    using Volo.Abp.Application.Dtos;
    
    namespace Zto.BookStore.Books
    {
        public class BookDto : AuditedEntityDto<Guid>
        {
            public Guid AuthorId { get; set; }
    
            public string AuthorName { get; set; }
    
            public string Name { get; set; }
    
            public BookType Type { get; set; }
    
            public DateTime PublishDate { get; set; }
    
            public float Price { get; set; }
        }
    }
    
    • DTO类被用来在 表示层应用层 传递数据.查看DTO文档查看更多信息.
    • 为了在页面上展示书籍信息,BookDto被用来将书籍数据传递到表示层.
    • BookDto继承自 AuditedEntityDto<Guid>.跟上面定义的 Book 实体一样具有一些审计属性.

    CreateUpdateBookDto

    using System;
    using System.ComponentModel.DataAnnotations;
    
    
    namespace Zto.BookStore.Books
    {
        public class CreateUpdateBookDto
        {
            public Guid AuthorId { get; set; }
    
            [Required]
            [StringLength(BookConsts.MaxNameLength)]
            public string Name { get; set; }
    
            [Required]
            public BookType Type { get; set; } = BookType.Undefined;
    
            [Required]
            [DataType(DataType.Date)]
            public DateTime PublishDate { get; set; } = DateTime.Now;
    
            [Required]
            public float Price { get; set; }
        }
    }
    
    
    • 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
    • 它定义了数据注释属性(如[Required])来定义属性的验证. DTO由ABP框架自动验证.

    IBookAppService

    using System;
    using Volo.Abp.Application.Dtos;
    using Volo.Abp.Application.Services;
    
    namespace Zto.BookStore.Books
    {
        public interface IBookAppService:
               ICrudAppService<     //Defines CRUD methods
                BookDto,            //Used to show books
                Guid,               //Primary key of the book entity
                PagedAndSortedResultRequestDto, //Used for paging/sorting
                CreateUpdateBookDto>            //Used to create/update a book
        {
    
        }
    }
    

    继承ICrudAppService<>

    1.7 *.BookStore.Application

    基本设置

    • 修改默认命名空间为Zto.BookStore

    • 创建文件夹Books

    项目引用

    • *.Application.Contracts

    依赖包

    • Volo.Abp.Ddd.Application

    创建AbpModule

    在文件夹Books下创建AbpModule:

    using Volo.Abp.Localization;
    using Volo.Abp.Modularity;
    
    namespace Zto.BookStore
    {
        [DependsOn(
            typeof(BookStoreDomainModule),
            typeof(BookStoreApplicationContractsModule),
             typeof(AbpLocalizationModule)
            )]
        public class BookStoreApplicationModule : AbpModule
        {
        }
    }
    
    

    特别指出的是,依赖模块AbpLocalizationModule,支持本地化

    对象映射

    知识点 AutoMap

    文档

    AutoMapper——Map之实体的桥梁

    AutoMapper官网

    官方文档

    基本使用
    var config = new MapperConfiguration(cfg => {
        cfg.AddProfile<AppProfile>();
        cfg.CreateMap<Source, Dest>();
    });
    
    var mapper = config.CreateMapper();
    // or
    IMapper mapper = new Mapper(config);
    var dest = mapper.Map<Source, Dest>(new Source());
    

    Starting with 9.0, the static API is no longer available.

    • Gathering configuration before initialization

    AutoMapper also lets you gather configuration before initialization:

    var cfg = new MapperConfigurationExpression();
    cfg.CreateMap<Source, Dest>();
    cfg.AddProfile<MyProfile>();
    MyBootstrapper.InitAutoMapper(cfg);
    
    var mapperConfig = new MapperConfiguration(cfg);
    IMapper mapper = new Mapper(mapperConfig);
    
    • Profile Instances

    A good way to organize your mapping configurations is with profiles. Create classes that inherit from Profile and put the configuration in the constructor:

    (通过自定义``Profile 的子类,设置映射配置)

    // This is the approach starting with version 5
    public class OrganizationProfile : Profile
    {
    	public OrganizationProfile()
    	{
    		CreateMap<Foo, FooDto>();
    		// Use CreateMap... Etc.. here (Profile methods are the same as configuration methods)
    	}
    }
    
    • Assembly Scanning for auto configuration

    Profiles can be added to the main mapper configuration in a number of ways, either directly:

    (通过AddProfile将自定义``Profile 的子类添加到映射配置中)

    cfg.AddProfile<OrganizationProfile>();
    cfg.AddProfile(new OrganizationProfile());
    

    or by automatically scanning for profiles:

    (通过程序集扫描profiles类到映射配置中)

    // Scan for all profiles in an assembly
    // ... using instance approach:
    
    var config = new MapperConfiguration(cfg => {
        cfg.AddMaps(myAssembly);
    });
    var configuration = new MapperConfiguration(cfg => cfg.AddMaps(myAssembly));
    
    // Can also use assembly names:
    var configuration = new MapperConfiguration(cfg =>
        cfg.AddMaps(new [] {
            "Foo.UI",
            "Foo.Core"
        });
    );
    
    // Or marker types for assemblies:
    var configuration = new MapperConfiguration(cfg =>
        cfg.AddMaps(new [] {
            typeof(HomeController),
            typeof(Entity)
        });
    );
    

    AutoMapper will scan the designated assemblies for classes inheriting from Profile and add them to the configuration.

    配置对象映射关系

    在将Book返回到表示层时,需要将Book实体转换为BookDto对象. AutoMapper库可以在定义了正确的映射时自动执行此转换.

    因此你只需在*.BookStore.Application项目的中:

    中定义映射:

    • 第一步:自定义BookStoreApplicationAutoMapperProfile继承自 Profile,对象映射配置都在这里设置

    BookStoreApplicationAutoMapperProfile.cs

        public class BookStoreApplicationAutoMapperProfile : Profile
        {
            public BookStoreApplicationAutoMapperProfile()
            {
                CreateMap<Book, BookDto>();
                CreateMap<CreateUpdateBookDto, Book>();
            }
        }
    
    • 第二步:配置AbpAutoMapperOptions

      使BookStoreApplicationModule模块依赖AbpAutoMapperModule模块,并在的ConfigureServices方法中配置AbpAutoMapperOptions,本示例是通过扫描程序集的方式搜索Porfile类,并添加到AutoMapper配置中

      using Volo.Abp.AutoMapper;
      using Volo.Abp.Localization;
      using Volo.Abp.Modularity;
      
      namespace Zto.BookStore
      {
          [DependsOn(
              ...
              typeof(AbpAutoMapperModule)
              )]
          public class BookStoreApplicationModule : AbpModule
          {
              public override void ConfigureServices(ServiceConfigurationContext context)
              {
                  Configure<AbpAutoMapperOptions>(options =>
                  {
                      //通过扫描程序集的方式搜索`Porfile`类,并添加到AutoMapper配置中
                      options.AddMaps<BookStoreApplicationModule>(); 
                  });
              }
          }
      }
      
    源码代码分析

    以下代码:

    options.AddMaps<BookStoreApplicationModule>(); 
    

    调用源码:

       public class AbpAutoMapperOptions
       {
            public AbpAutoMapperOptions()
            {
                Configurators = new List<Action<IAbpAutoMapperConfigurationContext>>();
                ValidatingProfiles = new TypeList<Profile>();
            }
           
           public void AddMaps<TModule>(bool validate = false)
            {
                var assembly = typeof(TModule).Assembly;
    
                Configurators.Add(context =>
                {
                    context.MapperConfiguration.AddMaps(assembly);
                });
               
                ......
       }
    

    这里使用

    context.MapperConfiguration.AddMaps(assembly);
    

    扫描程序集的方式搜索Profile类添加到AutoMapper配置中

    对象转换

    配置对象映射关系后,可以使用如下代码进行对象转换:

     var bookDto = ObjectMapper.Map<Book, BookDto>(book);
     var bookDtos = ObjectMapper.Map<List<Book>, List<BookDto>>(books)
    

    其中,

    ObjectMappersApplicationService类内置的对象,只要xxxAppService继承自ApplicationService即可使用

    源码分析

    IObjectMapper:

    namespace Volo.Abp.ObjectMapping
    {
        //
        // 摘要:
        //     Defines a simple interface to automatically map objects.
        public interface IObjectMapper
        {
            //
            // 摘要:
            //     Gets the underlying Volo.Abp.ObjectMapping.IAutoObjectMappingProvider object
            //     that is used for auto object mapping.
            IAutoObjectMappingProvider AutoObjectMappingProvider
            {
                get;
            }
            TDestination Map<TSource, TDestination>(TSource source); //A
            TDestination Map<TSource, TDestination>(TSource source, TDestination destination);//A
        }
    }
    

    在模块AbpObjectMappingModule

    public class AbpObjectMappingModule : AbpModule
     {
            ......
                
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddTransient(
                    typeof(IObjectMapper<>),
                    typeof(DefaultObjectMapper<>)
                );
            }
      }
    

    设置了IObjectMapper的默认实现类DefaultObjectMapper

       public class DefaultObjectMapper : IObjectMapper, ITransientDependency
       {
            public IAutoObjectMappingProvider AutoObjectMappingProvider { get; }
           
            public virtual TDestination Map<TSource, TDestination>(TSource source)
            {
                .....
    
                return AutoMap(source, destination);
            }
           public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
            {
                ....
                return AutoMap(source, destination);
            }
           
            protected virtual TDestination AutoMap<TSource, TDestination>(object source)
            {
                return AutoObjectMappingProvider.Map<TSource, TDestination>(source);
            }
    
            protected virtual TDestination AutoMap<TSource, TDestination>(TSource source, TDestination destination)
            {
                return AutoObjectMappingProvider.Map<TSource, TDestination>(source, destination);
            }
       }
    

    ​ 根据以上代码可以看出:ObjectMapper.Map<S,D>()最终调用的都是

    AutoObjectMappingProvider.Map<TSource, TDestination>(source);
    or
    AutoObjectMappingProvider.Map<TSource, TDestination>(source, destination);
    

    -->IAutoObjectMappingProvider AutoObjectMappingProvider-->AutoMapperAutoObjectMappingProvider

      public class AutoMapperAutoObjectMappingProvider : IAutoObjectMappingProvider
      {
            public IMapperAccessor MapperAccessor { get; }
          
            public virtual TDestination Map<TSource, TDestination>(object source)
            {
                return MapperAccessor.Mapper.Map<TDestination>(source); //B
            }
    
            public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
            {
                return MapperAccessor.Mapper.Map(source, destination);  //B
            }
      }
    

    -->IMapperAccessor MapperAccessor

        public interface IMapperAccessor
        {
            IMapper Mapper { get; }
        }
    

    -->即调用的是MapperAccessor.MapperMap()方法,

    MapperAccessor.Mapper到底是谁呢?

    -->AbpAutoMapperModule模块

        [DependsOn(
            typeof(AbpObjectMappingModule),
            typeof(AbpObjectExtendingModule),
            ....
            )]
        public class AbpAutoMapperModule : AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddAutoMapperObjectMapper();
    
                var mapperAccessor = new MapperAccessor();
                context.Services.AddSingleton<IMapperAccessor>(_ => mapperAccessor);
                context.Services.AddSingleton<MapperAccessor>(_ => mapperAccessor);
            }
    
            public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
            {
                CreateMappings(context.ServiceProvider);
            }
            
             private void CreateMappings(IServiceProvider serviceProvider)
            {
                using (var scope = serviceProvider.CreateScope())
                {
                    var options = scope.ServiceProvider.GetRequiredService<IOptions<AbpAutoMapperOptions>>().Value;
                    ......
                    var mapperConfiguration = new MapperConfiguration(mapperConfigurationExpression =>
                    {
                        ConfigureAll(new AbpAutoMapperConfigurationContext(mapperConfigurationExpression, scope.ServiceProvider));
                    });
                   ......
                     var mapperConfiguration = new MapperConfiguration(
                    {
                        ....
                    });
                    scope.ServiceProvider.GetRequiredService<MapperAccessor>().Mapper = mapperConfiguration.CreateMapper(); //C
                }
            }
    
    

    --> var mapperAccessor = new MapperAccessor();注册了单例

    -->scope.ServiceProvider.GetRequiredService<MapperAccessor>().Mapper = mapperConfiguration.CreateMapper();

    这样步骤C的代码使得步骤B中的MapperAccessor.Mapper(其类型为:Volo.Abp.AutoMapper.IMapperAccessor)得到了实例化

    综上所有步骤,等价于

    AutoMapperAutoObjectMappingProvider.MapperAccessor.Mapper = mapperConfiguration.CreateMapper(); 
    

    这就是我们熟悉的:

    var config = new MapperConfiguration(cfg => {
        cfg.AddProfile<AppProfile>();
        cfg.CreateMap<Source, Dest>();
    });
    
    IMapper mapper = config.CreateMapper();
    var dest = mapper.Map<Source, Dest>(new Source());
    

    BookStoreAppService

    在文件夹Books下创建BookStoreAppService.cs

    这是一个抽象类,其它xxxApplicationService都将继续自它:

        /// <summary>
        /// Inherit your application services from this class.
        /// </summary>
        public abstract class BookStoreAppService : ApplicationService
        {
            protected BookStoreAppService()
            {
                LocalizationResource = typeof(BookStoreResource);
            }
        }
    

    设置本地化资源

    LocalizationResource = typeof(BookStoreResource);
    

    BookAppService.cs

    BookAppService继承上一节定义的抽象类BookStoreAppService

    using System;
    using Volo.Abp.Application.Dtos;
    using Volo.Abp.Application.Services;
    using Volo.Abp.Domain.Repositories;
    
    namespace Zto.BookStore.Books
    {
        public class BookAppService :
                CrudAppService<
                    Book,                //The Book entity
                    BookDto,             //Used to show books
                    Guid,                //Primary key of the book entity
                    PagedAndSortedResultRequestDto, //Used for paging/sorting
                    CreateUpdateBookDto>,           //Used to create/update a book
                IBookAppService                     //implement the IBookAppService
        {
    
            public BookAppService(IRepository<Book, Guid> repository)
                : base(repository)
            {
            }
    
        }
    }
    

    2.Authors领域

    这一部分在第一部分的搭建好基础框架的基础上,创建Authors 的相关知识,

    文本档可参见

    [Authors: Domain layer][https://docs.abp.io/en/abp/latest/Tutorials/Part-6?UI=MVC&DB=EF]

  • 相关阅读:
    二分查找
    合并两个或多个有序链表
    前缀和
    田忌赛马
    小根堆实现
    汉化破解:ASPack 2.12 &gt; Alexey Solodovnikov Dump
    EXT是一款强大的AJAX框架
    var TempViewPanel = Ext
    【Azure 事件中心】如何查看事件中心的消息中具体报文内容呢?
    【Azure Developer】Azure Logic App 示例: 解析 Request Body 的 JSON 的表达式? triggerBody()?
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/14110496.html
Copyright © 2020-2023  润新知