ASP.NET CORE Startup文件优化

27 篇文章 1 订阅
9 篇文章 0 订阅

概要

在ASP.NET CORE开发大型项目过程中,我们一般都会需要大量第三方的软件或服务配合使用,例如EntityFramwork,redist,Jwt等。根据ASP.NET CORE的设计思想“Pay for what you use”,像EntityFramwork/DBContext,redis,Jwt, Swagger的服务初始化配置内容,都要通过依赖注入的方式引入项目。

这就意味着在开发过程中,大量的服务配置相关的代码,都要放到Startup.cs文件中。造成该文件非常庞大并且不易管理。在多团队配合开发过程中,该文件可能被频繁merge,造成各种编译错误。

基于上述问题,本文介绍两种解决方案,实现Startup.cs文件中代码的优化管理。

已有解决方案的弊端

按照集中式管理方式,我们将所有的服务配置代码全部放到Startup.cs中的ConfigureServices方法中,如下所示,文件变得非常庞大。 中间件部分代码不是本次讨论重点,下面代码中已经略去。

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

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services
                .AddDbContext<BranchMngtDbContext>(options => options
                .UseSqlServer(configuration.GetDefaultConnectionString()));
                .AddControllersWithViews();
            services.AddIdentity<User, IdentityRole>(options =>
                {
                    options.Password.RequiredLength = 6;
                    options.Password.RequireDigit = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequireNonAlphanumeric = false;
                    options.Password.RequireUppercase = false;
                })
                .AddEntityFrameworkStores<BranchMngtDbContext>();
              services
                .AddAuthentication(x =>
                {
                    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(x =>
                {
                    x.RequireHttpsMetadata = false;
                    x.SaveToken = true;
                    x.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });
			services
                .AddSwaggerGen(c =>
                    {
                        c.SwaggerDoc(
                            version, 
                            new OpenApiInfo
                            {
                                Title = "Project Name", 
                                Version = "v 1.0"
                            });
                    });
            // 注入用户自定义服务        
			//services.AddScoped<>();
			//services.Singleton<>();
			//services.Transient<>();
        }
    }

对于有一定规模的团队开发而言,不同的小team负责不同的模块,很可能要在项目中注入不同的第三方服务,这样就造成了单文件多负责人的情况,从而对未来的开发和运维造成了很大的隐患。

分布式管理的解决方案

本文以一个银行分行管理系统为例,说明如何实现Startup文件的分布式管理。

解决方案1

基于分布式管理方式,我们解耦当前的文件,Startup文件只负责决定注入哪些服务,具体的服务配置代码,放到各自服务对应的文件中,结构图如下:

在这里插入图片描述
我们为按照OOP的思想,为每个服务的配置代码封装 一个类。在类内方法中完成具体配置。每个类的构造方法接受一个IServiceCollection参数。

基本思路

我们为IServiceCollection定义一个扩展方法,该方法负责实例化所有服务配置相关的类,并调用相关的服务配置方法。

考虑到未来的可扩展行,我们在扩展方法中采用泛型参数,一旦有新的服务配置类,我们不需要修改已有的代码。

如果采用常规的方式,我们还是要把配置类实例化的具体代码放到Startup文件中,这又造成该文件管理很多细节代码。所以在扩展方法中,我们采用表达式目录树的方式,实现泛型参数类的自动实例化。

代码实现

