asp.net-mvcaction-filteractionfilterattributecustom-action-filter

How to implement some code in many actions of many controllers


I have controllers with actions, which works with some entity (Driver). Also, each Driver linked with identity profile.

        public async Task<ActionResult> Details(int? id)
        {
            if ((id == null))
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            DriverDetailVM model = mapper.Map<DriverDetailVM>(db.Drivers.Find(id));
            if ((model == null))
            {
                return HttpNotFound();
            }
            return View(model);
        }

        public async Task<ActionResult> Edit(int? id = null, bool isNewUser = false)
        {
/////////
        }

If user has role "Superadmin" then he has access to pages with any id value. If user has role "Driver" then we should have access only if id value is the same, as his profile. I try to implement it on ActionFilter:

public class DriverAccessActionFilterAttribute : ActionFilterAttribute
{
    public string IdParamName { get; set; }
    public int DriverID { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.HttpContext.User.IsInRole("Driver"))
        {
            if (filterContext.ActionParameters.ContainsKey(IdParamName))
            {
                var id = filterContext.ActionParameters[IdParamName] as Int32;
                if (id != DriverID)
                    filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden);
            }
            else
                filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.BadRequest); 
        }
        else
            base.OnActionExecuting(filterContext);
    }
}

but when I try to use this code:

    [DriverAccessActionFilter(DriverID = currentUser.DriverId, IdParamName = "id")]
    public async Task<ActionResult> Details(int? id)
    {

it does not want to be compiled, because

An object reference is required for the non-static field, method, or property

how to implement it?


Solution

  • Attribute parameters are evaluated at compile-time, not at runtime. So they have to be compile time constants. You can't pass a value to an action attribute at run-time. i.e in [DriverAccessActionFilter(DriverID = currentUser.DriverId, IdParamName = "id")] you are passing DriverID = currentUser.DriverId. An attribute is used as controller/action metadata and metadata needs to be compiled in assembly. This is why attributes can take only constant values.

    You have to change your attribute as follows:

    1. Use Dependency Injection to Inject your service which returns logged in user.
    2. Or Implement a custom Principal and assign it current request principal.

    You can modify your attribute as follow in case you implement CustomPrinicpal:

    public class DriverAccessActionFilterAttribute : ActionFilterAttribute
        {
            public string IdParamName { get; set; }
            private int DriverID { get; set; }
    
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {            
                if (filterContext.HttpContext.User.IsInRole("Driver"))
                {
                    var customPrincipal = filterContext.HttpContext.User as CustomPrincipal;
                    DriverID = customPrincipal.Id;
                    // Rest of you logic                
                }
                else
                    base.OnActionExecuting(filterContext);
            }
        }
    

    In case you choose DI path then you can use the following snippet:

    public class DriverAccessActionFilterAttribute : ActionFilterAttribute
    {
        public string IdParamName { get; set; }
        private int DriverID { get; set; }
    
        public DriverAccessActionFilterAttribute(IYourIdentityProvider provider)
        {
            DriverID = provider.LoggedInUserID;
        }
    
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {            
            // Your logic
        }
    }
    

    and then use your attribute as [DriverAccessActionFilter(IdParamName = "id")]