Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GetCustomAttributes on ParameterDescriptor in IAsyncActionFilter is missing

Using .NET Core 2.1.

I am trying to access the attributes on the action parameter inside of an IAsyncActionFilter.

public IActionResult DoSomething([MyAttribute] MyParameter p) { ... }

In my IAsyncActionFilter, I would like to access the MyAttribute on the parameter p, but GetCustomAttributes does not exist.

public class MyActionFilter : IAsyncActionFilter
{
    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // GetCustomAttributes does not exist here...
        var attributes = context.ActionDescriptor.Parameters[0].GetCustomAttributes<MyAttribute>(); 
        return next();
    }
}

In ASP.NET MVC 5.2, you can use GetCustomAttributes:

https://learn.microsoft.com/en-us/dotnet/api/system.web.mvc.parameterdescriptor.getcustomattributes?view=aspnet-mvc-5.2#System_Web_Mvc_ParameterDescriptor_GetCustomAttributes_System_Boolean_

What is the way to achieve the same in .NET Core?


UPDATE 1

It seems we can cast the ActionDescriptor to ControllerActionDescriptor to access the underlying MethodInfo and then the parameters and their attributes.

public class TempDataActionFilter : IAsyncActionFilter
{
    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var actionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor;
        var parameters = 
            from p in actionDescriptor.MethodInfo.GetParameters()
            where p.GetCustomAttributes(typeof(MyAttribute), true) != null
            select p;

        var controller = context.Controller as Controller;
        foreach (var p in parameters)
        {
            // Do something with the parameters that have an attribute 
        }
        return next();
    }
}

This feels wrong. I am always dismayed to see solutions of this type being proposed in Microsoft's own documentation. This is a runtime error waiting to happen. Is there a better way?

like image 824
CPerson Avatar asked Jan 22 '26 18:01

CPerson


1 Answers

Approach 1

Seems that you're trying to bind action arguments. If that's the case, ModelBinding is preferred over filter, thus you don't have to cast the ActionDescriptor to ControllerActionDescriptor to inspect whether a parameter has a specified attribute,

In your scenario, a much easier & safer way is to make your FromTempDataAttribute implement the IBindingSourceMetadata to indicate you want to bind data from TempData:

internal class FromTempDataAttribute : Attribute, IBindingSourceMetadata
{
    public static readonly BindingSource Instance = new BindingSource(
            "id-FromTempData",
            "TempData Binding Source",
            true,
            true
        );
    public BindingSource BindingSource {get{
        return FromTempDataAttribute.Instance;
    }} 
}

And then create a ModelBinder and a related Provider:

public class MyFromTempDataModelBinder : IModelBinder
{
    private readonly IServiceProvider sp;

    public MyFromTempDataModelBinder(IServiceProvider sp)
    {
        this.sp = sp;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var factory = this.sp.GetRequiredService<ITempDataDictionaryFactory>();
        var tempData = factory.GetTempData(bindingContext.HttpContext);
        var name = bindingContext.FieldName;
        var o = tempData.Peek(name);
        if (o == null) {
            bindingContext.ModelState.AddModelError(name, $"cannot get {name} from TempData");
        } else {
            var result = Convert.ChangeType(o,bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        return Task.CompletedTask;
    }

}

public class FromTempDataBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }
        var has = context.BindingInfo?.BindingSource == FromTempDataAttribute.Instance;
        if(has){
            return new BinderTypeModelBinder(typeof(MyFromTempDataModelBinder));
        }
        return null;
    }
}

The Provider returns an instance of MyFromTempDataModelBinder if the context.BindingInfo.BindingSource equals the required attribute.

Also don't forget to register this provider in your startup:

services.AddMvc(opts => {
    opts.ModelBinderProviders.Insert(0, new FromTempDataBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Finally, you can get the data automatically:

public IActionResult Test([FromTempDataAttribute] string a, string b )
{
    return Json(new {A = a, B = b,});
}

Approach 2

In case you insist on Filter, you can also make the FromTempDataAttribute implement the IBindingSourceMetadata interface as we do above, and then you can get those parameters as below:

public class TempDataActionFilter : IAsyncActionFilter
{
    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var parameteres = context.ActionDescriptor.Parameters.Where(p => p.BindingInfo?.BindingSource == FromTempDataAttribute.Instance);
        foreach(var p in parameteres){
            // ...
        }
        return next();
    }
}
like image 50
itminus Avatar answered Jan 24 '26 08:01

itminus