当前位置:网站首页>Avoid using asp Net core 3.0 to inject services for startup classes

Avoid using asp Net core 3.0 to inject services for startup classes

2022-06-12 22:39:00 Wind god Shura envoy

How to upgrade to ASP.NET Core 3.0 The second in a series .

In this blog , I will describe from ASP.NET Core 2.x Application upgrade to .NET Core 3.0 One modification that needs to be made : You don't need to be in Startup The service is injected into the constructor .

stay ASP.NET Core 3.0 Migrate to a common host

stay .NET Core 3.0 in , ASP.NET Core 3.0 The hosting foundation of has been redesigned as a general-purpose host , Instead of using it in parallel . So this is for those who are using ASP.NET Core 2.x Developers who develop applications , What does that mean ? At this stage , I have migrated multiple applications , up to now , Everything is going well . The official migration guidance document can guide you to complete the required steps , therefore , I strongly recommend that you read this document .

During the migration , The two most common problems I encounter are :

  • ASP.NET Core 3.0 The recommended way to configure Middleware in is to use endpoint routing (Endpoint Routing).
  • General purpose host is not allowed to be Startup Class injection service

The first of these , I've explained it before . Endpoint routing (Endpoint Routing) Is in ASP.NET Core 2.2 Introduced in , But it's limited to MVC Use in . stay ASP.NET Core 3.0 in , Endpoint routing has been implemented by the recommended terminal middleware , Because it provides many benefits . The most important one is , It allows middleware to get which endpoint will eventually be executed , And you can retrieve metadata about this endpoint (metadata). for example , You can authorize the health check endpoint application .

Endpoint routing requires special attention when configuring middleware order . I suggest you upgrade your app before , First read the instructions in the official migration document , Later, I will write a blog to introduce how to convert terminal middleware into endpoint routing .

Second point , It's the service injection that has been mentioned Startup class , But not enough publicity . I'm not sure if it's because not many people do it , Still in some scenes , It's easy to solve . In this paper , I will show some problem scenarios , And provide some solutions .

ASP.NET Core 2.x Start the service injected into the class

stay ASP.NET Core 2.x In the version , There is a little-known feature , That's where you can be Program.cs Configure your dependency injection container in the file . I've used this method for strongly typed options before , Then use these configurations when configuring the rest of the dependency injection container .

So let's see ASP.NET Core 2.x Example :

public class Program
{
    
    public static void Main(string[] args)
    {
    
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .ConfigureSettings(); //  Configure the service , The following will be in Startup Use in 
}

Have you noticed here that CreateWebHostBuilder I have called one ConfigureSettings() Methods ? This is an extension method I use to configure and apply strongly typed options . for example , This extension method may look like this :

public static class SettingsinstallerExtensions
{
    
    public static IWebHostBuilder ConfigureSettings(this IWebHostBuilder builder)
    {
    
        return builder.ConfigureServices((context, services) =>
        {
    
            var config = context.Configuration;

            services.Configure<ConnectionStrings>(config.GetSection("ConnectionStrings"));
            services.AddSingleton<ConnectionStrings>(
                ctx => ctx.GetService<IOptions<ConnectionStrings>>().Value)
        });
    }
}

So here ,ConfigureSettings() Method is called IWebHostBuilder Example of ConfigureServices() Method , Some settings are configured . Because these services will be in Startup It is configured into the dependency injection container before initialization , So in Startup Class in the constructor , These configured services can be injected .

public static class Startup
{
    
    public class Startup
    {
    
        public Startup(
            IConfiguration configuration, 
            ConnectionStrings ConnectionStrings) //  Inject preconfigured Services 
        {
    
            Configuration = configuration;
            ConnectionStrings = ConnectionStrings;
        }

        public IConfiguration Configuration {
     get; }
        public ConnectionStrings ConnectionStrings {
     get; }

        public void ConfigureServices(IServiceCollection services)
        {
    
            services.AddControllers();

            //  Use the connection string in the configuration 
            services.AddDbContext<BloggingContext>(options =>
                options.UseSqlServer(ConnectionStrings.BloggingDatabase));
        }

