Autofac: ITenantIdentificationStrategy with RouteValues - asp.net-core

I'm having issues making multitenancy work. I've tried to follow the sample here and can't see what my implementation is doing differently.
The tenants are identified by a routing parameter in the address field. This seems to work without issues (calling TryIdentifyTenant returns the correct one). I am using ASP.NET Core 3.1, together with Autofac.AspNetCore-Multitenant v3.0.1 and Autofac.Extensions.DependencyInjection v6.0.0.
I have made a simplification of the code (which is tested and still doesn't work). Two tenants are configured, "terminal1" and "terminal2". The output should differ depending on the tenant. However, it always returns the base implementation. In the example below, inputing "https://localhost/app/terminal1" returns "base : terminal1" and "https://localhost/app/terminal2" returns "base : terminal2". It should return "userhandler1 : terminal1" and "userhandler2 : terminal2".
HomeController:
public class HomeController : Controller
{
private readonly IUserHandler userHandler;
private readonly TerminalResolverStrategy terminalResolverStrategy;
public HomeController(IUserHandler userHandler, TerminalResolverStrategy terminalResolverStrategy)
{
this.userHandler = userHandler;
this.terminalResolverStrategy = terminalResolverStrategy;
}
public string Index()
{
terminalResolverStrategy.TryIdentifyTenant(out object tenant);
return userHandler.ControllingVncUser + " : " + (string)tenant;
}
}
UserHandler:
public interface IUserHandler
{
public string ControllingVncUser { get; set; }
}
public class UserHandler : IUserHandler
{
public UserHandler()
{
ControllingVncUser = "base";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler1 : IUserHandler
{
public UserHandler1()
{
ControllingVncUser = "userhandler1";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler2 : IUserHandler
{
public UserHandler2()
{
ControllingVncUser = "userhandler2";
}
public string ControllingVncUser { get; set; }
}
Startup:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddControllersWithViews();
services.AddAutofacMultitenantRequestServices();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<TerminalResolverStrategy>();
builder.RegisterType<UserHandler>().As<IUserHandler>();
}
public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
{
var strategy = new TerminalResolverStrategy(
container.Resolve<IOptions<TerminalAppSettings>>(),
container.Resolve<IHttpContextAccessor>());
var mtc = new MultitenantContainer(strategy, container);
mtc.ConfigureTenant("terminal1", b => b.RegisterType<UserHandler1>().As<IUserHandler>());
mtc.ConfigureTenant("terminal2", b => b.RegisterType<UserHandler2>().As<IUserHandler>());
return mtc;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
loggerFactory.AddLog4Net();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{terminal}/{controller=Home}/{action=Index}/{id?}");
});
}
}
Program:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
ITenantIdentificationStrategy:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
public IHttpContextAccessor Accessor { get; private set; }
private readonly TerminalAppSettings settings;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
Accessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
HttpContext httpCtx = Accessor.HttpContext;//
terminal = null;
try
{
if (httpCtx != null &&
httpCtx.Request != null &&
httpCtx.Request.RouteValues != null &&
httpCtx.Request.RouteValues.ContainsKey("terminal"))
{
string requestedTerminal = httpCtx.Request.RouteValues["terminal"].ToString();
bool terminalExists = settings.Terminals.ContainsKey(requestedTerminal);
if (terminalExists)
{
terminal = requestedTerminal;
}
}
}
catch (Exception) {}
return terminal != null;
}
}
}
What am i doing wrong? Thanks in advance.

