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

重构项目结构,引入 Avalonia-Common、Avalonia-EFCore、Avalonia-Services,实现 API 与桌面端统一端点注册、过滤器、鉴权和标准响应格式。支持多数据库自动迁移与配置,集成 Serilog 日志系统。移除旧路由与控制器,提升接口一致性与可维护性。
This commit is contained in:
luoqian 2026-05-11 14:35:34 +08:00
parent 99631df085
commit 271e9714ff
38 changed files with 2073 additions and 182 deletions

6
.gitignore vendored
View File

@ -17,4 +17,8 @@
/avalonia-web-react/obj
/avalonia-web-react/obj
/avalonia-web-react/node_modules
/avalonia-web-react/dist
/avalonia-web-react/dist
/Avalonia-EFCore/bin
/Avalonia-EFCore/obj
/Avalonia-Common/bin
/Avalonia-Common/obj

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"chat.tools.terminal.autoApprove": {
"ForEach-Object": true,
"dotnet list": true
}
}

View File

@ -9,10 +9,17 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Services\Avalonia-Services.csproj" />
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\Avalonia-EFCore\Avalonia-EFCore.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
<ActiveDebugProfile>http</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

View File

@ -1,15 +1,38 @@
using Avalonia_Services.Services;
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
using Avalonia_Services.Database;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Avalonia_API.Configuration
{
public static class ServicesConfiguration
{
public static void ConfigureServices(this IServiceCollection services)
/// <summary>
/// 注册统一端点及其依赖的服务(含数据库)。
/// 所有业务端点定义在 Avalonia-Services/Endpoints/AppEndpoints.cs。
/// </summary>
public static IServiceCollection AddUnifiedApiServices(this IServiceCollection services)
{
// Register your services here
// For example:
// services.AddSingleton<IMyService, MyService>();
// ---- 数据库 ----
// 从 appsettings.json 读取 DatabaseConfiguration 节
// 注册默认数据库提供程序SQLite / MySQL / PostgreSQL / SqlServer
DatabaseProviderRegistry.RegisterDefaults();
// 注册 AppDataContext共享数据上下文
services.AddAppDatabase<AppDataContext>(DatabaseConfiguration.ForSQLite("app.db"));
// ---- 业务服务 ----
services.AddScoped<WeatherForecastService>();
// ---- 统一端点 ----
var endpointBuilder = new ServiceEndpointBuilder();
AppEndpoints.Configure(endpointBuilder);
var endpoints = endpointBuilder.Build();
services.AddSingleton(endpoints);
return services;
}
}
}

View File

@ -1,20 +0,0 @@
using Avalonia_Services.Models;
using Avalonia_Services.Services;
using Microsoft.AspNetCore.Mvc;
namespace Avalonia_API.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController(WeatherForecastService weatherForecastService) : ControllerBase
{
private readonly WeatherForecastService _weatherForecastService = weatherForecastService;
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return _weatherForecastService.GetWeatherForecasts();
}
}
}

View File

@ -0,0 +1,163 @@
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!;
}
}
}

View File

@ -1,23 +1,57 @@
var builder = WebApplication.CreateBuilder(args);
using Avalonia_API.Configuration;
using Avalonia_API.Extensions;
using Avalonia_Common.Infrastructure;
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
using Avalonia_Services.Database;
using Serilog;
// Add services to the container.
// 初始化日志系统
Log.Logger = LoggingConfiguration.CreateDefaultLogger(logDir: "logs");
Log.Information("Avalonia-API 正在启动...");
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
try
{
app.MapOpenApi();
var builder = WebApplication.CreateBuilder(args);
// 使用 Serilog 作为日志提供程序
builder.Host.UseSerilog();
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddOpenApi();
// 注册统一端点及业务服务(入口在 Avalonia-Services/Endpoints/AppEndpoints.cs
builder.Services.AddUnifiedApiServices();
var app = builder.Build();
// 初始化数据库(自动迁移 + 种子数据)
app.Services.InitializeDatabase<AppDataContext>();
// 启动时打印所有接口
var endpoints = app.Services.GetRequiredService<ServiceEndpointCollection>();
EndpointPrinter.PrintEndpoints(endpoints, "Avalonia-API 接口列表");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
// 将统一端点映射到 ASP.NET Core 路由
app.MapUnifiedEndpoints(endpoints, app.Services);
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Avalonia-API 启动失败");
}
finally
{
Log.CloseAndFlush();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -5,5 +5,12 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"DatabaseConfiguration": {
"Provider": "SQLite",
"ConnectionString": "Data Source=avalonia-api.db",
"AutoMigrate": true,
"EnableDetailedLog": false,
"Timeout": 30
}
}

