AvaloniaStack/Avalonia-Services/Extensions/DesktopEndpointAdapter.cs
luoqian 5cdc7052e0 feat: 完善统一端点响应与请求绑定框架
- 新增 IApiResponse 统一响应契约,覆盖普通响应和分页响应
- 扩展端点映射,支持 IApiResponse 和带请求 DTO 的 MapXxx 重载
- 增加 Body、Query、Route Values 到请求 DTO 的自动绑定
- 增加 PC 端路由模式匹配,支持 {id} 和 {id:int}
- API 与 PC 端点上下文补充路由参数传递
- 调整 OpenAPI 请求类型处理,避免 GET/DELETE 被标记为 JSON Body
- 鉴权端点迁移到强类型请求绑定和 IApiResponse 返回
- 增加统一端点文件流响应支持
2026-05-22 11:42:38 +08:00

244 lines
9.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Avalonia_Services.Core;
namespace Avalonia_Services.Extensions
{
/// <summary>
/// Desktop (Avalonia-PC) 端点适配器。
/// 将统一端点转换为桌面端可用的路由处理器,支持过滤器和鉴权管道。
/// </summary>
public class DesktopEndpointAdapter
{
/// <summary>
/// 统一端点集合。
/// </summary>
private readonly ServiceEndpointCollection _endpoints;
/// <summary>
/// 鉴权服务。
/// </summary>
private readonly IAuthService _authService;
/// <summary>
/// DI 服务提供程序。
/// </summary>
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 匹配后的路由结果(与原有 RouteDispatchResult 兼容)。
/// </summary>
public class RouteResult
{
/// <summary>
/// 获取是否匹配到路由。
/// </summary>
public bool IsMatched { get; init; }
/// <summary>
/// 获取 HTTP 状态码。
/// </summary>
public int StatusCode { get; init; } = 200;
/// <summary>
/// 获取状态描述文本。
/// </summary>
public string StatusMessage { get; init; } = "";
/// <summary>
/// 获取响应数据。
/// </summary>
public object? Data { get; init; }
/// <summary>
/// 获取响应头字典。
/// </summary>
public Dictionary<string, string> ResponseHeaders { get; init; } = new();
/// <summary>
/// 创建成功响应结果。
/// </summary>
/// <param name="data">响应数据。</param>
/// <param name="ctx">端点上下文。</param>
/// <returns>路由结果。</returns>
public static RouteResult Success(object? data, ServiceEndpointContext ctx)
{
return new RouteResult
{
IsMatched = true,
StatusCode = ctx.StatusCode,
StatusMessage = ctx.StatusMessage,
Data = data,
ResponseHeaders = new Dictionary<string, string>(ctx.ResponseHeaders, StringComparer.OrdinalIgnoreCase),
};
}
/// <summary>
/// 创建 404 未找到响应。
/// </summary>
/// <returns>表示未匹配的路由结果。</returns>
public static RouteResult NotFound() => new()
{
IsMatched = false,
StatusCode = 404,
StatusMessage = "Not Found",
};
}
/// <summary>
/// 初始化桌面端点适配器。
/// </summary>
/// <param name="endpoints">端点集合。</param>
/// <param name="authService">鉴权服务。</param>
/// <param name="serviceProvider">DI 服务提供程序。</param>
public DesktopEndpointAdapter(
ServiceEndpointCollection endpoints,
IAuthService authService,
IServiceProvider serviceProvider)
{
_endpoints = endpoints;
_authService = authService;
_serviceProvider = serviceProvider;
}
/// <summary>
/// 处理来自前端WebView2 Bridge的请求。
/// </summary>
/// <param name="path">规范化路径,如 "api/wData"</param>
/// <param name="method">HTTP 方法</param>
/// <param name="body">请求体字符串</param>
/// <param name="headers">请求头字典</param>
/// <param name="query">查询参数字典</param>
public async Task<RouteResult> HandleRequestAsync(
string path,
string method,
string? body,
Dictionary<string, string>? headers = null,
Dictionary<string, string>? query = null)
{
// 查找匹配的端点(忽略大小写 + 方法匹配)
var match = _endpoints.Endpoints
.Where(e =>
e.SupportsHost(EndpointHostTarget.Pc) &&
string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase))
.Select(e => new
{
Endpoint = e,
IsMatched = ServiceEndpointPatternMatcher.TryMatch(e.Pattern, path, out var routeValues),
RouteValues = routeValues,
})
.FirstOrDefault(candidate => candidate.IsMatched);
var endpoint = match?.Endpoint;
if (endpoint is null)
{
return RouteResult.NotFound();
}
// 构建上下文
var ctx = new ServiceEndpointContext
{
Path = path,
Method = method,
Body = body,
Headers = headers ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
Query = query ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
RouteValues = match!.RouteValues,
Items = { ["ServiceProvider"] = _serviceProvider },
};
try
{
// 1. 鉴权检查
if (endpoint.RequireAuthorization)
{
var user = await _authService.AuthenticateAsync(ctx);
if (user is null)
{
ctx.StatusCode = 401;
ctx.StatusMessage = "Unauthorized";
ctx.ResponseBody = new { success = false, error = "Unauthorized" };
return RouteResult.Success(ctx.ResponseBody, ctx);
}
if (endpoint.Roles.Count > 0)
{
var authorized = await _authService.AuthorizeAsync(user, $"roles:{string.Join(',', endpoint.Roles)}");
if (!authorized)
{
ctx.StatusCode = 403;
ctx.StatusMessage = "Forbidden";
ctx.ResponseBody = new { success = false, error = "Forbidden" };
return RouteResult.Success(ctx.ResponseBody, ctx);
}
}
else if (!string.IsNullOrEmpty(endpoint.Policy))
{
var authorized = await _authService.AuthorizeAsync(user, endpoint.Policy);
if (!authorized)
{
ctx.StatusCode = 403;
ctx.StatusMessage = "Forbidden";
ctx.ResponseBody = new { success = false, error = "Forbidden" };
return RouteResult.Success(ctx.ResponseBody, ctx);
}
}
ctx.Items["User"] = user;
}
// 2. 构建过滤管道:全局过滤器 → 端点过滤器 → 处理器
var pipeline = BuildPipeline(endpoint);
// 3. 执行管道
await pipeline(ctx);
return RouteResult.Success(ctx.ResponseBody, ctx);
}
catch (Exception ex)
{
ctx.StatusCode = 500;
ctx.StatusMessage = "Internal Server Error";
ctx.ResponseBody = new { success = false, error = ex.Message };
return RouteResult.Success(ctx.ResponseBody, ctx);
}
}
/// <summary>
/// 构建过滤管道(全局过滤器 + 端点过滤器 → 端点处理器)。
/// </summary>
private EndpointFilterDelegate BuildPipeline(ServiceEndpoint endpoint)
{
// 最内层:端点处理器
EndpointFilterDelegate handler = async (ctx) =>
{
ctx.ResponseBody = await endpoint.Handler(ctx);
};
// 先包裹端点专属过滤器(后注册的先执行)
var filters = new List<IEndpointFilter>();
filters.AddRange(_endpoints.GlobalFilters);
filters.AddRange(endpoint.Filters);
for (int i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
var next = handler;
handler = (ctx) => filter.InvokeAsync(ctx, next);
}
return handler;
}
}
/// <summary>
/// Desktop 端的辅助扩展。不依赖 IServiceCollection由宿主项目自行完成 DI 注册)。
/// </summary>
public static class DesktopServiceExtensions
{
/// <summary>
/// 快速构建 DesktopEndpointAdapter用于非 DI 场景如 MainWindow
/// </summary>
public static DesktopEndpointAdapter CreateAdapter(
this ServiceEndpointCollection endpoints,
IServiceProvider serviceProvider)
{
var auth = (serviceProvider.GetService(typeof(IAuthService)) as IAuthService) ?? new AnonymousAuthService();
return new DesktopEndpointAdapter(endpoints, auth, serviceProvider);
}
}
}