ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

《Abp.vNext从0到1系列》之 BookStore

2020-12-11 10:34:21  阅读:1537  来源: 互联网

标签:vNext Zto Volo Abp BookStore using public


目录

前言

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

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

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

1.初步构建项目结构

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

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

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

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

应用程序启动模板

项目结构

layered-project-dependencies

1.1 *.Domain.Shared 项目

创建一个.NetCore类库项目

基本设置

  • 修改默认命名空间为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 项目

创建一个.NetCore类库项目

基本设置

  • 修改默认命名空间为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 项目

创建一个.NetCore类库项目

基本设置

  • 修改默认命名空间为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仅仅用于数据库迁移,故:

  • 它仅仅用于数据库迁移,运行时使用的还是BookStoreDbContext

  • DbSet<>将不用加了

     public DbSet<Book> Books { get; set; }
    

    这样的DbSet<>代码就不用添加了。

BookStoreMigrationsDbContext.cs代码如下:

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>
就可以使用命令行执行数据库迁移,例如:

  • 在 NET Core CLI中执行: dotnet ef database update

  • 在 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中使用诸如如下命令执行数据库迁移

//vs中使用
Add-Migration

//or NET Core CLI 中使用
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 项目

新建.Net Core控制台项目*.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给表示层.

创建一个.NetCore类库项目

基本设置

  • 修改默认命名空间为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 项目

创建一个.NetCore类库项目

基本设置

  • 修改默认命名空间为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)
        {
        }

    }
}

1.8 *.HttpApi 项目

用于定义API控制器.

大多数情况下,你不需要手动定义API控制器,因为ABP的动态API功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方.

  • 它依赖 .Application.Contracts 项目,因为它需要注入应用服务接口.

创建一个.NetCore类库项目

基本设置

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

项目引用

  • *.Application.Contracts: 注意哦,不是:*.Application

依赖包

  • Volo.Abp.AspNetCore.Mvc

创建`AbpModule

using Volo.Abp.Modularity;

namespace Zto.BookStore
{
    [DependsOn(
        typeof(BookStoreApplicationContractsModule)
        )]
    public class BookStoreHttpApiModule : AbpModule
    {
    }
}

Controllers

​ 创建Controllers文件夹,并在其中创建一个BookStoreController,继承直AbpController

using Volo.Abp.AspNetCore.Mvc;
using Zto.BookStore.Localization;

namespace Zto.BookStore.Controllers
{
    /* Inherit your controllers from this class.
    */
    public abstract class BookStoreController : AbpController
    {
        protected BookStoreController()
        {
            LocalizationResource = typeof(BookStoreResource);
        }
    }
}

1.9 *.HttpApi.Client 项目

定义C#客户端代理使用解决方案的HTTP API项目. 可以将上编辑共享给第三方客户端,使其轻松的在DotNet应用程序中使用你的HTTP API(其他类型的应用程序可以手动或使用其平台的工具来使用你的API).

ABP有动态 C# API 客户端功能,所以大多数情况下你不需要手动的创建C#客户端代理.

.HttpApi.Client.ConsoleTestApp 项目是一个用于演示客户端代理用法的控制台应用程序.

  • 它依赖 .Application.Contracts 项目,因为它需要使用应用服务接口和DTO.

如果你不需要为API创建动态C#客户端代理,可以删除此项目和依赖项

综上所述,BookStore项目目前并没有打算给第三方客户端提供Api,先创建该项目,然后将可其卸载

这个项目的意义就是了为了满足类型如下的场景应运而生的

一个第三方客户端App

或者在微服务架构中其它开发团队开发的其它模块。

他们的共同需求就是

  • 也是使用.Net技术

  • 想使用BooksStore在项目Application.Contracts定义的接口服务

我们BookStore项目组,只是提供*.HttpApi.Client项目生成的.dll即可,其它项目直接已入这个.dll,就可以像调用本地的实例对象一样调用远程Api。

这种场景,就相当于阿里云的云服务提供的基于`.Net Standard 2.0SDK

创建一个.Net Standard 2.0的类库项目