"Multitenancy doesn't seem to work at all" is a somewhat ambiguous statement that's hard to address. Unfortunately, I don't personally have the time to download all of your example code and try to repro the whole thing and debug into it and see exactly what's wrong. Perhaps someone else does. However, I can offer some tips as to places I'd look and things I'd try to see what's up.
Tenant ID strategy set up twice. I see in Startup.ConfigureContainer that there's a builder.RegisterType<TerminalResolverStrategy>(); line - that's going to register your strategy type as instance-per-dependency, so every time it's needed it'll be resolved fresh. I also see in Startup.ConfigureMultitenantContainer that you're manually instantiating the strategy that gets used by the multitenant container. There's a non-zero possibility that something is getting messed up there. I would pick one way to get that done - either register the strategy or manually create it - and I'd make sure that thing is a stateless singleton. (It's not registered in the example.)
Route pattern possibly questionable. I see the route pattern you have registered looks like this: {terminal}/{controller=Home}/{action=Index}/{id?}. I also see your URLs look like this: https://localhost/app/terminal1 Have you stepped into your tenant ID strategy to make sure the route parsing mechanism works right? That is, app isn't being picked up as the terminal value? Route parsing/handling can be tricky.
Possibly bad settings. The tenant ID strategy only successfully identifies a tenant if there are options that specify that the specific terminal value exists. I don't see where any of those options are configured, which means in this repo there are no tenants defined. Your strategy won't identify anything without that.
If it was me, I'd probably start with a breakpoint in that tenant ID strategy and see what's getting resolved and what's not. It seems somewhat complex from an outside perspective and that's where I'd begin. If that is working, then I'd probably also look at cleaning up the registrations so the ID strategy isn't registered twice. Finally, I get the impression that this isn't all the code in the app; I'd probably look at making a super minimal reproduction that's about the size you actually have posted here. I'd then focus on making that minimal repro work; then once it works, I'd figure out what the difference is between the repro and my larger app.
Unfortunately, that's about all I can offer you. As I mentioned, with the current environment and my current workload, I won't be able to actually sit down and reproduce the whole thing with your code. I know the integration works because I have production apps using it; and there are a lot of tests (unit and integration) to validate it works; so the part that isn't working is likely in your code somewhere... and those are the places I'd start.

So after having identified the tenant identification as the problem, it seems like RouteValues isn't resolved with the HttpContext until later in the request chain. Thus, no tenant gets resolved. It seems to me like a bug in .NET Core. The problem got bypassed by using the request path instead:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
private readonly TerminalAppSettings settings;
private readonly IHttpContextAccessor httpContextAccessor;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
this.httpContextAccessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
var httpCtx = httpContextAccessor.HttpContext;
terminal = null;
try
{
if (httpCtx != null)
{
string thisPath = httpCtx.Request.Path.Value;
var allTerminals = settings.Terminals.GetEnumerator();
while (allTerminals.MoveNext())
{
if (thisPath.Contains(allTerminals.Current.Key)) {
terminal = allTerminals.Current.Key;
return true;
}
}
}
}
catch (Exception) { }
return false;
}
}

Related

.NET Core Middleware - access IApplicationBuilder in a controller?

