Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Dependency Injection inside Startup.Configure

I am using the Cookie Middleware to authenticate the user. I have been following this official tutorial.

Inside my Startup class, an excerpt from my Configure method looks like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
  // ...

  // Cookie-based Authentication
  app.UseCookieAuthentication(new CookieAuthenticationOptions()
  {
    AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,        
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    Events = new CustomCookieAuthenticationEvents(app),
  });

  // ...
}

The CustomCookieAuthenticationEvents class is defined as follows:

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
  private IApplicationBuilder _app;
  private IMyService _myService = null;
  private IMyService MyService
  {
    get
    {
      if(_myService != null)
      {
        return _myService;
      } else
      {
        return _myService = (IMyService) _app.ApplicationServices.GetService(typeof(IMyService));
      }
    }
  }

  public CustomCookieAuthenticationEvents(IApplicationBuilder app)
  {
    _app = app;
  }

  public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
  {
    string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
    LogonSession response = null;

    var response = await MyService.CheckSession(sessionToken);

    if (response == null)
    {
      context.RejectPrincipal();
      await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }
  }
}

Since the dependency injection is not available at Startup.Configure (the services are not even registered at that point), I made a bit of a workaround:

  1. Pass IApplicationBuilder service to the CustomCookieAuthenticationEvents class
  2. Fetch IMyService upon first request inside a read-only property (singleton pattern)

tl;dr

My solution works, but it's ugly. There is no dependency injection involved, as it is not possible at that time.

The essence of the problem is that I must instantiate CustomCookieAuthenticationEvents. As far as I have read the source code, there is no way around this, because the UseCookieAuthentication throws an exception if I omit the options parameter.

Any suggestion how can one make my current solution nicer?

like image 520
alesc Avatar asked Apr 14 '17 11:04

alesc


2 Answers

Startup.ConfigureServices() is called before Startup.Configure() (see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup for more information). So Dependency Injection is available at that time ;)
As a consequence, you can resolve your dependence in your configure method like this:

app.ApplicationServices.GetRequiredService<CustomCookieAuthenticationEvents>()
like image 115
arnaudauroux Avatar answered Sep 23 '22 06:09

arnaudauroux


You should be really careful when you resolve services inside middleware. Your current approach (and the one suggested by @arnaudauroux) can result in difficulties when you use/need/require scoped services (i.e. usage of DbContext).

Resolving via app.ApplicationServices results in static (singleton) services, when the service is registered as scoped (transient are resolved per call, so they are not affected). It would be better to resolve your service during the request from HttpContext inside ValidatePrincipal method.

public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
    string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
    LogonSession response = null;

    var myService = context.HttpContext.RequestServices.GetService<IMyService >();
    var response = await myService.CheckSession(sessionToken);

    if (response == null)
    {
        context.RejectPrincipal();
        await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }
}

With this approach you don't need to pass any dependencies inside your CustomCookieAuthenticationEvents class at all. HttpContext.RequiredServices is made specifically for such classes (any other can be solved via constructor injection, but not middleware and http context related pipeline, as there is no other otherway to correctly resolve scoped services in middlewares - Middleware instance is static and only instantiated once per request)

This way you won't have lifetime issues with your scoped services. When you resolve transient services, they will be disposed at the end of request. Whereas transient services resolved via app.ApplicationServices will be resolved at some point in future after the request is finished and when garbage collection triggers (means: your resources will be freed at the earliest possible moment, which is when the request ends).

like image 43
Tseng Avatar answered Sep 25 '22 06:09

Tseng