Host 層

目前模組基礎架構已完成。為了提升擴展性並確保未來能直接轉成微服務,我們需建立一個專屬的 API Host 專案。

這個 API Host 專案扮演的是模組使用者的角色,所以並不要在這裡寫商業邏輯,應該把它當作指揮中心負責組裝與調動需要的模組, 另外還需要提供啟動 Web 的工作環境。



WebApplication 啟動

由於模組中的 ConsoleDbMigrator 專案採用的是 .NET Generic Host 模式, 只適合用在 Console 或 Worker 類型專案上,但缺乏 Web 環境所需的 HTTP 管道、Middleware 及路由等基礎設施。 因此,API Host 專案要改用 WebApplication 模式啟動。

這部分跟標準的 .NET Web 使用方式一致,透過 WebApplication.CreateBuilder(args) 初始化 Builder 環境, 接下來調用 ABP 的擴充方法 AddApplicationAsync 掃描模組依賴關係並根據順序註冊所有依賴注入。

await builder.AddApplicationAsync<MyProjectHostModule>();

此步驟會啟動模組掃描機制,ABP 會根據 DependsOn 計算模組依賴。接著, 它會按正確的順序執行各個模組中的 ConfigureServices 方法,自動完成所有 DI 註冊。

當執行 builder.Build() 取得 WebApplication 實例後,調用:

await app.InitializeApplicationAsync();

這個是 ABP 的關鍵。傳統 ASP.NET Core 需要在 Program.cs 手動排定 app.Use... 的順序, 但在 ABP 會依據模組間的依賴關係,自動決定 Middleware 的掛載順序,並且將原本堆積在 Program.cs 的 Middleware 配置, 分散到各個模組的 OnApplicationInitialization 生命週期方法中,這樣使用者就不需要關心模組的啟動順序,實現即插即用。



Host 實做

接下來新增 BookStoreScratch.HttpApi.Host 專案

mkdir host
dotnet new web -o host/BookStoreScratch.HttpApi.Host
dotnet sln add host/BookStoreScratch.HttpApi.Host
dotnet add host/BookStoreScratch.HttpApi.Host package Volo.Abp.Autofac --version 9.0.2
dotnet add host/BookStoreScratch.HttpApi.Host package Volo.Abp.Swashbuckle --version 9.0.2
dotnet add host/BookStoreScratch.HttpApi.Host package Volo.Abp.EntityFrameworkCore.PostgreSql --version 9.0.2
dotnet add host/BookStoreScratch.HttpApi.Host package Serilog.AspNetCore --version 8.0.2
dotnet add host/BookStoreScratch.HttpApi.Host package Serilog.Sinks.Async --version 2.1.0
dotnet add host/BookStoreScratch.HttpApi.Host package Serilog.Sinks.Console --version 6.1.1
dotnet add host/BookStoreScratch.HttpApi.Host package Serilog.Extensions.Logging --version 9.0.2

dotnet add host/BookStoreScratch.HttpApi.Host/BookStoreScratch.HttpApi.Host.csproj reference src/BookStoreScratch.Application/BookStoreScratch.Application.csproj                                                        
dotnet add host/BookStoreScratch.HttpApi.Host/BookStoreScratch.HttpApi.Host.csproj reference src/BookStoreScratch.EntityFrameworkCore/BookStoreScratch.EntityFrameworkCore.csproj                                                        
dotnet add host/BookStoreScratch.HttpApi.Host/BookStoreScratch.HttpApi.Host.csproj reference src/BookStoreScratch.HttpApi/BookStoreScratch.HttpApi.csproj                                                        

建立 BookStoreScratchHttpApiHostModule 模組,這裡註冊 SwaggerPostgreSQL 與設定必要的 Middleware。

using BookStoreScratch.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Volo.Abp;
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.PostgreSql;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;

namespace BookStoreScratch.HttpApi.Host;