I need to access IApplicationBuilder inside a controller.
What I have tried :-
I have written middleware (app.UseMyMiddleware) as follows
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMyMiddleware();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
///TODO - Pass IApplicationBuilder to HttpContext
await _next(context);
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
but I can't figure out how I can pass IApplicationBuilder to HttpContext in Invoke method. so, that I can use it in a controller.
I have also referred following stackoverflow question-answer
how to access IApplicationBuilder in a controller?
.Net Core Middleware - Getting Form Data from Request
Question(s) :-
How can pass IApplicationBuilder to HttpContext in Invoke method to use it in controller?
Is there any better way to access IApplicationBuilder inside controller apart from middleware?
IApplicationBuilder was not designed to work the way you want it to. Instead, if you have some data created at build time that you want to be available to middleware add a Singleton to the services and inject the singleton into the middleware.
You cannot access IApplicationBuilder anywhere later after completing the application building phase (after running Configure method). It's not available for injection at all.
However for the purpose of plugging-in or configuring middlewares at runtime based on request data (from HttpContext), you can use .UseWhen. Another one for terminal middleware is .MapWhen but I think that's not for your case. Here is an example of .UseWhen:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
var allOptions = new [] {"option 1","option 2"};
foreach(var option in allOptions){
var currentOption = option;
builder.UseWhen(context => {
//suppose you can get the user's selected option from query string
var selectedOption = context.Request.Query["option_key"];
return selectedOption == currentOption;
}, app => {
//your MyMiddleware is supposed to accept one argument
app.UseMiddleware<MyMiddleware>(currentOption);
});
}
return builder;
}
}
To simplify it I suppose your options are just strings, you must know beforehand all possible options that the user can select via UI. Each one will be an exact match for the condition to plug-in a middleware and they must be all exclusive (so just one of them can enable one corresponding middleware), otherwise there will be duplicate middlewares, which may cause some issue.
By expressing the foreach above more clearly, it may represent something as follows:
//kind of pseudo code
if(selectedOption1){
app.UseMiddleware<MyMiddleware>("option 1");
} else if(selectedOption2){
app.UseMiddleware<MyMiddleware>("option 2");
}
...
You must decide how you get the selected option from the user (in the example above I get it from query string). You can get it from Cookie as well (to remember the user's selection) or from other sources such as route data, headers, form, request body. I think that's another issue, so if you have problem with that, please ask in another question.
First up all thanks to #Kingking and #GlennSills for there solution and valuable comments.
I have solved this problem as
Created one class which inherit from Hangfire.JobStorage as follows
public class HangfireSqlServerStorageExtension : Hangfire.JobStorage
{
private readonly HangfireSqlServerStorage _hangfireSqlServerStorage = new HangfireSqlServerStorage();
public HangfireSqlServerStorageExtension(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = new SqlServerStorageOptions();
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public HangfireSqlServerStorageExtension(string nameOrConnectionString, SqlServerStorageOptions options)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = options;
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public void UpdateConnectionString(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public override IStorageConnection GetConnection()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetConnection();
}
public override IMonitoringApi GetMonitoringApi()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetMonitoringApi();
}
}
HangfireSqlServerStorage.cs
Used in HangfireSqlServerStorageExtension class above
public class HangfireSqlServerStorage
{
public SqlServerStorage SqlServerStorage { get; set; }
public SqlServerStorageOptions SqlServerStorageOptions { get; set; }
}
Startup.cs
In Startup file add singleton service for HangfireSqlServerStorageExtension instance and configure hangfire dashboard as follows
public class Startup
{
///Other necessary code here
public static HangfireSqlServerStorageExtension HangfireSqlServerStorageExtension { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
///Other necessary code here
HangfireSqlServerStorageExtension = new HangfireSqlServerStorageExtension("DBConnecttionString"));
services.AddSingleton<HangfireSqlServerStorageExtension>(HangfireSqlServerStorageExtension);
services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
//Other necessary code here
app.UseHangfireDashboard("/Dashboard", new DashboardOptions(), HangfireSqlServerStorageExtension);
//Other necessary code here
}
}
Inside controller I have used it as follows
HangfireController.cs
public class HangfireController : Controller
{
protected readonly HangfireSqlServerStorageExtension
hangfireSqlServerStorageExtension;
public HangfireController(HangfireSqlServerStorageExtension hangfireSqlServerStorageExtension)
{
this.hangfireSqlServerStorageExtension = hangfireSqlServerStorageExtension;
}
public IActionResult DisplayHangfireDashboard()
{
// Update connString as follows
hangfireSqlServerStorageExtension.UpdateConnectionString(connString);
var hangfireDashboardUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}" + "/Dashboard";
return Json(new { url = hangfireDashboardUrl });
}
}

How to make an IOptions section optional in .NET Core?

