autofac / Autofac.Owin

OWIN integration for Autofac
MIT License
23 stars 15 forks source link

Autofac - SingleInstance get's instantiated for every API request #39

Closed merlinschumacher closed 1 year ago

merlinschumacher commented 1 year ago

Describe the Bug

I'm working on an ASP.NET (not Core, but based on .NET Framework) WebAPI project. It uses Autofac for DI. I need to implement a Singleton/SingleInstance via Autofac. But the class gets instantiated for every API request. What I want is an object that exists for the entire runtime of the application and is always the same. Independent of any API request. The controller requests the object in the constructor and Autofac properly delivers an instance of the object, but it's always a new one. The only exception was, when I started the SingleInstance with AutoActivate it was created, then the first request got the same instance, but every following one received a new instance.

I already asked this on StackOverflow, but got no answer, so I hoped I'll find a response here. I'm thankful for any suggestions.

The class in question is named ScriptExecutionHost. It requests only a string as a parameter and two other singletons. See below for details.

Steps to Reproduce

Here is the relevant code:

Startup.cs


        public void Configuration(IAppBuilder app)
        {

            var container = ContainerConfig.Configure();
            var config = GetHttpConfiguration(container);

            app.UseAutofacMiddleware(container);
            app.UseAutofacLifetimeScopeInjector(container);
            app.UseCors(CorsOptions.AllowAll);
            ConfigureOAuth(app);

#if TESTSYSTEM || DEBUG
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
            SwaggerConfig.Register(config);
#endif

            var physicalFileSystem = new PhysicalFileSystem("./app");
            // Make ./app the default root of the static files in our Web Application.
            app.UseFileServer(new FileServerOptions
            {
                RequestPath = new PathString(string.Empty),
                EnableDirectoryBrowsing = true,
                EnableDefaultFiles = true,
                FileSystem = physicalFileSystem,
                StaticFileOptions = { FileSystem = physicalFileSystem, ServeUnknownFileTypes = true },
                DefaultFilesOptions = { DefaultFileNames = new[] { "index.html" } }
            });

            app.UseStageMarker(PipelineStage.MapHandler);

            app.UseAutofacWebApi(config);
            app.UseWebApi(config);
        }

        private static HttpConfiguration GetHttpConfiguration(IContainer container)
        {
            // Create HttpConfiguration instance
            var config = new HttpConfiguration
            {
                DependencyResolver = new AutofacWebApiDependencyResolver(container)
            };
            config.Formatters.Remove(config.Formatters.XmlFormatter);

            // tell the application to treat dates as UTC
            config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling =
                DateTimeZoneHandling.Utc;
            config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling =
                ReferenceLoopHandling.Ignore;
            config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
                new CamelCasePropertyNamesContractResolver();

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                "DefaultApi",
                "api/{controller}/{id}",
                new { id = RouteParameter.Optional }
            );
            return config;
        }

        private static void ConfigureOAuth(IAppBuilder app)
        {
            var oAuthServerOptions = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                Provider = new AuthorizationServerProvider()
            };

            // Token Generation
            app.UseOAuthAuthorizationServer(oAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }

ContainerConfig.Configure()

        public static IContainer Configure()
        {
            var builder = new ContainerBuilder();

            // Register your Web API controllers.
            builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

            // Register Credentials Service
            builder.Register(_ => new CredentialsService(CredentialManagerConfiguration.LoadFromWebConfig()))
                .As<CredentialsService>().SingleInstance();

            // Register our DatabaseContext
            builder.RegisterType<DatabaseContext>().AsSelf().InstancePerRequest();

            // Register the CredentialManagerConfiguration using the web.config
            builder.Register(_ => CredentialManagerConfiguration.LoadFromWebConfig()).AsSelf().SingleInstance();
            // Register the EmailConfiguration using the web.config
            builder.Register(_ => EmailConfiguration.LoadFromWebConfig()).AsSelf().SingleInstance();

            // Register the EmailClient using the EmailConfiguration from the web.config
            builder.RegisterType<EmailRepo>().As<IEmailRepo>().SingleInstance();

            // Register the UnitOfWork using the DatabaseContext, FileTemplates and EmailClient
            builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().AsSelf()
                .WithParameter("templateFileFolder", _fileTemplateBaseDirectory)
                .WithParameter("downloadDir", _fileDownloadBaseDirectory)
                .InstancePerRequest();

            builder.RegisterType<ScriptExecutionHost>().AsSelf().WithParameter("fileDownloadBaseDirectory", _fileDownloadBaseDirectory).SingleInstance().AutoActivate();

            // OPTIONAL: Register the Autofac model binder provider.
            builder.RegisterWebApiModelBinderProvider();

            return builder.Build();
        }

