Search code examples
c#asp.net-core-2.0simple-injector

Simple Injector w/ ASP.Net Core 2.0 IOptionsSnapshot injection


I was trying what was suggested in bug #429 and am getting the same error he reported there but then never provided the stack trace for. I have also read, and have used until recently, your guidance on not using IOptions and related classes. We are at a point where the IOptionsSnapshot is really needed as we are running stuff in Azure and need to be able to flip options on/off on the fly as we hit limits and restarting the service is not an option as it takes upwards of 5 minutes to start initially due to some third party pieces we require.

This is what we have setup:

  • Simple Injector 4.3.0
  • .NET Core 2.0 Web API

interface ISearchSettings -> class SearchSettings
(basically all of the properties in here except for 1 boolean we can singleton if needed. The one boolean is a bit telling us whether to use inhouse or azure searching)

When the app is starting, I receive the following error:

System.InvalidOperationException: The configuration is invalid. Creating the instance for type IOptionsSnapshot<SearchSettings> failed. The registered delegate for type IOptionsSnapshot<SearchSettings> threw an exception. Unable to request service 'IOptionsSnapshot<SearchSettings> from ASP.NET Core request services. Please make sure this method is called within the context of an active HTTP request.

In Configure Services:

services.AddOptions();  
services.Configure<ISearchSettings>(
    this.Configuration.GetSection("AzureSearchSettings"));  
services.Configure<SearchSettings>(
    this.Configuration.GetSection("AzureSearchSettings"));  
// The next line was added trying some other suggestions from similar
// errors. It didn't resolve the issue  
services.AddScoped(
    cfg => cfg.GetService<IOptionsSnapshot<SearchSettings>>().Value);  
...  
services.AddMvc();  
...  
IntegrateSimpleInjector();  

In IntegrateSimpleInjector:

this.container.Options.DefaultScopedLifestyle =
    new AsyncScopedLifestyle();

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IControllerActivator>(
    new SimpleInjectorControllerActivator(this.container));
services.AddSingleton<IViewComponentActivator>(
    new SimpleInjectorViewComponentActivator(this.container));

services.EnableSimpleInjectorCrossWiring(this.container);
services.UseSimpleInjectorAspNetRequestScoping(this.container);

In InitializeContainer:

// I have tried both Lifestyle Transient and Scoped
this.container.Register<IOptionsSnapshot<SearchSettings>>(
    () => app.GetRequestService<IOptionsSnapshot<SearchSettings>>(),
    Lifestyle.Transient);
...
this.container.AutoCrossWireAspNetComponents(app);

Stacktrace:

at SimpleInjector.SimpleInjectorAspNetCoreIntegrationExtensions.GetRequestServiceProvider(IApplicationBuilder builder, Type serviceType)
at SimpleInjector.SimpleInjectorAspNetCoreIntegrationExtensions.GetRequestService[T](IApplicationBuilder builder)
at QuotingService.Startup.<>c__DisplayClass9_0.<InitializeContainer>b__0() in E:\Repos\QuotingService\QuotingService\Startup.cs:line 299
at lambda_method(Closure )
at SimpleInjector.InstanceProducer.BuildAndReplaceInstanceCreatorAndCreateFirstInstance()
at SimpleInjector.InstanceProducer.GetInstance()
--- End of inner exception stack trace ---
at SimpleInjector.InstanceProducer.GetInstance()
at SimpleInjector.InstanceProducer.VerifyInstanceCreation()
--- End of inner exception stack trace ---
at SimpleInjector.InstanceProducer.VerifyInstanceCreation()
at SimpleInjector.Container.VerifyInstanceCreation(InstanceProducer[] producersToVerify)
at SimpleInjector.Container.VerifyInternal(Boolean suppressLifestyleMismatchVerification)
at SimpleInjector.Container.Verify()
at QuotingService.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime) in E:\Repos\QuotingService\QuotingService\Startup.cs:line 229
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()

Any ideas on what needs to be changed to get this working?
Thanks for the awesome product and any help provided.


Solution

  • You should prevent making calls to GetRequestService inside a Simple Injector-registered delegate, since this such call requires the existence of an active HTTP request, which will not be available during application startup.

    Instead, rely upon AutoCrossWireAspNetComponents to get IOptionsSnapshot<SearchSettings> from ASP.NET Core.

    For this to work, however, you need to have called services.Configure<SearchSettings>.

    Here is a working configuration:

    public class Startup
    {
        private Container container = new Container();
    
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json");
            this.Configuration = builder.Build();
        }
    
        public IConfigurationRoot Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            // ASP.NET default stuff here
            services.AddMvc();
    
            this.IntegrateSimpleInjector(services);
    
            services.Configure<SearchSettings>(
                Configuration.GetSection("SearchSettings"));
        }
    
        private void IntegrateSimpleInjector(IServiceCollection services)
        {
            container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
    
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
            services.AddSingleton<IControllerActivator>(
                new SimpleInjectorControllerActivator(container));
    
            services.EnableSimpleInjectorCrossWiring(container);
            services.UseSimpleInjectorAspNetRequestScoping(container);
        }
    
        public void Configure(IApplicationBuilder app)
        {
            container.AutoCrossWireAspNetComponents(app);
            container.RegisterMvcControllers(app);
    
            container.Verify();
    
            // ASP.NET default stuff here
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
    

    With this config, you can inject IOptionsSnapshot<T> everywhere. For instance inside your HomeController:

    public class HomeController : Controller
    {
        private readonly IOptionsSnapshot<SearchSettings> snapshot;
    
        public HomeController(
            IOptionsSnapshot<SearchSettings> snapshot)
        {
            this.snapshot = snapshot;
        }
    }