简体   繁体   中英

asp.net core custom attribute to get object from filter

I am having some ActionFilter s in my asp.net core mvc application, which validate some input data. For example, the client sends a userId inside the header, the filter loads that user from a repository, and validates, if the user exists, is active, has a license, and so on. This filter is attached to a Controller method. The Controller method also needs to collect the same user object. Because of performance, I want to pass that user object, collected inside the filter, to the controller, so the controller does not need to load the same user object again. I know there are ways to do so, like mentioned here .

Because of clean code, I wonder if this would be possible, coding an attribute which defines what to retrieve, like the [FromBody] attribute does, for instance.

I could imagine this attribute named [FromFilter("User")] , which takes a parameter to specify the key inside the HttpContext.Items

A basic implementation could be something like this:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFilterAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
    /// <inheritdoc />
    public BindingSource BindingSource => BindingSource.Custom;
        
    /// <inheritdoc />
    public string Name { get; set; }
}

Neither do I know if this would a be a good idea, nor how to implement such a feature. Hopefully someone can me point into the right direction

As far as I know, we couldn't directly pass the object from filter to action.

In my opinion, the best solution is creating a custom model binding and then find the user from the repository and pass the user to the action.

Since the model binding is triggered before the filter, you could get the custom model binding result from the ActionExecutingContext .

Order of execution:

UserModelBinder --> OnActionExecuting --> Index action

More details about to do it, you could refer to below codes:

Custom model binding:

public class UserModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var model = new UserModel()
        {
            id = 1,
            name = "test"
        };

         bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;

    }
}

Controller action and OnActionExecuting method:

OnActionExecuting:

    public override void OnActionExecuting(ActionExecutingContext context)
    {   
        //ActionArguments["user"] is the parameter name of the action parameter
        var user = context.ActionArguments["user"] as UserModel;


        // Do something before the action executes.
        base.OnActionExecuting(context);
    }

Action method:

    public async Task<IActionResult> Index([ModelBinder(BinderType = typeof(UserModelBinder))] UserModel user)
    {
       int i =0;
       return View();

    }

Result:

Filter onexecuting:

在此处输入图像描述

Action parameter:

在此处输入图像描述

You can use HttpContext.Items for this and create HttpContextItemsModelBinder which will bind model from HttpContext.Items

public class HttpContextItemsModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var items = bindingContext.HttpContext.Items;
        string name = bindingContext.BinderModelName ?? bindingContext.FieldName;

        bindingContext.Result = items.TryGetValue(name, out object item)
            ? ModelBindingResult.Success(item)
            : ModelBindingResult.Failed();

        return Task.CompletedTask;
    }
}

Create and register model binder provider

public static class CustomBindingSources
{
    public static BindingSource HttpContextItems { get; } = new BindingSource("HttpContextItems", "HttpContext Items", true, true);
}

public class HttpContextItemsModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.BindingInfo.BindingSource == CustomBindingSources.HttpContextItems)
        {
            return new HttpContextItemsModelBinder();
        }

        return null;
    }
}

In Startup.cs

services
    .AddMvc(options =>
    {
        options.ModelBinderProviders.Insert(0, new HttpContextItemsModelBinderProvider());
        //...
    })

Create an attribute which will set correct BindingSource to use HttpContextItemsModelBinder

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromHttpContextItemsAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
    public string Name { get; set; }

    public BindingSource BindingSource => CustomBindingSources.HttpContextItems;

    public FromHttpContextItemsAttribute(string name)
    {
        Name = name;
    }

    public FromHttpContextItemsAttribute() { }
}

Usage:

//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems("UserItem")]UserItemModel model)
{
    return Ok(model);
}

//your action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
       //...
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var model = new UserItemModel
        {
            Id = 45,
            Name = "Some user name"
        };
        context.HttpContext.Items["UserItem"] = model;
    }
}

Important note

Pay attention that I save user model to HttpContext.Items during OnAuthorization and not OnActionExecuting because model binding happens before any action filters run, so HttpContext.Items won't contain user and model binding will fail. You might need to adjust filter code to your needs and to make the solution work as expected.

Usage without specifying item name. Parameter name in action method should match key ( "userModel" ) used to store value in HttpContext.Items :

//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems]UserItemModel userModel)
{
    return Ok(userModel);
}

//action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
       //...
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //...
        context.HttpContext.Items["userModel"] = model;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM