.NET Core Minimal API自动注册API端点的最佳实现方式
需求可行性
这个需求实现难度极低,完全基于.NET原生的反射、依赖注入、Minimal API原生能力就能实现,不需要修改现有业务基类的设计,整体代码量不到100行。
最佳实现方案
前置说明
所有EndpointBase<TRequest,TResponse>的非抽象实现类,需要先注册到依赖注入容器,推荐使用Scoped生命周期,和普通Minimal API的服务生命周期保持一致。
你现有示例里的UserEnpoint存在拼写错误,实际使用建议修正为UserEndpoint避免后续维护混淆。
完整实现代码
首先编写DI注册扩展方法,负责扫描指定程序集里所有符合要求的Endpoint类型:
public static class EndpointServiceCollectionExtensions { private static readonly Type EndpointBaseOpenType = typeof(EndpointBase<,>); public static IServiceCollection AddEndpoints(this IServiceCollection services, params Assembly[] assembliesToScan) { // 未指定扫描程序集时,默认扫描应用入口程序集 if (assembliesToScan.Length == 0) { assembliesToScan = new[] { Assembly.GetEntryAssembly()! }; } // 查找所有非抽象、继承自EndpointBase<,>的实现类 var endpointTypes = assembliesToScan .SelectMany(asm => asm.GetTypes()) .Where(t => !t.IsAbstract && IsSubclassOfEndpointBase(t)) .ToList(); foreach (var endpointType in endpointTypes) { var closedBaseType = GetEndpointBaseClosedType(endpointType); // 以基类为服务类型注册Scoped生命周期,方便后续直接通过基类解析实例 services.AddScoped(closedBaseType, endpointType); } // 缓存找到的Endpoint类型集合,避免后续重复反射扫描 services.AddSingleton(endpointTypes); return services; } private static bool IsSubclassOfEndpointBase(Type type) { var current = type.BaseType; while (current != null) { if (current.IsGenericType && current.GetGenericTypeDefinition() == EndpointBaseOpenType) return true; current = current.BaseType; } return false; } private static Type GetEndpointBaseClosedType(Type endpointType) { var current = endpointType.BaseType; while (current != null) { if (current.IsGenericType && current.GetGenericTypeDefinition() == EndpointBaseOpenType) return current; current = current.BaseType; } throw new InvalidOperationException($"类型{endpointType.FullName}未继承自EndpointBase基类"); } }
然后实现你预期的UseEndpoints扩展方法,负责自动映射公开端点:
public static class EndpointApplicationExtensions { private static readonly Type EndpointBaseOpenType = typeof(EndpointBase<,>); public static void UseEndpoints(this WebApplication app) { var endpointTypes = app.Services.GetRequiredService<List<Type>>(); using var startupScope = app.Services.CreateScope(); foreach (var endpointType in endpointTypes) { var closedBaseType = GetEndpointBaseClosedType(endpointType); // 启动阶段临时解析实例,读取端点配置 var endpointInstance = startupScope.ServiceProvider.GetRequiredService(closedBaseType); // 非公开端点直接跳过 var isPublic = (bool)closedBaseType.GetProperty(nameof(EndpointBase<object, object>.IsPublic))! .GetValue(endpointInstance)!; if (!isPublic) continue; // 读取路由配置 var route = (string)closedBaseType.GetProperty(nameof(EndpointBase<object, object>.Route))! .GetValue(endpointInstance)!; // 反射调用泛型映射方法,避免手动构造表达式树 var genericArgs = closedBaseType.GetGenericArguments(); typeof(EndpointApplicationExtensions) .GetMethod(nameof(MapSingleEndpoint), BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(genericArgs) .Invoke(null, new object[] { app, route }); } } private static void MapSingleEndpoint<TRequest, TResponse>(WebApplication app, string route) where TRequest : class, IRequest where TResponse : class { // 实际请求处理逻辑,每次请求从当前请求容器解析实例,符合Scoped生命周期 app.MapGet(route, async (TRequest request, IServiceProvider sp) => { var endpoint = sp.GetRequiredService<EndpointBase<TRequest, TResponse>>(); return await endpoint.PerformAction(request); }) .WithOpenApi(); // 需要集成Swagger则保留该行,不需要可删除 } private static Type GetEndpointBaseClosedType(Type endpointType) { var current = endpointType.BaseType; while (current != null) { if (current.IsGenericType && current.GetGenericTypeDefinition() == EndpointBaseOpenType) return current; current = current.BaseType; } throw new InvalidOperationException($"类型{endpointType.FullName}未继承自EndpointBase基类"); } }
使用方式
在Program.cs中按顺序调用扩展方法即可:
var builder = WebApplication.CreateBuilder(args); // 其他服务注册(如DbContext、Swagger等) builder.Services.AddEndpoints(); var app = builder.Build(); // 其他中间件配置(如静态文件、认证授权中间件等) app.UseEndpoints(); app.Run();
可选优化点
- 目前实现默认映射为GET请求,如果你需要支持POST/PUT/DELETE等HTTP方法,可以给
EndpointBase<TRequest,TResponse>加一个抽象的HttpMethod字符串属性,实现类返回HttpMethods.Get/HttpMethods.Post等对应值,注册的时候根据方法值调用MapGet/MapPost等对应映射方法即可。 - 如果需要给端点加统一过滤器、异常处理、响应缓存策略,直接在
MapSingleEndpoint方法的app.MapGet之后链式调用对应的Minimal API扩展方法即可,和手写端点的配置方式完全一致。 - 对于非公开端点(IsPublic=false),你可以选择直接跳过映射,也可以映射后调用
.RequireAuthorization()添加授权要求,根据自身业务需求调整即可。 - TRequest参数的绑定规则和普通Minimal API参数完全一致:GET请求默认从QueryString绑定,POST请求默认从Body绑定,你也可以给TRequest类加
[AsParameters]、[FromRoute]等特性自定义绑定来源。
内容的提问来源于stack exchange,提问作者Norbert Maziarz
相关产品推荐
相关产品推荐

