手動從0搭建ABP框架-ABP官方完整解決方案和手動搭建簡化解決方案實踐
- 2022 年 7 月 26 日
- 筆記
本文主要講解了如何把ABP官方的在線生成解決方案運行起來,並說明了解決方案中項目間的依賴關係。然後手動實踐了如何從0搭建了一個簡化的解決方案。ABP官方的在線生成解決方案源碼下載參考[3],手動搭建的簡化的解決方案源碼下載參考[4]。
一.ABP官方在線生成解決方案
1.將在線生成解決方案跑起來
首先進入頁面//abp.io/get-started,然後創建項目:

然後頭腦中要有一個項目之間的依賴關係圖,不清楚的可以參考《基於ABP實現DDD》:

截止目前為止,項目使用的.NET版本是6.0,ABP版本是5.3.3。
使用Rider打開項目Acme.BookStore後,會提示使用yarn安裝package,安裝包後:

在整個解決方案中搜索ConnectionStrings,發現其在Acme.BookStore.HttpApi.Host、Acme.BookStore.DbMigrator和Acme.BookStore.IdentityServer這3個啟動項目中出現:

將ConnectionStrings中的內容替換為:
"ConnectionStrings": {
"Default": "Server=127.0.0.1;Database=BookStore;Trusted_Connection=True;User ID=sa;Password=913292836;"
},
然後開始運行Acme.BookStore.DbMigrator進行數據種子遷移:

出現上面圖片輸出結果,基本上表示遷移成功完成了,接下來看看資料庫:

運行Acme.BookStore.Web項目如下:

啟動後發現報錯了,發現主要是3個問題:一個是Redis沒有啟動,另一個問題是IDS4服務沒有啟動,最後一個問題是Acme.BookStore.HttpApi.Host沒有啟動:

啟動IDS4服務的時候發現報錯Volo.Abp.AbpException: Could not find the bundle file '/libs/abp/core/abp.css' for the bundle 'Basic.Global'!:

在該項目下執行命令abp install-libs:

發現消息提示說ABP CLI有個更新的5.3.3版本,通過命令dotnet tool update -g Volo.Abp.Cli進行升級。再次運行Acme.BookStore.IdentityServer項目,發現不報錯誤了。如果在啟動其它項目(特指Acme.BookStore.Web項目)的時候報同樣的錯誤,那麼同樣執行命令abp install-libs即可解決問題。同時啟動這3個項目如下:

啟動成功後就可以見到熟悉的介面:

下面是項目Swagger的介面:

至此,已經把從官方下載下來的項目成功地運行起來了。
2.ABP運行流程
下面是在網上[1]找到的一張圖,很清晰的說明了AspNet Core和ABP模組的運行流程,個人認為圖上的Startup.ConfigureServices應該是Startup.Configure,已經在圖中做了修改。

(1)AspNet Core運行流程
簡單理解,基本上就是在Startup.ConfigureServices中進行依賴注入配置,然後在Startup.Configure中配置管道中間件,訪問的時候就像洋蔥模型。
(2)ABP模組運行流程
- 在ABP模組中對Startup.ConfigureServices做了擴展,增加了PreConfigureServices和PostConfigureServices。對Startup.Configure也做了擴展,當然名字也修改了,Startup.Configure相當於是OnApplicationInitialization,同時增加了OnPreApplicationInitialization和OnPostApplicationInitialization。
- 在ABP解決方案中有多個項目,每個項目都會有一個類繼承自AbpModule。並且通過DependsOn描述了該模組依賴的模組。這樣在ABP解決方案中就會有很多模組之間的依賴關係,通過拓撲排序演算法對模組進行排序,從最深層的模組依次載入,直到啟動所有模組。
(3)AbpModule抽象類中的方法
除了主要的PreConfigureServices()、ConfigureServices()、PostConfigureServices()、OnPreApplicationInitialization()、OnApplicationInitialization()、OnPostApplicationInitialization()方法外,還有一些其它的方法。abp\framework\src\Volo.Abp.Core\Volo\Abp\Modularity\AbpModule.cs:

二.手動創建解決方案
0.創建解決方案
首先創建一個目錄BookStoreHand用於存放解決方案:

然後創建一個解決方案,執行命令dotnet new sln -n Acme.BookStore:

用Rider打開後解決方案是空的,然後手動創建2個New Solution Folder,分別為src和test:

1.創建領域共享層和領域層
(1)Acme.BookStore.Domain.Shared[領域共享層]
通常定義的常量和枚舉,都放在該項目中。通過命令dotnet new classlib -n Acme.BookStore.Domain.Shared和dotnet sln ../Acme.BookStore.sln add Acme.BookStore.Domain.Shared創建領域共享層,並將其添加到解決方案當中:

然後就是創建模組類BookStoreDomainSharedModule如下:
namespace Acme.BookStore.Domain.Shared
{
public class BookStoreDomainSharedModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
}
說明:接下來創建項目、添加項目間的引用等都使用Rider,而不再使用CLI操作,覺得CLI並不方便操作。基本思路順序都是:創建項目,設置引用關係,創建模組,其它操作。
(2)Acme.BookStore.Domain[領域層]
該項目包含實體、值對象、領域服務、規約、倉儲介面等。通過Rider創建Class Library項目Acme.BookStore.Domain如下:

Acme.BookStore.Domain項目依賴於Acme.BookStore.Domain.Shared項目:

創建模組類BookStoreDomainSharedModule如下:
[DependsOn(
typeof(BookStoreDomainSharedModule) //依賴領域共享模組
)]
public class BookStoreDomainModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
創建領域實體Book:
public class Book: Entity<int>
{
public string BookName { get; set; } //名字
public string Author { get; set; } //作者
public DateTime PublishDate { get; set; } //出版日期
public double Price { get; set; } //價格
}
創建倉儲IBookRepository介面:
public interface IBookRepository: IRepository<Book, int>
{
}
2.創建基礎設施層
(1)創建項目
基礎設施層Acme.BookStore.EntityFrameworkCore是EF Core核心基礎依賴項目,包含數據上下文、資料庫映射、EF Core倉儲實現等。通過Rider創建Class Library項目Acme.BookStore.EntityFrameworkCore如下:

Acme.BookStore.EntityFrameworkCore項目依賴於Acme.BookStore.Domain項目:

(2)創建模組
創建模組類BookStoreEntityFrameworkCoreModule如下:
[DependsOn(
typeof(BookStoreDomainModule),
typeof(AbpEntityFrameworkCoreSqlServerModule)
)]
public class BookStoreEntityFrameworkCoreModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
// 給所有的實體都增加默認倉儲
options.AddDefaultRepositories(includeAllEntities: true);
});
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer();
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
(3)創建資料庫上下文
創建資料庫上下文BookStoreDbContext:
public class BookStoreDbContext: AbpDbContext<BookStoreDbContext>
{
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BookStoreDbContext).Assembly);
}
}
ApplyConfigurationsFromAssembly應用來自IEntityTypeConfiguration中的配置。定義實體映射BookDbMapping如下:
public class BookDbMapping: IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
// 配置主鍵
builder.HasKey(b => b.Id).HasName("Id");
// 配置表和欄位
builder.ToTable("AbpBook");
builder.Property(t => t.BookName).IsRequired().HasColumnName("BookName").HasComment("書名");
builder.Property(t => t.Author).IsRequired().HasColumnName("Author").HasComment("作者");
builder.Property(t => t.PublishDate).IsRequired().HasColumnName("PublishDate").HasComment("出版日期");
builder.Property(t => t.Price).IsRequired().HasColumnName("Price").HasComment("價格");
// 配置關係
}
}
(4)創建倉儲實現
定義IBookRepository的實現BookRepository如下:
public class BookRepository: EfCoreRepository<BookStoreDbContext, Book, int>, IBookRepository
{
public BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider) : base(dbContextProvider)
{
}
}
3.創建應用契約層和應用層
(1)Acme.BookStore.Application.Contracts[應用契約層]
包含應用服務介面和數據傳輸對象。該項⽬被應⽤程式客戶端引用,比如Web項目、API客戶端項目。通過Rider創建Class Library項目Acme.BookStore.Application.Contracts:

Acme.BookStore.Application.Contracts項目依賴於Acme.BookStore.Domain.Shared項目如下:

創建模組類BookStoreApplicationContractsModule如下:
[DependsOn(
typeof(BookStoreDomainSharedModule), //依賴於BookStoreDomainSharedModule
typeof(AbpObjectExtendingModule) //依賴於AbpObjectExtendingModule
)]
public class BookStoreApplicationContractsModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
創建服務介面IBookAppService如下:
public interface IBookAppService: IApplicationService
{
/// <summary>
/// 獲取書籍
/// </summary>
Task<BookDto> GetBookAsync(int id);
}
創建輸出DTO為BookDto如下:
public class BookDto
{
public int Id { get; set; } //主鍵
public string BookName { get; set; } //名字
public string Author { get; set; } //作者
public DateTime PublishDate { get; set; } //出版日期
public double Price { get; set; } //價格
}
(2)Acme.BookStore.Application[應用層]
實現在Contracts項目中定義的接⼝。通過Rider創建Class Library項目Acme.BookStore.Application如下:

Acme.BookStore.Application項目依賴於
Acme.BookStore.Application.Contracts和Acme.BookStore.Domain項目:

創建模組類BookStoreApplicationModule如下:
[DependsOn(
typeof(AbpAutoMapperModule), //依賴於AutoMapper
typeof(BookStoreDomainModule), //依賴於BookStoreDomainModule
typeof(BookStoreApplicationContractsModule) //依賴於BookStoreApplicationContractsModule
)]
public class BookStoreApplicationModule: AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var services = context.Services;
// 添加ObjectMapper注入
services.AddAutoMapperObjectMapper<BookStoreApplicationModule>();
// Abp AutoMapper設置
Configure<AbpAutoMapperOptions>(config =>
{
config.AddMaps<BookStoreApplicationAutoMapperProfile>();
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
創建自動映類BookStoreApplicationAutoMapperProfile如下:
public class BookStoreApplicationAutoMapperProfile: Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<BookDto, Book>();
CreateMap<Book, BookDto>();
}
}
創建IBookAppService類的實現類BookAppService如下:
public class BookAppService: ApplicationService, IBookAppService
{
private readonly IBookRepository _bookRepository;
public BookAppService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public async Task<BookDto> GetBookAsync(int id)
{
var queryable = await _bookRepository.GetQueryableAsync();
var book = queryable.FirstOrDefault(t => t.Id == id);
if (book == null)
{
throw new ArgumentNullException(nameof(book));
}
return ObjectMapper.Map<Book, BookDto>(book);
}
}
4.創建種子遷移
Acme.BookStore.DbMigrator是控制台應用程式,主要是遷移資料庫結構並初始化種子數據。通過Rider創建ASP.NET Core Web Application的Empty項目Acme.BookStore.DbMigrator如下:

Acme.BookStore.DbMigrator項目依賴於Acme.BookStore.Application.Contracts和Acme.BookStore.EntityFrameworkCore項目如下:

創建模組類BookStoreDbMigratorModule如下:
[DependsOn(
typeof(AbpAutofacModule),
typeof(BookStoreEntityFrameworkCoreModule),
typeof(BookStoreApplicationContractsModule)
)]
public class BookStoreDbMigratorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
在Program.cs中添加services.AddHostedService

說明:Program.cs、DbMigratorHostedService.cs和BookStoreDbMigrationService.cs的源碼等完整項目源碼參考[4]。
5.創建遠程服務層
Acme.BookStore.HttpApi[遠程服務層],簡單理解就是很薄的控制層,該項目主要用於定義HTTP API,即應用服務層的包裝器,將它們公開給遠程客戶端調用。通過Rider創建Class Library項目Acme.BookStore.HttpApi如下:

Acme.BookStore.HttpApi項目依賴於Acme.BookStore.Application.Contracts項目如下:

