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.
Related
I have used this snippet to setup my application:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(Startup.Register)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
public static void Register(ContainerBuilder builder)
{
builder.RegisterType<UserService>().As<IUserServcice>().InstancePerLifetimeScope();
}
and I have utilized it in the manner mentioned below:
public interface IUserServcice
{
public long Tick { get; }
}
public class UserService : IUserServcice
{
private long _tick;
public UserService()
{
_tick = DateTime.Now.Ticks;
}
public long Tick => _tick;
}
public WeatherForecastController(IUserServcice userServcice)
{
// _logger = logger;
iUserServcice = userServcice;
var g = Startup.AutofacContainer.Resolve<IUserServcice>();
tick2 = g.Tick;
}
private async Task Get1()
{
var list = new List<long>();
list.Add(iUserServcice.Tick);
var g=Startup.AutofacContainer.Resolve<IUserServcice>();
list.Add(g.Tick);
list.Add(tick2);
//using (var scope= SorviceLocator.Container.BeginLifetimeScope("t1"))
// {
for (int i = 0; i < 3; i++)
{
await Task.Factory.StartNew(() =>
{
var sr = Startup.AutofacContainer.Resolve<IUserServcice>();
list.Add(sr.Tick);
});
}
// }
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
await Get1();
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
and unfortunately the result of debug is like image uploaded below:
as you can see in the picture item in the top is the result of controller constructor injection and, other items, are inside the controller and my question is that how can I have all these items with the same value.
When you use ASP.NET Core, while you can have Autofac be the backing container, for the most part you give up working with Autofac directly when outside the Startup class. You register your stuff in Startup, but in controllers and elsewhere, it's all standard dependency injection (no Autofac references) and the Microsoft dependency injection abstraction.
This is important because it'll help you Google for answers. Don't look for "How do I do this with Autofac?" - look for "How do I do this in ASP.NET Core?"
First, avoid service location. I see what you're doing, I see what you're getting at... but the fact you need to use service location to demonstrate the issue seems like a red flag.
Now that's out of the way:
What you want is HttpContext.RequestServices. When you have a controller, you'll have the HttpContext and the RequestServices object there is the request lifetime scope. It's backed by Autofac but the interface is the Microsoft interface.
You can read about RequestServices in the Microsoft docs.
private readonly IUserService injected;
public WeatherForecastController(IUserService userService)
{
this.injected = userService;
}
public async Task Get()
{
var located = this.HttpContext.RequestServices.GetService<IUserService>();
// located and injected will be the same instance.
}
If you need to begin a child lifetime scope, again, that's an MS DI thing. You'll need an IServiceScopeFactory. That can be a constructor dependency or you can use service location like you were doing before.
var scopeFactory = this.HttpContext.RequestServices.GetService<IServiceScopeFactory>();
using(var scope = scopeFactory.CreateScope())
{
// Now you have a scope to work with.
}
If you absolutely must get the Autofac lifetime from an IServiceProvider for whatever reason, you can resolve one. Resolving a lifetime scope from a lifetime scope returns itself.
var requestScope = this.HttpContext.RequestServices.GetService<ILifetimeScope>();
But, again, you'll notice everything we're doing here is working with the Microsoft DI abstraction, so when you're looking for answers, I'd recommend looking more broadly and not limiting your search to Autofac. This answer is basically the same regardless of the backing container you use.
I have an AspNetCore (core 2.1) web appl that works fine in any single server environment, but times out after a few seconds in the environment with 2 load-balanced web servers.
Here are my startup.cs and other classes, and a screenshot of my AppSessionState table. I hope someone can point me to the right path. I've spent 2 days on this and can't find anything else that needs settings or what's wrong with what I'm doing.
Some explanation of below code:
As seen, I've followed the steps to configure the app to use Distributed SQL Server caching and have a helper static class HttpSessionService which handles adding/getting values from the Session State. Also, I have a Session-Timeout attribute that I annotate each of my controllers to control the session timeouts. And after a few seconds or clicks in the app, as each controller action makes this call
HttpSessionService.Redirect()
this Redirect() method gets a NULL user session from this line, which causes the app to timeout.
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
I've attached two VS debuggers to both servers and I've noticed that even when all sessions coming to one of the debugger instance (one server) the AspNet Session still returned NULL for the above userSession value.
Again, this ONLY happens on a distributed environment, i.e. if I stop one of the sites on one of the web servers everything works fine.
I have looked and implemented the session state distributed caching with SQLServer as explained (the same) in different pages, here are few.
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-3.0
https://www.c-sharpcorner.com/article/configure-sql-server-session-state-in-asp-net-core/
And I do see sessions being written to my created AppSessionState table, yet the app continues to timeout in the environment with 2 load-balanced servers.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Session State distributed cache configuration against SQLServer.
var aspStateConnStr = ConfigurationManager.ConnectionStrings["ASPState"].ConnectionString;
var aspSessionStateSchemaName = _config.GetValue<string>("AppSettings:AspSessionStateSchemaName");
var aspSessionStateTbl = _config.GetValue<string>("AppSettings:AspSessionStateTable");
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
});
....
services.AddSession(options =>
{
options.IdleTimeout = 1200;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
services.AddMvc().AddJsonOptions(opt => opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IDistributedCache distCache)
{
var distCacheOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5));
// Session State distributed cache configuration.
lifetime.ApplicationStarted.Register(() =>
{
var currentTimeUTC = DateTime.UtcNow.ToString();
byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
distCache.Set("cachedTimeUTC", encodedCurrentTimeUTC, distCacheOptions);
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession(); // This must be called before the app.UseMvc()
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
HttpSessionService.Configure(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(), distCache, distCacheOptions);
}
HttpSessionService (helper class):
public class HttpSessionService
{
private static IHttpContextAccessor _httpContextAccessor;
private static IDistributedCache _distributedCache;
private static ISession _session => _httpContextAccessor.HttpContext.Session;
public static void Configure(IHttpContextAccessor httpContextAccessor, IDistributedCache distCache)
{
_httpContextAccessor = httpContextAccessor;
_distributedCache = distCache;
}
public static void SetValues<T>(string key, T value)
{
_session.Set<T>(key, value);
}
public static T GetValues<T>(string key)
{
var sessionValue = _session.Get<T>(key);
return sessionValue == null ? default(T) : sessionValue;
}
public static bool Redirect()
{
var result = false;
var userSession = GetValues<UserIdentityView>(SessionKeys.User);
if (userSession == null || userSession?.IsAuthenticated == false)
{
result = true;
}
return result;
}
}
SessionTimeoutAttribute:
public class SessionTimeoutAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var redirect = HttpSessionService.Redirect();
if (redirect)
{
context.Result = new RedirectResult("~/Account/SessionTimeOut");
return;
}
base.OnActionExecuting(context);
}
}
MyController
[SessionTimeout]
public class MyController : Controller
{
// Every action in this and any other controller time out and I get redirected by SessionTimeoutAttribute to "~/Account/SessionTimeOut"
}
Sorry for the late reply on this. I've changed my original implementation, by injecting IDistributedCache interface to all of my controllers and using this setting in the Statusup.cs class in ConfigureServices() function.
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = aspStateConnStr;
options.SchemaName = aspSessionStateSchemaName;
options.TableName = aspSessionStateTbl;
options.ExpiredItemsDeletionInterval = null;
});
That made it work in a web farm.
As you can see I'm setting the ExpiredItemsDeletionInterval to null to prevent some basic cache entries from clearing out of cache, but with doing so I ran into another problem that when I attempt to get them I still get null back even if the entry is in the database table. So, that's another thing I'm trying to figure out.
It looks like you're capturing the Session value from HttpContext in your static HttpSessionService instance. That value is per-request so it's definitely going to randomly fail if you capture it like that. You need to go through the IHttpContextAccessor every time you want to access an HttpContext value, if you want to get the latest value.
Also, I'd suggest you pass an HttpContext in to your helper methods rather than using IHttpContextAccessor. It has performance implications and should generally only be used if you absolutely can't pass an HttpContext through. The places you show here seem to have an HttpContext available, so I'd recommend using that instead of the accessor.
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.
I want to check that the type registrations I established in Startup.cs are all valid at runtime (either when starting up the service or as part of a test suite). There's a feature like this in Lamar and other containers.
ASP.NET Core 3.x actually introduced a feature for scope and provider validation. Both of these are useful in different contexts (see the below post and sample code).
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
options.ValidateOnBuild = true;
});
https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
You can iterate the services that you want to validate, and try to initialize a service with GetRequiredService<T>. It will throw an exception if there is something wrong. More info on
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice?view=aspnetcore-2.2
The IServiceCollection is actually enumerable over ServiceDescriptor which contains type information on the registered service and implementation. The service collection isn't usually registered, but it should be possible to capture both the service collection and service provider in a hosted service.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddServicesValidation();
}
The right extension method can capture the services collection without actually registering it (which feels "safer").
public static class ValidateServicesExtensions
{
public static IServiceCollection AddServicesValidation(this IServiceCollection services)
{
services.AddHostedService<ValidateServices>(provider => new ValidateServices(services, provider));
return services;
}
}
Now, the hosted service can iterate over the registered services & implementations. Although, this code bombs on the first generic IOptions<TOption>, but I'm sure we can figure something out?
ValidateServices.cs
public class ValidateServices : BackgroundService
{
private readonly IServiceCollection services;
private readonly IServiceProvider provider;
public ValidateServices(
IServiceCollection services,
IServiceProvider provider
)
{
this.services = services;
this.provider = provider;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = provider.CreateScope();
foreach (var service in services)
{
_ = scope.ServiceProvider.GetRequiredService(service.ServiceType);
}
return Task.CompletedTask;
}
}
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();
}
}