What's the correct way to pass an IHttpContextAccessor into the multi-tenant strategy with Autofac? I can't seem to find this documented anywhere. I tried constructing an instance of the HttpContextAccessor and passing it into the strategy, but this results in the HttpContext always being null.
Startup
public IServiceProvider ConfigureServices(IServiceCollection services) {
services.AddMvc();
var builder = new ContainerBuilder();
builder.Populate(services);
var container = builder.Build();
var strategy = new FooTenantStrategy(new HttpContextAccessor());
var mtc = new MultitenantContainer(strategy, container);
Startup.ApplicationContainer = mtc;
return new AutofacServiceProvider(mtc);
}
Program
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
// This enables the request lifetime scope to be properly spawned from
// the container rather than be a child of the default tenant scope.
// The ApplicationContainer static property is where the multitenant container
// will be stored once it's built.
.UseAutofacMultitenantRequestServices(() => Startup.ApplicationContainer)
.UseStartup<Startup>();
After digging through some source code I found a sample from a test that does the trick:
var strategy = new FooTenantStrategy(container.Resolve<IHttpContextAccessor>(), container.Resolve<ILogger<SonicFoundryTenantStrategy>>());
The key part being pulling the context from the container that was previously built.
Related
[Context]
I'm testing an identity provider. And I came across an intriguing scenario where ... the introspect endpoint was still missing which means, i'm going to validate the token by myself like so:
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
var response = await client.GetAsync("url_to_jwks");
var content = await response.Content.ReadAsStringAsync();
var jwk = new JsonWebKeySet(content).GetSigningKeys().First();
services.AddAuthentication()
.AddJwtBearer("SelfValidationKey", opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = jwk
};
opt.RequireHttpsMetadata = false;
});
services.AddOcelot();
There are others parameters to check of course, but just for simplicity, we'll just focus on the signature, and because that's also my problem :)
[The idea]
Instead of verifying it with a certificate, why not use the jwk provided by the IdP well-known document (the jwk is not always the same). And that's what I did, and it works, but! When I try to make an http call inside my configure services, before adding authentification, it crashes:
System.NullReferenceException: Object reference not set to an instance of an object.
at Ocelot.Middleware.OcelotMiddlewareExtensions.CreateConfiguration(IApplicationBuilder builder)
at Ocelot.Middleware.OcelotMiddlewareExtensions.UseOcelot(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
at Ocelot.Middleware.OcelotMiddlewareExtensions.UseOcelot(IApplicationBuilder builder)
at ApiGateway.Startup.Configure
I thought I couldn't make http calls inside my service configuration, but acutally I checked and I do receive the response I was expecting. I've tried it on a different startup.cs and it works just fine, so my guess ... it has to do with Ocelot (api gateway) somehow, am I making the async/await statement in the wrong way? The exception is thrown at Programs .cs which is very basic, and I never reach Services.AddAuthentication():
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var builder = WebHost.CreateDefaultBuilder(args);
builder.ConfigureServices(s => s.AddSingleton(builder))
.ConfigureAppConfiguration(
ic => ic.AddJsonFile("ocelot.json"))
.UseStartup<Startup>();
var host = builder.Build();
return host;
}
One way was to ...
Make a call through my static class IdpDocument in my program.cs, pass the value I need to the configuration via UseSetting method, which is then injected into the startup.cs, and it looks like this:
public static async Task Main(string[] args)
{
jwks = await IdpDocument.FetchJwks();
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var builder = WebHost.CreateDefaultBuilder(args);
builder.ConfigureServices(s => s.AddSingleton(builder))
.ConfigureAppConfiguration(
ic => ic.AddJsonFile("ocelot.json"))
.UseSetting("jwks", jwks)
.UseStartup<Startup>();
var host = builder.Build();
return host;
}
And in my startup.cs, i get the value:
public void ConfigureServices(IServiceCollection services)
{
var jwks = Configuration.GetValue<string>("jwks");
// and the rest of the code ...
}
But ... my only concern with that is .. I only make the request once. So, what if the jwks get updated? (which is not this program responsibility)
The only way to get the right keys, is to re-run this program which .. is not really clean.
I've an ASP.NET Core 3 WebAPI with a simple Main and a CreateHostBuilder.
public static void Main(String[] args)
{
CreateHostBuilder(args).Build().Run();
}
Later logging in my controllers etc. works fine.
But how can I log possible errors in the Main?
You can get get the logger using
// Build the host and configurations
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
// Get the registered logger service
var logger = scope.ServiceProvider.GetRequiredService<ILogger<YourClassName>>();
logger.LogInformation("Logger test");
}
// finally run the host
host.Run();
Another way.....the advantage to this one is this is a separate logger from the HostBuilder's, so it can log things before the Host is even built. It also can be used throughout the class and outside Main in Program.cs. The disadvantage is it cant use the appsettings.json file to configure it (unless someone can show me how).
public class Program
{
// The Program console apps private logger
private static ILogger _logger;
public static void Main(string[] args)
{
// Now create a logging object for the Program class.
ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder
.AddConsole()
.AddDebug()
);
_logger = loggerFactory.CreateLogger<Program>();
_logger.LogInformation("PROGRAM CLASS >>> The Program Console Class has started...");
}
}
For Net 6.0, this gist works for me https://gist.github.com/filippovd/edc28b511ef0d7dae9ae8f6eb54c30ed
var builder = WebApplication.CreateBuilder(args);
var logger = LoggerFactory
.Create(loggingBuilder =>
{
// Copy all the current providers that was set within WebApplicationBuilder
foreach (var serviceDescriptor in builder.Logging.Services)
{
loggingBuilder.Services
.Add(serviceDescriptor);
}
})
.CreateLogger<Program>();
;
// Add services to the container.
logger.LogInformation("Add services to the container...");
// Console
// ....
info: Program[0]
Add services to the container...
I'm using .Net Core 2.1 and an Aggregate / Facade pattern for my dependencies (which I happily do elsewhere using Ninject / .net 4.6). But when I try to pass through options I get a null (Debugging I can see there being picked up) but there not passed to Autofac (I'm fairly sure its my as they weren't when I tried Ninject either).
I've made a simple test project (new .net core web application /2.1) and then added a minimal amount of code to replicate
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.Configure<ApiEndpointsConfiguration>(Configuration);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Create the container builder.
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterAggregateService<IViewModelProvider>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => x.FullName.StartsWith("TEST")).ToArray();
builder.RegisterAssemblyTypes(assemblies)
.Where(t => t.IsClass)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
builder.RegisterAggregateService<IDomainServiceProvider>();
ApplicationContainer = builder.Build();
var chkOptions = ApplicationContainer.Resolve<IOptions<ApiEndpointsConfiguration>>();
// Create the IServiceProvider based on the container.
return new AutofacServiceProvider(ApplicationContainer);
}
Program.cs
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureServices(services => services.AddAutofac())
.UseStartup<Startup>();
}
IViewModelProvider.cs
public interface IViewModelProvider
{
IProgrammeViewModelBuilder ProgrammeViewModel { get; }
}
IProgrammeViewModelBuilder.cs
public interface IProgrammeViewModelBuilder
{
ProgrammeViewModel GetProgrammeViewModel();
}
My initial issue was that in my service, controller calls the injected viewmodelbuilder
var viewModel = _viewModels.ProgrammeViewModel.GetProgrammeViewModel();
which in turn calls the service -
readonly IOptions<ApiEndpointsConfiguration> _apiSettings;
public ProgrammeService(IOptions<ApiEndpointsConfiguration> apiSettings) : base (new Uri(apiSettings.Value.BaseAddress))
{
_apiSettings = apiSettings;
}
but at that point (the constructor firing) the service configuration items were null so I've stepped through and I can see that services has the values for "ApiEndpointsConfiguration" picked up but when they get passed through to the "builder" the values are null
ApplicationContainer.Resolve<IOptions<ApiEndpointsConfiguration>>();
shows null for the values inside.
Not sure what it is I'm doing wrong?
:( Truly this is when the answer is so much simpler thank it looks. Kudos to anyone who spots it;
services.Configure<ApiEndpointsConfiguration>(Configuration.GetSection("ApiEndpointsConfiguration"));
rather than
services.Configure<ApiEndpointsConfiguration>(Configuration);
So essentially whilst I thought I could see it debugging I was seeing the raw JSON provided values not the "configured service". I'll leave this here as a lesson to myself to check the simple things first.
Not sure what what was actually being "registered" in my first effort.
What is the equivalent to the method Configure<TOptions> of the OptionsConfigurationServiceCollectionExtensions when using Autofac modules?
My ConfigureServices method looks like this, but I want to move the services.Configure<MyOptions>(Configuration.GetSection("MyOptions")) to MyModule.
public IServiceProvider ConfigureServices(IServiceCollection services) {
services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
var containerBuilder = new ContainerBuilder();
containerBuilder.Populate(services);
containerBuilder.RegisterModule<MyModule>();
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
How does the registration look like in the Load-method of the Module
protected override void Load(ContainerBuilder builder)
{
// configure options here
}
I'm not familiar with Autofac personally, but generally speaking, all Configure<T> does is 1) bind a particular configuration section to a class and 2) register that class with the service collection, so it can be injected directly.
As a result, you can instead use the following to bind your strongly-typed configuration:
var config = config.GetSection("MyOptions").Get<MyOptions>();
And, then you'd simply register that with Autofac as a constant in singleton-scope.
I recently encountered this same issue, I implemented the following so that you can still use IOptions, IOptionsMonitor and IOptionsSnapshot, but register the configuration from the AutoFac Module.
The prerequisite is that you call services.AddOptions() in ConfigureServices method:
var sfConfig = _configuration.GetSection("MyOptions");
builder.Register(ctx => new ConfigurationChangeTokenSource<MyOptions>(Options.DefaultName, sfConfig))
.As<IOptionsChangeTokenSource<MyOptions>>()
.SingleInstance();
builder.Register(ctx => new NamedConfigureFromConfigurationOptions<MyOptions>(Options.DefaultName, sfConfig, _ => { }))
.As<IConfigureOptions<MyOptions>>()
.SingleInstance();
This requires that you run services.AddOptions() within the ConfigureServices method.
In the example above, "MyOptions" is the section name in your configuration, and MyOptions type is the POCO class that has the fields to hold the result.
This is basically a conversion of what microsoft has here: https://github.com/aspnet/Options/blob/master/src/Microsoft.Extensions.Options.ConfigurationExtensions/OptionsConfigurationServiceCollectionExtensions.cs
Startup.cs
public void ConfigureContainer(ContainerBuilder builder)
{
// Register your own things directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory
// for you.
builder.RegisterModule(new AutofacModule(Configuration));
}
AutofacModule.cs
public class AutofacModule: Module
{
private IConfiguration configuration;
public AutofacModule(IConfiguration configuration)
{
this.configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
builder.Register(p => configuration.GetSection("AppAPIKey").Get<ConfigSettings>()).SingleInstance();
builder.RegisterType<TestService>()
.As<ITestService>()
.SingleInstance();
}
}
I have an ASP.NET Core2 application. I am using both builtin and Autofac IoC containers. I am setting up all the component registrations in my Startup.cs file. While doing this, I am also setting up my DBContext which inherits from a custom DataContext which in turn inherits from DbContext and implements a custom IDataContextAsync. This DbContext expects a connection string as a constructor parameter.
My problem is that the connection string is stored in the Redis Cache which is an IDistributedCache. The cache is setup in the startup.cs file. The Connection String also is required in the same ConfigureServices method in Startup.cs. So, I don't seem to have access to this cache at this point.
Everything was working when I was using the HttpContext Session to store the connection string. Now that the application is being deployed to a Web farm, I can't use in proc session. We are using Redis for state management. This is where I am having a problem with.
Here is my ConfigureServices method from startup.cs file (unnecessary code removed for brevity).
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(op => op.SerializerSettings.ContractResolver = new DefaultContractResolver());
services.AddSession(opt =>
{
opt.IdleTimeout = TimeSpan.FromMinutes(20);
opt.Cookie.Name = "apexportal.RulesSession";
opt.Cookie.HttpOnly = true;
});
services.AddDistributedRedisCache(o =>
{
var host = Configuration.GetValue<string>($"{AppConstants.REDIS}:{AppConstants.REDISHOST}");
var port = Configuration.GetValue<string>($"{AppConstants.REDIS}:{AppConstants.REDISPORT}");
o.Configuration = $"{host}";
o.InstanceName = Configuration.GetValue<string>($"{AppConstants.REDIS}:{AppConstants.REDISNAME}");
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//services.AddTransient<IConnectionStringProvider, ConnectionStringProvider>();
services.AddTransient<IDataContextAsync>(s => new PortalEFContext(GetPortalConnectionString()));
services.AddAuthentication(IISDefaults.AuthenticationScheme);
ContainerBuilder builder = new ContainerBuilder();
builder.Populate( services );
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
and here is my GetPortalConnectionString() method which is also in the startup.cs file. I want to replace the line accessor.HttpContext.Session.Get() with an injected RedisCache.Get().
private string GetPortalConnectionString()
{
IHttpContextAccessor accessor = new HttpContextAccessor();
//this is where I need to access the RedisCache and access the stored properties
// instead of using HttpContext.Session. But I don't know how to inject the IDistributedCache
// to this spot.
var connString = accessor.HttpContext.Session.Get<string>(AppConstants.SPCONNSTRING);
return connString ?? Configuration.GetConnectionString("PortalEFContext");
}
Later when the user has selected a database to use in the application, I am storing the connectionstring to that database in Redis Cache like so.
Here is my BaseController class which does that.
public abstract class BaseController : Controller
{
//private readonly IRulesEngineService reService;
protected readonly IHttpContextAccessor httpCtxAccessor;
protected readonly IConfiguration config;
private readonly IAuthService authService;
protected readonly IDistributedCache redisCache;
public BaseController(IHttpContextAccessor _httpContext, IConfiguration _config, IAuthService _authService, IDistributedCache _redisCache)
{
//reService = _reService;
httpCtxAccessor = _httpContext;
config = _config;
authService = _authService;
redisCache = _redisCache;
//SetupCurrentWindowsUserAsync();
}
protected async Task<string> SetCurrentDBConnString( int dbId )
{
var currDbId = await GetCurrentDBId();
if ( currDbId == 0 || currDbId != dbId )
{
var envConnStr = config.GetConnectionString( AppConstants.ENVCONNSTRING );
var connStr = await AppHelper.SetCurrentDBConnectionString( dbId, envConnStr );
//httpCtxAccessor.HttpContext.Session.Set<string>( AppConstants.SPCONNSTRING, connStr );
//httpCtxAccessor.HttpContext.Session.Set<int>( AppConstants.CURRDBID, dbId );
await redisCache.SetAsync<string>( AppConstants.SPCONNSTRING, connStr );
await redisCache.SetAsync<int>( AppConstants.CURRDBID, dbId );
await SetupCurrentWindowsUserAsync();
return connStr;
}
return null;
}
}
Can someone please tell me how I can access the Redis cache in my startup.cs file? Thanks.
It's actually very simple. You were almost there already.
Take a closer look at this line in your startup:
services.AddTransient<IDataContextAsync>(s => new PortalEFContext(GetPortalConnectionString()));
See the s parameter in the lambda? This is the DI container of .NET Core called IServiceProvider. This is what you were looking for. Just pass it down into your function and use it there to resolve anything you want.
So, the code will be the following:
public IServiceProvider ConfigureServices(IServiceCollection services)
...
services.AddTransient<IDataContextAsync>(s => new PortalEFContext(GetPortalConnectionString(s))); // <-- pass the container to the function
...
}
private string GetPortalConnectionString(IServiceProvider container)
{
// Here you go:
var cache = container.GetService<IDistributedCache>();
// and now do whatever you want with it.
var connString = cache.Get<string>(AppConstants.SPCONNSTRING);
// BTW, configuration can be resolved from container as well in order to avoid hard dependency on global Configuration object:
var config = container.GetService<IConfiguration>();
return connString ?? config.GetConnectionString("PortalEFContext");
}