Consider an example service that optionally supports LDAP authentication, otherwise, it does something like local Identity authentication. When LDAP is completely configured, appsettings.json might look like this...
{
"LdapOptions": {
"Host": "ldap.example.com",
"Port": 389
}
}
With an options class.
public class LdapOptions
{
public string Host { get; set; }
public int Port { get; set; } = 389;
}
And Startup has the expected Configure call.
service.Configure<LdapOptions>(nameof(LdapOptions));
This work great when I have a complete valid "LdapOptions" section. But, it's not so great if I intentionally leave the section out of my appsettings.
An IOptions<TOptions> instance resolves even if I leave the section out of my appsettings entirely; it even resolves if I remove the Startup configure call entirely! I get an object that appears, based on property values, to be default(TOptions).
public AuthenticationService(IOptions<LdapOptions> ldapOptions)
{
this.ldapOptions = ldapOptions.Value; // never null, sometimes default(LdapOptions)!
}
I don't want to depend on checking properties if a section is intentionally left out. I can imagine scenarios where all of the properties in an object have explicit defaults and this wouldn't work. I'd like something like a Maybe<TOptions> with a HasValue property, but I'll take a null.
Is there any way to make an options section optional?
Update: Be aware that I also intend to validate data annotations...
services.AddOptions<LdapOptions>()
.Configure(conf.GetSection(nameof(LdapOptions)))
.ValidateDataAnnotations();
So, what I really want is for optional options to be valid when the section is missing (conf.Exists() == false) and then normal validations to kick in when the section is partially or completely filled out.
I can't imagine any solution working with data annotation validations that depends on the behavior of creating a default instance (for example, there is no correct default for Host, so a default instance will always be invalid).
The whole idea of IOptions<T> is to have non-null default values, so that your settings file doesn't contain hundreds/thousands sections to configure the entire ASP pipeline
So, its not possible to make it optional in the sense that you will get null, but you can always defined some "magic" property to indicate whether this was configured or not:
public class LdapOptions
{
public bool IsEnabled { get; set; } = false;
public string Host { get; set; }
public int Port { get; set; } = 389;
}
and your app settings file:
{
"LdapOptions": {
"IsEnabled: true,
"Host": "ldap.example.com",
"Port": 389
}
}
Now, if you keep 'IsEnabled' consistently 'true' in your settings, if IsEnabled is false, that means the section is missing.
An alternative solution is to use a different design approach, e.g. put the auth type in the settings file:
public class LdapOptions
{
public string AuthType { get; set; } = "Local";
public string Host { get; set; }
public int Port { get; set; } = 389;
}
And your app settings:
{
"LdapOptions": {
"AuthType : "LDAP",
"Host": "ldap.example.com",
"Port": 389
}
}
This is IMO a cleaner & more consistent approach
If you must have a logic that is based on available/missing section, you can also configure it directly:
var section = conf.GetSection(nameof(LdapOptions));
var optionsBuilder = services.AddOptions<LdapOptions>();
if section.Value != null {
optionsBuilder.Configure(section).ValidateDataAnnotations();
}
else {
optionsBuilder.Configure(options => {
// Set defaults here
options.Host = "Deafult Host";
}
}
I wanted to avoid lambdas in Startup that would need to be copy/pasted correctly for every "optional" section and I wanted to be very explicit about optionality (at the expense of some awkward naming).
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddOption<Optional<LdapOptions>>()
.ConfigureOptional(conf.GetSection(nameof(LdapOptions)))
.ValidateOptionalDataAnnotations();
}
The Optional type is pretty straightforward, but may need a better name (to avoid interfering with other implementations of the generic Option/Some/Maybe pattern). I thought about just using null, but that seemed contrary to Options insistence on returning something no matter what.
Optional.cs
public class Optional<TOptions> where TOptions : class
{
public TOptions Value { get; set; }
public bool HasValue { get => !(Value is null); }
}
The configure extension method takes into account section existence.
OptionalExtensions.cs
public static class OptionalExtensions
{
public static OptionsBuilder<Optional<TOptions>> ConfigureOptional<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder, IConfigurationSection config) where TOptions : class
{
return optionsBuilder.Configure(options =>
{
if (config.Exists())
{
options.Value = config.Get<TOptions>();
}
});
}
public static OptionsBuilder<Optional<TOptions>> ValidateOptionalDataAnnotations<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddSingleton<IValidateOptions<Optional<TOptions>>>(new DataAnnotationValidateOptional<TOptions>(optionsBuilder.Name));
return optionsBuilder;
}
}
The validate extension method works with a custom options validator that also takes into account how missing sections work (like the comment says, "missing optional options are always valid").
DataAnnotationValidateOptional.cs
public class DataAnnotationValidateOptional<TOptions> : IValidateOptions<Optional<TOptions>> where TOptions : class
{
private readonly DataAnnotationValidateOptions<TOptions> innerValidator;
public DataAnnotationValidateOptional(string name)
{
this.innerValidator = new DataAnnotationValidateOptions<TOptions>(name);
}
public ValidateOptionsResult Validate(string name, Optional<TOptions> options)
{
if (options.Value is null)
{
// Missing optional options are always valid.
return ValidateOptionsResult.Success;
}
return this.innerValidator.Validate(name, options.Value);
}
}
Now, anywhere you need to use an optional option, like, say, a login controller, you can take the following actions...
LdapLoginController.cs
[ApiController]
[Route("/api/login/ldap")]
public class LdapLoginController : ControllerBase
{
private readonly Optional<LdapOptions> ldapOptions;
public LdapLoginController(IOptionsSnapshot<Optional<LdapOptions>> ldapOptions)
{
// data annotations should trigger here and possibly throw an OptionsValidationException
this.ldapOptions = ldapOptions.Value;
}
[HttpPost]
public void Post(...)
{
if (!ldapOptions.Value.HasValue)
{
// a missing section is valid, but indicates that this option was not configured; I figure that relates to a 501 Not Implemented
return StatusCode((int)HttpStatusCode.NotImplemented);
}
// else we can proceed with valid options
}
}

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:
an arbitrary string for the development and staging environment
the 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?
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));
}
}

how to handle DbContext lifetime in API / backgroundworker

the past days I have been struggling with injecting a DbContext in MY background worker. On the one hand, I want to inject the dbContext in my backgorund worker, but on the other hand I also want to use it in my API.
The injecting in my API seems to work fine, but since my worker is a singleton, I can not follow the standard lifetime of scoped for my dbcontext, and I have to add it as transient.
I have already tried to create a unit of work, in which I can refresh the context myself in my worker, effectively creating some kind of scoped service. I would refresh the context every time the worker went through his loop once again. This worked, and the application was working as I wanted, but I was no longer able to properly test, since I would create a new DbContext myself in the code. I feel like there must be a better way for to handle this.
My project structure looks like the following:
API => contains controlers + models I use for post requests.
The API project needs to use my database, to get and post data. It uses the repositories for this
Core (class library) => contains some core models
Domain(class library) => Contains my domain models + repositories.
All database work goes through here
Worker => Contains some logic.
The worker needs to use my database, to get and post data. It uses the repositories for this
Services (class library) => Some services that contain some logic.
The worker uses my repositories to get to the database
Tests => Tests for all code.
I want to be able to to integrationTesting as well here.
I currently inject all repositories and services in both my API and worker:
Worker configureservices:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddDbContext<CatAPIDbContext>(ServiceLifetime.Transient);
services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
services.AddTransient(typeof(IMotorController), typeof(MotorController));
services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient(typeof(IFeedingTimeChecker), typeof(FeedingTimeChecker));
services.AddHostedService<Worker>();
});
(EDIT)Worker code:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public IFeedingTimeChecker _feedingTimeChecker { get; }
public Worker(ILogger<Worker> logger, IFeedingTimeChecker feedingTimeChecker)
{
_logger = logger;
_feedingTimeChecker = feedingTimeChecker;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
_feedingTimeChecker.ResetFeedingTimesGivenIfNeeded();
_feedingTimeChecker.FeedIfNeeded();
}
catch(Exception ex)
{
_logger.LogError(ex.ToString());
}
await Task.Delay(10000, stoppingToken);
}
}
}
(EDIT)FeedingTimeChecker (called by worker)
private FeedingProfile _currentProfile { get; set; }
public DateTime lastResetDataFeedingTimes;
public DateTime lastProfileRefresh;
private readonly ILogger<FeedingTimeChecker> _logger;
private IFeedingProfileRepository _feedingProfileRepository { get; set; }
private IFeedingTimesRepository _feedingTimesRepository { get; set; }
private IFoodDispenser _foodDispenser { get; }
public FeedingTimeChecker(IFeedingProfileRepository feedingProfileRepository, IFeedingTimesRepository feedingTimesRepository,IFoodDispenser foodDispenser, ILogger<FeedingTimeChecker> logger)
{
lastResetDataFeedingTimes = DateTime.MinValue.Date;
lastProfileRefresh = DateTime.MinValue.Date;
_foodDispenser = foodDispenser;
_logger = logger;
_feedingTimesRepository = feedingTimesRepository;
_feedingProfileRepository = feedingProfileRepository;
}
public void UpdateCurrentProfile()
{
if(Time.GetDateTimeNow - TimeSpan.FromSeconds(5) > lastProfileRefresh)
{
_logger.LogInformation("Refreshing current profile");
_currentProfile = _feedingProfileRepository.GetCurrentFeedingProfile();
lastProfileRefresh = Time.GetDateTimeNow;
}
}
API configureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddDbContext<CatAPIDbContext>();
services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
services.AddTransient(typeof(IMotorController), typeof(MotorController));
services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
}
in my repositories I use the dbContext like the following:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
public CatAPIDbContext _dbContext { get; set; }
public GenericRepository(CatAPIDbContext dbContext)
{
_dbContext = dbContext;
}
public T GetById(object id)
{
return _dbContext.Set<T>().Find(id);
}
}
The result I would expect, is for my worker and API to behave correctly, always getting the lastest data and disposing of the dbContext on every single request, since I use a transient lifetime for my dbContext.
However, in my worker, I always get the following error:
The instance of entity type 'FeedingTime' cannot be tracked because another instance another instance of this type with the same key is already being tracked.
This error occurs when I try to set a column in the FeedingTime table.
A feedingProfile has 0-many feedingTimes, and the feedingProfile constantly retrieved.
Any solution where I can keep a testable clean codebase, but yet not run into this problem would be very welcome.
Thanks in advance