The relevant controllers constructor

        public ScriptsController(IUnitOfWork unitOfWork, ScriptExecutionHost scriptExecutionHost) : base(unitOfWork)
        {
            _unitOfWork = unitOfWork;
            _adService = new ActiveDirectoryService(new ActiveDirectoryConfiguration());
            _scripExecutionHost = scriptExecutionHost;
        }

The ScriptExecutionHost constructor

 public class ScriptExecutionHost : IRegisteredObject
    {
...
       public ScriptExecutionHost(string fileDownloadBaseDirectory, CredentialManagerConfiguration credentialManagerConfiguration, IEmailRepo emailRepo)
        {
            // Register ourself to the hosting environment to be able to shut down gracefully
            // This is necessary to ensure we can finish our work before the application pool is recycled
            HostingEnvironment.RegisterObject(this);

            _fileDownloadBaseDirectory = fileDownloadBaseDirectory;
            _credentialManagerConfiguration = credentialManagerConfiguration;
            _emailRepo = emailRepo;

            _logger.Debug("ScriptExecutionHost initialized.");
            _logger.Debug("File download base directory: {0}", _fileDownloadBaseDirectory);

// I used this to check if Autofac truly creates a new instance of this class every time.
            initTime = DateTime.Now;
        }

Expected Behavior

I excpect a SingleInstance to generate a singleton type object, that only exists once for the entire lifetime of the application.

Dependency Versions

Autofac: 7.0.1 Autofac.Owin: 7.1.0 Autofac.WebApi2: 6.1.1 Autofac.WebApi2.Owin: 6.2.1 CsvHelper: 30.0.1 Microsoft.AspNet.WebApi: 5.2.9 Microsoft.AspNet.WebApi.Client: 5.2.9 Microsoft.AspNet.WebApi.Core: 5.2.9 Microsoft.AspNet.WebApi.Cors: 5.2.9 Microsoft.AspNet.WebApi.Owin: 5.2.9 Microsoft.AspNet.WebApi.WebHost: 5.2.9 Microsoft.CodeDom.Providers.DotNetCompilerPlatform: 4.1.0 Microsoft.IdentityModel.JsonWebTokens: 6.30.0 Microsoft.IdentityModel.Logging: 6.30.0 Microsoft.IdentityModel.Tokens: 6.30.0 Microsoft.Owin: 4.2.2 Microsoft.Owin.Cors: 4.2.2 Microsoft.Owin.FileSystems: 4.2.2 Microsoft.Owin.Host.SystemWeb: 4.2.2 Microsoft.Owin.Security: 4.2.2 Microsoft.Owin.Security.Jwt: 4.2.2 Microsoft.Owin.Security.OAuth: 4.2.2 Microsoft.Owin.StaticFiles: 4.2.2 Microsoft.Web.Infrastructure: 2.0.0 Newtonsoft.Json: 13.0.3 NLog: 5.1.4 NLog.Web: 5.2.3 Owin: 1.0.0 Swashbuckle: 5.6.0 System.IdentityModel.Tokens.Jwt: 6.30.0

tillig commented 1 year ago

The reason I didn't answer it on Stack Overflow is because it's not a minimal reproducible example.

In this case, minimal is the key word. There is a lot you can do to reduce the problem and figure this out yourself, or at least help other folks help you. To start...

I guarantee singletons work. Now you've established that the issue is with app code, not Autofac, and that the question is definitely a "how do I?" question and not a bug. (This is why I'm going to close this issue after I comment here.)

Now add a single string parameter to the minimal ScriptExecutionHost.

You should end up with a fraction of the NuGet dependencies and none of the irrelevant code to do with credentials managers and file downloads and all that junk.

Cool, so now you can start figuring out how to inject the string parameter, for which we have an FAQ that can help give you ideas.

While I'd love to be able to personally work with folks and answer every question, the reason we point folks to SO and very explicitly away from issues is that none of the Autofac maintained have the time to offer free consulting hours. There's no paid team here, no one has a "job writing and supporting Autofac," so we have to rely on the community. The community doesn't monitor issues, so they're reserved for actual issues. We do monitor SO but there's no way we can personally answer each question, especially if a question looks like it's going to eat a lot of time to answer - those invariably turn from "question" into "let's iterate over the code and have a discussion and get clarification..." and it becomes a days-long support issue. (I think the Code Review Stack Overflow handles the iteration and discussion questions.) Just writing this answer up was a half hour. Answers take time. Lots of time.

Anyway, that's all I can offer here. I hope it helps unblock you. Sorry I can't offer more.