.NET - Minimal API 幂等性实现
目录
1. 概述
1.1 为什么使用 Minimal API?
Minimal API 是 .NET 6+ 推出的轻量级 API 开发模式,相比传统 Controller:
优势:
- ✅ 代码更简洁:减少样板代码
- ✅ 性能更好:更少的抽象层
- ✅ 启动更快:更少的反射操作
- ✅ 更适合微服务:轻量、快速
适用场景:
- RESTful API
- 微服务
- Serverless 函数
- 原型开发
1.2 幂等性实现方案
在 Minimal API 中,有两种主要方式实现幂等性:
- 中间件(Middleware):全局或路由级别
- Endpoint Filter:针对特定端点(.NET 7+)
2. Minimal API 基础
2.1 基础示例
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddDbContext<OrderDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
var app = builder.Build();
// 定义端点
app.MapGet("/api/orders/{id}", async (Guid id, OrderDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
});
app.MapPost("/api/orders", async (CreateOrderRequest request, OrderDbContext db) =>
{
var order = new Order
{
Id = Guid.NewGuid(),
UserId = request.UserId,
TotalAmount = request.TotalAmount,
CreatedAt = DateTime.UtcNow
};
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/api/orders/{order.Id}", order);
});
app.Run();
public record CreateOrderRequest(Guid UserId, decimal TotalAmount);3. 基于中间件的幂等性
3.1 请求ID去重中间件
csharp
namespace Idempotency.MinimalApi.Middleware
{
public class RequestIdempotencyMiddleware
{
private readonly RequestDelegate _next;
private readonly IDatabase _redis;
private readonly ILogger<RequestIdempotencyMiddleware> _logger;
public RequestIdempotencyMiddleware(
RequestDelegate next,
IConnectionMultiplexer redis,
ILogger<RequestIdempotencyMiddleware> logger)
{
_next = next;
_redis = redis.GetDatabase();
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 只处理 POST/PUT 请求
if (!IsIdempotentMethod(context.Request.Method))
{
await _next(context);
return;
}
// 提取请求ID
var requestId = ExtractRequestId(context);
if (string.IsNullOrEmpty(requestId))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Missing X-Request-ID header");
return;
}
// 检查是否重复请求
var cacheKey = $"idempotency:{requestId}";
var cachedResponse = await _redis.StringGetAsync(cacheKey);
if (cachedResponse.HasValue)
{
_logger.LogDebug("Duplicate request detected: {RequestId}", requestId);
// 返回缓存的响应
var response = JsonSerializer.Deserialize<CachedResponse>(cachedResponse!);
if (response != null)
{
context.Response.ContentType = response.ContentType;
context.Response.StatusCode = response.StatusCode;
await context.Response.WriteAsync(response.Body);
}
return;
}
// 捕获响应
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
try
{
// 继续处理
await _next(context);
// 读取响应
responseBody.Seek(0, SeekOrigin.Begin);
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
// 缓存成功响应
if (context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)
{
var cachedResponse = new CachedResponse
{
StatusCode = context.Response.StatusCode,
ContentType = context.Response.ContentType,
Body = responseText
};
var json = JsonSerializer.Serialize(cachedResponse);
await _redis.StringSetAsync(cacheKey, json, TimeSpan.FromMinutes(10));
_logger.LogInformation("Response cached: {RequestId}", requestId);
}
// 写回响应
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
finally
{
context.Response.Body = originalBodyStream;
}
}
private bool IsIdempotentMethod(string method)
{
return method.Equals("POST", StringComparison.OrdinalIgnoreCase) ||
method.Equals("PUT", StringComparison.OrdinalIgnoreCase);
}
private string? ExtractRequestId(HttpContext context)
{
// 优先从 Header 获取
if (context.Request.Headers.TryGetValue("X-Request-ID", out var requestId))
{
return requestId.ToString();
}
// 从请求体获取(如果是 JSON)
if (context.Request.ContentType?.Contains("application/json") == true)
{
context.Request.EnableBuffering();
try
{
using var reader = new StreamReader(
context.Request.Body,
leaveOpen: true);
var body = reader.ReadToEndAsync().GetAwaiter().GetResult();
context.Request.Body.Position = 0;
using var doc = JsonDocument.Parse(body);
if (doc.RootElement.TryGetProperty("requestId", out var idElement))
{
return idElement.GetString();
}
}
catch
{
// 忽略解析错误
}
}
return null;
}
}
public class CachedResponse
{
public int StatusCode { get; set; }
public string ContentType { get; set; } = "application/json";
public string Body { get; set; } = string.Empty;
}
}3.2 注册中间件
csharp
// Program.cs
using Idempotency.MinimalApi.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = builder.Configuration.GetConnectionString("Redis");
return ConnectionMultiplexer.Connect(config);
});
var app = builder.Build();
// 使用幂等性中间件
app.UseMiddleware<RequestIdempotencyMiddleware>();
// 定义端点...
app.MapPost("/api/orders", HandleCreateOrder);
app.Run();4. 基于 Endpoint Filter 的幂等性
4.1 Token 验证 Filter(.NET 7+)
csharp
namespace Idempotency.MinimalApi.Filters
{
public class TokenValidationFilter : IEndpointFilter
{
private readonly IDatabase _redis;
private readonly ILogger<TokenValidationFilter> _logger;
public TokenValidationFilter(
IConnectionMultiplexer redis,
ILogger<TokenValidationFilter> logger)
{
_redis = redis.GetDatabase();
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// 从 Header 提取 Token
var httpContext = context.HttpContext;
if (!httpContext.Request.Headers.TryGetValue("X-Idempotency-Token", out var tokenHeader))
{
return Results.BadRequest(new { error = "Missing X-Idempotency-Token header" });
}
var token = tokenHeader.ToString();
// 验证并消耗 Token
var isValid = await ValidateAndConsumeTokenAsync(token);
if (!isValid)
{
return Results.Conflict(new { error = "Invalid or expired token" });
}
// Token 有效,继续处理
return await next(context);
}
private async Task<bool> ValidateAndConsumeTokenAsync(string token)
{
var key = $"token:{token}";
var value = await _redis.StringGetDeleteAsync(key);
return value.HasValue && value == "pending";
}
}
}4.2 使用 Filter
csharp
// Program.cs
using Idempotency.MinimalApi.Filters;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = builder.Configuration.GetConnectionString("Redis");
return ConnectionMultiplexer.Connect(config);
});
var app = builder.Build();
// 对特定端点应用 Filter
app.MapPost("/api/orders", HandleCreateOrder)
.AddEndpointFilter<TokenValidationFilter>();
app.MapPost("/api/payments", HandlePayment)
.AddEndpointFilter<TokenValidationFilter>();
app.Run();
async Task<IResult> HandleCreateOrder(CreateOrderRequest request, OrderDbContext db)
{
var order = new Order
{
Id = Guid.NewGuid(),
UserId = request.UserId,
TotalAmount = request.TotalAmount,
CreatedAt = DateTime.UtcNow
};
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/api/orders/{order.Id}", order);
}5. 完整示例:订单API
5.1 项目结构
MinimalApiOrderService/
├── Program.cs
├── Endpoints/
│ └── OrderEndpoints.cs
├── Services/
│ ├── IOrderService.cs
│ └── OrderService.cs
├── Models/
│ └── Order.cs
└── DTOs/
├── CreateOrderRequest.cs
└── OrderResponse.cs5.2 Program.cs
csharp
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using MinimalApiOrderService.Endpoints;
using MinimalApiOrderService.Services;
var builder = WebApplication.CreateBuilder(args);
// 数据库
builder.Services.AddDbContext<OrderDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = builder.Configuration.GetConnectionString("Redis");
return ConnectionMultiplexer.Connect(config);
});
// 服务
builder.Services.AddScoped<IOrderService, OrderService>();
// OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Swagger
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 订单端点
app.MapGroup("/api/orders")
.MapOrderEndpoints();
app.Run();5.3 端点定义
csharp
namespace MinimalApiOrderService.Endpoints
{
public static class OrderEndpoints
{
public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group)
{
// 创建订单(带幂等性)
group.MapPost("/", async (
CreateOrderRequest request,
IOrderService orderService,
HttpContext context) =>
{
var result = await orderService.CreateOrderAsync(request);
if (!result.IsSuccess)
{
return Results.BadRequest(result);
}
return Results.Created($"/api/orders/{result.Data}", result.Data);
})
.WithName("CreateOrder")
.WithOpenApi();
// 查询订单
group.MapGet("/{id:guid}", async (
Guid id,
OrderDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
})
.WithName("GetOrder")
.WithOpenApi();
// 取消订单
group.MapDelete("/{id:guid}", async (
Guid id,
IOrderService orderService) =>
{
var result = await orderService.CancelOrderAsync(id);
if (!result.IsSuccess)
{
return Results.BadRequest(result);
}
return Results.NoContent();
})
.WithName("CancelOrder")
.WithOpenApi();
return group;
}
}
}5.4 订单服务
csharp
namespace MinimalApiOrderService.Services
{
public interface IOrderService
{
Task<Result<Guid>> CreateOrderAsync(CreateOrderRequest request);
Task<Result> CancelOrderAsync(Guid orderId);
}
public class OrderService : IOrderService
{
private readonly OrderDbContext _dbContext;
private readonly ILogger<OrderService> _logger;
public OrderService(OrderDbContext dbContext, ILogger<OrderService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
public async Task<Result<Guid>> CreateOrderAsync(CreateOrderRequest request)
{
try
{
var order = new Order
{
Id = Guid.NewGuid(),
OrderNo = GenerateOrderNo(),
UserId = request.UserId,
ProductId = request.ProductId,
Quantity = request.Quantity,
TotalAmount = request.TotalAmount,
Status = OrderStatus.Pending,
CreatedAt = DateTime.UtcNow
};
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Order created: {OrderId}", order.Id);
return Result.Success(order.Id);
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
_logger.LogWarning("Duplicate order attempt for user {UserId}, product {ProductId}",
request.UserId, request.ProductId);
// 返回已存在的订单
var existingOrder = await _dbContext.Orders
.Where(o => o.UserId == request.UserId
&& o.ProductId == request.ProductId
&& o.Status == OrderStatus.Pending)
.FirstOrDefaultAsync();
return existingOrder != null
? Result.Success(existingOrder.Id)
: Result.Fail<Guid>("Failed to create order");
}
}
public async Task<Result> CancelOrderAsync(Guid orderId)
{
var order = await _dbContext.Orders.FindAsync(orderId);
if (order == null)
{
return Result.Fail("Order not found");
}
if (order.Status != OrderStatus.Pending)
{
return Result.Fail("Only pending orders can be cancelled");
}
order.Status = OrderStatus.Cancelled;
order.CancelledAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
return Result.Success();
}
private bool IsUniqueViolation(DbUpdateException ex)
{
return ex.InnerException is NpgsqlException npgEx && npgEx.SqlState == "23505";
}
private string GenerateOrderNo()
{
return $"ORD{DateTime.UtcNow:yyyyMMddHHmmss}{Guid.NewGuid():N[..8]}";
}
}
}6. 最佳实践
6.1 组织端点代码
csharp
// ✅ 推荐:按功能分组
app.MapGroup("/api/orders")
.WithTags("Orders")
.MapOrderEndpoints();
app.MapGroup("/api/payments")
.WithTags("Payments")
.MapPaymentEndpoints();
// ❌ 避免:所有端点写在一起
app.MapPost("/api/orders", ...);
app.MapGet("/api/orders/{id}", ...);
app.MapPost("/api/payments", ...);6.2 使用依赖注入
csharp
// ✅ 推荐:注入服务
app.MapPost("/api/orders", async (
CreateOrderRequest request,
IOrderService orderService) =>
{
return await orderService.CreateOrderAsync(request);
});
// ❌ 避免:直接使用 DbContext
app.MapPost("/api/orders", async (
CreateOrderRequest request,
OrderDbContext db) =>
{
// 业务逻辑混在端点中
});6.3 统一错误处理
csharp
// 全局异常处理中间件
app.UseExceptionHandler(async context =>
{
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
context.Response.StatusCode = exception switch
{
BusinessRuleViolationException => StatusCodes.Status409Conflict,
ValidationException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
context.Response.ContentType = "application/json";
var error = new { error = exception?.Message };
await context.Response.WriteAsJsonAsync(error);
});6.4 性能优化
csharp
// 启用响应压缩
app.UseResponseCompression();
// 使用输出缓存(.NET 7+)
app.MapGet("/api/products/{id}", async (Guid id, OrderDbContext db) =>
{
return await db.Products.FindAsync(id);
})
.WithOutputCache(options =>
{
options.Expire(TimeSpan.FromMinutes(5));
});总结
Minimal API 提供了简洁高效的方式实现幂等性:
核心要点
- 中间件:适合全局幂等性控制
- Endpoint Filter:适合特定端点的细粒度控制
- 服务层:业务逻辑封装,便于测试
- 依赖注入:保持代码清晰
最佳实践
- 使用 Endpoint Filter 实现 Token 验证
- 使用中间件实现请求ID去重
- 将业务逻辑放在服务层
- 使用
RouteGroupBuilder组织端点
Minimal API + 幂等性设计 = 简洁、高性能的 API 服务 🚀