Managing RavenDB IDocumentSession lifecycles with StructureMap for NServiceBus and MVC

I am using NServiceBus v4.3, MVC4, RavenDB 2.5 and StructureMap 2.6.4 in our solution.
I am having a similar issue under StructureMap to that described in this question's responses where I require different lifecycles for the MVC Controller and NServiceBus Handler use of RavenDB's IDocumentSession in my Web project.
Specifically in my case what happens is that if I use the HybridHttpOrThreadLocalScoped (as the above answer suggests for Windsor) lifecycle the sessions are not properly disposed of and I soon hit the 30 transaction limit error. If I use the HttpContext lifecycle my NSB event Handlers in the Web project do not get called.
In my Controllers the session is wrapped in a unit of work applied via an MVC ActionFilter. I also use the UoW within the Handlers as my Registry is wired up to retrieve the session from the UoW. The code is as such:
RavenDbWebRegistry.cs
public sealed class RavenDbWebRegistry : Registry
{
public RavenDbWebRegistry()
{
// register RavenDB document store
ForSingletonOf<IDocumentStore>().Use(() =>
{
var documentStore = new DocumentStore
{
ConnectionStringName = "RavenDB",
Conventions =
{
IdentityPartsSeparator = "-",
JsonContractResolver = new PrivatePropertySetterResolver(),
},
};
documentStore.Initialize();
return documentStore;
});
For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Add(ctx =>
{
var uow = (IRavenDbUnitOfWork)ctx.GetInstance<IUnitOfWork>();
return uow.DocumentSession;
});
For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<WebRavenDbUnitOfWork>();
}
}
Example of Web project Handler:
public class SiteCreatedEventHandler : IHandleMessages<ISiteCreatedEvent>
{
public IBus Bus { get; set; }
public IUnitOfWork Uow { get; set; }
public IDocumentSession DocumentSession { get; set; }
public void Handle(ISiteCreatedEvent message)
{
try
{
Debug.Print(#"{0}{1}", message, Environment.NewLine);
Uow.Begin();
var site = DocumentSession.Load<Site>(message.SiteId);
Uow.Commit();
//invoke Hub and push update to screen
var context = GlobalHost.ConnectionManager.GetHubContext<AlarmAndNotifyHub>();
//TODO make sure this SignalR function is correct
context.Clients.All.displayNewSite(site, message.CommandId);
context.Clients.All.refreshSiteList();
}
catch (Exception ex)
{
Uow.Rollback();
}
}
}
Usage of ActionFilter:
[RavenDbUnitOfWork]
public ViewResult CreateNew(int? id)
{
if (!id.HasValue || id.Value <= 0)
return View(new SiteViewModel { Guid = Guid.NewGuid() });
var targetSiteVm = MapSiteToSiteViewModel(SiteList(false)).FirstOrDefault(s => s.SiteId == id.Value);
return View(targetSiteVm);
}
WebRegistry (that sets up NSB in my MVC project)
public sealed class WebRegistry : Registry
{
public WebRegistry()
{
Scan(x =>
{
x.TheCallingAssembly();
x.Assembly("IS.CommonLibrary.ApplicationServices");
x.LookForRegistries();
});
IncludeRegistry<RavenDbWebRegistry>();
FillAllPropertiesOfType<IUnitOfWork>();
FillAllPropertiesOfType<IDocumentSession>();
FillAllPropertiesOfType<StatusConversionService>();
FillAllPropertiesOfType<IStateRepository<TieState>>();
FillAllPropertiesOfType<IStateRepository<DedState>>();
FillAllPropertiesOfType<ITieService>();
FillAllPropertiesOfType<IDedService>();
FillAllPropertiesOfType<IHbwdService>();
//NServiceBus
ForSingletonOf<IBus>().Use(
NServiceBus.Configure.With()
.StructureMapBuilder()
.DefiningCommandsAs(t => t.Namespace != null && t.Namespace.EndsWith("Command"))
.DefiningEventsAs(t => t.Namespace != null && t.Namespace.EndsWith("Event"))
.DefiningMessagesAs(t => t.Namespace == "Messages")
.RavenPersistence("RavenDB")
.UseTransport<ActiveMQ>()
.DefineEndpointName("IS.Argus.Web")
.PurgeOnStartup(true)
.UnicastBus()
.CreateBus()
.Start(() => NServiceBus.Configure.Instance
.ForInstallationOn<Windows>()
.Install())
);
//Web
For<HttpContextBase>().Use(() => HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current));
For<ModelBinderMappingDictionary>().Use(GetModelBinders());
For<IModelBinderProvider>().Use<StructureMapModelBinderProvider>();
For<IFilterProvider>().Use<StructureMapFilterProvider>();
For<StatusConversionService>().Use<StatusConversionService>();
For<ITieService>().Use<TieService>();
For<IDedService>().Use<DedService>();
For<IHbwdService>().Use<HbwdService>();
For<ISiteService>().Use<SiteService>();
IncludeRegistry<RedisRegistry>();
}
I have tried configuring my Registry using every possible combination I can think of to no avail.
Given that the StructureMap hybrid lifecycle does not work as I would expect, what must I do to achieve the correct behaviour?
Is the UoW necessary/beneficial with RavenDB? I like it (having adapted it from my earlier NHibernate UoW ActionFilter) because of the way it manages the lifecycle of my sessions within Controller Actions, but am open to other approaches.
What I would ideally like is a way to - within the Web project - assign entirely different IDocumentSessions to Controllers and Handlers, but have been unable to work out any way to do so.
Firstly, RavenDB already implements unit of work by the wrapping IDocumentSession, so no need for it. Opening a session, calling SaveChanges() and disposing has completed the unit of work
Secondly, Sessions can be implemented in a few ways for controllers.
The general guidance is to set up the store in the Global.asax.cs. Since there is only 1 framework that implements IDocumentSession - RavenDB, you might as well instantiate it from the Global. If it was NHibernate or Entity Framework behind a repository, I'd understand. But IDocumentSession is RavenDB specific, so go with a direct initialization in the Application_Start.
public class Global : HttpApplication
{
public void Application_Start(object sender, EventArgs e)
{
// Usual MVC stuff
// This is your Registry equivalent, so insert it into your Registry file
ObjectFactory.Initialize(x=>
{
x.For<IDocumentStore>()
.Singleton()
.Use(new DocumentStore { /* params here */ }.Initialize());
}
public void Application_End(object sender, EventArgs e)
{
var store = ObjectFactory.GetInstance<IDocumentStore>();
if(store!=null)
store.Dispose();
}
}
In the Controllers, add a base class and then it can open and close the sessions for you. Again IDocumentSession is specific to RavenDB, so dependency injection doesn't actually help you here.
public abstract class ControllerBase : Controller
{
protected IDocumentSession Session { get; private set; }
protected override void OnActionExecuting(ActionExecutingContext context)
{
Session = ObjectFactory.GetInstance<IDocumentStore>().OpenSession();
}
protected override void OnActionExecuted(ActionExecutedContext context)
{
if(this.IsChildAction)
return;
if(content.Exception != null && Session != null)
using(context)
Session.SaveChanges();
}
}
Then from there, inherit from the base controller and do your work from there:
public class CustomerController : ControllerBase
{
public ActionResult Get(string id)
{
var customer = Session.Load<Customer>(id);
return View(customer);
}
public ActionResult Edit(Customer c)
{
Session.Store(c);
return RedirectToAction("Get", c.Id);
}
}
Finally, I can see you're using StructureMap, so it only takes a few basic calls to get the Session from the DI framework:
public class SiteCreatedEventHandler : IHandleMessages<ISiteCreatedEvent>
{
public IBus Bus { get; set; }
public IUnitOfWork Uow { get; set; }
public IDocumentSession DocumentSession { get; set; }
public SiteCreatedEventHandler()
{
this.DocumentSession = ObjectFactory.GetInstance<IDocumentStore>().OpenSession();
}
public void Handle(ISiteCreatedEvent message)
{
using(DocumentSession)
{
try
{
Debug.Print(#"{0}{1}", message, Environment.NewLine);
///// Uow.Begin(); // Not needed for Load<T>
var site = DocumentSession.Load<Site>(message.SiteId);
//// Uow.Commit(); // Not needed for Load<T>
// invoke Hub and push update to screen
var context = GlobalHost.ConnectionManager.GetHubContext<AlarmAndNotifyHub>();
// TODO make sure this SignalR function is correct
context.Clients.All.displayNewSite(site, message.CommandId);
context.Clients.All.refreshSiteList();
}
catch (Exception ex)
{
//// Uow.Rollback(); // Not needed for Load<T>
}
}
}