View File

@ -0,0 +1,8 @@
2026-05-11 13:39:27.320 [INF] Avalonia-API 正在启动...
2026-05-11 13:39:58.813 [INF] Avalonia-API 正在启动...
2026-05-11 13:40:13.058 [INF] Avalonia-API 正在启动...
2026-05-11 13:40:13.566 [INF] Now listening on: http://localhost:5206
2026-05-11 13:40:13.603 [INF] No action descriptors found. This may indicate an incorrectly configured application or missing application parts. To learn more, visit https://aka.ms/aspnet/mvc/app-parts
2026-05-11 13:40:13.633 [INF] Application started. Press Ctrl+C to shut down.
2026-05-11 13:40:13.635 [INF] Hosting environment: Development
2026-05-11 13:40:13.635 [INF] Content root path: D:\QiChengProject\Avalonia-Stack\Avalonia-API

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Avalonia_Common</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,176 @@
using System.Text.Json.Serialization;
namespace Avalonia_Common.Core
{
/// <summary>
/// 统一 API 返回格式。
/// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。
/// </summary>
/// <typeparam name="T">业务数据类型</typeparam>
public class ApiResponse<T>
{
/// <summary>是否成功</summary>
[JsonPropertyName("success")]
public bool Success { get; set; }
/// <summary>HTTP 状态码</summary>
[JsonPropertyName("code")]
public int Code { get; set; }
/// <summary>消息(成功时可为 null失败时包含错误描述</summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary>业务数据</summary>
[JsonPropertyName("data")]
public T? Data { get; set; }
/// <summary>时间戳</summary>
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; } = DateTime.Now;
/// <summary>请求追踪 ID用于排查问题</summary>
[JsonPropertyName("traceId")]
public string? TraceId { get; set; }
// ---- 快捷工厂方法 ----
/// <summary>成功返回(有数据)</summary>
public static ApiResponse<T> Ok(T data, string? message = null)
{
return new ApiResponse<T>
{
Success = true,
Code = 200,
Message = message,
Data = data,
};
}
/// <summary>失败返回</summary>
public static ApiResponse<T> Fail(int code, string message, T? data = default)
{
return new ApiResponse<T>
{
Success = false,
Code = code,
Message = message,
Data = data,
};
}
/// <summary>400 参数错误</summary>
public static ApiResponse<T> BadRequest(string message = "参数错误")
=> Fail(400, message);
/// <summary>401 未授权</summary>
public static ApiResponse<T> Unauthorized(string message = "未授权")
=> Fail(401, message);
/// <summary>403 无权限</summary>
public static ApiResponse<T> Forbidden(string message = "无权限")
=> Fail(403, message);
/// <summary>404 未找到</summary>
public static ApiResponse<T> NotFound(string message = "资源不存在")
=> Fail(404, message);
/// <summary>500 服务器内部错误</summary>
public static ApiResponse<T> ServerError(string message = "服务器内部错误")
=> Fail(500, message);
}
/// <summary>
/// 无数据的统一返回格式object? 版本)。
/// </summary>
public class ApiResponse : ApiResponse<object?>
{
/// <summary>成功返回(无数据)</summary>
public static ApiResponse Succeed(string? message = null)
{
return new ApiResponse
{
Success = true,
Code = 200,
Message = message,
Data = null,
};
}
/// <summary>失败返回</summary>
public static ApiResponse Failure(int code, string message)
{
return new ApiResponse
{
Success = false,
Code = code,
Message = message,
Data = null,
};
}
}
/// <summary>
/// 分页返回格式
/// </summary>
public class PagedResponse<T>
{
[JsonPropertyName("success")]
public bool Success { get; set; } = true;
[JsonPropertyName("code")]
public int Code { get; set; } = 200;
[JsonPropertyName("items")]
public List<T> Items { get; set; } = new();
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("page")]
public int Page { get; set; } = 1;
[JsonPropertyName("pageSize")]
public int PageSize { get; set; } = 20;
[JsonPropertyName("totalPages")]
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
public static PagedResponse<T> From(List<T> items, int total, int page, int pageSize)
{
return new PagedResponse<T>
{
Items = items,
Total = total,
Page = page,
PageSize = pageSize,
};
}
}
/// <summary>
/// 端点返回辅助方法 —— 在 AppEndpoints 中快捷构建统一响应。
/// </summary>
public static class ResponseHelper
{
/// <summary>成功返回</summary>
public static ApiResponse<T> Ok<T>(T data, string? message = null)
=> ApiResponse<T>.Ok(data, message);
/// <summary>成功返回(无数据)</summary>
public static ApiResponse Succeed(string? message = null)
=> ApiResponse.Succeed(message);
/// <summary>失败返回</summary>
public static ApiResponse<T> Fail<T>(int code, string message, T? data = default)
=> ApiResponse<T>.Fail(code, message, data);
/// <summary>失败返回(无数据)</summary>
public static ApiResponse Failure(int code, string message)
=> ApiResponse.Failure(code, message);
/// <summary>分页返回</summary>
public static PagedResponse<T> Paged<T>(List<T> items, int total, int page, int pageSize)
=> PagedResponse<T>.From(items, total, page, pageSize);
}
}

