Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't use registered singetons in ConfigureServices without them being instantiated twice

I have a .Net Core project that registers a number of Singletons as follows:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    services.AddLogging();

    services.AddSingleton<IConfiguration>(Configuration);
    services.AddSingleton<IDbFactory, DefaultDbFactory>();
    services.AddSingleton<IUserRepository, UserRepository>();    
    services.AddSingleton<IEmailService, EmailService>();          
    services.AddSingleton<IHostedService, BackgroundService>();
    services.AddSingleton<ISettingsRepository, SettingsRepository>();
    services.AddSingleton(typeof(TokenManager));

    var sp = services.BuildServiceProvider();
    var userRepository = sp.GetService<IUserRepository>();
    // ...
}

This is the only place these classes are registered, and no other instances are created anywhere, however I have noticed that the constructors are called twice. Why is that?

like image 960
pcdev Avatar asked Jul 30 '18 09:07

pcdev


People also ask

Can you call BuildServiceProvider multiple times?

Calling BuildServiceProvider multiple times can cause serious trouble, because each call to BuildServiceProvider results in a new container instance with its own cache. This means that registrations that are expected to have the Singleton lifestyle, suddenly are created more than once.

What is difference between configure and ConfigureServices?

They are the ConfigureServices method and the Configure method. In the Startup class, we actually do two things: Configure Service(): It is used to add services to the container and configure those services. basically, service is a component that is intended for common consumption in the application.

Is configure or ConfigureServices called first?

The ConfigureServices methodCalled by the host before the Configure method to configure the app's services.

What is the difference between transient and Singleton?

Singleton is a single instance for the lifetime of the application domain. Scoped is a single instance for the duration of the scoped request, which means per HTTP request in ASP.NET. Transient is a single instance per code request.


1 Answers

TL;DR - If you need one of your registered singletons to be passed in as a configuration option to AddMvc, Don't do what I did and use GetService in the ConfigureServices method. Skip down to EDIT 2 below.

I found this answer to a similar question but the answer wasn't clear about the order with respect to the service registration, however it did point out that calling services.BuildServiceProvider() builds a new container which causes the services to be reregistered. Kind of makes sense in hindsight...

EDIT:

My original fix was to move services.BuildServiceProvider() before the AddSingleton registrations, however it turns out this doesn't always work, as Buddha Buddy pointed out.

I've just seen this question which gives a lot more detail into what's happening. The original registrations are not discarded, as I thought they were. I was thinking about it all wrong.

Calling services.BuildServiceProvider() does build a new service provider/container, but this has nothing to do with registrations. The class implementing IUserRepository and all dependent services are instantiated by the new service provider when sp.GetService<IUserRepository>() is called, but the original IServiceProvider remains, and that's what the rest of the application will use.

So down the line when the application requires an instance of IUserRepository (say because it's an injected dependency of UsersController, which has now been requested), the IUserRepository service (and its dependencies, if any) are again instantiated, because it's now under the original IServiceProvider.

A comment in the answer to the question above states that you can prevent this and use the one IServiceProvider, "by returning the service provider instance from the ConfigureServices method so that will be the container your application uses as well". I'm not sure how you would do that short of storing a class variable pointing to it, so it can be swapped out in the Configure method by setting app.ApplicationServices - but that doesn't work for me either, because the new service provider is missing all the MVC services.

Some people recommend using the app.ApplicationServices in Configure() to access the required service instead, but that won't work for me as I need to use it in ConfigureServices as follows:

//...

serviceProvider = services.BuildServiceProvider();
var userRepository = serviceProvider.GetService<IUserRepository>();

// Add framework services
services.AddMvc(

    config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                        .RequireAuthenticatedUser()
                        .Build();
        config.Filters.Add(new RolesAuthorizationFilter(userRepository));
    });

EDIT 2:

Found the solution! This brilliant post described what I was trying to achieve and presented a very tidy solution.

If you need to pass a reference to one of your registered singletons in as MVC configuration options, rather than try and instantiate it inside ConfigureServices, you can simply create a new class that implements IConfigureOptions<MvcOptions> which can take injected dependencies, register this class as a singleton - and everything else is taken care of - brilliant!

Example:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    services.AddLogging();

    services.AddSingleton<IConfiguration>(Configuration);
    services.AddSingleton<IDbFactory, DefaultDbFactory>();
    services.AddSingleton<IUserRepository, UserRepository>();    
    services.AddSingleton<IEmailService, EmailService>();          
    services.AddSingleton<IHostedService, BackgroundService>();
    services.AddSingleton<ISettingsRepository, SettingsRepository>();
    services.AddSingleton(typeof(TokenManager));

    // var sp = services.BuildServiceProvider(); // NO NEED for this after all
    // var userRepository = sp.GetService<IUserRepository>();

    services.AddMvc(); // No config options required any more
    services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();  // Here be the magic...

    // ...
}

Then create the new class:

public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
    private readonly IUserRepository userRepository;
    public ConfigureMvcOptions(IUserRepository userRepository)
    {
        this.userRepository = userRepository;
    }

    public void Configure(MvcOptions options)
    {
        var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
        options.Filters.Add(new RolesAuthorizationFilter(userRepository));
    }
}

Thanks to the magic of DI, everything else is taken care of. My user repository singleton and its dependencies are only instantiated once.

like image 151
pcdev Avatar answered Sep 29 '22 07:09

pcdev