namespace BranchMngt.Web.Infrastructure
{
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using BranchMngt.Common.Settings;
    public static class ServiceCollectionExtension
    {
        public static IServiceCollection CreateExtensionService<T>(this IServiceCollection services, Action<T> action){
            // IServiceCollection => new T(IServiceCollection);
            ParameterExpression parameter = Expression.Parameter(typeof(IServiceCollection),"builder");
            ConstructorInfo ctor = typeof(T).GetConstructor(
                BindingFlags.Instance | BindingFlags.Public, 
                null, 
                CallingConventions.HasThis, 
                new[] { typeof(IServiceCollection) }, new ParameterModifier[] {}
            );
            var expressionNew = Expression.New(ctor,new Expression[]{parameter});            
            LambdaExpression  instantiateLambda = Expression.Lambda<Func<IServiceCollection,T>>(expressionNew, new ParameterExpression[]{parameter});
            var instantiateDelegate = (Func<IServiceCollection,T>) instantiateLambda.Compile();
            var builder = instantiateDelegate.Invoke(services);
            action(builder);
            return services;
        }
    }
}
  1. 我们通过表达式目录树构建一个Lambda表达式IServiceCollection => new T(IServiceCollection);,调用该表达式创建的委托,可以实例化任何配置类T;
  2. 构建Lambda表达式中的参数表达式;
  3. 在泛型类中找到包含IServiceCollection参数的构造函数表达式;
  4. 构建类实例化的New表达式;
  5. 利用构建好的表达式new T(IServiceCollection);和参数表达式构造Lambda表达式IServiceCollection => new T(IServiceCollection);
  6. 利用Lambda表达式构建委托;
  7. 调用委托实例化具体的泛型类;
  8. 调用实例中的方法,完成第三方服务配置。
  9. 为了支持责任链方式调用,将I配置好对应服务的ServiceCollection实例返回。

DBContext服务配置类的定义

根据基本思路中的要求,我们定义DBContextBuilder类来完成BranchMngtDbContext的依赖注入和EntityFramework的基本配置。代码如下,IConfiguration 的扩展方法详见附录。

namespace BranchMngt.Web.Infrastructure.Extensions
{
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.EntityFrameworkCore;
    using BranchMngt.Data;
    using BranchMngt.Common.DI;
    public class DBContextBuilder
    {
        private readonly IServiceCollection _services;
        public DBContextBuilder(IServiceCollection services)
        {
            _services = services;
        }
        public void UseBranchMngtDbContext( IConfiguration configuration){
            _services
                .AddDbContext<BranchMngtDbContext>(options => options
                    .UseSqlServer(configuration.GetDefaultConnectionString()));
        }
    }
}
  1. DBContextBuilder类的构造方法包含IServiceCollection参数
  2. UseBranchMngtDbContext方法未来将以Action的形式作为参数,传入扩展方法CreateExtensionService中,完成BranchMngtDbContext的依赖注入。

IServiceCollection的扩展方法CreateExtensionService的调用

通过调用CreateExtensionService方法,我们可以将服务配置类中的配置方法在Startup文件的ConfigureService方法中逐一执行,代码如下。IdentityBuilder,JwtAuthBuilder和SwaggerBuilder代码组织方式与DBContextBuilder类似,不在赘述,详见附录。

 public void ConfigureServices(IServiceCollection services){
			services
                .CreateExtensionService((DBContextBuilder builder) => builder.UseBranchMngtDbContext(this.Configuration))
                .CreateExtensionService((IdentityBuilder builder) => builder.UseIdentity())
                .CreateExtensionService((JwtAuthBuilder builder) => builder.UseJwtAuth(this.Configuration))
                .CreateExtensionService((SwaggerBuilder builder) => builder.UseSwagger(this.Configuration))
                .CreateExtensionServices(this.Configuration)
                .AddControllersWithViews();
}
  1. CreateExtensionService方法会返回IServiceCollection实例,所以可以进行链式调用。
  2. Startup文件已经定义了Configuration,所以在执行服务配置的方式时候,如果需要读取项目配置,可以直接通过Configuration获取。

基于上述代码,我们实现了Startup文件和具体依赖注入服务的配置代码的解耦,
在Startup文件只决定注入什么服务,具体服务配置代码未来可以由不同的负责人来管理,从而避免了单个文件多负责人的隐患。

分布式管理的解决方案 2

解决方案1实现了Startup文件和具体服务配置代码之间的解耦,但是如果我们当前的项目要使用一个新的第三方的服务,例如缓存服务,我们需要使用Redis相关的配置代码,这个时候,我们就要修改Startup,如下图所示:

在这里插入图片描述
在Asp.NET CORE 项目的代码管理中,个人建议应该尽量减少对Startup文件的修改。

基于上述问题,我们提出方案2,实现服务配置代码的自动发现和执行。这样也就避免了对重要配置文件的频繁修改。

基本思路

方案1中,我们由Startup来决定哪些服务需要注入,从而造成了新服务添加,还要修改该文件的问题,所以在方案2中,我们将该控制权交给服务配置类。该问题也就自然解决。

我们要代码可以自动发现服务配置相关的类,就需要这些类具有一定的特征,例如都继承自某一个基类。