基本设置

  • 目标框架为:.Net Standard 2.0

    https://docs.microsoft.com/zh-cn/dotnet/standard/net-standard#net-5-and-net-standard

    如果你不需要支持 .NET Framework,可以选择 .NET Standard 2.1 或 .NET 5。 我们建议你跳过 .NET Standard 2.1,而直接选择 .NET 5。 大多数广泛使用的库最终都将同时以 .NET Standard 2.0 和 .NET 5 作为目标。 支持 .NET Standard 2.0 可提供最大的覆盖范围,而支持 .NET 5 可确保你可以为已使用 .NET 5 的客户利用最新的平台功能。

​ 本示例是基于目前最新的.Net5.0, 该项目的目标框架设置为.Net Standard 2.0报错:

项目“..\Zto.BookStore.Application.Contracts\Zto.BookStore.Application.Contracts.csproj”指向“net5.0”。它不能被指向“.NETStandard,Version=v2.0”的项目引用。	Zto.BookStore.HttpApi.Client	C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets	1662	

目前没有什么好的解决办法,故将该项目的目标框架设置改为.Net5, 右键项目文件,选择【编辑项目文件】

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    //......
  </PropertyGroup>

修改为:

  <PropertyGroup>
    <TargetFramework>.net5.0</TargetFramework>
    //......
  </PropertyGroup>
  • 修改默认命名空间为Zto.BookStore

项目引用

  • *.Application.Contracts: 注意哦,不是:*.Application

依赖包

  • Volo.Abp.Http.Client:有动态 C# API 客户端的功能

创建AbpModule

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Modularity;

namespace Zto.BookStore
{
    [DependsOn(
        typeof(BookStoreApplicationContractsModule), //包含应用服务接口
        typeof(AbpHttpClientModule)                  //用来创建客户端代理
    )]
    public class BookStoreHttpApiClientModule : AbpModule
    {
        public const string RemoteServiceName = "BookStore";

        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            //创建动态客户端代理
            context.Services.AddHttpClientProxies(
                typeof(BookStoreApplicationContractsModule).Assembly,
                RemoteServiceName
            );
        }
    }
}

注意事项

这里的

public const string RemoteServiceName = "BookStore";

定义了服务的名称,这就要求直接引用*.HttpApi.Client 项目或其生成的*.HttpApi.Client.dll的第三方项目(如:下面要创建的*.HttpApi.Client.ConsoleTestApp 测试项目)在配置文件appsettings.jsonRemoteServices节点也要定义一个名为BookStore服务配置节点,如下所示:

*.HttpApi.Client.ConsoleTestApp 测试项目的appsettings.json:

{
  "RemoteServices": {
    "BookStore": {
      "BaseUrl": "https://localhost:8000"
    }
  }
}

