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.
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;
}
}