其他分享
首页 > 其他分享> > 《Abp.vNext从0到1系列》之 BookStore

《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类库项目

基本设置

依赖包

知识点: 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开发之前,我们首先要准备本地化的文本(这是通常在开发应用程序时需要做的).

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

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

    [LocalizationResourceName("BookStore")]
    public class BookStoreResource
    {

    }

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

特别注意

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

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

1.2 *.Domain 项目

创建一个.NetCore类库项目

基本设置

项目引用

依赖包

创建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类库项目

基本设置

项目引用

依赖包

创建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();
            });
        }
    }
}

代码解析:

创建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();
        }
    }
}

代码解析:

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);

            });
        }
    }
}

其中:

命令行中执行数据库迁移

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

这时,我们可以随便创建一个控制台程序,并添加配置文件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 项目

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

基本设置

项目引用

依赖包

创建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仅仅用于数据库迁移,故:

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

实现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中使用命令行执行数据库迁移有如下局限性:

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

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

迁移接口: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命令行)统一管理所有数据库迁移任务,比如:

其中,关键性代码如下:

完整代码如下:

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);
        }
    }
}

代码解析:

注意

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

简化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,以后所有的数据库迁移都推荐使这个控制台项目进行

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

基本设置

特别注意

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

项目引用

依赖包

创建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);
    }
}

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类库项目

基本设置

项目引用

依赖包

创建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; }
    }
}

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; }
    }
}

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类库项目

基本设置

项目引用

依赖包

创建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.

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);

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)
	}
}

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.cs

    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
            CreateMap<CreateUpdateBookDto, Book>();
        }
    }
源码代码分析

以下代码:

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控制器,那么它是最合适的地方.

创建一个.NetCore类库项目

基本设置

项目引用

依赖包

创建`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 项目是一个用于演示客户端代理用法的控制台应用程序.

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

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

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

一个第三方客户端App

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

他们的共同需求就是

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

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

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

基本设置

​ 本示例是基于目前最新的.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>

项目引用

依赖包

创建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"
    }
  }
}

特别注意:

测试程序

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

1.10 *.BookStore.HttpApi.Host 项目

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

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

项目引用

依赖包

修改端口

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

{
     //......
    "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();
        }
   }
}

创建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的控制台项目

项目引用

依赖包

发布*.BookStore.HttpApi.Host 项目

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

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

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

image-20201211094148878

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

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

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

创建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

创建宿主服务

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;
    }
}

创建客户端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