Davide Vitali
Davide Vitali

Reputation: 1035

How to inject HttpContextAccessor directly from ConfigureServices method

My goal is to set a username string based on the environment I'll be working on that must be:

This is because I have to be able to simulate different kind of users and I achieve this by calling the FindByIdAsync method on my custom implementation of UserIdentity using this username string as a parameter, like this:

public class HomeController : Controller
{
    UserManager<AppUser> userManager;
    AppUser connectedUser;

    public HomeController(UserManager<AppUser> usrMgr, IContextUser ctxUser)
    {
        connectedUser = usrMgr.FindByNameAsync(ctxUser.ContextUserId).Result;
    }
}

I started creating three appsettings.{environment}.json file for the three usual development, staging and production environments; development and staging .json files both have this configuration:

...
"Data": {
  ...
  "ConnectedUser" : "__ADMIN"
}
...

while the production environment configuration file doesn't have this key.

I have created a simple interface

public interface IContextUser
{
    public string ContextUserId { get; }
}

and its implementation:

public class ContextUser : IContextUser
{
    string contextUser;
    IHttpContextAccessor contextAccessor;

    public ContextUser(IHttpContextAccessor ctxAccessor, string ctxUser = null)
    {
        contextUser = ctxUser;
        contextAccessor = ctxAccessor;
    }

    public string ContextUserId => contextUser ?? contextAccessor.HttpContext.User.Identity.Name;
}

Now, I thought of simply configuring the ConfigureServices method in the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    // --- add other services --- //

    string ctxUser = Configuration["Data:ConnectedUser"];
    services.AddSingleton(service => new ContextUser( ??? , ctxUser ));

}

but it needs an IHttpContextAccessor object, that seems unavailable at this stage of the application. How can I solve this issue?

Upvotes: 3

Views: 10054

Answers (1)

Steven
Steven

Reputation: 172646

The HttpContextAccessor makes use of a static AsyncLocal<T> property under the covers, which means that any HttpContextAccessor implementation will access the same data. This means you can simply do the following:

services.AddSingleton(c => new ContextUser(new HttpContextAccessor(), ctxUser));

// Don't forget to call this; otherwise the HttpContext property will be
// null on production.
services.AddHttpContextAccessor();

If you find this too implicit, or don't the HttpContextAccessor implementation from breaking in the future, you can also do the following:

var accessor = new HttpContextAccessor();

services.AddSingleton<IHttpContextAccessor>(accessor);
services.AddSingleton(c => new ContextUser(accessor, ctxUser));

Or you can "pull out" the registered instance out of the ServiceCollection class:

services.AddHttpContextAccessor();

var accessor = (IHttpContextAccessor)services.Last(
    s => s.ServiceType == typeof(IHttpContextAccessor)).ImplementationInstance;

services.AddSingleton(c => new ContextUser(accessor, ctxUser));

What I find a more pleasant solution, however, especially from a design perspective, is to split the ContextUser class; it currently seems to implement two different solutions. You can split those:

public sealed class HttpContextContextUser : IContextUser
{
    private readonly IHttpContextAccessor accessor;

    public HttpContextContextUser(IHttpContextAccessor accessor) =>
        this.accessor = accessor ?? throw new ArgumentNullException("accessor");

    public string ContextUserId => this.accessor.HttpContext.User.Identity.Name;
}


public sealed class FixedContextUser : IContextUser
{
    public FixedContextUser(string userId) =>
        this.ContextUserId = userId ?? throw new ArgumentNullException("userId");

    public string ContextUserId { get; }
}

Now, depending on the environment you're running in, you register either one of them:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();

    if (this.Configuration.IsProduction())
    {
        services.AddSingleton<IContextUser, HttpContextContextUser>();
    }
    else
    {
        string ctxUser = Configuration["Data:ConnectedUser"];
        services.AddSingleton<IContextUser>(new FixedContextUser(ctxUser));
    }
}

Upvotes: 10

Related Questions