在 Startup.cs 之外实现依赖注入

Posted

技术标签:

【中文标题】在 Startup.cs 之外实现依赖注入【英文标题】:Implement dependency injection outside of Startup.cs 【发布时间】:2017-03-11 10:55:41 【问题描述】:

我想在 ASP.NET CORE 1 中实现依赖注入。我知道 .Net Core 中的一切都是关于 DI 的。例如

   public void ConfigureServices(IServiceCollection services)
   
      // Add application services.
     services.AddTransient<IDateTime, SystemDateTime>();
   

但是对于拥有超过 20 个实体和服务的大型项目,在 ConfigureServices 中编写所有这些代码行非常困难且难以阅读。我想知道这是否可能,然后将其添加到服务中。

感谢您的回答。

【问题讨论】:

你可以编写 IServiceCollection 的扩展方法,在 Startup 中每个扩展方法一行代码添加很多东西 我知道扩展方法。但是例如你能写答案吗? 对一组注册应用“提取方法”重构怎么样?稍后这些方法也可以移动到其他类中。 @Steven,你能写出示例代码作为答案吗? 【参考方案1】:

可以编写IServiceCollection的扩展方法,将大量的服务注册封装到Startup.cs中的1行代码中

例如,这是我项目中的一个:

using cloudscribe.Core.Models;
using cloudscribe.Core.Models.Setup;
using cloudscribe.Core.Web;
using cloudscribe.Core.Web.Components;
using cloudscribe.Core.Web.Components.Editor;
using cloudscribe.Core.Web.Components.Messaging;
using cloudscribe.Core.Web.Navigation;
using cloudscribe.Web.Common.Razor;
using cloudscribe.Web.Navigation;
using cloudscribe.Web.Navigation.Caching;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;

namespace Microsoft.Extensions.DependencyInjection

    public static class StartupExtensions
    
        public static IServiceCollection AddCloudscribeCore(this IServiceCollection services, IConfigurationRoot configuration)
        
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.Configure<MultiTenantOptions>(configuration.GetSection("MultiTenantOptions"));
            services.Configure<SiteConfigOptions>(configuration.GetSection("SiteConfigOptions"));
            services.Configure<UIOptions>(configuration.GetSection("UIOptions"));
            services.Configure<CkeditorOptions>(configuration.GetSection("CkeditorOptions"));
            services.Configure<CachingSiteResolverOptions>(configuration.GetSection("CachingSiteResolverOptions"));
            services.AddMultitenancy<SiteContext, CachingSiteResolver>();
            services.AddScoped<CacheHelper, CacheHelper>();
            services.AddScoped<SiteManager, SiteManager>();
            services.AddScoped<GeoDataManager, GeoDataManager>();
            services.AddScoped<SystemInfoManager, SystemInfoManager>();
            services.AddScoped<IpAddressTracker, IpAddressTracker>();
            services.AddScoped<SiteDataProtector>();
            services.AddCloudscribeCommmon();
            services.AddScoped<ITimeZoneIdResolver, RequestTimeZoneIdResolver>();
            services.AddCloudscribePagination();
            services.AddScoped<IVersionProviderFactory, VersionProviderFactory>();
            services.AddScoped<IVersionProvider, CloudscribeCoreVersionProvider>();
            services.AddTransient<ISiteMessageEmailSender, SiteEmailMessageSender>();
            services.AddTransient<ISmsSender, SiteSmsSender>();
            services.AddSingleton<IThemeListBuilder, SiteThemeListBuilder>();
            services.TryAddScoped<ViewRenderer, ViewRenderer>();
            services.AddSingleton<IOptions<NavigationOptions>, SiteNavigationOptionsResolver>();
            services.AddScoped<ITreeCacheKeyResolver, SiteNavigationCacheKeyResolver>();
            services.AddScoped<INodeUrlPrefixProvider, FolderTenantNodeUrlPrefixProvider>();
            services.AddCloudscribeNavigation(configuration);

            services.AddCloudscribeIdentity();

            return services;
        


    

在 Startup.cs 中,我用一行代码调用该方法

services.AddCloudscribeCore(Configuration);

【讨论】:

您好,Joe.. 我们又遇到一个问题... 这是一种方法,有效,但需要大量维护。我个人会使用程序集扫描(如我的回答中所述)。【参考方案2】:

有几种方法可以采用,但有些只是在类之间移动代码;我建议您考虑Assembly Scanning,因为我将其描述为下面的第二个选项:

1. “解决问题”:扩展方法

初始选项是使用extension methods 来配置服务。

这是一个将多个服务注册包装到一个扩展方法中的示例:

    public static IServiceCollection AddCustomServices(this IServiceCollection services)
    
        services.AddScoped<IBrowserConfigService, BrowserConfigService>();
        services.AddScoped<IManifestService, ManifestService>();
        services.AddScoped<IRobotsService, RobotsService>();
        services.AddScoped<ISitemapService, SitemapService>();
        services.AddScoped<ISitemapPingerService, SitemapPingerService>();

        // Add your own custom services here e.g.

        // Singleton - Only one instance is ever created and returned.
        services.AddSingleton<IExampleService, ExampleService>();

        // Scoped - A new instance is created and returned for each request/response cycle.
        services.AddScoped<IExampleService, ExampleService>();

        // Transient - A new instance is created and returned each time.
        services.AddTransient<IExampleService, ExampleService>();

        return services;
    

这可以在ConfigureServices内调用:

services.AddCustomServices();