[DependsOn(
    typeof(BookStoreScratchApplicationModule),
    typeof(BookStoreScratchHttpApiModule),
    typeof(BookStoreScratchEntityFrameworkCoreModule),
    typeof(AbpEntityFrameworkCorePostgreSqlModule),
    typeof(AbpAutofacModule),
    typeof(AbpSwashbuckleModule)
)]
public class BookStoreScratchHttpApiHostModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpDbContextOptions>(options =>
        {
            AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
            options.UseNpgsql();
        });

        context.Services.AddAbpSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo { Title = "EventHubPublic API", Version = "v1" });
            options.DocInclusionPredicate((docName, description) => true);
            options.CustomSchemaIds(type => type.FullName);
        });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();

        app.UseRouting();

        app.UseSwagger();
        app.UseAbpSwaggerUI(options =>
        {
            options.RoutePrefix = "swagger";
            options.DefaultModelsExpandDepth(-1);
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "Public API");
        });

        app.UseConfiguredEndpoints();
    }
}

修改 Program.cs 內容,這裡只留下必要的程式。

using BookStoreScratch.HttpApi.Host;
using Serilog;
using Serilog.Events;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning)
#if DEBUG
    .MinimumLevel.Override("EventHub", LogEventLevel.Debug)
#else
    .MinimumLevel.Override("EventHub", LogEventLevel.Information)
#endif
    .Enrich.FromLogContext()
    .WriteTo.Async(c => c.Console())
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);

builder.Host
    .UseAutofac()
    .UseSerilog();

await builder.Services.AddApplicationAsync<BookStoreScratchHttpApiHostModule>();
var webApplication = builder.Build();
await webApplication.InitializeApplicationAsync();
await webApplication.RunAsync();
return 0;

設定 UnifiedDbContextUnifiedDbContextFactoryappsettings.json,可以直接從之前的 DbMigrator 專案複製。

using BookStoreScratch.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;

namespace BookStoreScratch.HttpApi.Host;

public class UnifiedDbContext : AbpDbContext<UnifiedDbContext>
{
    public UnifiedDbContext(DbContextOptions<UnifiedDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.ConfigureBookStoreScratch();
    }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace BookStoreScratch.HttpApi.Host;

public class UnifiedDbContextFactory : IDesignTimeDbContextFactory<UnifiedDbContext>
{
    public UnifiedDbContext CreateDbContext(string[] args)
    {
        // https://www.npgsql.org/efcore/release-notes/6.0.html#opting-out-of-the-new-timestamp-mapping-logic
        AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

        var configuration = BuildConfiguration();

        var builder = new DbContextOptionsBuilder<UnifiedDbContext>()
            .UseNpgsql(configuration.GetConnectionString("Default"));

        return new UnifiedDbContext(builder.Options);
    }

    private static IConfigurationRoot BuildConfiguration()
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false);

        return builder.Build();
    }
}
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "Default": "Host=localhost;Port=5432;Database=BookStoreScratch;User ID=postgres;Password=myPassw0rd;"
  }
}

設定完成後將專案運行起來瀏覽器會開啟 swagger 網址(/swagger/index.html)可以看到我們的 Book Curd 成功顯示

  • GET /api/book/
  • PUT /api/book/
  • DELETE /api/book/
  • GET /api/book
  • POST /api/book

先使用 POST /api/book 新增一筆書籍到資料庫內

{
  "name": "mybook",
  "bookType": 1,
  "publishDate": "2023-07-06",
  "price": 10
}

成功插入後會映射 BookDto 後回傳,我們透過實體 id 來進行查詢

{
  "name": "mybook",
  "type": 1,
  "publishDate": "2023-07-06T00:00:00",
  "price": 10,
  "id": "767085e2-a2c3-d3f5-c37b-3a0c42d2f44e"
}

使用 Get 方法可以正常回傳資料 https://localhost:7225/api/book/767085e2-a2c3-d3f5-c37b-3a0c42d2f44e

{
  "name": "mybook",
  "type": 1,
  "publishDate": "2023-07-06T00:00:00",
  "price": 10,
  "id": "767085e2-a2c3-d3f5-c37b-3a0c42d2f44e"
}

今天的進度 Github