Automatically get ASP.NET Identity user with an assist from Ninject

How many times have you found yourself doing this:

[Authorize]
public async Task<ActionResult> Foo()
{
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        AuthenticationManager.SignOut();
        return RedirectToAction("Signin", "Account");
    }

    ...
}

Wouldn't it be so much nicer if you could just do:

[Authorize]
[GetUser]
public async Task<ActionResult> Foo(ApplicationUser user)
{
    ...
}

Well, thanks to the miracles of action filters and dependency injection, specifically via Ninject, now you can.

First, our action filter:

public class GetUserAttribute : ActionFilterAttribute
{
}

public class GetUserFilter : IActionFilter
{
    protected readonly ApplicationUserManager UserManager;
    protected readonly IAuthenticationManager AuthenticationManager;

    public GetUserFilter(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
    {
        this.UserManager = userManager;
        this.AuthenticationManager = authenticationManager;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var userId = filterContext.HttpContext.User.Identity.GetUserId();
        var user = AsyncHelper.RunSync<ApplicationUser>(() => UserManager.FindByIdAsync(userId));
        if (user == null)
        {
            AuthenticationManager.SignOut();
            filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary
                {
                    { "controller", "Account" },
                    { "action", "Signin" }
                }
            );
        }

        filterContext.ActionParameters["user"] = user;
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
    }
}

Let's walk through that. GetUserAttribute is just a placeholder. We need something we can decorate our action with that has a constructor without arguments. GetUserFilter is where the real meat happens, specifically in OnActionExecuting. It has a constructor that accepts the necessary dependencies, which is why we can't use this directly to decorate our action. The other method OnActionExecuted is merely required to implement the IActionFilter interface, and does nothing.

Annoyingly, action filters in MVC 5 and previous do not support async, and to add insult to injury, the UserManager.FindByIdAsync method has no sync version. That's where AsyncHelper comes in. This is something I stole from the Entity Framework 6 source. EF6 actually only implements async methods. They use AsyncHelper to run the async methods synchronously for the non-async methods. Unfortunately, AsyncHelper has internal visibility, so we can't just reference it from EF. Here's the source, though, so you can make use of it in your own projects:

public static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new
      TaskFactory(CancellationToken.None,
                  TaskCreationOptions.None,
                  TaskContinuationOptions.None,
                  TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return AsyncHelper._myTaskFactory
          .StartNew<Task<TResult>>(func)
          .Unwrap<TResult>()
          .GetAwaiter()
          .GetResult();
    }

    public static void RunSync(Func<Task> func)
    {
        AsyncHelper._myTaskFactory
          .StartNew<Task>(func)
          .Unwrap()
          .GetAwaiter()
          .GetResult();
    }
}

Finally, we just need our Ninject bindings. In App_Start\NinjectWebCommon.cs, find the RegisterServices method and add the following:

using Ninject.Web.Mvc;
using Ninject.Web.Mvc.FilterBindingSyntax;

...

kernel.BindFilter<GetUserFilter>(FilterScope.Controller, 0)
    .WhenActionMethodHas<GetUserAttribute>();    

This will get Ninject to replace our placeholder GetUser attribute with the actual GetUserFilter workhorse, injected with all the necessary dependencies. Obviously, you'll need to bind those dependencies, as well, but you should already be doing that if you're using Ninject. However, in case you haven't gotten that far, yet, you'll need add this to the same method:

using Microsoft.Owin.Security;

...

kernel.Bind<IAuthenticationManager>().ToMethod(c =>
    HttpContext.Current.GetOwinContext().Authentication).InRequestScope();
kernel.Bind<ApplicationUserManager>().ToMethod(c =>
    HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>()).InRequestScope();
comments powered by Disqus