View File

@ -0,0 +1,124 @@
using Serilog;
using Serilog.Events;
namespace Avalonia_Common.Infrastructure
{
/// <summary>
/// Serilog 日志配置 —— 可在 Avalonia-API 和 Avalonia-PC 中共享。
/// </summary>
public static class LoggingConfiguration
{
/// <summary>
/// 默认日志目录
/// </summary>
private static readonly string DefaultLogDir = Path.Combine(AppContext.BaseDirectory, "logs");
/// <summary>
/// 创建控制台日志记录器(开发环境)。
/// </summary>
public static ILogger CreateConsoleLogger(
LogEventLevel minimumLevel = LogEventLevel.Debug)
{
return new LoggerConfiguration()
.MinimumLevel.Is(minimumLevel)
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
}
/// <summary>
/// 创建控制台 + 文件日志记录器。
/// </summary>
/// <param name="minimumLevel">最低日志级别</param>
/// <param name="logDir">日志目录,默认 ./logs</param>
/// <param name="retainedDays">保留天数</param>
public static ILogger CreateDefaultLogger(
LogEventLevel minimumLevel = LogEventLevel.Information,
string? logDir = null,
int retainedDays = 30)
{
logDir ??= DefaultLogDir;
return new LoggerConfiguration()
.MinimumLevel.Is(minimumLevel)
//.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
//.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
path: Path.Combine(logDir, "log-.txt"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedDays,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
encoding: System.Text.Encoding.UTF8)
.CreateLogger();
}
/// <summary>
/// 创建只写文件的日志记录器(桌面应用静默模式)。
/// </summary>
public static ILogger CreateFileOnlyLogger(
LogEventLevel minimumLevel = LogEventLevel.Information,
string? logDir = null,
int retainedDays = 30)
{
logDir ??= DefaultLogDir;
return new LoggerConfiguration()
.MinimumLevel.Is(minimumLevel)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.File(
path: Path.Combine(logDir, "app-.txt"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: retainedDays,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
encoding: System.Text.Encoding.UTF8)
.CreateLogger();
}
}
/// <summary>
/// 静态日志访问器 —— 全局静态入口,方便在没有 DI 的场景下使用。
/// </summary>
public static class AppLog
{
private static ILogger? _logger;
/// <summary>
/// 初始化全局日志记录器。
/// </summary>
public static void Initialize(ILogger logger)
{
_logger = logger;
Log.Logger = logger;
}
public static ILogger Logger => _logger ?? Log.Logger;
public static void Debug(string messageTemplate, params object?[] propertyValues)
=> Logger.Debug(messageTemplate, propertyValues);
public static void Information(string messageTemplate, params object?[] propertyValues)
=> Logger.Information(messageTemplate, propertyValues);
public static void Warning(string messageTemplate, params object?[] propertyValues)
=> Logger.Warning(messageTemplate, propertyValues);
public static void Error(string messageTemplate, params object?[] propertyValues)
=> Logger.Error(messageTemplate, propertyValues);
public static void Error(Exception exception, string messageTemplate, params object?[] propertyValues)
=> Logger.Error(exception, messageTemplate, propertyValues);
public static void Fatal(string messageTemplate, params object?[] propertyValues)
=> Logger.Fatal(messageTemplate, propertyValues);
public static void Fatal(Exception exception, string messageTemplate, params object?[] propertyValues)
=> Logger.Fatal(exception, messageTemplate, propertyValues);
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Avalonia_EFCore</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 应用数据库上下文基类 —— 自动根据 DatabaseConfiguration 选择数据库提供程序。
/// 所有业务 DbContext 继承此类即可获得多数据库支持。
/// </summary>
public abstract class AppDbContext(DatabaseConfiguration dbConfig) : DbContext
{
private readonly DatabaseConfiguration _dbConfig = dbConfig;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured) return;
ConfigureProvider(optionsBuilder, _dbConfig);
if (_dbConfig.EnableDetailedLog)
{
optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
}
// 启用详细的 EF Core 错误信息
optionsBuilder.EnableDetailedErrors();
optionsBuilder.EnableSensitiveDataLogging(_dbConfig.EnableDetailedLog);
}
/// <summary>
/// 根据配置选择数据库提供程序。
/// 使用注册模式,由宿主项目注册具体的提供程序实现。
/// </summary>
public static void ConfigureProvider(DbContextOptionsBuilder optionsBuilder, DatabaseConfiguration config)
{
if (DatabaseProviderRegistry.TryGet(config.Provider, out var configurator))
{
configurator(optionsBuilder, config.ConnectionString, config.Timeout);
}
else
{
throw new NotSupportedException(
$"数据库提供程序 {config.Provider} 未注册。" +
$"请在宿主项目中安装对应的 EF Core NuGet 包并调用 DatabaseProviderRegistry.Register()。");
}
optionsBuilder.EnableDetailedErrors();
}
/// <summary>
/// 保存时自动设置时间戳。
/// </summary>
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
SetTimestamps();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
SetTimestamps();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SetTimestamps()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (var entry in entries)
{
var entity = entry.Entity;
// 使用反射设置 CreatedAt / UpdatedAt如果存在
var createdAtProp = entity.GetType().GetProperty("CreatedAt");
var updatedAtProp = entity.GetType().GetProperty("UpdatedAt");
if (entry.State == EntityState.Added && createdAtProp != null)
{
createdAtProp.SetValue(entity, DateTime.UtcNow);
}
if (updatedAtProp != null)
{
updatedAtProp.SetValue(entity, DateTime.UtcNow);
}
}
}
}
}

View File

@ -0,0 +1,87 @@
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 支持的数据库提供程序类型。
/// </summary>
public enum DatabaseProvider
{
/// <summary>SQLite本地文件数据库无需安装跨平台</summary>
SQLite,
/// <summary>MySQL / MariaDB</summary>
MySQL,
/// <summary>PostgreSQL</summary>
PostgreSQL,
/// <summary>SQL Server</summary>
SqlServer
}
/// <summary>
/// 数据库连接配置 —— 在 appsettings.json 中配置。
/// </summary>
public class DatabaseConfiguration
{
/// <summary>数据库提供程序</summary>
public DatabaseProvider Provider { get; set; } = DatabaseProvider.SQLite;
/// <summary>连接字符串</summary>
public string ConnectionString { get; set; } = "Data Source=app.db";
/// <summary>是否在启动时自动执行迁移</summary>
public bool AutoMigrate { get; set; } = true;
/// <summary>是否启用详细日志(会打印 SQL 语句)</summary>
public bool EnableDetailedLog { get; set; } = false;
/// <summary>连接超时(秒)</summary>
public int Timeout { get; set; } = 30;
// ---- 快捷构建方法 ----
/// <summary>SQLite 本地数据库</summary>
public static DatabaseConfiguration ForSQLite(string dataSource = "app.db")
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.SQLite,
ConnectionString = $"Data Source={dataSource}",
AutoMigrate = true,
};
}
/// <summary>MySQL 数据库</summary>
public static DatabaseConfiguration ForMySQL(string server, string database, string user, string password, uint port = 3306)
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.MySQL,
ConnectionString = $"Server={server};Port={port};Database={database};User={user};Password={password};",
};
}
/// <summary>PostgreSQL 数据库</summary>
public static DatabaseConfiguration ForPostgreSQL(string host, string database, string username, string password, int port = 5432)
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.PostgreSQL,
ConnectionString = $"Host={host};Port={port};Database={database};Username={username};Password={password};",
};
}
/// <summary>SQL Server 数据库</summary>
public static DatabaseConfiguration ForSqlServer(string server, string database, string? user = null, string? password = null)
{
var connStr = string.IsNullOrEmpty(user)
? $"Server={server};Database={database};Trusted_Connection=True;TrustServerCertificate=True;"
: $"Server={server};Database={database};User Id={user};Password={password};TrustServerCertificate=True;";
return new DatabaseConfiguration
{
Provider = DatabaseProvider.SqlServer,
ConnectionString = connStr,
};
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.Extensions.DependencyInjection;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库服务注册扩展 —— 在 Program.cs 中一行配置数据库。
/// </summary>
public static class DatabaseExtensions
{
/// <summary>
/// 注册数据库上下文及相关服务。
/// </summary>
/// <typeparam name="TContext">继承自 AppDbContext 的业务 DbContext</typeparam>
public static IServiceCollection AddAppDatabase<TContext>(
this IServiceCollection services,
DatabaseConfiguration config)
where TContext : AppDbContext
{
// 注册配置
services.AddSingleton(config);
// 注册 DbContext
services.AddDbContext<TContext>(options =>
{
AppDbContext.ConfigureProvider(options, config);
});
// 注册数据库管理器
services.AddScoped<DatabaseManager<TContext>>();
return services;
}
/// <summary>
/// 初始化数据库(在应用启动时调用一次)。
/// </summary>
public static IServiceProvider InitializeDatabase<TContext>(
this IServiceProvider serviceProvider,
Action<TContext, IServiceProvider?>? seeder = null)
where TContext : AppDbContext
{
using var scope = serviceProvider.CreateScope();
var dbManager = scope.ServiceProvider.GetRequiredService<DatabaseManager<TContext>>();
// 同步等待初始化(启动时阻塞)
dbManager.InitializeAsync(seeder).GetAwaiter().GetResult();
return serviceProvider;
}
}
}

