AvaloniaStack/Avalonia-API/Extensions/UnifiedEndpointExtensions.cs
luoqian 271e9714ff 统一服务端点架构,支持多端接口与数据库切换
重构项目结构,引入 Avalonia-Common、Avalonia-EFCore、Avalonia-Services,实现 API 与桌面端统一端点注册、过滤器、鉴权和标准响应格式。支持多数据库自动迁移与配置,集成 Serilog 日志系统。移除旧路由与控制器,提升接口一致性与可维护性。
2026-05-11 14:35:34 +08:00

164 lines
5.9 KiB
C#

using Avalonia_Services.Core;
using AspNetCoreFilterContext = Microsoft.AspNetCore.Http.EndpointFilterInvocationContext;
using AspNetCoreFilterDelegate = Microsoft.AspNetCore.Http.EndpointFilterDelegate;
// 解决与 ASP.NET Core 同名类型的冲突
using UnifiedFilter = Avalonia_Services.Core.IEndpointFilter;
namespace Avalonia_API.Extensions
{
/// <summary>
/// 将 Avalonia-Services 的统一端点映射到 ASP.NET Core Minimal API。
/// 支持鉴权、过滤器、中间件的完整 ASP.NET Core 管道。
/// </summary>
public static class UnifiedEndpointExtensions
{
/// <summary>
/// 将 ServiceEndpointCollection 中的所有端点注册到 ASP.NET Core 路由。
/// </summary>
public static IEndpointRouteBuilder MapUnifiedEndpoints(
this IEndpointRouteBuilder routeBuilder,
ServiceEndpointCollection endpoints,
IServiceProvider serviceProvider)
{
var apiGroup = routeBuilder.MapGroup("/");
foreach (var endpoint in endpoints.Endpoints)
{
var routeHandlerBuilder = MapEndpoint(apiGroup, endpoint, serviceProvider);
// 全局过滤器 → ASP.NET Core Endpoint Filters
foreach (var globalFilter in endpoints.GlobalFilters)
{
routeHandlerBuilder.AddEndpointFilter(
async (context, next) => await ConvertFilterAsync(globalFilter, context, next));
}
// 端点专属过滤器
foreach (var filter in endpoint.Filters)
{
routeHandlerBuilder.AddEndpointFilter(
async (context, next) => await ConvertFilterAsync(filter, context, next));
}
// 鉴权(使用 ASP.NET Core 原生鉴权机制)
if (endpoint.RequireAuthorization)
{
if (!string.IsNullOrEmpty(endpoint.Policy))
{
routeHandlerBuilder.RequireAuthorization(endpoint.Policy);
}
else
{
routeHandlerBuilder.RequireAuthorization();
}
}
if (!string.IsNullOrEmpty(endpoint.Name))
{
routeHandlerBuilder.WithName(endpoint.Name);
}
}
return routeBuilder;
}
private static RouteHandlerBuilder MapEndpoint(
IEndpointRouteBuilder group,
ServiceEndpoint endpoint,
IServiceProvider serviceProvider)
{
var handler = CreateAspNetCoreHandler(endpoint.Handler, serviceProvider);
return endpoint.HttpMethod.ToUpperInvariant() switch
{
"GET" => group.MapGet(endpoint.Pattern, handler),
"POST" => group.MapPost(endpoint.Pattern, handler),
"PUT" => group.MapPut(endpoint.Pattern, handler),
"DELETE" => group.MapDelete(endpoint.Pattern, handler),
_ => group.MapGet(endpoint.Pattern, handler),
};
}
private static Delegate CreateAspNetCoreHandler(
Func<ServiceEndpointContext, Task<object?>> unifiedHandler,
IServiceProvider serviceProvider)
{
return async (HttpContext httpContext) =>
{
var ctx = await BuildContextFromHttpContext(httpContext);
ctx.Items["ServiceProvider"] = serviceProvider;
var result = await unifiedHandler(ctx);
// 同步响应状态
httpContext.Response.StatusCode = ctx.StatusCode;
foreach (var kvp in ctx.ResponseHeaders)
{
httpContext.Response.Headers[kvp.Key] = kvp.Value;
}
return result is not null ? Results.Json(result) : Results.Ok();
};
}
private static async Task<ServiceEndpointContext> BuildContextFromHttpContext(HttpContext httpContext)
{
var ctx = new ServiceEndpointContext
{
Path = httpContext.Request.Path.Value ?? "/",
Method = httpContext.Request.Method,
StatusCode = 200,
};
foreach (var header in httpContext.Request.Headers)
{
ctx.Headers[header.Key] = header.Value.ToString();
}
foreach (var query in httpContext.Request.Query)
{
ctx.Query[query.Key] = query.Value.ToString();
}
if (httpContext.Request.ContentLength > 0)
{
using var reader = new StreamReader(httpContext.Request.Body);
ctx.Body = await reader.ReadToEndAsync();
}
ctx.Items["HttpContext"] = httpContext;
return ctx;
}
private static async ValueTask<object?> ConvertFilterAsync(
UnifiedFilter unifiedFilter,
AspNetCoreFilterContext aspContext,
AspNetCoreFilterDelegate aspNext)
{
var httpContext = aspContext.HttpContext;
var ctx = httpContext.Items["UnifiedContext"] as ServiceEndpointContext
?? await BuildContextFromHttpContext(httpContext);
httpContext.Items["UnifiedContext"] = ctx;
await unifiedFilter.InvokeAsync(ctx, async (c) =>
{
httpContext.Response.StatusCode = c.StatusCode;
foreach (var kvp in c.ResponseHeaders)
{
httpContext.Response.Headers[kvp.Key] = kvp.Value;
}
await aspNext(aspContext);
});
if (ctx.ResponseBody is not null)
{
return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode);
}
return null!;
}
}
}