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


目錄

前言

本系列參照官方文檔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】:

using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.DependencyInjection;
using Zto.BookStore.Books;

namespace Zto.BookStore.HttpApi.Client.ConsoleTestApp
{
    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

(待續......)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM