如何为ASP.NET控制器方法创建组合多特性的自定义特性?
为ASP.NET控制器方法创建组合式自定义特性
编写ASP.NET控制器时,常需为Action方法重复添加多个特性。若能创建可自动组合其他特性的自定义特性,能大幅保持代码整洁。已有方案通过System.ComponentModel.PropertyDescriptor为属性实现类似功能,但因不存在System.ComponentModel.MethodDescriptor,无法直接移植到方法场景。下面说明如何为方法实现该功能:
实现思路
ASP.NET Core的特性处理依赖元数据提供程序(MetadataProvider),我们可以通过自定义IActionModelConvention扫描控制器方法上的自定义组合特性,将其拆解为内部包含的多个特性并添加到方法元数据中。
具体步骤
1. 定义组合特性基类
先创建一个基类,让所有组合特性继承它,方便后续识别:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public abstract class CompositeAttribute : Attribute { // 由子类返回需要组合的特性集合 public abstract IEnumerable<Attribute> GetAttributes(); }
2. 创建具体的组合特性
针对示例中的重复特性,创建针对性的组合特性:
// 组合HttpPost+200OK+500错误响应的特性 [<AttributeUsage(AttributeTargets.Method, AllowMultiple = false)>] type ApiCommandAttribute(route: string) = inherit CompositeAttribute() override _.GetAttributes() = seq { yield HttpPostAttribute(route) yield ProducesResponseTypeAttribute(StatusCodes.Status200OK) yield ProducesResponseTypeAttribute(typeof<Dto.Exception>, StatusCodes.Status500InternalServerError) } // 组合HttpGet+指定响应类型+500错误的特性 [<AttributeUsage(AttributeTargets.Method, AllowMultiple = false)>] type ApiQueryAttribute(route: string, responseType: Type) = inherit CompositeAttribute() override _.GetAttributes() = seq { yield HttpGetAttribute(route) yield ProducesResponseTypeAttribute(responseType, StatusCodes.Status200OK) yield ProducesResponseTypeAttribute(typeof<Dto.Exception>, StatusCodes.Status500InternalServerError) }
3. 实现约定类处理组合特性
创建IActionModelConvention实现,扫描Action方法的组合特性并替换为内部特性:
public class CompositeAttributeConvention : IActionModelConvention { public void Apply(ActionModel action) { var compositeAttributes = action.Attributes.OfType<CompositeAttribute>().ToList(); if (!compositeAttributes.Any()) return; foreach (var compositeAttr in compositeAttributes) { // 添加组合特性包含的所有子特性 foreach (var attr in compositeAttr.GetAttributes()) { action.Attributes.Add(attr); action.Selectors.ForEach(s => s.EndpointMetadata.Add(attr)); } // 移除原组合特性(可选) action.Attributes.Remove(compositeAttr); action.Selectors.ForEach(s => s.EndpointMetadata.Remove(compositeAttr)); } } }
4. 注册约定到ASP.NET Core
在Program.cs中注册自定义约定:
builder.Services.AddControllers(options => { options.Conventions.Add(new CompositeAttributeConvention()); });
5. 在控制器中使用组合特性
简化后的控制器代码:
[<ApiQuery("list-products", typeof<Dto.Product list>)>] member this.ListProducts() = this.Http(Market.Product.all() |> AsyncSeq.map Dto.Of) [<ApiQuery("find-product", typeof<Dto.Product>)>] [<ProducesResponseType(StatusCodes.Status404NotFound)>] member this.FindProduct([<Required>] product: string) = this.Http(Market.Product.withName_ product |> Async.map (Option.map Dto.Of >> Option.toObj)) [<ApiCommand("start-feed")>] member this.StartFeed([<Required>] contract: string) = this.ForwardToService(MineService, StartFeed contract) [<ApiCommand("stop-feed")>] member this.StopFeed([<Required>] contract: string) = this.ForwardToService(MineService, StopFeed contract) [<ApiCommand("recalc-bars")>] member this.RecalcBars([<Required>] product: string, fromUtc_: Nullable<DateTime>) = this.ForwardToService(MineService, RecalcBars (product, Option.ofNullable fromUtc_)) [<ApiCommand("reset-chart")>] member this.ResetChart([<Required>] product: string) = this.ForwardToService(ChartService, ResetChart product)
关键说明
- 该方式依赖ASP.NET Core的模型约定,在应用启动时处理所有控制器元数据,无额外运行时开销。
- 可根据需求扩展组合特性,比如添加权限验证、日志等特性的组合。
- 组合特性支持参数传递(如路由、响应类型),只需在构造函数中接收参数并传递给内部特性即可。
内容的提问来源于stack exchange,提问作者Franco Tiveron
相关产品推荐
相关产品推荐

