隨著Aspire發佈preview5的發佈,Microsoft.Extensions.ServiceDiscovery隨之更新, 服務註冊發現這個屬於老掉牙的話題解決什麼問題就不贅述了,這裡主要講講Microsoft.Extensions.ServiceDiscovery(preview5)以及如何 ...
隨著Aspire發佈preview5的發佈,Microsoft.Extensions.ServiceDiscovery隨之更新,
服務註冊發現這個屬於老掉牙的話題解決什麼問題就不贅述了,這裡主要講講Microsoft.Extensions.ServiceDiscovery(preview5)以及如何擴展其他的中間件的發現集成 .
Microsoft.Extensions.ServiceDiscovery官方預設提供的Config,DNS,YARP三種Provider,使用也比較簡單 :
builder.Services.AddServiceDiscovery();
builder.Services.AddHttpClient<CatalogServiceClient>(static client =>
{
client.BaseAddress = new("http://todo");
});
builder.Services.ConfigureHttpClientDefaults(static http =>
{
// 全局對HttpClient啟用服務發現
http.UseServiceDiscovery();
});
然後 appsettings.json 為名為 todo 的服務配置終結點:
"Services": {
"todo": {
"http": [
"http://localhost:5124"
]
}
}
然後使用服務發現:
#region 模擬服務端的todo介面:
var sampleTodos = new Todo[] {
new(1, "Walk the dog"),
new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
new(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
new(4, "Clean the bathroom"),
new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
};
var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
#endregion
public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
#region 測試服務發現和負載
app.MapGet("/test", async (IHttpClientFactory clientFactory) =>
{
//這裡服務發現將自動解析配置文件中的服務
var client = clientFactory.CreateClient("todo");
var response = await client.GetAsync("/todos");
var todos = await response.Content.ReadAsStringAsync();
return Results.Content(todos, contentType: "application/json");
});
#endregion
運行程式後將會發現成功執行:
當然對於這樣寫死配置的服務發現一點都不靈活,因此應運而生了 YARP和DNS這些Provider, 目前服務註冊發現使用Consul的還是挺多的,當然還有很多其他的輪子就不贅述了,這裡我們來擴展一個Consul的服務發現Provider :
實現核心介面IServiceEndPointProvider
internal class ConsulServiceEndPointProvider(ServiceEndPointQuery query, IConsulClient consulClient, ILogger logger)
: IServiceEndPointProvider, IHostNameFeature
{
const string Name = "Consul";
private readonly string _serviceName = query.ServiceName;
private readonly IConsulClient _consulClient = consulClient;
private readonly ILogger _logger = logger;
public string HostName => query.ServiceName;
#pragma warning disable CA1816 // Dispose 方法應調用 SuppressFinalize
public ValueTask DisposeAsync() => default;
public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken)
{
var flag = ServiceNameParts.TryParse(_serviceName, out var serviceNameParts);
var sum = 0;
if (flag)
{
var queryResult = await _consulClient.Health.Service(serviceNameParts.Host, string.Empty, true, cancellationToken);
foreach (var serviceEntry in queryResult.Response)
{
var address = $"{serviceEntry.Service.Address}:{serviceEntry.Service.Port}";
var isEndpoint = ServiceNameParts.TryCreateEndPoint(address, out var endPoint);
if (isEndpoint)
{
++sum;
var serviceEndPoint = ServiceEndPoint.Create(endPoint!);
serviceEndPoint.Features.Set<IServiceEndPointProvider>(this);
serviceEndPoint.Features.Set<IHostNameFeature>(this);
endPoints.EndPoints.Add(serviceEndPoint);
_logger.LogInformation($"ConsulServiceEndPointProvider Found Service {_serviceName}:{address}");
}
}
}
if (sum == 0)
{
_logger.LogWarning($"No ConsulServiceEndPointProvider were found for service '{_serviceName}' ('{HostName}').");
}
}
/// <inheritdoc/>
public override string ToString() => Name;
}
實現 IServiceEndPointProviderFactory:
internal class ConsulServiceEndPointProviderFactory(IConsulClient consulClient, ILogger<ConsulServiceEndPointProviderFactory> logger) : IServiceEndPointProviderFactory
{
private readonly IConsulClient _consulClient = consulClient;
private readonly ILogger<ConsulServiceEndPointProviderFactory> _logger = logger;
public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
resolver = new ConsulServiceEndPointProvider(query, _consulClient, _logger);
return true;
}
}
接著擴展一下IServiceCollection
public static IServiceCollection AddConsulServiceEndpointProvider(this IServiceCollection services)
{
services.AddServiceDiscoveryCore();
services.AddSingleton<IServiceEndPointProviderFactory, ConsulServiceEndPointProviderFactory>();
return services;
}
最後添加一行代碼 :
// 使用Microsoft.Extensions.ServiceDiscovery實現負載均衡
builder.Services.AddServiceDiscovery()
.AddConfigurationServiceEndPointResolver() //config
.AddConsulServiceEndpointProvider(); //consul
下麵是Consul中註冊完成的服務:
然後我們請求 ./test 調用服務,觀察調試日誌,成功了!
完整的代碼:
https://github.com/vipwan/Biwen.Microsoft.Extensions.ServiceDiscovery.Consul
當然你也可以直接使用nuget引用 Biwen.Microsoft.Extensions.ServiceDiscovery.Consul 我已經發佈到了nuget上 , 最後因為Aspire還在不停的迭代所以Biwen.Microsoft.Extensions.ServiceDiscovery.Consul後面還會存在一些變化, 前面的幾個早期版本我都做了適配以最新的為準