- 前言
- 1.初步構建項目結構
- 項目結構
- 1.1 *.Domain.Shared 項目
- 1.2 *.Domain 項目
- 1.3 *.EntityFrameworkCore 項目
- 1.4 *.EntityFrameworkCore.DbMigrations 項目
- 1.5 *.DbMigrator 項目
- 1.6 *.Application.Contracts 項目
- 1.7 *.BookStore.Application 項目
- 1.8 *.HttpApi 項目
- 1.9 *.HttpApi.Client 項目
- 1.10 *.BookStore.HttpApi.Host 項目
- 1.11 *.HttpApi.Client.ConsoleTestApp 測試項目
- 1.12 *.IdentityServer
- 2.Authors領域
前言
本系列參照官方文檔BookStore,創建一個BookStore應用程序。
旨在從零開始(ZeroToOne, zto)不使用模板,
從創建一個空的解決方案開始,一步一步地去了解如何使用Abp.vNext
去構建一個應用程序。
1.初步構建項目結構
創建一個空的解決方案Zto.BookStore
,然后依次添加如下項目,
注意:以下創建的項目,都的以Zto.BookStore
.為前綴,為了敘述的簡單,故省略之,比如:
*.Domain
指的是項目Zto.BookStore.Domain
項目結構

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 *.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"
}
}
-
設置控制台程序為默認啟動項目,
-
打開程【序包管理器控制台】,並將【默認項目】設置為項目:
.EntityFrameworkCore.DbMigrations
, -
執行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<>
綜上,
-
確保已入如下
Nuget
包:-
Microsoft.EntityFrameworkCore.Design
-
Volo.Abp.EntityFrameworkCore.SqlServer
如果使用的是MySql數據庫,引入的包是
Volo.Abp.EntityFrameworkCore.MySQL
-
-
在文件夾
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 CLI或Visual 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"
}
}
-
設置控制台程序為默認啟動項目,
不過,如果現在已經通過以下代碼在
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
的根目中,所以這時可以不用將設置控制台程序為默認啟動項目 -
打開程【程序包管理器控制台】,並將【默認項目】設置為項目:*
.EntityFrameworkCore.DbMigrations
, -
執行EF數據庫遷移命令
add-migration initDb
這時,命令行提示:
PM> add-migration initDb Build started... Build succeeded. To undo this action, use Remove-Migration.
-
把掛起的
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
:遷移快照
-
-
數據庫也自動生成了數據庫及其相關表
在項目*.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
由於目前缺乏對
- 多租戶
IDataSeeder
的了解,所以把跟它們相關的功能代碼注釋掉,簡化后的``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
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
服務,控制台程序自動將執行HostService
的StartAsync()
方法
執行數據庫遷移
設置控制台程序為啟動項目,並運行,執行數據庫遷移。
控制台輸出日志:
[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.
執行完成后,自動生成數據庫及其相關表:

特別注意:
這個控制台程序最終的本質是執行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
表,多了兩條種子數據:

dataSeeder.SeedAsync(tenant?.Id)干了啥?
_dataSeeder
是個什么呢?
相關源碼如下:
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));
}
}
}
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;
}
}
}
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
它內部調用 IDataSeedContributor
的SeedAsync
方法去完成數據播種
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
基本使用
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)
其中,
ObjectMappers
是ApplicationService
類內置的對象,只要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.Mapper
的Map()
方法,
那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.0的SDK
創建一個.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.json
的RemoteServices
節點也要定義一個名為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.cs
和Startup.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,查看錯誤信息,排除問題

訪問
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證書:

第二步:修改遠程服務地址
添加*.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
的相關業務
文本檔可參見
(待續......)