43

I'm trying to create a custom ActionFilter which operates on a set of parameters that would be passed to it from the controller.

So far, my customer ActionFilter looks like this:

public class CheckLoggedIn : ActionFilterAttribute
{
    public IGenesisRepository gr { get; set; }
    public Guid memberGuid { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Member thisMember = gr.GetActiveMember(memberGuid);
        Member bottomMember = gr.GetMemberOnBottom();

        if (thisMember.Role.Tier <= bottomMember.Role.Tier)
        {
            filterContext
                .HttpContext
                .Response
                .RedirectToRoute(new { controller = "Member", action = "Login" });
        }

        base.OnActionExecuting(filterContext);
    }
}

I know I still need to check for nulls, etc. but I can't figure out why gr and memberGuid aren't successfully being passed. I'm calling this Filter like this:

    [CheckLoggedIn(gr = genesisRepository, memberGuid = md.memberGUID)]
    public ActionResult Home(MemberData md)
    {
        return View(md);
    }

genesisRepository and md are being set in the controller's constructor.

I'm not able to get this to compile. The error I get is:

Error   1   'gr' is not a valid named attribute argument because it is not a valid attribute parameter type
Error   2   'memberGuid' is not a valid named attribute argument because it is not a valid attribute parameter type

I double checked that gr and memberGuid were the same types as genesisRepority and md.memberGUID, What is causing these errors?

Solution

Thanks to jfar for offering a solution.

Here's the Filter I ended up using:

public class CheckLoggedIn : ActionFilterAttribute
{

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var thisController = ((MemberController)filterContext.Controller);

        IGenesisRepository gr = thisController.GenesisRepository;
        Guid memberGuid = ((MemberData)filterContext.HttpContext.Session[thisController.MemberKey]).MemberGUID;

        Member thisMember = gr.GetActiveMember(memberGuid);
        Member bottomMember = gr.GetMemberOnBottom();

        if (thisMember.Role.Tier >= bottomMember.Role.Tier)
        {
            filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary(
                    new { 
                        controller = "Member", 
                        action = "Login" 
                    }));
        }

        base.OnActionExecuting(filterContext);
    }
}
quakkels
  • 11,676
  • 24
  • 92
  • 149
  • Your question is super but solution is much more awesome than the question. I was looking a solution for sending parameters to global filter attributes. This like a painkiller for me :) – Serhat MERCAN Jan 03 '17 at 07:40

3 Answers3

40

This is a way to make this work. You have access to the ControllerContext and therefore Controller from the ActionFilter object. All you need to do is cast your controller to the type and you can access any public members.

Given this controller:

public GenesisController : Controller
{
    [CheckLoggedIn()]
    public ActionResult Home(MemberData md)
    {
        return View(md);
    }
}

ActionFilter looks something like

public class CheckLoggedIn : ActionFilterAttribute
{
    public IGenesisRepository gr { get; set; }
    public Guid memberGuid { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        /* how to get the controller*/
        var controllerUsingThisAttribute = ((GenesisController)filterContext.Controller);

        /* now you can use the public properties from the controller */
        gr = controllerUsingThisAttribute .genesisRepository;
        memberGuid = (controllerUsingThisAttribute .memberGuid;

        Member thisMember = gr.GetActiveMember(memberGuid);
        Member bottomMember = gr.GetMemberOnBottom();

        if (thisMember.Role.Tier <= bottomMember.Role.Tier)
        {
            filterContext
                .HttpContext
                .Response
                .RedirectToRoute(new { controller = "Member", action = "Login" });
        }

        base.OnActionExecuting(filterContext);
    }
}

Of course this is assuming the ActionFilter isn't used across multiple controllers and you're ok with the coupling. Another Option is to make a ICheckedLoggedInController interface with the shared properties and simply cast to that instead.

John Farrell
  • 24,673
  • 10
  • 77
  • 110
  • 1
    Well, this gets me a lot closer to a solution. thanks jfar. The actionFilter will be used across multiple controllers though. – quakkels Dec 03 '10 at 18:04
  • I'm having a lot o trouble getting the controller.`var thisController = ((MemberController)ControllerContext.Controller);` returns error: `An object reference is required for the non-static field, method, or property 'System.Web.Mvc.ControllerContext.Controller.get'` – quakkels Dec 03 '10 at 18:15
  • @quakkels - Oops, doing it off the top of my head. You get the controller via `filterContext.Controller`. Update the answer too. – John Farrell Dec 03 '10 at 18:58
  • Thanks... this accomplished what I needed and I'll post my code in the question... But since @Matthew Abbott answered the "why" of my question the check has to go to him. – quakkels Dec 03 '10 at 20:09
  • @quakkels, Uh Thanks? You didn't ask why, you asked how. Eh, whatever. – John Farrell Dec 03 '10 at 20:36
  • 3
    `[CheckLoggedIn(gr = genesisRepository, memberGuid = md.memberGUID)]` ??? How it works? as @DanP said, you can only use constant values for attribute properties. – Jalal Nov 04 '12 at 08:08
  • This answer in nonsence, you can't pass object instances into attribute properties, only constant can be passed. How this answer got 18 Upvotes?! – Tomas Nov 07 '14 at 11:51
  • 1
    I just noticed that as well. 18 upvotes for an answer that won't compile. I wonder if that's a record. – John Farrell Nov 08 '14 at 14:05
8

You can only use constant values for attribute properties; see a this page for a full explanation.

DanP
  • 6,310
  • 4
  • 40
  • 68
4

Attributes are essentially metadata added to a type. They can only use const values, instead of instance variables. In your case you are tying to pass in your instance variables of genisisRepository, etc. This will fail to compile as they are not compile time constants.

You should look into Dependency Injection for Action Filters to achieve this, typically using an IoC container.

Also, if your ActionFilter is performing a post ActionResult action, such as OnActionExecuted, you could probably get away with storing something in the route data:

public ActionResult Index()
{
  ControllerContext.RouteData.DataTokens.Add("name", "value");
  return View();
}
Matthew Abbott
  • 60,571
  • 9
  • 104
  • 129
  • +1 for providing further guidance...dependancy injection is the ideal solution in this case; service location is another (simpler) option to consider as well. – DanP Dec 03 '10 at 17:25
  • Well... I'm not sure that this is possible then. `md.memberGUID` is bound to the session and cannot be a const because the site will need access to update login data. – quakkels Dec 03 '10 at 17:29
  • because I don't want to create a dependency to a specific session key. I would rather that checking the global.aspx.cs would reveal the bind and a future developer could easily make changes – quakkels Dec 03 '10 at 17:33
  • Urmmm...why not just use a constant for that? You could pass that in as an attribute param... – DanP Dec 03 '10 at 17:34
  • 1
    Adding things into route data that aren't in the URL is a pretty horrible way to store data. That leads to hard to find bugs. -- Dependency Injection doesn't really help solve this issue. Anything technique that provides the repositories to the ActionFilter will work. Now of course DI is better to help stop the service location issues but its not THE solution, just one way of enabling the solution. Good article on how to achieve this: http://www.lostechies.com/blogs/jimmy_bogard/archive/2010/05/03/dependency-injection-in-asp-net-mvc-filters.aspx – John Farrell Dec 03 '10 at 17:42
  • 1
    You don't need IoC to inject it, but IoC might make it easier. You could declare the filter in your filter config, inject its dependencies into the constructor, and then register the filter. – crush Jun 19 '15 at 17:50