如何通过控制器约定/自定义提供程序启用限流(无需逐个添加EnableRateLimiting特性)
我太懂这种痛点了——要给几十上百个控制器挨个加[EnableRateLimiting]不仅繁琐,还容易漏加。而且你说的IControllerModelConvention里Attributes集合只读的问题,确实是个容易踩的坑。不过别慌,有几个实用的方案能帮你批量搞定这个需求,原理都是绕开只读的Attributes集合,直接从筛选器管道入手。
方案一:用IControllerModelConvention添加限流筛选器(最推荐)
虽然ControllerModel.Attributes是只读的,但限流特性本质上是一个IFilterMetadata实现类。我们完全可以跳过修改特性集合,直接把EnableRateLimitingAttribute的实例加到控制器的Filters集合里——效果和在类上标记特性一模一样。
假设你的控制器都带有自定义的标记特性(比如[MyApiControllerMarker]),可以这样写约定:
public class BatchRateLimitConvention : IControllerModelConvention { private readonly string _rateLimitPolicyName; public BatchRateLimitConvention(string rateLimitPolicyName) { _rateLimitPolicyName = rateLimitPolicyName; } public void Apply(ControllerModel controller) { // 检查当前控制器是否带有你的自定义标记特性 var hasMarker = controller.ControllerType .GetCustomAttributes(typeof(MyApiControllerMarkerAttribute), inherit: true) .Any(); if (hasMarker) { // 直接添加限流筛选器,替代手动加特性 controller.Filters.Add(new EnableRateLimitingAttribute(_rateLimitPolicyName)); // 可选:如果控制器上有[DisableRateLimiting],可以在这里移除它 var disableFilter = controller.Filters .OfType<DisableRateLimitingAttribute>() .FirstOrDefault(); if (disableFilter != null) { controller.Filters.Remove(disableFilter); } } } }
然后在Program.cs里注册这个约定:
builder.Services.AddControllers(options => { // 传入你定义好的限流策略名称 options.Conventions.Add(new BatchRateLimitConvention("DefaultApiRateLimit")); });
方案二:用IApplicationModelProvider做底层修改
如果你的需求更复杂(比如要同时修改控制器和操作的元数据),可以用更底层的IApplicationModelProvider扩展点,它能覆盖整个应用模型的构建过程:
public class RateLimitApplicationModelProvider : IApplicationModelProvider { private readonly string _policyName; // 控制执行顺序,0表示在默认提供程序之前执行 public int Order => 0; public RateLimitApplicationModelProvider(string policyName) { _policyName = policyName; } public void OnProvidersExecuting(ApplicationModelProviderContext context) { foreach (var controller in context.Result.Controllers) { // 这里可以替换成你的匹配逻辑:比如基类、命名空间、标记特性 if (controller.ControllerType.Namespace.StartsWith("YourApp.Api.V1")) { controller.Filters.Add(new EnableRateLimitingAttribute(_policyName)); } } } public void OnProvidersExecuted(ApplicationModelProviderContext context) { // 后续处理逻辑(一般不需要) } }
注册方式稍微不同,需要把它添加到服务集合中:
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IApplicationModelProvider, RateLimitApplicationModelProvider>(sp => new RateLimitApplicationModelProvider("DefaultApiRateLimit")));
方案三:按规则匹配控制器(无需标记特性)
如果你不需要标记特性,也可以通过控制器的基类、命名空间或名称规则来批量匹配:
比如给所有继承自BaseApiController的控制器加限流:
public void Apply(ControllerModel controller) { if (typeof(BaseApiController).IsAssignableFrom(controller.ControllerType)) { controller.Filters.Add(new EnableRateLimitingAttribute("DefaultApiRateLimit")); } }
或者给某个命名空间下的所有控制器加限流:
public void Apply(ControllerModel controller) { if (controller.ControllerType.Namespace?.Contains("YourApp.Api.Admin") ?? false) { controller.Filters.Add(new EnableRateLimitingAttribute("AdminRateLimit")); } }
关键原理说明
你之前遇到的Attributes集合只读问题,是因为这个集合是控制器类型上原始反射得到的特性集合,ASP.NET Core不允许修改它。但实际上,特性的核心作用是向框架提供元数据,最终会被转换成筛选器加入管道。我们直接操作Filters集合,就绕开了只读限制,效果和标记特性完全一致。
注意事项
- 确保你已经在Program.cs中正确配置了限流策略,比如:
builder.Services.AddRateLimiter(options => { options.AddPolicy("DefaultApiRateLimit", context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.User.Identity?.Name ?? context.Request.HttpContext.Connection.RemoteIpAddress?.ToString(), factory: _ => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); }); - 如果有个别控制器需要排除限流,可以在约定里添加排除逻辑(比如检查是否有
[SkipRateLimit]自定义特性)。
内容来源于stack exchange