        public void Configure(IApplicationBuilder app)
        {
    

        }
    }
}

I find , When I first want to be in ConfigureServices When configuring other services with strongly typed option objects in the method , This model is very useful . In my example above ,ConnectionStrings Object is a strongly typed object , And this object is when the program enters Startup Before , Non null validation has been performed . This is not a formal basic technology , But real-time proof is very easy to use .

PS: How to ASP.NET Core Add validation for strongly typed option objects

However , If you switch to ASP.NET Core 3.0 After the universal host , You will find that this implementation will receive the following error message at run time .

Unhandled exception. System.InvalidOperationException: Unable to resolve service for type 'ExampleProject.ConnectionStrings' while attempting to activate 'ExampleProject.Startup'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
   at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
   at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass12_0.<UseStartup>b__0(HostBuilderContext context, IServiceCollection services)
   at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at ExampleProject.Program.Main(String[] args) in C:\repos\ExampleProject\Program.cs:line 21

In this way ASP.NET Core 3.0 Is no longer supported in . You can Startup Class's constructor injection IHostEnvironment and IConfiguration, But that's all . As for the reason , It should be that the previous implementation will bring some problems , Now I will give you a detailed description .

Be careful : If you insist on ASP.NET Core 3.0 Use in IWebHostBuilder, Instead of using a general-purpose host , You can still use the previous implementation . But I strongly advise you not to do this , And try to migrate to a common host as much as possible .

Two single cases ?

Inject services into Startup The fundamental problem with classes is , It will cause the system to build the dependency injection container twice . In the example I showed before ,ASP.NET Core Know you need one ConnectionStrings object , But the only way to know how to build this object is based on “ part ” The build IServiceProvider( In the previous example , We use ConfigureSettings() The extension method provides this “ part ” To configure ).

So why is this a problem ? The problem is this ServiceProvider It's a temporary one “ root ”ServiceProvider. It creates services and injects them into Startup in . then , The remaining dependency injection container configuration will be used as ConfigureServices Part of the method runs , And temporary ServiceProvider At this time, it has been discarded . Then a new ServiceProvider Will be created , It contains applications “ complete ” Configuration of .

such , Even if the service configuration uses Singleton Life cycle , It will also be created twice :

  • When using “ part ”ServiceProvider when , Created once , And aim at Startup Injected
  • When using " complete "ServiceProvider when , Created once

For my use case , Strongly typed options , This may be irrelevant . The system can not only have one configuration instance , It's just a better choice . But this is not always the case . This kind of service “ Let the cat out of the ” It seems to be the main reason for changing the behavior of general-purpose hosts - It makes things look safer .

So if I need ConfigureServices What about the internal service ?

Although we can't configure the service as before , But we still need an alternative way to meet the needs of some scenes !

One of the most common scenarios is to inject services into Startup, in the light of Startup.ConfigureServices Method to control the state of other services registered in the . for example , The following is a very basic example .

public class Startup
{
    
    public Startup(IdentitySettings identitySettings)
    {
    
        IdentitySettings = identitySettings;
    }

    public IdentitySettings IdentitySettings {
     get; }

    public void ConfigureServices(IServiceCollection services)
    {
    
        if(IdentitySettings.UseFakeIdentity)
        {
    
            services.AddScoped<IIdentityService, FakeIdentityService>();
        }
        else
        {
    
            services.AddScoped<IIdentityService, RealIdentityService>();
        }
    }

    public void Configure(IApplicationBuilder app)
    {
    
        // ...
    }
}

In this case , The code is injected by checking IdentitySettings Boolean property in object , To determine the IIdentityService Which implementation does the interface use to register : Or use fake Services , Or use services .

By transforming the static service registration into a factory function , Can make it necessary to inject IdentitySetting The implementation of the object is compatible with the general host . for example :

public class Startup
{
    
    public Startup(IConfiguration configuration)
    {
    
        Configuration = configuration;
    }

