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

China☆狼群 提交于 2020-01-01 14:34:13

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/51591072/cant-use-registered-singetons-in-configureservices-without-them-being-instantia

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!