我们在执行服务注册代码的时候,有些特殊情况,是要考虑执行顺序问题的,所以当我们自动发现了所有服务配置类并实例化以后,需要进行一定的排序。

代码实现

AutoServiceBuilder类是服务配置类被自动发现的开关,如果具体的配置类内的配置代码希望被自动发现并执行,则请继承AutoServiceBuilder类。

namespace BranchMngt.Web.Infrastructure
{
    using Microsoft.Extensions.Configuration;

    public abstract class AutoServiceBuilder
    {
        public int Priority {get; set;} = 0;
        public abstract void InjectService(IConfiguration configuration);
    }
}
  1. Priority为权值,用于决定该中代码的执行顺序,默认为0,权值越高,则执行越优先执行。
  2. 继承AutoServiceBuilder的子类要复写InjectService方法,该方法中定义具体的服务配置代码。
  3. InjectService方法参数IConfiguration,即项目的配置文件appsettings,json,任何服务配置相关参数可以通过该文件读取,例如Swagger的版本号,DBContext的数据库链接字符等。
  4. 取消该继承关系,则该类内服务配置代码不会被自动执行。

我们还是通过为IServiceCollection定义一个扩展方法,来实现配置服务类的自动发现,实例化和执行相关配置的操作。

   public static IServiceCollection CreateExtensionServices(this IServiceCollection services, IConfiguration configuration){
            var builderInstances = typeof(AutoServiceBuilder)
                .Assembly
                .GetExportedTypes()
                .Where(t => t.IsClass && ! t.IsAbstract && typeof(AutoServiceBuilder).IsAssignableFrom(t))
                .Select(s => (AutoServiceBuilder)Activator.CreateInstance(s, services)) 
                .OrderByDescending( s => s.Priority);          
            foreach(var builder in builderInstances){
                builder.InjectService(configuration);
            }
            return services;
        }
  1. 获取当前程序集Assembly;
  2. 在程序集中获取所有的public的类,并且这些类继承自抽象类AutoServiceBuilder;
  3. 将所有服务配置类实例化并且按照权值大小,降序排列。
  4. 执行每个实例中的依赖注入代码,完成服务的配置。
  5. 为了便于和方案1混合使用,所以该方法最后依然返回IServiceCollection实例。

IServiceCollection的扩展方法CreateExtensionServices的调用

通过调用CreateExtensionServices方法我们可以将服务配置类中的配置方法在Startup文件的ConfigureService方法中自动执行。

 public void ConfigureServices(IServiceCollection services){
			services
                .CreateExtensionServices(this.Configuration)
                .AddControllersWithViews();
}

基于上述代码,我们实现了Startup文件不在关系那个服务要进行依赖注入,它只关心具体的服务配置类自动发现的开关是否打开。这样我们要增加对Redis的配置代码,只需要将服务配置类RedisBuilder继承自AutoServiceBuilder并复写InjectService方法。

namespace BranchMngt.Web.Infrastructure.Extensions
{
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.OpenApi.Models;
    using Microsoft.Extensions.Configuration;
    using BranchMngt.Data;
    using BranchMngt.Common.Settings;
    public class RedisBuilder : AutoServiceBuilder
    {
        private readonly IServiceCollection _services;
        public RedisBuilder(IServiceCollection services)
        {
            _services = services;       
        }
        public override void InjectService(IConfiguration configuration){
            this.UseRedis(configuration);
        }
        public void UseRedis(IConfiguration configuration){
            AppSettings settings = _services.GetApplicationSettings(configuration);
            _services.AddDistributedRedisCache(options =>
            {
                options.InstanceName = settings.Redis.InstanceName;
                options.Configuration = settings.Redis.Configuration;
            });
            
        }
    }
}

其它服务配置类代码类似,不再赘述。

FAQ

两个解决方案各适用于什么场景?

解决方案1中,如果添加新的服务,还是需要手动修改Startup配置文件,所以个人觉得适用于中小规模的项目场景,因为毕竟中小规模项目使用的第三方服务相对固定。

解决方案2中,自动化程度更高,适合于多团队开发的大型项目。当然方案1和方案2也可以混合使用。

方案1中,为什么需要通过表达式目录树来实例化泛型类,为什么不能用new实例化?