    public IConfiguration Configuration {
     get; }

    public void ConfigureServices(IServiceCollection services)
    {
    
        //  Inject containers for dependencies , To configure IdentitySetting
        services.Configure<IdentitySettings>(Configuration.GetSection("Identity")); 

        //  Register different implementations 
        services.AddScoped<FakeIdentityService>();
        services.AddScoped<RealIdentityService>();

        //  according to IdentitySetting To configure , Return a correct implementation at runtime 
        services.AddScoped<IIdentityService>(ctx => 
        {
    
            var identitySettings = ctx.GetRequiredService<IdentitySettings>();
            return identitySettings.UseFakeIdentity ? ctx.GetRequiredService<FakeIdentityService>()
                : ctx.GetRequiredService<RealIdentityService>();
            }
        });
    }

    public void Configure(IApplicationBuilder app)
    {
    
        // ...
    }
}

This implementation is obviously much more complex than previous versions , But at least it can be compatible with the general-purpose host .

actually , If only one strongly typed option is required , Then this method is a little too much . Contrary , Here I may just rebind the configuration :

public class Startup
{
    
    public Startup(IConfiguration configuration)
    {
    
        Configuration = configuration;
    }

    public IConfiguration Configuration {
     get; }

    public void ConfigureServices(IServiceCollection services)
    {
    
        //  Inject containers for dependencies , To configure IdentitySetting
        services.Configure<IdentitySettings>(Configuration.GetSection("Identity")); 

        //  Recreate strongly typed option objects , And bind the 
        var identitySettings = new IdentitySettings();
        Configuration.GetSection("Identity").Bind(identitySettings)

        //  Configure the correct service according to the conditions 
        if(identitySettings.UseFakeIdentity)
        {
    
            services.AddScoped<IIdentityService, FakeIdentityService>();
        }
        else
        {
    
            services.AddScoped<IIdentityService, RealIdentityService>();
        }
    }

    public void Configure(IApplicationBuilder app)
    {
    
        // ...
    }
}

besides , If you only need to load a string from the configuration file , I may not use strongly typed options at all . This is a .NET Core Configuration in the default template ASP.NET Core The method of identity system - Directly through IConfiguration Instance to retrieve the connection string .

public class Startup
{
    
    public Startup(IConfiguration configuration)
    {
    
        Configuration = configuration;
    }

    public IConfiguration Configuration {
     get; }

    public void ConfigureServices(IServiceCollection services)
    {
    
        //  For dependency injection containers , To configure ConnectionStrings
        services.Configure<ConnectionStrings>(Configuration.GetSection("ConnectionStrings")); 

        //  Get the configuration directly , Do not use strongly typed options 
        var connectionString = Configuration["ConnectionString:BloggingDatabase"];

        services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(connectionString));
    }

    public void Configure(IApplicationBuilder app)
    {
    
        // ...
    }
}

This implementation is not the best , But they can all meet our needs , And most of the scenes . If you didn't know before Startup Service injection features , Then you must have used one of the above methods .

Use IConfigureOptions Come on IdentityServer To configure

Another common scenario for using injection configuration is configuration IdentityServer Validation of the .

public class Startup
{
    
    public Startup(IdentitySettings identitySettings)
    {
    
        IdentitySettings = identitySettings;
    }

    public IdentitySettings IdentitySettings {
     get; }

    public void ConfigureServices(IServiceCollection services)
    {
    
        //  To configure IdentityServer Verification method of 
        services
            .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
    
                //  Use the strongly typed option to configure the authentication processor 
                options.Authority = identitySettings.ServerFullPath;
                options.ApiName = identitySettings.ApiName;
            });
    }

    public void Configure(IApplicationBuilder app)
    {
    
        // ...
    }
}

In this case ,IdentityServer The basic address and address of the instance API Resource names are all through strongly typed options IdentitySettings Set up . This implementation is in .NET Core 3.0 Is no longer applicable , So we need an alternative solution . We can use the way mentioned before - Rebind strongly typed options or use directly IConfiguration Object retrieval configuration .