創建模組類BookStoreHttpApiModule如下:
[DependsOn(
typeof(BookStoreApplicationContractsModule) //依賴於BookStoreApplicationContractsModule
)]
public class BookStoreHttpApiModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
base.OnApplicationInitialization(context);
}
}
為了簡要說明問題,創建一個簡單的控制器類BookStoreController如下:
[RemoteService]
[Area("BookStore")]
[Route("api/app/book")]
public class BookStoreController: AbpControllerBase
{
private readonly IBookAppService _bookAppService;
public BookStoreController(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
[HttpGet]
[Route("get-book")]
public Task<BookDto> GetBookAsync(int id)
{
return _bookAppService.GetBookAsync(id);
}
}
6.創建展示層
Acme.BookStore.HttpApi.Host這個是前後端分離時的項目命名方式。通過Rider創建ASP.NET Core Web Application的Empty項目Acme.BookStore.HttpApi.Host如下:

Acme.BookStore.HttpApi.Host項目依賴於Acme.BookStore.Application、Acme.BookStore.EntityFrameworkCore和Acme.BookStore.HttpApi項目如下:

創建模組類BookStoreHttpApiHostModule如下:
[DependsOn(
typeof(BookStoreHttpApiModule), //依賴於BookStoreHttpApiModule
typeof(AbpAutofacModule), //依賴於AbpAutofacModule
typeof(BookStoreApplicationModule), //依賴於BookStoreApplicationModule
typeof(BookStoreEntityFrameworkCoreModule), //依賴於BookStoreEntityFrameworkCoreModule
typeof(AbpAspNetCoreSerilogModule), //依賴於AbpAspNetCoreSerilogModule
typeof(AbpSwashbuckleModule) //依賴於AbpSwashbuckleModule
)]
public class BookStoreHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var services = context.Services;
var configuration = services.GetConfiguration();
ConfigureConventionalControllers();
ConfigureCors(context, configuration);
ConfigureSwaggerServices(context, configuration);
}
private void ConfigureConventionalControllers()
{
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(BookStoreApplicationModule).Assembly);
});
}
private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",builder =>
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
}
private static void ConfigureSwaggerServices(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "BookStore API", Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var env = context.GetEnvironment();
var app = context.GetApplicationBuilder();
var configuration = context.GetConfiguration();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("AllowAll");
if (configuration["UseSwagger"] == "true")
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Acme.BookStore API");
});
}
app.UseRouting();
app.UseConfiguredEndpoints();
}
}
說明:HomeController.cs、Program.cs和配置文件等項目完整源碼參考[4]。
將Acme.BookStore.HttpApi.Host項目啟動起來後如下:

通過Swagger介面測試//localhost:7016/api/app/book/get-book?id=1介面如下:

奇怪的是在線生成解決方案的時候,UI框架選擇了MVC,但是還是出現了這個項目,並且在啟動Acme.BookStore.Web項目的時候,如果不啟動Acme.BookStore.HttpApi.Host項目,還會報錯Volo.Abp.AbpException: Remote service 'AbpMvcClient' was not found and there is no default configuration,並且還沒有找到Acme.BookStore.Web項目在哪裡用到了Acme.BookStore.HttpApi.Host項目。因為自己主要關注前後端分離的項目,所以就不糾結這個細節了。
在線生成解決方案中還包括:Acme.BookStore.IdentityServer(認證授權項目),Acme.BookStore.HttpApi.Client(遠程服務代理層),Acme.BookStore.Web(前後端不分離的展示層),Acme.BookStore.TestBase(其它項目共享或使用的類),Acme.BookStore.Domain.Tests(測試領域層對象),Acme.BookStore.EntityFrameworkCore.Tests(測試自定義倉儲實現或EF Core映射),Acme.BookStore.Application.Tests(測試應用層對象),Acme.BookStore.HttpApi.Client.ConsoleTestApp(從.NET控制台中調用HTTP API)等。一篇文章放不下,後面繼續解說實踐。
參考文獻:
[1]聊一聊ABP vNext的模組化系統://www.sohu.com/a/436373048_468635
[2]Abp vNext源碼分析文章目錄://www.cnblogs.com/myzony/p/10722506.html
[3]手動從0搭建ABP框架-ABP官方完整解決方案源碼://url39.ctfile.com/f/2501739-625678611-787336?p=2096 (訪問密碼: 2096)
[4]手動從0搭建ABP框架-手動搭建簡化解決方案源碼://url39.ctfile.com/f/2501739-625678627-091eb9?p=2096 (訪問密碼: 2096)