因为每个服务配置类的构建方法都包含参数,所以不能用new来实例化。具体请参考我的文章C#表达式目录树系列之4 – 解决C#泛型约束与无法创建带参数的泛型实例的矛盾

按照解决方案2配置Staratup文件,执行数据库Migration命令会失败,为什么?

是的,因为在Migration过程中找不到数据库连接字符串,所以Migration命令会失败。
自动加载服务配置代码是通过反射实现的,所以这个过程发生在代码运行时,Migration命令是在编译阶段寻找连接字符串,所以找不到。

解决方案: 将DBContext相关的服务按照方案一去组织,其他服务按照方案2来组织。代码如下:

```csharp
 public void ConfigureServices(IServiceCollection services){
			services
				.CreateExtensionService((DBContextBuilder builder) => builder.UseBranchMngtDbContext(this.Configuration))
                .CreateExtensionServices(this.Configuration)
                .AddControllersWithViews();
}

注意DBContextBuilder 不要继承AutoServiceBuilder,关闭自动加载开关。

附录

方案1 IdentityBuilder类

namespace BranchMngt.Web.Infrastructure.Extensions
{
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.AspNetCore.Identity;
    using BranchMngt.Data;
    using BranchMngt.Entity.DB;
    public class IdentityBuilder
    {
        private readonly IServiceCollection _services;
        public IdentityBuilder(IServiceCollection services)
        {
            _services = services;
            this.Priority = 300;
        }
        public void UseIdentity(){
             System.Console.WriteLine("UseIdentity is called");
            _services
                .AddIdentity<User, IdentityRole>(options =>
                {
                    options.Password.RequiredLength = 6;
                    options.Password.RequireDigit = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequireNonAlphanumeric = false;
                    options.Password.RequireUppercase = false;
                })
                .AddEntityFrameworkStores<BranchMngtDbContext>();
        }
    }
}

方案1 JwtAuthBuilder类

namespace BranchMngt.Web.Infrastructure.Extensions
{
    using System.Text;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.IdentityModel.Tokens;
    using BranchMngt.Common.Settings;
    public class JwtAuthBuilder
    {
        private readonly IServiceCollection _services;
        public JwtAuthBuilder(IServiceCollection services)
        {
            _services = services;
        }
        public void UseJwtAuth(IConfiguration configuration){
            System.Console.WriteLine("UseJwtAuth is called");
            AppSettings settings = _services.GetApplicationSettings(configuration);
            var key = Encoding.ASCII.GetBytes(settings.Secret);
            _services
                .AddAuthentication(x =>
                {
                    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(x =>
                {
                    x.RequireHttpsMetadata = false;
                    x.SaveToken = true;
                    x.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });
        }
    }
}

方案1 SwaggerBuilder 类

namespace BranchMngt.Web.Infrastructure.Extensions
{
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.OpenApi.Models;
    using Microsoft.Extensions.Configuration;
    using BranchMngt.Data;
    using BranchMngt.Common.Settings;
    public class SwaggerBuilder : AutoServiceBuilder
    {
        private readonly IServiceCollection _services;
        public SwaggerBuilder(IServiceCollection services)
        {
            _services = services;   
        }
        public void UseSwagger(IConfiguration configuration){
            AppSettings settings = _services.GetApplicationSettings(configuration);
            var title = settings.Swagger.Title;
            var version = settings.Swagger.Version;
            _services
                .AddSwaggerGen(c =>
                    {
                        c.SwaggerDoc(
                            version, 
                            new OpenApiInfo
                            {
                                Title = title, 
                                Version = version
                            });
                    });
        }
    }
}

IServiceCollection 其他辅助扩展方法

public static AppSettings GetApplicationSettings(this IServiceCollection services, IConfiguration configuration){
            var setttings = configuration.GetSection("ApplicationSettings");
            services.Configure<AppSettings>(setttings);
            return setttings.Get<AppSettings>();
        } 

IConfiguration 扩展方法

namespace BranchMngt.Web.Infrastructure.Extensions
{
   using Microsoft.Extensions.Configuration;

    public static class ConfigurationExtensions
    {
        public static string GetDefaultConnectionString(this IConfiguration configuration)
            => configuration.GetConnectionString("DefaultConnection");
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值