Reputation: 1035
My goal is to set a username string based on the environment I'll be working on that must be:
HttpContext.User.Identity.Name
in production.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
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