手動從0搭建ABP框架-ABP官方完整解決方案和手動搭建簡化解決方案實踐

2022-07-27 06:02:40

  本文主要講解了如何把ABP官方的線上生成解決方案執行起來,並說明了解決方案中專案間的依賴關係。然後手動實踐瞭如何從0搭建了一個簡化的解決方案。ABP官方的線上生成解決方案原始碼下載參考[3],手動搭建的簡化的解決方案原始碼下載參考[4]。

一.ABP官方線上生成解決方案

1.將線上生成解決方案跑起來

首先進入頁面https://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.Shareddotnet 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()這個資料庫遷移主機服務,在DbMigratorHostedService類中包含StartAsync()和StopAsync()這2個方法,在StartAsync()中獲取BookStore資料庫遷移服務BookStoreDbMigrationService,並執行資料庫遷移方法MigrateAsync()。資料庫遷移的思路基本上就是在Acme.BookStore.DbMigrator目錄下執行命令:dotnet ef migrations add InitialCreate和dotnet ef database update,只不過使用的C#程式碼來實現的。自己通過Acme.BookStore.DbMigrator專案沒有遷移成功,最後還是通過命令列實現遷移的。遷移結果如下:

說明: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介面測試https://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的模組化系統:https://www.sohu.com/a/436373048_468635
[2]Abp vNext原始碼分析文章目錄:https://www.cnblogs.com/myzony/p/10722506.html
[3]手動從0搭建ABP框架-ABP官方完整解決方案原始碼:https://url39.ctfile.com/f/2501739-625678611-787336?p=2096 (存取密碼: 2096)
[4]手動從0搭建ABP框架-手動搭建簡化解決方案原始碼:https://url39.ctfile.com/f/2501739-625678627-091eb9?p=2096 (存取密碼: 2096)