目錄
Http Client 專案
今天要來處理 Http Client 專案,在一般的單體應用中並不是很重要,但是在微服務架構中可以省下手動呼叫內部服務的麻煩。
假設沒有這個專案,當服務A 需要呼叫服務B 就只能手動寫 HttpClient 並組裝 HttpRequest,這個過程非常繁瑣,
因此能透過 Http Client 專案將這個流程交給 ABP 處理,我們在使用上就可以像呼叫本地方法一樣,但實際是透過 HTTP 將呼叫送到遠端伺服器。
Http Client 原理
Http Client 原理是透過攔截器與 Proxy 來將方法呼叫發送到遠端伺服器,這些功能都包含在 Volo.Abp.Http.Client 函式庫內部,
產生 Proxy 的方式又分成兩種,動態生成與靜態生成。
因此在依賴注入取得服務時並不是真實的服務實例而是 Proxy 實例,例如呼叫 await _bookAppService.GetListAsync(),
現在的 bookAppService 並不是注入我們自己寫的應用服務,而是注入一個 Proxy 服務內部包裝 Http 請求並發送到遠端伺服器,交給真正的應用服務處理。
動態生成 Proxy
在應用程式的 Runtime 時期,利用 ProxyGenerator 憑空產生一個類別。
使用這種生成方式的優點是只需要寫服務的 Interface 即可,完全不需要任何實作類別,但缺點是啟動時速度較慢,因為要去動態生成 Proxy,
與額外的攔截器,並且查找錯誤時不太方便,另外最麻煩的是可能會隨著版本變更產生不同的動態 Proxy。
靜態生成 Proxy
在程式開發階段就透過專用命令 abp generate-proxy 將 Proxy 類別的內容確定下來,所以可以直接看到 HttpClient 的發送程式碼。
這種方式不需要額外的攔截器與動態產生 Proxy 的時間,另外可以直接看到真實的 Proxy 程式碼,如果真的有必要可以進行微調, 但缺點就是每次更新 API 就要呼叫一次命令。
Http Client Dynamic實做
首先建立新專案 BookStoreScratch.HttpApi.Client
dotnet new classlib -o src/BookStoreScratch.HttpApi.Client
dotnet sln add src/BookStoreScratch.HttpApi.Client
mkdir src/BookStoreScratch.HttpApi.Client/BookStoreScratch
rm src/BookStoreScratch.HttpApi.Client/Class1.cs
安裝必要的函式庫
dotnet add src/BookStoreScratch.HttpApi.Client package Volo.Abp.Http.Client --version 9.0.2
根據上一段提到的原理,我們只需要服務介面即可,不需要真實實例,所以參考 Contract 專案即可
dotnet add src/BookStoreScratch.HttpApi.Client/BookStoreScratch.HttpApi.Client.csproj reference src/BookStoreScratch.Application.Contracts/BookStoreScratch.Application.Contracts.csproj
建立 BookStoreScratchHttpApiClientModule,這裡呼叫 AddHttpClientProxies 方法,背後會動態產生 Proxy。
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace BookStoreScratch.HttpApi.Client;
[DependsOn(
typeof(BookStoreScratchApplicationContractsModule),
typeof(AbpHttpClientModule))]
public class BookStoreScratchHttpApiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(BookStoreScratchApplicationContractsModule).Assembly,
"BookStoreScratch"
);
}
}
這樣 HttpApi.Client 就準備完畢,接下來到 BookStoreScratch.Console 專案測試看看,首先修改專案依賴只有 HttpApi.Client,
並且要安裝 Autofac。
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>dotnet-BookStoreScratch.Console-6bc61e3c-e573-4924-8460-b1ee23f476c6</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Volo.Abp.Core" Version="9.0.2" />
<PackageReference Include="Volo.Abp.Autofac" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BookStoreScratch.HttpApi.Client\BookStoreScratch.HttpApi.Client.csproj" />
</ItemGroup>
</Project>
建立 GetBookHostedService HostedService,等到應用程式初始完畢後呼叫服務,要想像這裡模擬的是呼叫者的應用程式,並不是我端的應用。
using System.Threading;
using System.Threading.Tasks;
using BookStoreScratch.Books;
using Microsoft.Extensions.Hosting;
namespace BookStoreScratch.Console;
public class GetBookHostedService : IHostedService
{
private readonly IBookAppService _bookService;
public GetBookHostedService(IBookAppService bookService)
{
_bookService = bookService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var books = await _bookService.GetListAsync(new BookGetListInput());
foreach (var book in books.Items)
{
System.Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
建立 BookStoreScratchConsoleModule,由於 AbpHttpClientModule 的依賴已經在我們的模組中定義,所以使用者只需要依賴我們的模組即可
using BookStoreScratch.HttpApi.Client;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
namespace BookStoreScratch.Console;
[DependsOn(
typeof(AbpAutofacModule),
typeof(BookStoreScratchHttpApiClientModule)
)]
public class BookStoreScratchConsoleModule : AbpModule
{
}
接下來修改 Program.cs,確保建立 Autofac 與註冊 GetBookHostedService
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using Volo.Abp;
namespace BookStoreScratch.Console;
public class Program
{
public static async Task<int> Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.Console())
.CreateLogger();
try
{
Log.Information("Starting console host.");
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders().AddSerilog();
builder.ConfigureContainer(builder.Services.AddAutofacServiceProviderFactory());
builder.Services.AddHostedService<GetBookHostedService>();
await builder.Services.AddApplicationAsync<BookStoreScratchConsoleModule>();
var host = builder.Build();
await host.InitializeAsync();
await host.RunAsync();
return 0;
}
catch (Exception ex)
{
if (ex is HostAbortedException)
{
throw;
}
Log.Fatal(ex, "Host terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
最後是配置 appsettings.json,這樣 Proxy 才知道要發送到哪個遠端伺服器,注意如果只有一個遠端服務那服務名稱可以用 Default 就好,
但如果需要多個遠端服務就需要與 AddHttpClientProxies 設定服務名稱相同 BookStoreScratch
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "http://localhost:5023/"
},
"BookStoreScratch": {
"BaseUrl": "http://localhost:5023/"
}
}
}
都完成後需要先啟動 HttpApi.Host 專案,才能啟動 Console 專案,否則會出現伺服器錯誤。
可以在終端機看到確實有發送請求到 localhost:5023/api/book 並顯示兩本書名稱。
[18:17:17 INF] Start processing HTTP request GET http://localhost:5023/api/book?*
[18:17:17 INF] Sending HTTP request GET http://localhost:5023/api/book?*
[18:17:17 INF] Received HTTP response headers after 17.7716ms - 200
[18:17:17 INF] End processing HTTP request after 19.0057ms - 200
[BOOK 3a1e70fc-43f2-8293-17fa-ae958540e69a] Name=mybook
[BOOK 3a1e6263-2830-e133-fe08-0c384395f7a2] Name=The Hitchhiker's Guide to the Galaxy
Http Client Static 實做
建立新專案 BookStoreScratch.HttpApi.Client.Static
dotnet new classlib -o src/BookStoreScratch.HttpApi.Client.Static
dotnet sln add src/BookStoreScratch.HttpApi.Client.Static
mkdir src/BookStoreScratch.HttpApi.Client.Static/BookStoreScratch/
rm src/BookStoreScratch.HttpApi.Client.Static/Class1.cs
首先執行 API Host 專案並產生靜態 Proxy,這裡使用 without-contracts 模式,這樣模型可以與 Contract 專案共用避免完全複製。
abp generate-proxy -t csharp -m book -u http://localhost:5023 --without-contracts
會產生三個檔案,關鍵是 [ExposeServices(typeof(IBookAppService), typeof(BookClientProxy))],將 IBookAppService 依賴注入改成 BookClientProxy,
以及生成的 generate-proxy.json 檔案,背後就是透過此 json 檔案才知道該怎麼打 API。
Create ClientProxies\BookStoreScratch\Controllers\BookClientProxy.cs
Create ClientProxies\BookStoreScratch\Controllers\BookClientProxy.Generated.cs
Create ClientProxies\book-generate-proxy.json
建立 BookStoreScratchHttpApiClientStaticModule,這裡呼叫 AddStaticHttpClientProxies 方法,背後會靜態產生 Proxy,
注意這裡需要 VirtualFile 模組確保 json 檔案註冊到模組中。
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace BookStoreScratch.HttpApi.Client.Static;
[DependsOn(
typeof(AbpHttpClientModule),
typeof(AbpVirtualFileSystemModule),
typeof(BookStoreScratchApplicationContractsModule)
)]
public class BookStoreScratchHttpApiClientStaticModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddStaticHttpClientProxies(
typeof(BookStoreScratchApplicationContractsModule).Assembly
);
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<BookStoreScratchHttpApiClientStaticModule>();
});
}
}
最後到 Console 專案修改專案依賴
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>dotnet-BookStoreScratch.Console-6bc61e3c-e573-4924-8460-b1ee23f476c6</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Volo.Abp.Core" Version="9.0.2" />
<PackageReference Include="Volo.Abp.Autofac" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BookStoreScratch.HttpApi.Client.Static\BookStoreScratch.HttpApi.Client.Static.csproj" />
</ItemGroup>
</Project>
並修改 BookStoreScratchConsoleModule 依賴新的靜態專案
using BookStoreScratch.HttpApi.Client.Static;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
namespace BookStoreScratch.Console;
[DependsOn(
typeof(AbpAutofacModule),
typeof(BookStoreScratchHttpApiClientStaticModule)
)]
public class BookStoreScratchConsoleModule : AbpModule
{
}
完成後同樣先運行 Http API Host 在運行 Console 同樣能透過靜態模式取得資料
[19:54:38 INF] Start processing HTTP request GET http://localhost:5023/api/book?*
[19:54:38 INF] Sending HTTP request GET http://localhost:5023/api/book?*
[19:54:38 INF] Received HTTP response headers after 98.4867ms - 200
[19:54:38 INF] End processing HTTP request after 118.3049ms - 200
[BOOK 3a1e70fc-43f2-8293-17fa-ae958540e69a] Name=mybook
[BOOK 3a1e6263-2830-e133-fe08-0c384395f7a2] Name=The Hitchhiker's Guide to the Galaxy
今天的進度 Github