2026-05-21 15:52:36 +08:00
|
|
|
|
using Avalonia_Services.Core;
|
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
|
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.ForHost(EndpointHostTarget.Api))
|
|
|
|
|
|
{
|
|
|
|
|
|
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 (endpoint.Roles.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.RequireAuthorization(new AuthorizeAttribute
|
|
|
|
|
|
{
|
|
|
|
|
|
Roles = string.Join(',', endpoint.Roles),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (!string.IsNullOrEmpty(endpoint.Policy))
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.RequireAuthorization(endpoint.Policy);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.RequireAuthorization();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(endpoint.Name))
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.WithName(endpoint.Name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(endpoint.OpenApiTag))
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.WithTags(endpoint.OpenApiTag);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(endpoint.OpenApiDescription))
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.WithDescription(endpoint.OpenApiDescription);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(endpoint.OpenApiSummary))
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.WithSummary(endpoint.OpenApiSummary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (endpoint.OpenApiRequestType is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.Accepts(endpoint.OpenApiRequestType, "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (endpoint.OpenApiResponseType is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
routeHandlerBuilder.Produces(200, endpoint.OpenApiResponseType, "application/json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return routeBuilder;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据端点的 HTTP 方法(GET/POST/PUT/DELETE)将其映射到 ASP.NET Core 路由。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="group">路由组。</param>
|
|
|
|
|
|
/// <param name="endpoint">统一端点定义。</param>
|
|
|
|
|
|
/// <param name="serviceProvider">服务提供程序。</param>
|
|
|
|
|
|
/// <returns>路由处理器构建器,用于叠加过滤器等配置。</returns>
|
|
|
|
|
|
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),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建适配 ASP.NET Core 的委托处理器,将统一处理器包装为 ASP.NET Core 可识别的委托。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unifiedHandler">统一端点处理器。</param>
|
|
|
|
|
|
/// <param name="serviceProvider">服务提供程序。</param>
|
|
|
|
|
|
/// <returns>ASP.NET Core 兼容的委托。</returns>
|
|
|
|
|
|
private static Delegate CreateAspNetCoreHandler(
|
|
|
|
|
|
Func<ServiceEndpointContext, Task<object?>> unifiedHandler,
|
|
|
|
|
|
IServiceProvider serviceProvider)
|
|
|
|
|
|
{
|
|
|
|
|
|
return async (HttpContext httpContext) =>
|
|
|
|
|
|
{
|
2026-05-21 16:45:56 +08:00
|
|
|
|
var ctx = httpContext.Items["UnifiedContext"] as ServiceEndpointContext
|
|
|
|
|
|
?? await BuildContextFromHttpContext(httpContext);
|
2026-05-21 15:52:36 +08:00
|
|
|
|
ctx.Items["ServiceProvider"] = serviceProvider;
|
|
|
|
|
|
ctx.Items["User"] = httpContext.User;
|
2026-05-21 16:45:56 +08:00
|
|
|
|
httpContext.Items["UnifiedContext"] = ctx;
|
2026-05-21 15:52:36 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从 ASP.NET Core 的 HttpContext 构建统一的 ServiceEndpointContext,
|
|
|
|
|
|
/// 提取路径、方法、请求头、查询参数和请求体。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="httpContext">ASP.NET Core 的 HttpContext。</param>
|
|
|
|
|
|
/// <returns>构建好的统一端点上下文。</returns>
|
|
|
|
|
|
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;
|
|
|
|
|
|
ctx.Items["User"] = httpContext.User;
|
|
|
|
|
|
|
|
|
|
|
|
return ctx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将统一过滤器转换为 ASP.NET Core 端点过滤器,
|
|
|
|
|
|
/// 在调用统一过滤器前后桥接上下文和状态。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unifiedFilter">统一过滤器。</param>
|
|
|
|
|
|
/// <param name="aspContext">ASP.NET Core 过滤器调用上下文。</param>
|
|
|
|
|
|
/// <param name="aspNext">ASP.NET Core 过滤器管道中的下一个委托。</param>
|
|
|
|
|
|
/// <returns>过滤器执行结果,可能包含短路响应体。</returns>
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-21 16:45:56 +08:00
|
|
|
|
object? nextResult = null;
|
2026-05-21 15:52:36 +08:00
|
|
|
|
await unifiedFilter.InvokeAsync(ctx, async (c) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
httpContext.Response.StatusCode = c.StatusCode;
|
|
|
|
|
|
foreach (var kvp in c.ResponseHeaders)
|
|
|
|
|
|
{
|
|
|
|
|
|
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
|
|
|
|
|
}
|
2026-05-21 16:45:56 +08:00
|
|
|
|
nextResult = await aspNext(aspContext);
|
2026-05-21 15:52:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (ctx.ResponseBody is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:45:56 +08:00
|
|
|
|
return nextResult;
|
2026-05-21 15:52:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|