ASP.NET MVC Filter Attributes

In the past, I've often needed to retrieve a current user object in the beginning of an action, and then use that object in updates to the model.

For example, say my application is a bug tracking application, and I want to create a new bug.

Previously, I would utilizing binding to map the posted form parameters to the new Issue object, (id is used to determine which sprint we will add the issue to). But simple using the DefaultModelBinder to bind is not enough, as we need to also add properties managed by the server, like TimeCreated, Status, Creator, etc.

[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult AddIssue(int? id, [Bind(Exclude = "Id")]Issue issue)
{
    if (id != null)
    {
        string username = System.Web.HttpContext.Current.User.Identity.Name;
        User currentUser = MembershipService.GetUserByUsername(username);
        Sprint sprint = ProjectService.GetSprintById(id.Value);
        if (currentUser != null && sprint != null)
        {
            try
            {
                issue.Creator = currentUser;
                issue.TimeCreated = DateTime.Now;
                issue.Status = IssueStatus.New.ToString();
                issue.Project = sprint.Project;
                issue.Sprint = sprint;
                ProjectService.CreateIssue(issue);
                return RedirectToAction("Details", "Sprint", new { id = id });
            }
            catch (InvalidModelException ex)
            {
                ModelState.AddDomainErrors(ex.Errors);
            }
        }
    }
    return View();
}

These two lines would be repeated often throughout an application and I think they clutter the action code a bit:

string username = System.Web.HttpContext.Current.User.Identity.Name;
User currentUser = MembershipService.GetUserByUsername(username);

So... what can we do to avoid doing this over and over again? Well, one way that we can do this is to create an ActionFilter Attribute.

Action Filters provide two methods to override, OnActionExecuting and OnActionExecuted.

OnActionExecuting is run when the action is called – The Authorization filter would use this, for example.

OnActionExecuted runs when the Action has finished executing – You could potentially use this for logging.

In each method, we are also provided with an ActionExecutingContext which gives us information about the Action's parameters, as well as the Controller's context. I intend to use this to "inject" the current user into the method, and avoid having the method get the user every time.

I created a simple ActionFilter and called it "BindCurrentUserAttribute". Remember that for attributes you don't need to type 'Attribute' out at the end when you use it, we will be able to use it like [BindCurrentUser].

public class BindCurrentUserAttribute : ActionFilterAttribute
{
    public string ParameterName { get; set; }
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!String.IsNullOrEmpty(ParameterName))
        {
            IMembershipService service = DependencyManager.Container.Resolve<IMembershipService>();
            string username = filterContext.Controller.ControllerContext.HttpContext.User.Identity.Name;
            User user = service.GetUserByUsername(username);
            if (filterContext.ActionParameters.Keys.Contains(ParameterName))
            {
                filterContext.ActionParameters[ParameterName] = user;
            }
        }
    }
}

Now, when this action is used in an Action, it will look like this:

[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
[BindCurrentUser(ParameterName = "currentUser")]
public ActionResult AddIssue(int? id, [Bind(Exclude = "Id")]Issue issue, User currentUser)
{
    if (id != null)
    {
        Sprint sprint = ProjectService.GetSprintById(id.Value);
        if (currentUser != null && sprint != null)
        {
            try
            {
                issue.Creator = currentUser;
                issue.TimeCreated = DateTime.Now;
                issue.Status = IssueStatus.New.ToString();
                issue.Project = sprint.Project;
                issue.Sprint = sprint;
                ProjectService.CreateIssue(issue);
                return RedirectToAction("Details", "Sprint", new { id = id });
            }
            catch (InvalidModelException ex)
            {
                ModelState.AddDomainErrors(ex.Errors);
            }
        }
    }
    return View();
}

By specifying ParameterName, we tell our filter which parameter to set to the current user. I think this makes it a bit cleaner and a lot more reusable.. now all we need to do to get the current user object is add an attribute to our action.

Comments...

Like to leave a comment ?


Title

Comment

Name