View File

@ -0,0 +1,154 @@
using Avalonia_Common.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库管理器 —— 负责连接测试、自动迁移、种子数据、版本检查。
/// 在应用启动时调用,确保数据库结构与应用代码同步。
/// </summary>
public class DatabaseManager<TContext> where TContext : AppDbContext
{
private readonly TContext _context;
private readonly DatabaseConfiguration _config;
private readonly IServiceProvider? _serviceProvider;
public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null)
{
_context = context;
_config = config;
_serviceProvider = serviceProvider;
}
/// <summary>
/// 初始化数据库:测试连接 → 自动迁移 → 种子数据。
/// </summary>
public async Task InitializeAsync(Action<TContext, IServiceProvider?>? seeder = null)
{
// 1. 测试数据库连接
var canConnect = await CanConnectAsync();
if (!canConnect)
{
throw new InvalidOperationException(
$"无法连接到数据库 [{_config.Provider}],请检查连接字符串和数据库服务状态。");
}
// 2. 自动迁移(如果启用)
if (_config.AutoMigrate)
{
await MigrateAsync();
}
// 3. 种子数据
if (seeder != null)
{
seeder(_context, _serviceProvider);
await _context.SaveChangesAsync();
}
}
/// <summary>
/// 测试数据库连接是否正常。
/// </summary>
public async Task<bool> CanConnectAsync()
{
try
{
return await _context.Database.CanConnectAsync();
}
catch
{
return false;
}
}
/// <summary>
/// 执行待处理的迁移。
/// 使用 EF Core 原生迁移机制,自动检测并应用 Schema 变更。
/// </summary>
public async Task MigrateAsync()
{
try
{
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
AppLog.Information(
"检测到 {Count} 个待执行的数据库迁移: {Migrations}",
pendingMigrations.Count(),
string.Join(", ", pendingMigrations));
await _context.Database.MigrateAsync();
AppLog.Information("数据库迁移完成({Count} 个迁移已应用)", pendingMigrations.Count());
}
else
{
AppLog.Information("数据库已是最新版本,无需迁移");
}
}
catch (Exception ex)
{
AppLog.Error(ex, "数据库迁移失败");
throw;
}
}
/// <summary>
/// 获取数据库当前版本信息。
/// </summary>
public async Task<DatabaseVersionInfo> GetVersionInfoAsync()
{
var appliedMigrations = await _context.Database.GetAppliedMigrationsAsync();
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
return new DatabaseVersionInfo
{
Provider = _config.Provider.ToString(),
AppliedMigrations = appliedMigrations.ToList(),
PendingMigrations = pendingMigrations.ToList(),
IsLatest = !pendingMigrations.Any(),
CanConnect = await CanConnectAsync(),
};
}
/// <summary>
/// 生成从指定迁移到最新版本的 SQL 脚本(用于生产环境审计)。
/// </summary>
public string GenerateMigrationScript(string? fromMigration = null)
{
var migrator = _context.GetService<IMigrator>();
return fromMigration is null
? migrator.GenerateScript()
: migrator.GenerateScript(fromMigration);
}
/// <summary>
/// 确保数据库已创建(不执行迁移,适用于简单场景)。
/// </summary>
public bool EnsureCreated()
{
return _context.Database.EnsureCreated();
}
}
/// <summary>
/// 数据库版本信息 DTO。
/// </summary>
public class DatabaseVersionInfo
{
public string Provider { get; set; } = string.Empty;
public List<string> AppliedMigrations { get; set; } = new();
public List<string> PendingMigrations { get; set; } = new();
public bool IsLatest { get; set; }
public bool CanConnect { get; set; }
}
}

