前言
FastEndpoints 是一个轻量级的 REST API 开发框架,它摒弃了传统的 Controller/Action 模式,是基于REPR(Request-Endpoint-Response)设计模式设计的API。性能与Minimal APIs 不相上下。对比传统 MVC 性能上要上不少。对于性能的要求高的微服务非常推荐。
REPR 设计模式
FastEndpoints 摒弃 MVC 的「Controller(控制器)- Action(动作方法)」分层,采用 REPR(Request-Endpoint-Response)模式,将 API 的核心逻辑收敛到单一「Endpoint(端点)」类中,结构更紧凑:
- Request(请求):强类型的请求模型(如 CreateUserRequest),负责接收和验证 HTTP 请求参数(路径、查询、Body 等),内置数据验证(支持 DataAnnotations、FluentValidation 等)。
- Endpoint(端点):API 的核心逻辑载体,一个类对应一个 API 端点(如 CreateUserEndpoint),需实现 IEndpoint 接口或继承 Endpoint<TRequest, TResponse> 基类,重写 HandleAsync 方法处理业务逻辑。
- Response(响应):强类型的响应模型(如 CreateUserResponse),负责定义 API 的返回结构。
这种模式的核心价值在于 边界清晰:每个交互流程都被封装为独立的 “请求 - 端点 - 响应” 单元,避免业务逻辑与数据传输逻辑混杂,同时让接口的 “调用规则” 可被快速理解(无需阅读复杂代码,仅需看请求 / 响应定义即可)。
FastEndpoints基本使用
通过 NuGet 安装核心包:
1
| Install-Package FastEndpoints
|
在 Program.cs 中进行简单配置即可启用 FastEndpoints:
1 2 3 4 5 6 7
| var builder = WebApplication.CreateBuilder(args); builder.Services.AddFastEndpoints();
var app = builder.Build(); app.UseFastEndpoints(); app.Run();
|
创建一个简单的 “Hello World” 端点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| using FastEndpoints;
namespace WebApplication1;
public class HelloEndpoint : Endpoint<HelloRequest, HelloResponse> { public override void Configure() { Get("/api/hello"); AllowAnonymous(); }
public override async Task HandleAsync(HelloRequest req, CancellationToken ct) { await Send.OkAsync(new HelloResponse { Message = $"Hello, {req.Name}!", Timestamp = DateTime.Now }, ct); } }
public class HelloRequest { public string Name { get; set; } }
public class HelloResponse { public string Message { get; set; } public DateTime Timestamp { get; set; } }
|
如果路由简单还可以通过Attribute属性标识的方式来申明。
1 2 3 4 5 6 7 8 9 10 11 12 13
| [HttpGet("api/hello")] [AllowAnonymous] public class HelloEndpoint : Endpoint<HelloRequest, HelloResponse> { public override async Task HandleAsync(HelloRequest req, CancellationToken ct) { await Send.OkAsync(new HelloResponse { Message = $"Hello, {req.Name ?? "1"}!", }, cancellation: ct); } }
|
Endpoint 是 FastEndpoints 的核心,每个端点类都继承自Endpoint<TRequest, TResponse>抽象类
- TRequest:表示请求数据的 DTO
- TResponse:表示响应数据的 DTO
每个端点需要实现两个方法:
- Configure():配置路由、HTTP 方法、权限等元数据
- HandleAsync():处理请求的核心逻辑
默认提供了四种端点
- Endpoint - 只需要请求模型(TRequest),但不需要响应模型(无返回体,通常仅返回状态码)。
- Endpoint<TRequest, TResponse> - 既需要请求模型(TRequest),也需要响应模型(TResponse)。
- EndpointWithoutRequest - 不需要请求模型(无输入参数),也不需要响应模型(无返回体)。
- EndpointWithoutRequest - 不需要请求模型(无输入参数),但需要响应模型(TResponse,返回具体数据)。
Endpoint、EndpointWithoutRequest 并不限制返回数据,还是可以返回任何可序列化的数据。但是不建议这么使用。
EndpointWithoutRequest、EndpointWithoutRequest 虽然不接受输入参数,但是还是可以使用过 Route、 和Query 获取参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [HttpGet("api/hello/{id}")] [AllowAnonymous] public class HelloEndpoint : EndpointWithoutRequest { public override async Task HandleAsync(CancellationToken ct) { var id = Route<int>("id"); var name = Query<string>("name"); await Send.OkAsync(new HelloResponse { Message = $"Hello, {name},{id}!", }); } }
|
或者使用EmptyRequest 和 EmptyResponse 定义端点
1
| public class MyEndpoint : Endpoint<EmptyRequest,EmptyResponse> { }
|
路由配置
在 FastEndpoints 中,路由配置主要在端点类的Configure()方法中完成。
1 2 3 4 5
| public override void Configure() { Get("/api/hello"); Post("/api/hello"); }
|
对于需要支持多种 HTTP 方法的端点,可以使用Verbs()方法:
1 2 3 4 5
| public override void Configure() { Routes("/api/hello","/api/hello1"); Verbs(Http.GET, Http.POST); }
|
可以为所有端点配置全局路由前缀,在 Program.cs 中设置:
1 2 3 4
| builder.Services.AddFastEndpoints(options => { options.RoutePrefix = "api/v1"; });
|
还可以为特定端点或一组端点设置局部前缀:
1 2 3 4 5 6
| public override void Configure() { Routes("/api"); Get("/hello"); AllowAnonymous(); }
|
Swagger 集成
需要安装指定的包
1
| dotnet add package FastEndpoints.Swagger
|
在 Program.cs 中设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| using FastEndpoints; using FastEndpoints.Swagger;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints() .SwaggerDocument();
var app = builder.Build(); app.UseFastEndpoints() .UseSwaggerGen();
app.Run();
|
FastEndpoints 提供了 Summary() 方法,用于在 Configure() 中为指定端点添加描述文本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public override void Configure() { Get("api/hello");
Description(b => b .Produces(403) .Produces(500) ); Summary(s => { s.Summary = " 用于提供端点的简短描述"; s.Description = "提供更详细的端点说明,可以包含功能细节、使用场景、注意事项等"; s.ExampleRequest = new HelloRequest { Name = "example name" }; s.Responses[200] = "成功响应描述"; s.Responses[403] = "禁止访问描述"; s.Responses[500] = "错误描述"; });
AllowAnonymous(); }
|
模型绑定
对于需要路由参数的可以使用大括号{}定义,用于从 URL 路径中提取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public override void Configure() { Get("/api/hello/{name}"); Get("/api/hello/{name:length(1,2)}"); Get("/api/hello/{name:int}"); Get("/api/hello/{name:regex(^\\d{4}-\\d{4}$)}"); Get("/api/hello/{name:bool}"); Get("/api/hello/{name?}"); }
|
参数会自动绑定到请求 DTO 中同名的属性:
1 2 3 4
| public class HelloRequest { public string Name { get; set; } }
|
对于 POST、PUT、PATCH 等 HTTP 方法,请求数据通常放在请求体中(如 JSON、XML 或表单数据)。当 HTTP 方法为 POST、PUT、PATCH 时,自动从请求体读取数据,默认支持 JSON 格式(Content-Type: application/json)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public class CreateProductRequest { public string Name { get; set; } public decimal Price { get; set; } public bool InStock { get; set; } }
public override void Configure() { Post("/products"); }
|
对于传统表单提交(Content-Type: application/x-www-form-urlencoded 或 multipart/form-data),使用 [FormField] 特性指定绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class UploadProfileRequest { [FormField] public string Username { get; set; }
[FormField] public IFormFile? Avatar { get; set; } }
public override void Configure() { Post("/profile/upload"); AllowFileUploads(); }
|
可以从 HTTP 请求头中绑定数据,使用 [FromHeader] 特性显式指定。
1 2 3 4 5 6 7 8 9 10 11
| public class AuthenticatedRequest { [FromHeader("Authorization")] public string? AuthToken { get; set; }
[FromHeader("X-Request-Id")] public string? RequestId { get; set; } }
|
一个 DTO 可以同时从多个数据源绑定数据(路由 + 查询字符串 + 请求头 + 请求体),框架会自动从相应来源获取并组合数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class UpdateUserRequest { [FromRoute] public int Id { get; set; }
public bool IncludeDetails { get; set; }
[FromHeader("X-Request-Version")] public int Version { get; set; }
public string Name { get; set; } public string Email { get; set; } }
public override void Configure() { Post("/users/{id}"); }
|
如果希望某些属性不参与模型绑定(如只读属性),可以使用 [BindNever] 特性:
1 2 3 4 5 6 7 8 9
| public class CreateUserRequest { public string Username { get; set; } public string Password { get; set; }
[BindNever] public Guid UserId { get; set; } }
|
模型验证
FastEndpoints 模型验证是基于 FluentValidation 的,FluentValidation是一个基于 .NET 的流畅式验证库,支持通过链式语法定义强类型的验证规则,广泛用于验证实体类、DTO(数据传输对象)等数据的合法性,相比传统的注解式验证更灵活、可维护性更强。
- Validator:验证器基类,所有自定义验证器都需继承它,泛型 T 是待验证的目标类型(如 DTO、实体类)。
- RuleFor():验证规则的入口方法,用于指定要验证的属性(如 RuleFor(x => x.Name) 表示验证 T 类型的 Name 属性),后续通过链式方法追加具体验证逻辑(如非空、长度限制等)。
1 2 3 4 5 6 7 8 9
| public class RegisterUserDto { public string UserName { get; set; } public string Email { get; set; } public int Age { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }
|
创建自定义验证器继承 AbstractValidator,在构造函数中通过 RuleFor() 定义验证规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| using FluentValidation;
public class RegisterUserDtoValidator : Validator<RegisterUserDto> { public RegisterUserDtoValidator() { RuleFor(dto => dto.UserName) .NotNull().WithMessage("用户名不能为空") .Length(2, 20).WithMessage("用户名长度需在 2-20 个字符之间"); RuleFor(dto => dto.Email) .NotEmpty().WithMessage("邮箱不能为空") .EmailAddress().WithMessage("请输入合法的邮箱格式") .MaximumLength(100).WithMessage("邮箱长度不能超过 100 字符"); RuleFor(dto => dto.Age) .InclusiveBetween(18, 60).WithMessage("年龄需在 18-60 岁之间") .NotNull().WithMessage("年龄不能为空"); RuleFor(dto => dto.Password) .NotEmpty().WithMessage("密码不能为空") .MinimumLength(8).WithMessage("密码长度不能少于 8 字符") .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$") .WithMessage("密码需包含大小写字母和数字"); RuleFor(dto => dto.ConfirmPassword) .Equal(dto => dto.Password).WithMessage("两次输入的密码不一致") .NotEmpty().WithMessage("请确认密码"); } }
|
对于简单的输入验证,可以在请求 DTO 上使用 DataAnnotations 来实现。但是一些复杂的的还是建议使用 FluentValidation 来实现。性能方面 DataAnnotations 会比 DataAnnotations 稍快,但是基本可以忽略不计。
前后置处理器
前置 / 后置处理器(IPreProcessor/IPostProcessor)是 FastEndpoints 提供的横切逻辑处理机制核心目标是减少代码重复、实现关注点分离。
IPreProcessor 是用于在端点 HandleAsync 方法执行之前运行自定义逻辑的接口。它专注于请求处理前的准备工作,例如参数验证增强、请求数据转换、资源预热等。
1 2 3 4 5 6 7 8 9 10 11
| public class RequestLoggingPreProcessor<TRequest> : IPreProcessor<TRequest> { public Task PreProcessAsync(IPreProcessorContext<TRequest> context, CancellationToken ct) { var logger = context.HttpContext.Resolve<ILogger<TRequest>>(); var requestJson = JsonSerializer.Serialize(context.Request); logger.LogInformation("请求参数: {RequestJson}", requestJson); return Task.CompletedTask; } }
|
前置处理器可以通过两种方式应用到端点,第一种在端点的 Configure 方法中,使用 PreProcessors 方法指定当前端点要使用的前置处理器。
1 2 3 4 5 6
| public override void Configure() { Get("/health"); AllowAnonymous(); PreProcessors(new RequestLoggingPreProcessor<EmptyRequest>()); }
|
或者使用创建一个基础端点要其他端点继承该端点
1 2 3 4 5 6 7 8 9 10
| public abstract class BaseCrudEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse> { public override void Configure() { PreProcessors( new RequestLoggingPreProcessor<TRequest>() ); } }
|
IPostProcessor<TRequest, TResponse> 接口用于在端点的 HandleAsync 方法执行之后(响应发送之前)运行自定义逻辑。它适合处理响应日志记录、数据格式化、资源清理等操作。后置处理器还可以访问未处理的异常。
1 2 3 4 5 6 7 8 9 10 11 12
| public class MyResponseLogger<TRequest, TResponse> : IPostProcessor<TRequest, TResponse> { public Task PostProcessAsync(IPostProcessorContext<TRequest, TResponse> ctx, ...) { var logger = ctx.HttpContext.Resolve<ILogger<TResponse>>();
if (ctx.Response is CreateSaleResponse response) logger.LogWarning($"sale complete: {response.OrderID}");
return Task.CompletedTask; } }
|
如果想创建全局处理器可以使用 实现 IGlobalPreProcessor 和 IGlobalPostProcessor 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class RequestLoggingPreProcessor : IGlobalPreProcessor { public Task PreProcessAsync(IPreProcessorContext context, CancellationToken ct) { var logger = context.HttpContext.Resolve<ILogger<RequestLoggingPreProcessor>>(); var requestJson = JsonSerializer.Serialize(context.Request); logger.LogInformation("请求参数: {RequestJson}", requestJson); return Task.CompletedTask; } }
public Task PostProcessAsync(IPostProcessorContext context, CancellationToken ct) { var logger = context.HttpContext.Resolve<ILogger<ResponseLoggingPostProcessor>>(); var responseJson = JsonSerializer.Serialize(context.Response); logger.LogInformation( "响应结果 - 状态码: {StatusCode}, 响应内容: {ResponseJson}", context.HttpContext.Response.StatusCode, responseJson );
return Task.CompletedTask; }
|
在项目启动时进行基础配置
1 2 3 4 5 6 7 8 9 10 11 12
| var app = builder.Build(); app.UseFastEndpoints(c => { c.Endpoints.Configurator = ep => { ep.DontAutoSendResponse(); ep.PreProcessor<RequestLoggingPreProcessor>(Order.Before); ep.PostProcessor<ResponseLoggingPostProcessor>(Order.After); ep.AllowAnonymous(); }; }) .UseSwaggerGen();
app.Run();
|
总结
FastEndpoints 的能力远不止前文覆盖的 “基础功能”—— 它还提供了 Pub/Sub 消息订阅、Command Bus 命令总线等进阶特性。并提供了丰富的可配置性,可以通过 FastEndpoints 官方文档 查阅。