besides , The third option is to use IConfigureOptions, This is how I check AddIdentityServerAuthentication The underlying code of the method is found .

The fact proved that ,AddIdentityServerAuthentication() Methods can do different things . First , It's equipped with JWT Bearer verification , And the way of verification is specified through the strongly typed option . We can use it to delay the configuration of naming options (named options), Change to use IConfigureOptions example .

IConfigureOptions The interface allows you to use Service Provider Other dependencies in delay configuring strongly typed option objects . for example , If you want to configure my TestSettings The service , I need to call TestService A method in class , I can create one IConfigureOptions Object instances , The code is as follows :

public class MyTestSettingsConfigureOptions : IConfigureOptions<TestSettings>
{
    
    private readonly TestService _testService;
    public MyTestSettingsConfigureOptions(TestService testService)
    {
    
        _testService = testService;
    }

    public void Configure(TestSettings options)
    {
    
        options.MyTestValue = _testService.GetValue();
    }
}

TestService and IConfigureOptions<TestSettings> It's all in Startup.ConfigureServices Method .

public void ConfigureServices(IServiceCollection services)
{
    
    services.AddScoped<TestService>();
    services.ConfigureOptions<MyTestSettingsConfigureOptions>();
}

The most important point here is , You can inject a dependency using the standard constructor IOptions<TestSettings> object . There is no need to be in ConfigureServices In the method “ Partial construction ”Service Provider, You can configure TestSettings. Contrary , We registered the configuration TestSettings The intent of the , But the real configuration will be postponed until the configuration object is used .

So this is for our configuration IdentityServer, What's the help ?

AddIdentityServerAuthentication A variant of the strongly typed option is used , We call this naming option (named options). This method is very common when verifying the configuration , Like the example above .

In short , You can use IConfigureOptions Method will validate the naming options used by the handler IdentityServerAuthenticationOptions Configuration delay . therefore , You can create one that will IdentitySettings As a construction parameter ConfigureIdentityServerOptions object .

public class ConfigureIdentityServerOptions : IConfigureNamedOptions<IdentityServerAuthenticationOptions>
{
    
    readonly IdentitySettings _identitySettings;
    public ConfigureIdentityServerOptions(IdentitySettings identitySettings)
    {
    
        _identitySettings = identitySettings;
        _hostingEnvironment = hostingEnvironment;
    }

    public void Configure(string name, IdentityServerAuthenticationOptions options)
    {
     
        // Only configure the options if this is the correct instance
        if (name == IdentityServerAuthenticationDefaults.AuthenticationScheme)
        {
    
            //  Use strong type IdentitySettings Value in object 
            options.Authority = _identitySettings.ServerFullPath; 
            options.ApiName = _identitySettings.ApiName;
        }
    }

    // This won't be called, but is required for the IConfigureNamedOptions interface
    public void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options);
}

stay Startup.cs In file , You need to configure strong typing IdentitySettings object , Add the required IdentityServer service , And register ConfigureIdentityServerOptions class , So that when needed , It can be configured IdentityServerAuthenticationOptions.

public void ConfigureServices(IServiceCollection services)
{
    
    //  Configure strong typing IdentitySettings Options 
    services.Configure<IdentitySettings>(Configuration.GetSection("Identity"));

    //  To configure IdentityServer Verification mode 
    services
        .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication();

    //  Add other configurations 
    services.ConfigureOptions<ConfigureIdentityServerOptions>();
}

here , We don't have to ask Startup Inject anything into the class , But you can still get the benefits of strong typing options . So here we get a win-win result .

summary

In this paper , I described upgrading to ASP.NET Core 3.0 when , Yes, you need to be right Startup Class . I passed the Startup Class , It describes ASP.NET Core 2.x Problems in , And how to ASP.NET Core 3.0 Remove this feature from . Finally, I showed , Change how to do it when you need this implementation .

原网站

版权声明
本文为[Wind god Shura envoy]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202281140167888.html