View File

@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库提供程序注册表 —— 统一注册所有支持的提供程序配置委托。
/// 具体使用哪个提供程序由各宿主项目决定:
/// Avalonia-API从 appsettings.json 的 DatabaseConfiguration 节读取;
/// Avalonia-PC :固定使用 SQLite。
/// </summary>
public static class DatabaseProviderRegistry
{
/// <summary>
/// 提供程序配置委托optionsBuilder, connectionString, timeout → void
/// </summary>
public delegate void ProviderConfigurator(DbContextOptionsBuilder optionsBuilder, string connectionString, int timeout);
private static readonly Dictionary<DatabaseProvider, ProviderConfigurator> _providers = new();
/// <summary>
/// 注册一个数据库提供程序。
/// </summary>
public static void Register(DatabaseProvider provider, ProviderConfigurator configurator)
{
_providers[provider] = configurator;
}
/// <summary>
/// 尝试获取注册的提供程序配置。
/// </summary>
public static bool TryGet(DatabaseProvider provider, out ProviderConfigurator configurator)
{
return _providers.TryGetValue(provider, out configurator!);
}
/// <summary>
/// 注册所有内置提供程序的默认配置(四个包均已内置在 Avalonia-EFCore 中)。
/// 注册完成后由调用方根据自身需求选择具体的 <see cref="DatabaseProvider"/>。
/// </summary>
public static void RegisterDefaults()
{
Register(DatabaseProvider.SQLite, (opts, cs, timeout) =>
opts.UseSqlite(cs, o => o.CommandTimeout(timeout)));
Register(DatabaseProvider.SqlServer, (opts, cs, timeout) =>
opts.UseSqlServer(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
Register(DatabaseProvider.PostgreSQL, (opts, cs, timeout) =>
opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
Register(DatabaseProvider.MySQL, (opts, cs, timeout) =>
opts.UseMySql(cs, ServerVersion.AutoDetect(cs), o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
}
}
}

View File

@ -8,13 +8,19 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<Content Include="www\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Models\**" />
<Compile Remove="Models\**" />
<EmbeddedResource Remove="Models\**" />
<None Remove="Models\**" />
</ItemGroup>
<ItemGroup>
<None Include=".github\copilot-instructions.md" />
</ItemGroup>
@ -35,5 +41,7 @@
<ItemGroup>
<ProjectReference Include="..\Avalonia-Services\Avalonia-Services.csproj" />
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\Avalonia-EFCore\Avalonia-EFCore.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Avalonia-PC</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,7 @@
<Solution>
<Project Path="../Avalonia-API/Avalonia-API.csproj" Id="e33aba9a-a56b-4f6b-8eaa-3acbed65ebad" />
<Project Path="../Avalonia-Common/Avalonia-Common.csproj" Id="caed4118-2161-4382-90b8-35fb4efe3b5f" />
<Project Path="../Avalonia-EFCore/Avalonia-EFCore.csproj" Id="64557501-62a7-4863-b2bf-1570b8c6fecb" />
<Project Path="../Avalonia-Services/Avalonia-Services.csproj" Id="b8757cf9-5422-4c67-acae-3c967c95f866" />
<Project Path="../avalonia-web-react/avalonia-web-react.esproj">
<Build />

View File

@ -1,7 +1,13 @@
using Avalonia;
using Avalonia_Common.Infrastructure;
using Avalonia_EFCore.Database;
using Avalonia_PC.Views;
using Avalonia_Services.Core;
using Avalonia_Services.Database;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Services;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System;
namespace Avalonia_PC
@ -10,14 +16,23 @@ namespace Avalonia_PC
{
public static IServiceProvider Services { get; private set; } = null!;
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
// 初始化日志系统
AppLog.Initialize(LoggingConfiguration.CreateDefaultLogger(logDir: "logs"));
AppLog.Information("Avalonia-PC 正在启动...");
ConfigureServices();
// 初始化数据库(自动迁移 + 种子数据)
Services.InitializeDatabase<AppDataContext>();
// 启动时打印所有拦截的接口
var endpoints = Services.GetRequiredService<ServiceEndpointCollection>();
EndpointPrinter.PrintEndpoints(endpoints, "Avalonia-PC 拦截接口列表");
#if DEBUG
// 开启 WebView2 远程调试,启动后在 Edge 中访问 edge://inspect 调试网页
Environment.SetEnvironmentVariable(
@ -30,7 +45,24 @@ namespace Avalonia_PC
private static void ConfigureServices()
{
var services = new ServiceCollection();
// ---- 数据库 ----
// 注册默认数据库提供程序SQLite / MySQL / PostgreSQL / SqlServer
DatabaseProviderRegistry.RegisterDefaults();
// 桌面端固定使用 SQLite 本地数据库
services.AddAppDatabase<AppDataContext>(DatabaseConfiguration.ForSQLite("app.db"));
// ---- 业务服务 ----
services.AddSingleton<WeatherForecastService>();
// ---- 统一端点 ----
var endpointBuilder = new ServiceEndpointBuilder();
AppEndpoints.Configure(endpointBuilder);
var endpoints = endpointBuilder.Build();
services.AddSingleton(endpoints);
// 注册 Window
services.AddTransient<MainWindow>(sp => new MainWindow(sp));
Services = services.BuildServiceProvider();

View File

@ -0,0 +1,11 @@
{
"profiles": {
"Avalonia-PC": {
"commandName": "Project"
},
"WSL": {
"commandName": "WSL2",
"distributionName": ""
}
}
}

View File

@ -1,3 +1,6 @@
using Avalonia_Services.Core;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Extensions;
using Avalonia_Services.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
@ -8,42 +11,22 @@ namespace Avalonia_PC.Views
{
public partial class MainWindow
{
// 路由表key = 接口路径忽略大小写value = 处理方法
// 新增接口:在此方法中添加一行 _routes["api/xxx"] = ctx => ...
private Dictionary<string, Func<RouteRequestContext, Task<object?>>> _routes = [];
/// <summary>
/// 统一端点适配器(替代原来的 _routes 字典)。
/// 所有端点在 Avalonia-Services/AppEndpoints.cs 中统一定义。
/// </summary>
private DesktopEndpointAdapter _endpointAdapter = null!;
// 服务容器,通过构造函数注入,路由注册时按需解析服务
/// <summary>
/// 服务容器,通过构造函数注入。
/// </summary>
private IServiceProvider _services = null!;
private void RegisterRoutes()
{
var weather = _services.GetRequiredService<WeatherForecastService>();
// 新增服务示例var myService = _services.GetRequiredService<MyService>();
_routes = new Dictionary<string, Func<RouteRequestContext, Task<object?>>>(StringComparer.OrdinalIgnoreCase)
{
["api/getUser"] = _ => GetUserFromDatabaseAsync(),
["api/processData"] = ctx => ProcessDataAsync(ExtractInput(ctx)),
["api/wData"] = _ => Task.FromResult<object?>(weather.GetWeatherForecasts()),
};
}
/// <summary>
/// 示例:模拟读取用户数据。
/// </summary>
private static async Task<object?> GetUserFromDatabaseAsync()
{
await Task.Delay(100);
return new { id = 1, name = "张三", email = "zhangsan@example.com" };
}
/// <summary>
/// 示例:模拟处理输入数据。
/// </summary>
private static async Task<object?> ProcessDataAsync(string? input)
{
await Task.Delay(200);
return $"Processed: {input?.ToUpperInvariant()}";
// 从 DI 获取已构建的端点集合
var endpointCollection = _services.GetRequiredService<ServiceEndpointCollection>();
_endpointAdapter = endpointCollection.CreateAdapter(_services);
}
}
}

View File

@ -236,7 +236,7 @@ namespace Avalonia_PC.Views
}
/// <summary>
/// 统一请求处理:构建上下文、处理 OPTIONS、按前缀分发并封装标准响应
/// 统一请求处理:构建上下文、处理 OPTIONS、使用统一端点适配器分发
/// </summary>
private async Task<AppResponse> HandleAppRequestAsync(
string? id,
@ -257,7 +257,6 @@ namespace Avalonia_PC.Views
try
{
var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。"));
var requestContext = CreateRouteRequestContext(uri, body);
if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
@ -267,12 +266,25 @@ namespace Avalonia_PC.Views
return response;
}
var routeResult = await DispatchByPrefixAsync(requestContext);
// 使用统一端点适配器处理请求
var (normalizedPath, queryParams) = ParseRequestUri(uri);
var routeResult = await _endpointAdapter.HandleRequestAsync(
path: normalizedPath,
method: method ?? "GET",
body: body,
headers: headers,
query: queryParams);
if (routeResult.IsMatched)
{
response.StatusCode = routeResult.StatusCode;
response.StatusMessage = routeResult.StatusMessage;
response.Body = BuildSuccessResponseBody(routeResult.Data);
foreach (var kvp in routeResult.ResponseHeaders)
{
response.Headers[kvp.Key] = kvp.Value;
}
return response;
}
@ -291,31 +303,9 @@ namespace Avalonia_PC.Views
}
/// <summary>
/// 按路由表匹配并调用对应处理器
/// 从 URI 解析规范化路径和查询参数(供统一端点适配器使用)
/// </summary>
private async Task<RouteDispatchResult> DispatchByPrefixAsync(RouteRequestContext requestContext)
{
if (_routes.TryGetValue(requestContext.NormalizedPath, out var handler))
{
var data = await handler(requestContext);
return RouteDispatchResult.Success(data);
}
return RouteDispatchResult.NotMatched();
}
/// <summary>
/// 统一构建成功响应体,保持前后端响应结构一致。
/// </summary>
private static string BuildSuccessResponseBody(object? data)
{
return JsonSerializer.Serialize(new { success = true, data });
}
/// <summary>
/// 从 URI 解析路径段、查询参数和 body构建路由上下文。
/// </summary>
private static RouteRequestContext CreateRouteRequestContext(Uri uri, string? body)
private static (string normalizedPath, Dictionary<string, string> query) ParseRequestUri(Uri uri)
{
var host = uri.Host ?? string.Empty;
var absolutePath = uri.AbsolutePath ?? string.Empty;
@ -329,13 +319,15 @@ namespace Avalonia_PC.Views
var normalizedPath = string.Join('/', pathSegments);
var query = ParseQueryParameters(uri.Query);
return new RouteRequestContext
{
NormalizedPath = normalizedPath,
PathSegments = pathSegments,
Query = query,
Body = body,
};
return (normalizedPath, query);
}
/// <summary>
/// 统一构建成功响应体,保持前后端响应结构一致。
/// </summary>
private static string BuildSuccessResponseBody(object? data)
{
return JsonSerializer.Serialize(new { success = true, data });
}
/// <summary>
@ -367,33 +359,6 @@ namespace Avalonia_PC.Views
return query;
}
/// <summary>
/// 按 body -> query -> path 的优先级提取业务输入参数。
/// </summary>
private static string ExtractInput(RouteRequestContext requestContext)
{
if (!string.IsNullOrWhiteSpace(requestContext.Body))
{
using var jsonDocument = JsonDocument.Parse(requestContext.Body);
if (jsonDocument.RootElement.TryGetProperty("input", out var inputProperty))
{
return inputProperty.GetString() ?? string.Empty;
}
}
if (requestContext.Query.TryGetValue("input", out var inputFromQuery) &&
!string.IsNullOrWhiteSpace(inputFromQuery))
{
return inputFromQuery;
}
if (requestContext.PathSegments.Length > 2)
{
return string.Join('/', requestContext.PathSegments.Skip(2));
}
return string.Empty;
}
/// <summary>
/// 创建桥接响应的默认 JSON/CORS 头。
@ -660,45 +625,6 @@ namespace Avalonia_PC.Views
public Dictionary<string, string> Headers { get; set; } = new();
}
private sealed class RouteRequestContext
{
public string NormalizedPath { get; init; } = string.Empty;
public string[] PathSegments { get; init; } = [];
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public string? Body { get; init; }
}
private sealed class RouteDispatchResult
{
public bool IsMatched { get; init; }
public int StatusCode { get; init; } = 200;
public string StatusMessage { get; init; } = "OK";
public object? Data { get; init; }
public static RouteDispatchResult Success(object? data)
{
return new RouteDispatchResult
{
IsMatched = true,
Data = data,
};
}
public static RouteDispatchResult NotMatched()
{
return new RouteDispatchResult
{
IsMatched = false,
};
}
}
#endregion
}
}

View File

@ -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>

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();
}
}
}

View File

@ -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}");
}
}
}
}

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);
}
}
}

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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
});
}
}
}

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
}
}

View File

@ -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);
}
}
}

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;
}
}

View File

@ -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;
}
}