注意:这对于特定配置(例如,当服务需要向其传递多个选项时)作为“构建器模式”很有用,但不能解决必须通过手动编码注册多个服务;本质上和写同样的代码,但是在不同的类文件中没有什么不同,而且还是需要人工维护。

2。 “解决问题”:装配体扫描

“最佳实践”选项是Assembly Scanning,用于根据Implemented Interfaces 自动查找和注册组件;下面是一个 Autofac 示例:

var assembly= Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(assembly)
       .Where(t => t.Name.EndsWith("Repository"))
       .AsImplementedInterfaces();

处理注册的生命周期(或范围)的一个技巧是使用标记接口(空接口),例如IScopedService,并使用它来扫描和注册具有适当生命周期的服务。这是注册多个服务的最低摩擦方法,它是自动的,因此是“零维护”。

注意:内置的 ASP.Net Core DI 实现不支持Assembly Scanning(作为 pf 当前,2016 版本);然而,Github(和 Nuget)上的 Scrutor 项目添加了这个功能,它将服务和类型注册压缩到:

var collection = new ServiceCollection();

collection.Scan(scan => scan
    .FromAssemblyOf<ITransientService>()
        .AddClasses(classes => classes.AssignableTo<ITransientService>())
            .AsImplementedInterfaces()
            .WithTransientLifetime()
        .AddClasses(classes => classes.AssignableTo<IScopedService>())
            .As<IScopedService>()
            .WithScopedLifetime());

总结

Assembly Scanning,结合Extension Methods(如果适用)将为您节省大量维护工作,并在应用程序启动时执行一次,随后进行缓存。它消除了手动代码服务注册的需要。

【讨论】:

您应该考虑删除对 ASP.Net MVC Bolierplate 模板的引用,链接已损坏 @folius 感谢您的提醒 - 已删除。【参考方案3】:

可以写一个批量注册的扩展方法:

    public static void AddScopedFromAssembly(this IServiceCollection services, Assembly assembly)
    
        var allServices = assembly.GetTypes().Where(p =>
            p.GetTypeInfo().IsClass &&
            !p.GetTypeInfo().IsAbstract);
        foreach (var type in allServices)
        
            var allInterfaces = type.GetInterfaces();
            var mainInterfaces = allInterfaces.Except
                    (allInterfaces.SelectMany(t => t.GetInterfaces()));
            foreach (var itype in mainInterfaces)
            
                services.AddScoped(itype, type); // if you want you can pass lifetime as a parameter
            
        
    

及用法:

 services.AddScopedFromAssembly(assembly);

【讨论】:

【参考方案4】:

我最近实现了程序集扫描方法(成功),但最后发现 cluster_registrations_in_a_few_extension_methods 方法对于我自己和其他从事它的程序员来说更容易阅读。 如果您将注册集群保持在注册类的定义位置附近,则维护工作总是比注册类本身所涉及的维护工作少得多。

【讨论】:

我使用了扩展方法。它会起作用。我不清楚装配扫描方法。【参考方案5】:

DependenciesManager 类添加到您的项目并实现AddApplicationRepositories 方法。

 public static class DependenciesManager
 
        public static void AddApplicationRepositories(this IServiceCollection service)
        
            var assembly = Assembly.GetExecutingAssembly();

            var services = assembly.GetTypes().Where(type =>
            type.GetTypeInfo().IsClass && type.Name.EndsWith("Repository") &&
            !type.GetTypeInfo().IsAbstract);
      
            foreach (var serviceType in services)
            
                var allInterfaces = serviceType.GetInterfaces();
                var mainInterfaces = allInterfaces.Except
                (allInterfaces.SelectMany(t => t.GetInterfaces()));

                foreach (var iServiceType in mainInterfaces)
                
                    service.AddScoped(iServiceType, serviceType);
                
            
        
   

Startup 类中,在ConfigureServices 方法中添加services.AddApplicationRepositories();

public void ConfigureServices(IServiceCollection services)

     services.AddApplicationRepositories();

如果您需要注册不同的服务,只需在DependenciesManager 类中实现更多方法即可。比如需要注册一些Authorization Handler服务,实现AddAuthorizationHandlers方法即可:

 public static void AddAuthorizationHandlers(this IServiceCollection service)
  
        var assembly = Assembly.GetExecutingAssembly();

        var services = assembly.GetTypes().Where(type =>
            type.GetTypeInfo().IsClass && type.Name.EndsWith("Handler") &&
            !type.GetTypeInfo().IsAbstract);

        foreach (var serviceType in services)
        
            var allInterfaces = serviceType.GetInterfaces();
            var mainInterfaces = allInterfaces.Except
                (allInterfaces.SelectMany(t => t.GetInterfaces()));

            foreach (var iServiceType in mainInterfaces)
            
                service.AddScoped(iServiceType, serviceType);
            
        
    

Startup 类中添加:

 services.AddAuthorizationHandlers();

注意:根据我的回答,您要注册的服务名称及其实现必须以“Repository”或“Handler”结尾。

【讨论】:

以上是关于在 Startup.cs 之外实现依赖注入的主要内容,如果未能解决你的问题,请参考以下文章

实体框架核心 DbContext 和依赖注入

非控制器的网络核心依赖注入

.net core 3 依赖注入服务作为“配置”的参数

同一存储的依赖注入多个接口(具有不同的connectionString)

在依赖注入中使用 Entity Framework Core DbContext 时不调用 OnModelCreating

ASP.NET Core 2 中的依赖注入引发异常