I have been trying to setup my component instance per each tenant using InstancePerTenant. However, the InstancePerTenant somehow behave like a SingleInstance. Is there something wrong with my setup?
I have this ContainerBuilder extension which configure the multitenant related dependencies.
public static ContainerBuilder AddMultitenancy(this ContainerBuilder builder)
{
builder.RegisterType<HttpContextAccessor>().As<IHttpContextAccessor>().SingleInstance();
builder.RegisterType<TenantStore>().As<ITenantStore>().SingleInstance();
builder.RegisterType<TenantResolverStrategy>().As<ITenantIdentificationStrategy>().SingleInstance();
return builder
}
The tenant is identified by hostname:
public class TenantResolverStrategy : ITenantIdentificationStrategy
{
private readonly IHttpContextAccessor httpContextAccessor;
public TenantResolverStrategy(
IHttpContextAccessor httpContextAccessor
)
{
this.httpContextAccessor = httpContextAccessor;
}
public bool TryIdentifyTenant(out object tenantId)
{
// hostname is the tenantId
tenantId = httpContextAccessor.HttpContext?.Request?.Host.Value;
return (tenantId != null || tenantId == (object)"");
}
}
TenantStore is just a class to resolve the tenant entity from database based on the tenantId (hostname)
public class TenantStore : ITenantStore
{
private readonly ITenantIdentificationStrategy tenantIdentificationStrategy;
private readonly MemoryCacheStore cacheStore;
private readonly ITenantService tenantService;
public TenantStore(
ITenantIdentificationStrategy tenantIdentificationStrategy,
MemoryCacheStore cacheStore,
ITenantService tenantService
)
{
this.tenantIdentificationStrategy = tenantIdentificationStrategy;
this.cacheStore = cacheStore;
this.tenantService = tenantService;
}
public async Task<TenantEntity> GetTenantAsync(object tenantId)
{
var hostName = (string)tenantId;
var tenant = cacheStore.Get<TenantEntity>(CacheType.Tenant, hostName);
if (tenant == null)
{
tenant = await tenantService.GetTenantByHostNameFromDatabaseAsync(hostName);
cacheStore.Set(tenant, CacheType.Tenant, hostName);
}
return tenant ?? new TenantEntity();
}
}
In Startup.cs, I am registering TenantSpecific with InstancePerTenant:
public void ConfigureContainer(ContainerBuilder builder)
{
builder.AddMultitenancy();
builder.RegisterType<TenantSpecific>().As<ITenantSpecific>().InstancePerTenant();
}
public static MultitenantContainer ConfigureMultitenantContainer(IContainer container
{
var strategy = container.Resolve<ITenantIdentificationStrategy>();
var multitenantContainer = new MultitenantContainer(strategy, container);
// Nothing important here
multitenantContainer.RegisterMultitenantSpecificStuff();
return multitenantContainer;
}
TenantSpecific.cs and TenantSpecificController.cs:
public class TenantSpecific
{
public Guid Id { get; set; }
public TenantSpecific()
{
this.Id = Guid.NewGuid();
}
}
public class TenantSpecificController : ApiController
{
private readonly ITenantSpecific tenantSpecific;
public TenantSpecificController(ITenantSpecific tenantSpecific)
{
this.tenantSpecific = tenantSpecific;
}
[HttpGet]
public IActionResult Get()
{
return Ok(tenantSpecific.Id);
}
}
In Program.cs
public static IHostBuilder CreateHostBuilder(string[] args)
{
var host = Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder
.UseConfiguration(ConfigurationModule.GetConfiguration())
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>();
});
return host;
}
When I invoke http://tenant1.localhost/tenant-specific/ and http://tenant2.localhost/tenant-specific, the constructor of TenantSpecific is only called once like Singleton. The tenantSpecific.Id returns the same value. So I assume InstancePerTenant is not working here.
Is there something wrong with my setup?
As written in the documentation ASP.net core multitenant support
You should add a call to AddAutofacMultitenantRequestServices() to add the required middleware to the root container which is required for multitenancy to work.
public void ConfigureServices(IServiceCollection services)
{
// This will all go in the ROOT CONTAINER and is NOT TENANT SPECIFIC.
services.AddMvc();
services.AddControllers();
// This adds the required middleware to the ROOT CONTAINER and
// is required for multitenancy to work.
services.AddAutofacMultitenantRequestServices();
}
Related
I have a multi-tenant ASP.NET Core web application. The current tenancy model is every tenant has a separate web app and SQL database. I'm trying to rearchitect it so that multiple tenants will be served by a single web app (but maintaining a separate database per tenant). I've been following this series of blog posts but I've hit a bit of a roadblock with configuration.
The app makes heavy use of the ASP.NET Core configuration system, and has a custom EF Core provider that fetches config values from the database. I'd like to preserve this if possible, it would be an awful lot of work to rip out and replace with something else (dozens of config settings used in hundreds of places).
The existing code is very standard:
public class MyAppSettings
{
public string FavouriteColour { get; set; }
public int LuckyNumber { get; set; }
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<MyAppSettings>(Configuration.GetSection("MyAppSettings"));
// etc....
}
}
// custom EF Core config provider wired up in Program.Main, but that doesn't actually seem relevant
I've already updated our custom provider so that it fetches all configuration values from all known tenant databases, and adds them all to the configuration system, prefixed with a tenant identifier, so the list of all config values fetched from the n different databases might look something like this:
Key Value
===============================================
TenantABC:MyAppSettings:FavouriteColour Green
TenantABC:MyAppSettings:LuckyNumber 42
TenantDEF:MyAppsettings:FavouriteColour Blue
TenantDEF:MyAppSettings:LuckyNumber 37
...
TenantXYZ:MyAppSettings:FavouriteColour Yellow
TenantXYZ:MyAppSettings:LuckyNumber 88
What I'd like to be able to do is somehow customise the way that the configuration is bound so that it resolves the tenant for the current request, and then uses the appropriate values, e.g. a request on abc.myapp.com would observe config values "Green" and "42", etc, without having to change all the dependent places that inject IOptionsMonitor<AppSettings> (or IOptionsSnapshot, etc). The linked blog series has a post about configuration that covers some gotchas that I expect I'll eventually run into around caching etc, but it doesn't seem to cater for this scenario of using completely different settings for different tenants. Conceptually it seems simple enough, but I haven't been able to find the correct place to hook in. Please help!
Here is an idea (not tested yet, however). You can save the default IConfiguration instance passed to the constructor of your Startup class and then register in DI your own implementation of IConfiguration that will use that default one and HttpContextAccessor (to get the current tenant).
So the code will look something like:
public class Startup
{
private IConfiguration _defaultConfig;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
_defaultConfig = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
. . . .
services.AddScoped<IConfiguration>(serviceProvider => {
var httpContextAccessor =
serviceProvider.GetService<IHttpContextAccessor>();
return new MyConfig(_defaultConfig, httpContextAccessor);
});
}
. . . .
}
public class MyConfig : IConfiguration
{
private readonly IConfiguration _defaultConfig;
private readonly IHttpContextAccessor _httpContextAccessor;
public MyConfig(IConfiguration defaultConfig, IHttpContextAccessor httpContextAccessor)
{
_defaultConfig = defaultConfig;
_httpContextAccessor = httpContextAccessor;
}
public string this[string key] {
get {
var tenantId = GetTenantId();
return _defaultConfig[tenantId + ":" + key];
}
set {
var tenantId = GetTenantId();
_defaultConfig[tenantId + ":" + key] = value;
}
}
protected virtual string GetTenantId()
{
//this is just an example that supposes that you have "TenantId" claim associated with each user
return _httpContextAccessor.HttpContext.User.FindFirst("TenantId").Value; ;
}
public IEnumerable<IConfigurationSection> GetChildren()
{
return _defaultConfig.GetChildren();
}
public IChangeToken GetReloadToken()
{
return _defaultConfig.GetReloadToken();
}
public IConfigurationSection GetSection(string key)
{
var tenantId = GetTenantId();
return _defaultConfig.GetSection(tenantId + ":" + key);
}
}
Here are 3 solutions that may be helpful. I don't recommend you the IOptionsMonitor<T> because the tenant value is extracted from HttpContext, makes no sense to use the IOptionsMonitor.
Shared code:
public static class Extensions
{
public static string GetTenantName(this HttpContext context)
{
switch (context.Request.Host.Host)
{
case "abc.localhost.com":
return "TenantABC";
case "def.localhost.com":
return "TenantDEF";
default:
throw new IndexOutOfRangeException("Invalid host requested");
}
}
public static MyAppSettings GetAppSettingsByTenant(this IConfiguration config, string tenant)
{
return new MyAppSettings
{
LuckyNumber = int.Parse(config[$"{tenant}:MyAppSettings:LuckyNumber"]),
FavouriteColour = config[$"{tenant}:MyAppSettings:FavouriteColour"]
};
}
}
Solution 1: Scoped MyAppSettings object.
Registration (Startup->ConfigureServices(IServiceCollection)`
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped(sp =>
{
var contextAccessor = sp.GetService<IHttpContextAccessor>();
var config = sp.GetService<IConfiguration>();
return config.GetAppSettingsByTenant(contextAccessor.HttpContext.GetTenantName());
});
...
Usage:
public class TestController : Controller
{
private readonly MyAppSettings _settings;
public TestController(MyAppSettings settings)
{
_settings = settings;
}
[HttpGet]
public IActionResult Index()
{
return Json(_settings);
}
}
Solution 2: IOptions<MyAppSettings
Registration (Startup->ConfigureServices(IServiceCollection)`
public class MyAppSettingsOptions : IOptions<MyAppSettings>
{
public MyAppSettingsOptions(IConfiguration configuration, IHttpContextAccessor contextAccessor)
{
var tenant = contextAccessor.HttpContext.GetTenantName();
Value = configuration.GetAppSettingsByTenant(tenant);
}
public MyAppSettings Value { get; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOptions<MyAppSettings>, MyAppSettingsOptions>();
...
Usage
public class TestController : Controller
{
private readonly IOptions<MyAppSettings> _options;
public TestController(IOptions<MyAppSettings> options)
{
_options = options;
}
[HttpGet]
public IActionResult Index()
{
return Json(_options.Value);
}
}
Solution 3: IOptionsMonitor<MyAppSettings
Registration (Startup->ConfigureServices(IServiceCollection)`
public class MyAppSettingsOptionsMonitor : IOptionsMonitor<MyAppSettings>
{
public MyAppSettingsOptionsMonitor(IConfiguration configuration, IHttpContextAccessor contextAccessor)
{
var tenant = contextAccessor.HttpContext.GetTenantName();
CurrentValue = configuration.GetAppSettingsByTenant(tenant);
}
public MyAppSettings Get(string name)
{
throw new NotSupportedException();
}
public IDisposable OnChange(Action<MyAppSettings, string> listener)
{
return null;
}
public MyAppSettings CurrentValue { get; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOptionsMonitor<MyAppSettings>, MyAppSettingsOptionsMonitor>();
...
Usage
public class TestController : Controller
{
private readonly IOptionsMonitor<MyAppSettings> _options;
public TestController(IOptionsMonitor<MyAppSettings> options)
{
_options = options;
}
[HttpGet]
public IActionResult Index()
{
return Json(_options.CurrentValue);
}
}
You can use DI services to config options
Sample code of your option class
public class MyAppSettings
{
public string FavouriteColor { get; set; }
public int LuckNumber { get; set; }
}
public interface IMySettingServices
{
string GetFavouriteColor();
int GetLuckNumber();
}
public class MySettingServices : IMySettingServices
{
private IHttpContextAccessor httpContextAccessor;
public MySettingServices(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetFavouriteColor()
{
var headers = this.httpContextAccessor.HttpContext.Request.Headers;
//Write your logic with httpContextAccessor by extract tenant here, then return actual config by tenant name
if(this.httpContextAccessor.HttpContext.Request.Host.Host=="abc.test.com")
{
//Get color setting for abc.test.com
}
return "Green";
}
public int GetLuckNumber()
{
var headers = this.httpContextAccessor.HttpContext.Request.Headers;
//Write your logic with httpContextAccessor by extract tenant here, then return actual config by tenant name
if (this.httpContextAccessor.HttpContext.Request.Host.Host == "abc.test.com")
{
//Get luck number setting for abc.test.com
}
return 1;
}
}
Sample code of your ConfigureService
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddControllersWithViews();
services.AddSingleton<IMySettingServices, MySettingServices>();
services.AddOptions<MyAppSettings>().Configure<IMySettingServices>((setting, settingServices) => {
setting.FavouriteColor = settingServices.GetFavouriteColor();
setting.LuckNumber = settingServices.GetLuckNumber();
});//This is the key point of this answer, you are delegating your setting assignment to a services, so you can do whatever you want in your services, in your word, customise configuration binding
}
Sample code of use your configuration in controller
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyAppSettings> settings;
public HomeController(ILogger<HomeController> logger, IOptions<MyAppSettings> settings)
{
_logger = logger;
this.settings = settings;
}
public IActionResult Index()
{
var favColor = settings.Value.FavouriteColor;
return View();
}
}
please be aware that when you want to access httpcontext, do not directly add services.AddScoped/AddSingleton/AddTransit<IHttpContextAccessor,HttpContextAccessor>(), this will result to DI system unable to resolve IHttpContextAccessor during ConfigureServices phases. Use services.AddHttpContextAccessor(); is the best way to do that
Let's assume that we have the following Program.cs:
public static class Program
{
public static async Task Main(string[] args)
{
await CreateWebHostBuilder(args).Build().RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost
.CreateDefaultBuilder(args)
.ConfigureKestrel(options => { options.AllowSynchronousIO = false; })
.ConfigureFanciness()
.ConfigureLogging(ConfigureApplicationLogging)
.UseStartup<Startup>();
}
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var loggingConfiguration = context.Configuration.GetSection("Logging");
loggingBuilder.AddConfiguration(loggingConfiguration);
// var fancyService = SomehowGet<IFancyService>();
// if (fancyService.IsEnabled)
// loggingBuilder.AddEventLog(loggingConfiguration);
}
public static IWebHostBuilder ConfigureFanciness(this IWebHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(delegate (WebHostBuilderContext context, IServiceCollection services)
{
var fancinessConfiguration = context.Configuration.GetSection("Fanciness");
services.Configure<FancinessSettings>(fancinessConfiguration);
services.AddSingleton<IFancyService, FancyService>();
// var fancyService = SomehowGet<IFancyService>();
// fancyService.Initialize();
});
}
}
And the following FancyService.cs:
public sealed class FancyService : IFancyService
{
private readonly ILogger<FancyService> logger;
private readonly IOptions<FancinessSettings> settings;
public FancyService(ILogger<FancyService> logger, IOptions<FancinessSettings> settings)
{
this.logger = logger;
this.settings = settings;
}
public bool IsEnabled { get; private set; }
public void Initialize()
{
// do the initialization work and set IsEnabled to some value
}
}
As demonstrated in the example files, loggingBuilder.AddEventLog(loggingConfiguration) depends on IFancyService.IsEnabled that is set when IFancyService.Initialize() is called.
To do that, I need access to an instance of IFancyService; is there a way to achieve this?
Once services.AddSingleton<IFancyService, FancyService>() has been called, you can get the instance of FancyService by building a ServiceProvider:
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var fancyService = loggingBuilder.Services.BuildServiceProvider().GetService<IFancyService>();
fancyService.Initialize();
var loggingConfiguration = context.Configuration.GetSection("Logging");
loggingBuilder.AddConfiguration(loggingConfiguration);
if (fancyService.IsEnabled)
loggingBuilder.AddEventLog(loggingConfiguration);
}
As discussed in comments, if your FancyService has no constructor dependency on ILogger<> :
public sealed class FancyService : IFancyService
{
public FancyService(IOptions<FancinessSettings> settings)
{ ... }
to initialize the IFancyService, simply use an implementation factory to create a IFancyService instance:
public static IWebHostBuilder ConfigureFanciness(this IWebHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(delegate (WebHostBuilderContext context, IServiceCollection services)
{
var fancinessConfiguration = context.Configuration.GetSection("Fanciness");
services.Configure<FancinessSettings>(fancinessConfiguration);
services.AddSingleton<IFancyService, FancyService>(sp =>{
var fancy=ActivatorUtilities.CreateInstance(sp,typeof(FancyService)) as FancyService;
fancy.Initialize();
return fancy;
});
});
}
The same trick could also be used to register a logger provider. Since an ILogger<> service depends on IEnumerable<ILoggerProvider>, we could register an ILoggerProvider instance that provides an additional INullLogger in order to configure the logging behavior according to the current IFancyService :
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var loggingConfiguration = context.Configuration.GetSection("Logging");
var descriptor=ServiceDescriptor.Singleton<ILoggerProvider,NullLoggerProvider>(sp =>{
var provider = NullLoggerProvider.Instance;
var fancy = sp.GetRequiredService<IFancyService>();
if(fancy.IsEnabled)
{
loggingBuilder.AddDebug();
loggingBuilder.AddEventLog(loggingConfiguration);
loggingBuilder.AddEventSourceLogger();
// ... add more configuration as you like
}else{
loggingBuilder.AddConsole();
}
return provider;
});
loggingBuilder.Services.TryAddEnumerable(descriptor);
loggingBuilder.AddConfiguration(loggingConfiguration);
}
As a side note, be aware once the logger has been built, don't change the IFancyService.IsEnabled. That's because the ILogger<> service is a registered as a singleton and never changed once created.
I created a fresh ASP.NET Core Web API project. Here is ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var cache = serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
//_cahce.Count is 1
}
As you see I add an item to IMemoryCache. Here is my controller:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IMemoryCache _cache;
public ValuesController(IMemoryCache cache)
{
_cache = cache;
}
[HttpGet("{key}")]
public ActionResult<string> Get(string key)
{
//_cahce.Count is 0
if(!_cache.TryGetValue(key, out var value))
{
return NotFound($"The value with the {key} is not found");
}
return value + "";
}
}
When I request https://localhost:5001/api/values/key1, the cache is empty and I receive a not found response.
In short, the cache instance you're setting the value in is not the same as the one that is later being retrieved. You cannot do stuff like while the web host is being built (i.e. in ConfigureServices/Configure. If you need to do something on startup, you need to do it after the web host is built, in Program.cs:
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
var cache = host.Services.GetRequiredService<IMemoryCache>();
cache.Set("key1", "value1");
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
}
As #selloape saids, if you manullay call BuildServicesProvider, you are creating a new provider, that will be not used in your controllers.
You can use a hosted service to intialize your cache
public class InitializeCacheService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public InitializeCacheService (IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var cache = _serviceProvider.GetService<IMemoryCache>();
cache.Set("key1", "value1");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
Add it in your ConfigureServices
services.AddHostedService<InitializeCacheService>();
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2
I want to access JwtHelper from ExceptionHelper. But problem is ExceptionHelper must be static. And so, we can't create constructor and not access jwtHelper Method. How can I achieve access jwHelper from ExcewptionHelper.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<MyDbContext>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
context.Response.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
ExceptionHelper.cs
public static class ExceptionHelper
{
public static async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = jwtHelper.GetValueFromToken(token, "UserId");??????
//in this line I can't access jwtHelper.
}
}
JwtHelper.cs
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string stream, string propertyName)
{
var jwt = httpContextAccessor.HttpContext.Request.Headers["Authorization"];
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(stream.Replace("Bearer ", "")) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
If I were you I would register JwtHelper with a Interface known as IJwtHelper.
It would look like this then
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string propertyName)
{
var jwt= httpContextAccessor.HttpContext.Request.Headers["Authorization"];
// I can't access httpContextAccessor in this line.
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(jwt) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
public interface IJwtHelper
{
string GetValueFromToken(string propertyName);
}
In my startup.cs class I would then do
services.AddSingleton<IJwtHelper, JwtHelper>();
And then when you want to access your helper I would inject IJwtHelper
private IJwtHelper _jwtHelper;
public SomeConstructerOnClass(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public void SomeMethod(string property) {
var token = _jwtHelper.GetValueFromToken(property);
//Do something with token
}
where _jwtHelper is field of type IJwtHelper.
You will then be able to use GetValueFromToken quite fine anywhere you inject IJwtHelper
UPDATE
Your problem is that ExceptionHandler is Static , implement an interface and add it to container
public class ExceptionHelper : IExceptionHelper
{
private IJwtHelper _jwtHelper;
public ExceptionHelper(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = _jwtHelper.GetValueFromToken(token, "UserId");??????
}
}
public interface IExceptionHelper
{
Task AddApplicationError( HttpResponse response);
}
Then
services.AddSingleton<IExceptionHelper, ExceptionHelper>();
Now You will be able to inject it into your Configure method like so
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IExceptionHelper exceptionHelper)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
//Resolved and available!
exceptionHelper.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
If you follow me advice above from my initial response and my update everything should be fine and registered nicely in your container :)
You'll have to instantiate the JwtHelper class in order to access the instance variable (httpContextAccessor) from another class. Static methods, like GetValueFromToken, cannot access instance variables.
I have read Design-time DbContext Creation that there are 3 ways the EF Core tools (for example, the migration commands) obtain derived DbContext instance from the application at design time as opposed to at run time.
From application service provider
From any parameterless ctor
From a class implementing IDesignTimeDbContextFactory<T>
Here I am only interested in the first method by mimicking the pattern used in Asp.net Core. This code does not compile because I have no idea how to make EF Core tool obtain TheContext instance.
Minimal Working Example
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
public class TheContext : DbContext
{
public TheContext(DbContextOptions<TheContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
private static readonly IConfiguration _configuration;
private static readonly string _connectionString;
static Program()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();
_connectionString = _configuration.GetConnectionString("SqlServer");
}
static void ConfigureServices(IServiceCollection isc)
{
isc.AddSingleton(_ => _configuration);
isc.AddDbContextPool<TheContext>(options => options
.UseSqlServer(_connectionString));
isc.AddSingleton<TheApp>();
}
static void Main()
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
IServiceProvider isp = isc.BuildServiceProvider();
isp.GetService<TheApp>().Run();
}
}
class TheApp
{
readonly TheContext _theContext;
public TheApp(TheContext theContext) => _theContext = theContext;
public void Run()
{
// Do something on _theContext
}
}
Question
How to make EF Core tools obtain DbContext instance from service provider of a console application?
Edit:
I forgot to mention the appsettings.json as follows:
{
"ConnectionStrings": {
"Sqlite": "Data Source=MyDatabase.db",
"SqlServer": "Server=(localdb)\\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True"
}
}
Although the documentation topic is called From application services, it starts with
If your startup project is an ASP.NET Core app, the tools try to obtain the DbContext object from the application's service provider.
Looks like they don't expect project types other than ASP.NET Core app to use application service provider :)
Then it continues with
The tools first try to obtain the service provider by invoking Program.BuildWebHost() and accessing the IWebHost.Services property.
and example:
public static IWebHost BuildWebHost(string[] args) => ...
And here is the trick which works with the current (EF Core 2.1.3) bits. The tools actually are searching the class containing your entry point (usually Program) for a static (does not need to be public) method called BuildWebHost with string[] args parameters, and the important undocumented part - the return type does not need to be IWebHost! It could be any object having public property like this
public IServiceProvider Services { get; }
Which gives us the following solution:
class Program
{
// ...
// Helper method for both Main and BuildWebHost
static IServiceProvider BuildServiceProvider(string[] args)
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
return isc.BuildServiceProvider();
}
static void Main(string[] args)
{
BuildServiceProvider(args).GetService<TheApp>().Run();
}
// This "WebHost" will be used by EF Core design time tools :)
static object BuildWebHost(string[] args) =>
new { Services = BuildServiceProvider(args) };
}
Update: Starting from v2.1, you could also utilize the new CreateWebHostBuilder pattern, but IMHO it just adds another level of complexity not needed here (the previous pattern is still supported). It's similar, but now we need a method called CreateWebHostBuilder which returns an object having public method Build() returning object having public Services property returning IServiceProvider. In order to be reused from Main, we can't use anonymous type and have to create 2 classes, and also this makes it's usage from Main more verbose:
class AppServiceBuilder
{
public ServiceCollection Services { get; } = new ServiceCollection();
public AppServiceProvider Build() => new AppServiceProvider(Services.BuildServiceProvider());
}
class AppServiceProvider
{
public AppServiceProvider(IServiceProvider services) { Services = services; }
public IServiceProvider Services { get; }
}
class Program
{
// ...
static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Services.GetService<TheApp>().Run();
}
// This "WebHostBuilder" will be used by EF Core design time tools :)
static AppServiceBuilder CreateWebHostBuilder(string[] args)
{
var builder = new AppServiceBuilder();
ConfigureServices(builder.Services);
return builder;
}
}
Try this, i've added some changes to make it run :
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.FileExtensions;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
namespace IOCEFCore
{
public class TheContext : DbContext
{
public TheContext(DbContextOptions<TheContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
private static readonly IConfigurationRoot _configuration;
private static readonly string _connectionString;
static Program()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();
}
static void ConfigureServices(IServiceCollection isc)
{
isc.AddSingleton(_ => _configuration);
isc.AddDbContextPool<TheContext>(options => options.UseInMemoryDatabase("myContext"));
isc.AddSingleton<TheApp>();
}
static void Main()
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
IServiceProvider isp = isc.BuildServiceProvider();
isp.GetService<TheApp>().Run();
Console.ReadLine();
}
class TheApp
{
readonly TheContext _theContext;
public TheApp(TheContext theContext) => _theContext = theContext;
public void Run()
{
// Do something on _theContext
_theContext.Users.Add(new User {Id = 1, Name = "Me"});
_theContext.SaveChanges();
foreach (var u in _theContext.Users)
{
Console.WriteLine("{0} : {1}", u.Id, u.Name);
}
}
}
}
}
Now it can be easily done with generic hosts.
public class Product
{
public int Id { get; set; }
public string Description { get; set; } = default!;
public override string ToString() => $"Id: {Id}, Description: {Description}";
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Product>().HasData(new[]
{
new Product{Id=1, Description="Sql Server"},
new Product{Id=2, Description="Asp.Net Core"},
new Product{Id=3, Description=".NET MAUI"}
});
}
}
public class Application : IHostedService
{
private readonly AppDbContext context;
public Application(AppDbContext context)
{
this.context = context;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
foreach (var p in await context.Products.ToArrayAsync())
Console.WriteLine(p);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
public class Program
{
public static async Task Main()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureHostConfiguration(icb =>
{
// icb.AddUserSecrets<Program>();
});
builder.ConfigureServices((hbc, isc) =>
{
isc.AddDbContext<AppDbContext>(dcob =>
{
//var constr = hbc.Configuration.GetConnectionString("DefaultConnection");
var constr = "DefaultConnection";
dcob.UseSqlServer(constr);
}, ServiceLifetime.Singleton);
isc.AddHostedService<Application>();
});
//await builder.Build().RunAsync();
await builder.RunConsoleAsync();
}
}
The connection string is retrieved from appsettings.json or secret.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=EFCoreConsoleDb;Integrated Security=true;TrustServerCertificate=true"
}
}