特别注意:

  • *.HttpApi.Client.ConsoleTestApp 测试项目配置文件中的``appsettings.json`的

        "BookStore": {
          ....
        }
    

    要求必须与*.HttpApi项目中的模块BookStoreHttpApiClientModule定义的RemoteServiceName

        public class BookStoreHttpApiClientModule : AbpModule
        {
            public const string RemoteServiceName = "BookStore";
            //......
        }
    

    相同。

  • 配置文件中的

    "BaseUrl": "https://localhost:8000"
    

    是接下来我们要创建的.BookStore.HttpApi.Host项目的网站地址

测试程序

​ 看完这一节,直接跳转到章节【1.11 *.HttpApi.Client.ConsoleTestApp 测试项目】进行测试

1.10 *.BookStore.HttpApi.Host 项目

这是一个用于发布部署WebApi的Web应用程序。

在解决方案的src目录下,新建一个 基于Asp.Net Core 的WebApi应用程序。

项目引用

  • *.HttpApi: 因为UI层需要使用解决方案的API和应用服务接口.
  • *.Application
  • *.EntityFrameworkCore.DbMigrations:

依赖包

  • Volo.Abp.Autofac

  • Volo.Abp.AspNetCore.Serilog

  • Volo.Abp.Caching.StackExchangeRedis

  • Volo.Abp.Swashbuckle

  • Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared :错误页面UI

  • Microsoft.AspNetCore.DataProtection.StackExchangeRedis

修改端口

launchSettings.json文件中修改应用程序启动端口

  • https:44327
  • http:44328
{
     //......
    "launchUrl": "weatherforecast",
    //......
    "iisExpress": {
      "launchUrl": "weatherforecast",
      "applicationUrl": "http://localhost:12016",
      "sslPort": 44315
    }
  },

    "Zto.BookStore.HttpApi.Host": {
      "launchUrl": "weatherforecast", 
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
       //......
 }

修改为:

{
    
   //......
        "launchUrl": "Home",
    //......
    "iisExpress": {
      "launchUrl": "Home",
      "applicationUrl": "http://localhost:8001",
      "sslPort": 8000
    }
  },
  //......
      "applicationUrl": "https://localhost:8000;http://localhost:8001",
  //......
}

配置文件

appsetting.json:

{
  "App": {
    "CorsOrigins": "https://*.BookStore.com,http://localhost:4200,https://localhost:44307"
  },
  "ConnectionStrings": {
    "Default": "Server=.;Database=BookStore_Zto;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Redis": {
    "Configuration": "127.0.0.1"
  },
  "AuthServer": {
    "Authority": "https://localhost:44388",
    "RequireHttpsMetadata": "true",
    "SwaggerClientId": "BookStore_Swagger",
    "SwaggerClientSecret": "1q2w3e*"
  },
  "StringEncryption": {
    "DefaultPassPhrase": "iIpMRCMOnSTU6lxK"
  },
  "Settings": {
    "Abp.Mailing.Smtp.Host": "127.0.0.1",
    "Abp.Mailing.Smtp.Port": "25",
    "Abp.Mailing.Smtp.UserName": "",
    "Abp.Mailing.Smtp.Password": "",
    "Abp.Mailing.Smtp.Domain": "",
    "Abp.Mailing.Smtp.EnableSsl": "false",
    "Abp.Mailing.Smtp.UseDefaultCredentials": "true",
    "Abp.Mailing.DefaultFromAddress": "noreply@abp.io",
    "Abp.Mailing.DefaultFromDisplayName": "ABP application"
  }
}

编写相应的功能前,我们得改造下Program.csStartup.cs

Program.cs

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;

namespace Zto.BookStore
{
    public class Program
    {
        public static int Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
#if DEBUG
                .MinimumLevel.Debug()
#else
                .MinimumLevel.Information()
#endif
                .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
                .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
                .WriteTo.Async(c => c.Console())
#endif
                .CreateLogger();

            try
            {
                Log.Information("Starting Zto.BookStore.HttpApi.Host.");
                CreateHostBuilder(args).Build().Run();
                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly!");
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        internal static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                })
                .UseAutofac()
                .UseSerilog();
    }
}

   - .UseAutofac():使用Autofac
   - .UseSerilog(): 使用UseSerilog日志

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Zto.BookStore
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplication<BookStoreHttpApiHostModule>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            app.InitializeApplication();
        }
   }
}
  • 添加BookStoreHttpApiHostModule模块
  • 使用InitializeApplication初始化应用程序

创建AbpModule

BookStoreHttpApiHostModule.cs

配置Services

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Autofac;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Modularity;
using Zto.BookStore.EntityFrameworkCore;

namespace Zto.BookStore
{
    [DependsOn(
        typeof(AbpAutofacModule),
        typeof(AbpAuthorizationModule),
        typeof(BookStoreApplicationModule),
        typeof(BookStoreHttpApiModule),
        typeof(AbpAspNetCoreMvcUiModule),
        typeof(AbpCachingStackExchangeRedisModule),
        typeof(BookStoreEntityFrameworkCoreDbMigrationsModule),
        typeof(AbpAspNetCoreSerilogModule),
        typeof(AbpSwashbuckleModule)
     )]
    public class BookStoreHttpApiHostModule : AbpModule
    {
       private const string DefaultCorsPolicyName = "Default";

        //配置Services
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var configuration = context.Services.GetConfiguration();
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            
            ConfigureConventionalControllers();
            ConfigureAuthentication(context, configuration);
            ConfigureLocalization();
            ConfigureCache(configuration);
            ConfigureVirtualFileSystem(context);
            ConfigureRedis(context, configuration, hostingEnvironment);
            ConfigureCors(context, configuration);
            ConfigureSwaggerServices(context);
          
        }
        
        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            //在这里配置中间件
        }
    }
}

这是BookStoreHttpApiHostModule的基本框架,下面将一步步添加相应的功能

ConfigureConventionalControllers
        private void ConfigureConventionalControllers()
        {
            Configure<AbpAspNetCoreMvcOptions>(options =>
            {
                //自动生成API控制器
                options.ConventionalControllers.Create(typeof(BookStoreApplicationModule).Assembly);
            });
        }

上述代码让ABP可以按照惯例 自动 生成API控制器。

自动API控制器

官方文档

应用程序服务后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做.

ABP可以按照惯例 自动 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义.

ConfigureAuthentication

配置认证

        private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
        {
            context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = configuration["AuthServer:Authority"];
                    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                    options.Audience = "BookStore";
                });
        }
ConfigureLocalization

本地化

    private void ConfigureLocalization()
    {
        Configure<AbpLocalizationOptions>(options =>
        {
            options.Languages.Add(new LanguageInfo("en", "en", "English"));
            options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文"));
        });
    }
ConfigureCache

缓存配置

        private void ConfigureCache(IConfiguration configuration)
        {
            Configure<AbpDistributedCacheOptions>(options => { options.KeyPrefix = "BookStore:"; });
        }
ConfigureVirtualFileSystem

虚拟文件系统

   private void ConfigureVirtualFileSystem(ServiceConfigurationContext context)
    {
        var hostingEnvironment = context.Services.GetHostingEnvironment();

        if (hostingEnvironment.IsDevelopment())
        {
            Configure<AbpVirtualFileSystemOptions>(options =>
            {
                options.FileSets.ReplaceEmbeddedByPhysical<BookStoreDomainSharedModule>(
                    Path.Combine(hostingEnvironment.ContentRootPath,
                        $"..{Path.DirectorySeparatorChar}Zto.BookStore.Domain.Shared"));
                options.FileSets.ReplaceEmbeddedByPhysical<BookStoreDomainModule>(
                    Path.Combine(hostingEnvironment.ContentRootPath,
                        $"..{Path.DirectorySeparatorChar}Zto.BookStore.Domain"));
                options.FileSets.ReplaceEmbeddedByPhysical<BookStoreApplicationContractsModule>(
                    Path.Combine(hostingEnvironment.ContentRootPath,
                        $"..{Path.DirectorySeparatorChar}Zto.BookStore.Application.Contracts"));
                options.FileSets.ReplaceEmbeddedByPhysical<BookStoreApplicationModule>(
                    Path.Combine(hostingEnvironment.ContentRootPath,
                        $"..{Path.DirectorySeparatorChar}Zto.BookStore.Application"));
            });
        }
    }
ConfigureRedis

Redis

        private void ConfigureRedis(ServiceConfigurationContext context,
            IConfiguration configuration,
            IWebHostEnvironment hostingEnvironment)
        {
            if (!hostingEnvironment.IsDevelopment())
            {
                var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
                context.Services
                    .AddDataProtection()
                    .PersistKeysToStackExchangeRedis(redis, "BookStore-Protection-Keys");
            }
        }
ConfigureCors

跨越

        private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
        {
            context.Services.AddCors(options =>
            {
                options.AddPolicy(DefaultCorsPolicyName, builder =>
                {
                    builder
                        .WithOrigins(
                            configuration["App:CorsOrigins"]
                                .Split(",", StringSplitOptions.RemoveEmptyEntries)
                                .Select(o => o.RemovePostFix("/"))
                                .ToArray()
                        )
                        .WithAbpExposedHeaders()
                        .SetIsOriginAllowedToAllowWildcardSubdomains()
                        .AllowAnyHeader()
                        .AllowAnyMethod()
                        .AllowCredentials();
                });
            });
        }
ConfigureSwaggerServices

配置Swagger

private static void ConfigureSwaggerServices(ServiceConfigurationContext context, IConfiguration configuration)
        {
            context.Services.AddAbpSwaggerGenWithOAuth(
                configuration["AuthServer:Authority"],
                new Dictionary<string, string>
                {
                    {"BookStore", "BookStore API"}
                },
                options =>
                {
                    options.SwaggerDoc("v1", new OpenApiInfo { Title = "BookStore API", Version = "v1" });
                    options.DocInclusionPredicate((docName, description) => true);
                });
        }

配置中间件

public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseAbpRequestLocalization();

        if (!env.IsDevelopment())
        {
            app.UseErrorPage();
        }

        app.UseCorrelationId();
        app.UseVirtualFiles();
        app.UseRouting();
        app.UseCors(DefaultCorsPolicyName);
        app.UseAuthentication();

        if (MultiTenancyConsts.IsEnabled)
        {
            //app.UseMultiTenancy();//暂时不支持多租户
        }

        app.UseAuthorization();

        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "BookStore API");

            var configuration = context.GetConfiguration();
            options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
            options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);
        });

        app.UseAuditing();
        app.UseAbpSerilogEnrichers();
        app.UseConfiguredEndpoints();
    }

HomeController

Controllers文件夹下,创建HomeController.cs

using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;

namespace Zto.BookStore.Controllers
{
    public class HomeController : AbpController
    {
        public ActionResult Index()
        {
            return Redirect("~/swagger");
        }
    }
}

运行HttApi.Host

运行WebApiHost网站:跳转到swagger的首页:

Tips:

如果出现:Failed to load API definition.

可以访问:打开http://localhost:/swagger/v1/swagger.json,查看错误信息,排除问题

image-20201210202841427

访问

https://localhost:8000/api/app/book

返回(我们之前插入的种子数据):

{
  "totalCount": 2,
  "items": [
    {
      "authorId": "00000000-0000-0000-0000-000000000000",
      "authorName": null,
      "name": "The Hitchhiker's Guide to the Galaxy",
      "type": 7,
      "publishDate": "1995-09-27T00:00:00",
      "price": 42,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-12-08T22:17:08.6454076",
      "creatorId": null,
      "id": "ac1c9ff8-551e-4f97-9594-d50ed4f4f594"
    },
    {
      "authorId": "00000000-0000-0000-0000-000000000000",
      "authorName": null,
      "name": "1984",
      "type": 3,
      "publishDate": "1949-06-08T00:00:00",
      "price": 19.84,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-12-08T22:17:08.4731128",
      "creatorId": null,
      "id": "f27890cb-f01b-4965-b2af-19f3bacc1e40"
    }
  ]
}

但是如果我们插入一个Book对象

Curl

curl -X POST "https://localhost:8000/api/app/book" -H "accept: text/plain" -H "Content-Type: application/json" -d "{\"authorId\":\"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\"name\":\"string\",\"type\":0,\"publishDate\":\"2020-12-10\",\"price\":0}"

Request URL

https://localhost:8000/api/app/book

Server response

Code Details
400Undocumented Error:Response headerscontent-length: 0 date: Thu10 Dec 2020 12:37:51 GMT server: Kestrel status: 400 x-correlation-id: ff1b2a0878fa42fca971bffcfd0e570f

返回错误码:400,表示没有授权。授权我们将在*.IdentityServer项目中响应的功能

1.11 *.HttpApi.Client.ConsoleTestApp 测试项目

这是一个用于演示客户端代理用法的控制台应用程序。

在解决方案的test目录下,新建一个.Net的控制台项目

项目引用

  • *.Application.Contracts: 注意,并没有引用项目.Application,只依赖接口

依赖包

  • Microsoft.Extensions.Hosting

发布*.BookStore.HttpApi.Host 项目

为了测试,我们先做如下准备:

第一步:,我们先把*.BookStore.HttpApi.Host 项目发布到IIS,地址及其端口如下:

没有证书,可以选择IIS ExPress Development Certificate证书:

image-20201211094148878

第二步:修改远程服务地址

添加*.HttpApi.Client.ConsoleTestApp 测试项目的配置appsettings.json:

{
  "RemoteServices": {
    "BookStore": {
      "BaseUrl": "https://localhost:8100"
    }
  }
}
  • BookStore就是在创建客户端代码模块BookStoreHttpApiClientModule时,给定的RemoteServiceName的值,

    两者必须一致

    见代码:

  • [DependsOn(
        typeof(BookStoreApplicationContractsModule), //包含应用服务接口
        typeof(AbpHttpClientModule)                  //用来创建客户端代理
    )]
    public class BookStoreHttpApiClientModule : AbpModule
    {
        public const string RemoteServiceName = "BookStore";
    
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            //创建动态客户端代理
            context.Services.AddHttpClientProxies(
                typeof(BookStoreApplicationContractsModule).Assembly,
                RemoteServiceName
            );
        }
    }
    
  • "BaseUrl": "http://localhost:8101"就是第一步中*.BookStore.HttpApi.Host 项目的IIS发布地址

创建AbpModule

BookStoreConsoleApiClientModule

    [DependsOn(
        typeof(BookStoreHttpApiClientModule)
        )]
    public class BookStoreConsoleApiClientModule : AbpModule
    {
        public override void PreConfigureServices(ServiceConfigurationContext context)
        {
            PreConfigure<AbpHttpClientBuilderOptions>(options =>
            {
                options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
                {
                    clientBuilder.AddTransientHttpErrorPolicy(
                        policyBuilder => policyBuilder.WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)))
                    );
                });
            });
        }
    }

依赖模块BookStoreHttpApiClientModule

创建宿主服务

  • 创建宿主服务ConsoleTestAppHostedService, 用于承载客户端Demo类ClientDemoService:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;

namespace Zto.BookStore.HttpApi.Client.ConsoleTestApp
{
    public class ConsoleTestAppHostedService : IHostedService
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            using (var application = AbpApplicationFactory.Create<BookStoreConsoleApiClientModule>())
            {
                application.Initialize();

                var demo = application.ServiceProvider.GetRequiredService<ClientDemoService>();
                await demo.RunAsync();

                application.Shutdown();
            }
        }

        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }
}

  • 添加宿主服务

    Program.cs添加宿主服务:

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using System.Threading.Tasks;
    
    namespace Zto.BookStore.HttpApi.Client.ConsoleTestApp
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                await CreateHostBuilder(args).RunConsoleAsync();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureServices((hostContext, services) =>
                    {
                        services.AddHostedService<ConsoleTestAppHostedService>();
                    });
        }
    }
    
    

创建客户端Demo

ClientDemoService用于模拟客户端,通过调用客户端代理模块【BookStoreHttpApiClientModule】:

    public class ClientDemoService : ITransientDependency
    {
        private readonly IBookAppService _bookAppService;

        public ClientDemoService(IBookAppService bookAppService)
        {
            _bookAppService = bookAppService;
        }

        public async Task RunAsync()
        {
            var requstDto = new PagedAndSortedResultRequestDto
            {
                Sorting = "PublishDate desc"
            };

            PagedResultDto<BookDto> output = await _bookAppService.GetListAsync(requstDto);
            Console.WriteLine($"BookList:{JsonConvert.SerializeObject(output)}");
        }
    }

可以看到,客户端Demo可以像调用本地类库一样调用远程服务。

测试远程调用

*.HttpApi.Client.ConsoleTestApp测试项目设置为启动项,运行。

输入如下:

BookList:{"TotalCount":2,"Items":[{"AuthorId":"00000000-0000-0000-0000-000000000000","AuthorName":null,"Name":"The Hitchhiker's Guide to the Galaxy","Type":7,"PublishDate":"1995-09-27T00:00:00","Price":42.0,"LastModificationTime":null,"LastModifierId":null,"CreationTime":"2020-12-10T21:25:56.4359053","CreatorId":null,"Id":"fc013530-19df-44b9-8272-0a664a8178fb"},{"AuthorId":"00000000-0000-0000-0000-000000000000","AuthorName":null,"Name":"1984","Type":3,"PublishDate":"1949-06-08T00:00:00","Price":19.84,"LastModificationTime":null,"LastModifierId":null,"CreationTime":"2020-12-10T21:25:56.2250498","CreatorId":null,"Id":"e4738098-fecc-4486-a1d3-659d1947a13e"}]}

//.......

即:

{
  "TotalCount": 2,
  "Items": [
    {
      "AuthorId": "00000000-0000-0000-0000-000000000000",
      "AuthorName": null,
      "Name": "The Hitchhiker's Guide to the Galaxy",
      "Type": 7,
      "PublishDate": "1995-09-27T00:00:00",
      "Price": 42.0,
      "LastModificationTime": null,
      "LastModifierId": null,
      "CreationTime": "2020-12-10T21:25:56.4359053",
      "CreatorId": null,
      "Id": "fc013530-19df-44b9-8272-0a664a8178fb"
    },
    {
      "AuthorId": "00000000-0000-0000-0000-000000000000",
      "AuthorName": null,
      "Name": "1984",
      "Type": 3,
      "PublishDate": "1949-06-08T00:00:00",
      "Price": 19.84,
      "LastModificationTime": null,
      "LastModifierId": null,
      "CreationTime": "2020-12-10T21:25:56.2250498",
      "CreatorId": null,
      "Id": "e4738098-fecc-4486-a1d3-659d1947a13e"
    }
  ]
}

1.12 *.IdentityServer

(待续......)

2.Authors领域

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

文本档可参见

Authors: Domain layer

(待续......)

标签:vNext,Zto,Volo,Abp,BookStore,using,public
来源: https://www.cnblogs.com/majiangfang/p/14119060.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有