统一服务端点架构,支持多端接口与数据库切换

重构项目结构,引入 Avalonia-Common、Avalonia-EFCore、Avalonia-Services,实现 API 与桌面端统一端点注册、过滤器、鉴权和标准响应格式。支持多数据库自动迁移与配置,集成 Serilog 日志系统。移除旧路由与控制器,提升接口一致性与可维护性。
This commit is contained in:
2026-05-11 14:35:34 +08:00
parent 99631df085
commit 271e9714ff
38 changed files with 2073 additions and 182 deletions
@@ -7,4 +7,18 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\Avalonia-EFCore\Avalonia-EFCore.csproj" />
</ItemGroup>
</Project>
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Linq;
namespace Avalonia_Services.Core
{
/// <summary>
/// 端点列表打印工具 —— 在应用启动时输出所有已注册的拦截接口。
/// 类似 Swagger 的接口清单效果。
/// </summary>
public static class EndpointPrinter
{
/// <summary>
/// 打印所有已注册端点到控制台。
/// </summary>
public static void PrintEndpoints(ServiceEndpointCollection collection, string? title = null)
{
title ??= "API Endpoints";
var maxMethodLen = collection.Endpoints.Count > 0
? collection.Endpoints.Max(e => e.HttpMethod.Length)
: 4;
var maxPathLen = collection.Endpoints.Count > 0
? collection.Endpoints.Max(e => e.Pattern.Length)
: 8;
var totalWidth = maxMethodLen + maxPathLen + 5;
var separator = new string('─', Math.Max(totalWidth, 50));
Console.WriteLine();
Console.WriteLine($"╔═ {title} ═{new string('═', Math.Max(0, totalWidth - title.Length - 3))}╗");
Console.WriteLine($"║ {"Method".PadRight(maxMethodLen)} │ {"Path".PadRight(maxPathLen)} │ Auth ║");
Console.WriteLine($"╟{separator}╢");
foreach (var ep in collection.Endpoints.OrderBy(e => e.Pattern))
{
var auth = ep.RequireAuthorization ? (ep.Policy ?? "✓") : "—";
var methodColor = ep.HttpMethod switch
{
"GET" => ConsoleColor.Green,
"POST" => ConsoleColor.Blue,
"PUT" => ConsoleColor.Yellow,
"DELETE" => ConsoleColor.Red,
_ => ConsoleColor.Gray,
};
var savedColor = Console.ForegroundColor;
Console.Write("║ ");
Console.ForegroundColor = methodColor;
Console.Write(ep.HttpMethod.PadRight(maxMethodLen));
Console.ForegroundColor = savedColor;
Console.Write(" │ ");
Console.Write(ep.Pattern.PadRight(maxPathLen));
Console.Write(" │ ");
Console.Write(auth.PadRight(4));
Console.WriteLine(" ║");
}
Console.WriteLine($"╚{separator}╝");
Console.WriteLine($" Total: {collection.Endpoints.Count} endpoint(s)");
Console.WriteLine();
}
}
}
@@ -0,0 +1,93 @@
using Avalonia_Common.Core;
using System;
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 全局异常拦截过滤器 —— 自动包裹所有端点处理器,无需在每个方法中写 try-catch。
/// 所有未捕获异常会被转为统一的 ApiResponse 错误格式。
/// </summary>
public sealed class GlobalExceptionFilter : IEndpointFilter
{
private readonly bool _includeDetails;
/// <summary>
/// </summary>
/// <param name="includeDetails">是否在响应中包含异常详情(开发环境建议 true,生产环境 false</param>
public GlobalExceptionFilter(bool includeDetails = false)
{
_includeDetails = includeDetails;
}
public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
{
try
{
await next(context);
}
catch (OperationCanceledException)
{
// 取消操作不视为错误
context.StatusCode = 499;
context.StatusMessage = "Client Closed Request";
context.ResponseBody = ApiResponse<object>.Fail(499, "请求已取消");
}
catch (UnauthorizedAccessException ex)
{
context.StatusCode = 401;
context.StatusMessage = "Unauthorized";
context.ResponseBody = ApiResponse<object>.Unauthorized(
_includeDetails ? ex.Message : "未授权访问");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase))
{
context.StatusCode = 404;
context.StatusMessage = "Not Found";
context.ResponseBody = ApiResponse<object>.NotFound(
_includeDetails ? ex.Message : "资源不存在");
}
catch (ArgumentException ex)
{
context.StatusCode = 400;
context.StatusMessage = "Bad Request";
context.ResponseBody = ApiResponse<object>.BadRequest(
_includeDetails ? ex.Message : "参数错误");
}
catch (Exception ex)
{
// 记录完整日志(无论是否返回详情)
LogException(context, ex);
context.StatusCode = 500;
context.StatusMessage = "Internal Server Error";
context.ResponseBody = ApiResponse<object>.ServerError(
_includeDetails ? ex.Message : "服务器内部错误,请联系管理员");
// 可选:在开发环境附加堆栈信息
if (_includeDetails)
{
// 通过 Items 传递额外调试信息
context.Items["ExceptionDetail"] = ex.ToString();
}
}
}
private static void LogException(ServiceEndpointContext context, Exception ex)
{
try
{
// 使用 Serilog(如果已配置)
Serilog.Log.Error(ex,
"全局异常拦截 | {Method} {Path} | {ExceptionType}: {Message}",
context.Method, context.Path, ex.GetType().Name, ex.Message);
}
catch
{
// Serilog 不可用时回退到 Console
Console.Error.WriteLine(
$"[ERROR] {context.Method} {context.Path} | {ex.GetType().Name}: {ex.Message}");
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.Security.Claims;
namespace Avalonia_Services.Core
{
/// <summary>
/// 鉴权服务抽象 —— 各宿主按自己的方式实现(JWT / Cookie / Token 等)。
/// </summary>
public interface IAuthService
{
/// <summary>
/// 验证请求并返回用户主体;返回 null 表示未授权。
/// </summary>
Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context);
/// <summary>
/// 检查当前用户是否有指定权限。
/// </summary>
Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy);
}
/// <summary>
/// 无需鉴权的默认实现(开发/公开 API 场景)。
/// </summary>
public sealed class AnonymousAuthService : IAuthService
{
public Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
{
// 匿名用户,始终通过
var identity = new ClaimsIdentity("anonymous");
return Task.FromResult<ClaimsPrincipal?>(new ClaimsPrincipal(identity));
}
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
{
return Task.FromResult(true);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 端点过滤器抽象 —— 在请求处理前后执行逻辑。
/// 类似于 ASP.NET Core 的 IEndpointFilter,但可在任何宿主中使用。
/// </summary>
public interface IEndpointFilter
{
/// <summary>
/// 过滤器执行方法。
/// 调用 next(ctx) 继续管道;不调用则短路。
/// </summary>
Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next);
}
/// <summary>
/// 过滤器管道中的下一个委托。
/// </summary>
public delegate Task EndpointFilterDelegate(ServiceEndpointContext context);
/// <summary>
/// 用于包装匿名过滤器的简单实现。
/// </summary>
internal sealed class AnonymousEndpointFilter : IEndpointFilter
{
private readonly Func<ServiceEndpointContext, EndpointFilterDelegate, Task> _filter;
public AnonymousEndpointFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
{
_filter = filter;
}
public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
{
return _filter(context, next);
}
}
}
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 单个端点定义。
/// </summary>
public class ServiceEndpoint
{
/// <summary>路由路径,如 "api/wData"</summary>
public string Pattern { get; init; } = string.Empty;
/// <summary>HTTP 方法(GET/POST/PUT/DELETE</summary>
public string HttpMethod { get; init; } = "GET";
/// <summary>端点名称(用于 OpenAPI / 日志)</summary>
public string? Name { get; set; }
/// <summary>端点处理器</summary>
public Func<ServiceEndpointContext, Task<object?>> Handler { get; init; } = _ => Task.FromResult<object?>(null);
/// <summary>该端点专属的过滤器(按顺序执行)</summary>
public List<IEndpointFilter> Filters { get; init; } = new();
/// <summary>是否需要鉴权</summary>
public bool RequireAuthorization { get; set; }
/// <summary>鉴权策略名</summary>
public string? Policy { get; set; }
/// <summary>
/// 设置端点名称(Fluent API)。
/// </summary>
public ServiceEndpoint WithName(string name)
{
Name = name;
return this;
}
}
/// <summary>
/// 端点集合 —— 所有端点的注册中心。在 Avalonia-Services 中统一配置。
/// </summary>
public class ServiceEndpointCollection
{
/// <summary>所有已注册的端点</summary>
public List<ServiceEndpoint> Endpoints { get; } = new();
/// <summary>作用于所有端点的全局过滤器</summary>
public List<IEndpointFilter> GlobalFilters { get; } = new();
/// <summary>
/// 注册一个端点。
/// </summary>
public ServiceEndpoint MapGet(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "GET", handler);
}
/// <summary>
/// 注册一个 POST 端点。
/// </summary>
public ServiceEndpoint MapPost(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "POST", handler);
}
/// <summary>
/// 注册一个 PUT 端点。
/// </summary>
public ServiceEndpoint MapPut(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "PUT", handler);
}
/// <summary>
/// 注册一个 DELETE 端点。
/// </summary>
public ServiceEndpoint MapDelete(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "DELETE", handler);
}
/// <summary>
/// 添加全局过滤器(作用于所有端点)。
/// </summary>
public ServiceEndpointCollection AddGlobalFilter(IEndpointFilter filter)
{
GlobalFilters.Add(filter);
return this;
}
/// <summary>
/// 通过匿名函数添加全局过滤器。
/// </summary>
public ServiceEndpointCollection AddGlobalFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
{
GlobalFilters.Add(new AnonymousEndpointFilter(filter));
return this;
}
private ServiceEndpoint AddEndpoint(string pattern, string method, Func<ServiceEndpointContext, Task<object?>> handler)
{
var endpoint = new ServiceEndpoint
{
Pattern = pattern,
HttpMethod = method,
Handler = handler,
};
Endpoints.Add(endpoint);
return endpoint;
}
}
/// <summary>
/// 构建器 —— 提供 Fluent API 来配置所有端点。
/// </summary>
public class ServiceEndpointBuilder
{
/// <summary>
/// 端点集合
/// </summary>
public ServiceEndpointCollection Endpoints { get; } = new();
/// <summary>
/// 鉴权服务(默认匿名)
/// </summary>
public IAuthService AuthService { get; set; } = new AnonymousAuthService();
/// <summary>
/// 配置端点(在此方法中调用 endpoints.MapGet 等)。
/// </summary>
public ServiceEndpointBuilder ConfigureEndpoints(Action<ServiceEndpointCollection> configure)
{
configure(Endpoints);
return this;
}
/// <summary>
/// 设置鉴权服务。
/// </summary>
public ServiceEndpointBuilder UseAuthService(IAuthService authService)
{
AuthService = authService;
return this;
}
/// <summary>
/// 构建最终的端点集合。
/// </summary>
public ServiceEndpointCollection Build()
{
return Endpoints;
}
}
}
@@ -0,0 +1,79 @@
using System.Collections.Generic;
namespace Avalonia_Services.Core
{
/// <summary>
/// 抽象的请求上下文,屏蔽不同宿主(ASP.NET Core / Desktop WebView)的差异。
/// </summary>
public class ServiceEndpointContext
{
/// <summary>
/// 请求路径,例如 "api/wData"
/// </summary>
public string Path { get; init; } = string.Empty;
/// <summary>
/// HTTP 方法(GET, POST, PUT, DELETE 等)
/// </summary>
public string Method { get; init; } = "GET";
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string, string> Headers { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 请求体(原始字符串)
/// </summary>
public string? Body { get; set; }
/// <summary>
/// 查询参数
/// </summary>
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 响应状态码
/// </summary>
public int StatusCode { get; set; } = 200;
/// <summary>
/// 响应状态描述
/// </summary>
public string StatusMessage { get; set; } = "OK";
/// <summary>
/// 响应头
/// </summary>
public Dictionary<string, string> ResponseHeaders { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
["Content-Type"] = "application/json; charset=utf-8"
};
/// <summary>
/// 响应体
/// </summary>
public object? ResponseBody { get; set; }
/// <summary>
/// 存储在请求生命周期中的任意数据(由中间件/过滤器使用)
/// </summary>
public Dictionary<string, object?> Items { get; init; } = new();
/// <summary>
/// 获取请求头值
/// </summary>
public string? GetHeader(string key)
{
return Headers.TryGetValue(key, out var value) ? value : null;
}
/// <summary>
/// 设置响应头
/// </summary>
public void SetResponseHeader(string key, string value)
{
ResponseHeaders[key] = value;
}
}
}
@@ -0,0 +1,37 @@
using Avalonia_EFCore.Database;
using Avalonia_Services.Models;
using Microsoft.EntityFrameworkCore;
namespace Avalonia_Services.Database
{
/// <summary>
/// 应用数据库上下文 —— 继承自 Avalonia-EFCore 的 AppDbContext。
/// 所有业务实体在此注册 DbSet。
/// 这是 Avalonia-API 和 Avalonia-PC 共用的具体数据上下文。
/// </summary>
public class AppDataContext(DatabaseConfiguration dbConfig) : AppDbContext(dbConfig)
{
/// <summary>天气预报数据</summary>
public DbSet<WeatherForecastEntity> WeatherForecasts => Set<WeatherForecastEntity>();
/// <summary>用户数据</summary>
public DbSet<UserEntity> Users => Set<UserEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<WeatherForecastEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Summary).HasMaxLength(200);
});
modelBuilder.Entity<UserEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Email).HasMaxLength(200);
});
}
}
}
+140
View File
@@ -0,0 +1,140 @@
using Avalonia_Common.Core;
using Avalonia_Services.Core;
using Avalonia_Services.Database;
using Avalonia_Services.Models;
using Avalonia_Services.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Avalonia_Services.Endpoints
{
/// <summary>
/// 统一端点配置 —— 所有业务端点在此定义一次。
/// 这是 Avalonia-API 和 Avalonia-PC 的唯一入口。
/// </summary>
public static class AppEndpoints
{
/// <summary>
/// 配置所有业务端点。调用方传入 builder,按需叠加鉴权、过滤器等。
/// </summary>
/// <param name="builder">端点构建器</param>
/// <param name="includeDetails">是否在错误响应中包含异常详情(开发环境 true</param>
public static ServiceEndpointBuilder Configure(ServiceEndpointBuilder builder, bool includeDetails = false)
{
// ---- 全局异常拦截(自动捕获所有端点中未处理的异常) ----
builder.Endpoints.AddGlobalFilter(new GlobalExceptionFilter(includeDetails));
builder.ConfigureEndpoints(endpoints =>
{
// ---- 全局日志过滤器(记录每个请求) ----
endpoints.AddGlobalFilter(async (ctx, next) =>
{
Serilog.Log.Debug("→ {Method} {Path}", ctx.Method, ctx.Path);
await next(ctx);
Serilog.Log.Debug("← {Method} {Path} | {StatusCode}", ctx.Method, ctx.Path, ctx.StatusCode);
});
// ---- 业务端点注册 ----
// 天气预报(从数据库读取)
endpoints.MapGet("api/wData", GetWeatherForecastsAsync)
.WithName("GetWeatherForecast");
// 获取用户(演示从数据库查询)
endpoints.MapGet("api/getUser", GetUserFromDatabaseAsync)
.WithName("GetUser");
// 处理数据(POST — 演示参数处理)
endpoints.MapPost("api/processData", ProcessDataAsync)
.WithName("ProcessData");
// ---- 需要鉴权的端点示例 ----
// endpoints.MapGet("api/admin/dashboard", AdminDashboardAsync)
// .WithName("AdminDashboard")
// .RequireAuthorization = true
// .Policy = "AdminOnly";
});
return builder;
}
#region
/// <summary>
/// 从数据库查询天气预报(优先数据库,回退到内存生成)。
/// </summary>
private static async Task<object?> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 尝试从数据库读取
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
{
var dbForecasts = await db.WeatherForecasts
.OrderByDescending(f => f.Date)
.Take(5)
.ToListAsync();
if (dbForecasts.Count > 0)
{
return ResponseHelper.Ok(dbForecasts, "获取天气预报成功(来自数据库)");
}
}
// 回退:内存生成(数据库为空时)
var service = sp?.GetService(typeof(WeatherForecastService)) as WeatherForecastService
?? new WeatherForecastService();
var forecasts = service.GetWeatherForecasts();
return ResponseHelper.Ok(forecasts, "获取天气预报成功(内存生成)");
}
private static async Task<object?> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 尝试从数据库读取用户
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
{
var users = await db.Set<UserEntity>().Take(1).ToListAsync();
if (users.Count > 0)
{
return ResponseHelper.Ok(users[0], "获取用户成功(来自数据库)");
}
}
// 回退:演示数据
await Task.Delay(100);
var user = new { id = 1, name = "张三", email = "zhangsan@example.com" };
return ResponseHelper.Ok(user);
}
private static async Task<object?> ProcessDataAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 演示:将收到的数据存入数据库
var input = ctx.Body ?? string.Empty;
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db && !string.IsNullOrWhiteSpace(input))
{
var forecast = new WeatherForecastEntity
{
Date = DateOnly.FromDateTime(DateTime.Now),
TemperatureC = 20,
Summary = input,
};
db.WeatherForecasts.Add(forecast);
await db.SaveChangesAsync();
return ResponseHelper.Ok(forecast, "数据已存入数据库");
}
await Task.Delay(200);
return ResponseHelper.Ok(new { input, processed = input.ToUpperInvariant() });
}
#endregion
}
}
@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia_Services.Core;
namespace Avalonia_Services.Extensions
{
/// <summary>
/// Desktop (Avalonia-PC) 端点适配器。
/// 将统一端点转换为桌面端可用的路由处理器,支持过滤器和鉴权管道。
/// </summary>
public class DesktopEndpointAdapter
{
private readonly ServiceEndpointCollection _endpoints;
private readonly IAuthService _authService;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// 匹配后的路由结果(与原有 RouteDispatchResult 兼容)。
/// </summary>
public class RouteResult
{
public bool IsMatched { get; init; }
public int StatusCode { get; init; } = 200;
public string StatusMessage { get; init; } = "OK";
public object? Data { get; init; }
public Dictionary<string, string> ResponseHeaders { get; init; } = new();
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),
};
}
public static RouteResult NotFound() => new()
{
IsMatched = false,
StatusCode = 404,
StatusMessage = "Not Found",
};
}
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 endpoint = _endpoints.Endpoints.FirstOrDefault(e =>
string.Equals(e.Pattern, path, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase));
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),
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 (!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);
}
}
}
+26
View File
@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Avalonia_Services.Models
{
/// <summary>
/// 用户实体 —— 演示数据库 CRUD 操作。
/// </summary>
[Table("Users")]
public class UserEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[MaxLength(100)]
public string? Name { get; set; }
[MaxLength(200)]
public string? Email { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}
@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Avalonia_Services.Models
{
/// <summary>
/// 天气预报数据实体 —— 对应数据库中的 WeatherForecasts 表。
/// </summary>
[Table("WeatherForecasts")]
public class WeatherForecastEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
[MaxLength(200)]
public string? Summary { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}