分层架构的引导加载程序





5.00/5 (7投票s)
一个小型库,用于按约定加载和执行引导加载程序类。
问题
在典型的 DDD 分层架构(包含 UI、应用服务、领域、仓储等多个层)中,如果使用依赖注入,IoC 注册通常在应用程序的组合根完成。对于 ASP.NET MVC 应用程序,它位于 Global.asax 或 Startup.cs。理想情况下,我们期望拥有以下解决方案结构(遵循洋葱架构方法 - 箭头表示 Visual Studio 中的实际项目引用)
然而,在实际中,UI 项目需要引用解决方案中的所有其他项目,即使它不需要使用它们(橙色箭头表示多余的 Visual Studio 项目引用)
在上面的图中,它需要引用 Concrete Repository 项目、Concrete Application Services 项目,以便将具体的仓储/应用服务注册到它们的接口中到 IoC 容器,即使控制器不需要了解具体的类。
为什么这种额外的引用不好?
- 它用不需要的多余引用污染了 UI 项目,使其变得比实际需要更复杂
- 它使新加入的成员更容易破坏解决方案的层结构(例如,新成员可以直接在控制器中创建和使用具体的应用服务/仓储,或者在显示视图时直接使用领域模型而不是 DTO/View Model)
有些人可能会选择将此依赖项注册移动到一个单独的项目(例如 Bootstrapper)
但这并没有解决上述问题,它只是将问题转移到另一个项目。无论哪种方式,解决方案都有一个“上帝”项目,它了解所有其他项目
方法
为了解决上述问题,我的方法是让每个项目负责自己的配置和初始化。每个项目都有自己的 Bootstrapper 类,用于执行 IoC 注册和其他初始化(例如 AutoMapper 配置、数据库种子数据)
UI 项目创建并配置一个 BootstrapperLoader,后者通过反射触发 Bootstrapper 类中的方法。在上图中,绿色箭头表示通过反射触发方法(而不是项目引用)
我发现自己为每个新项目都重复此设置,所以我决定将该逻辑分离到一个小型库 (Sharpenter.BootstrapperLoader) 中,可以通过 NuGet 引入。
演示 walkthrough
其中包含一个 .NET Core Web 应用程序,演示了 Sharpenter.BootstrapperLoader 库的使用。这是上述架构的简化版本,它只有 3 层:UI (ASP.NET MVC Core)、Core (模型 + 仓储接口) 和 Repositories。请注意,UI 项目不需要对 Repository 项目有项目引用。
启动时,它简单地显示一个书籍列表(使用 Entity Framework Core 从数据库中检索)
让我们看看 BootstrapperLoaderDemo.Repository 中的 Bootstrapper 类。这个类有两个方法
- ConfigureContainer():用于将 BookRepository 注册到 ASP.NET MVC Core IServiceCollection 中,以便它可以注入到控制器中。它还配置了 Entity Framework 的依赖注入
public void ConfigureContainer(IServiceCollection services)
{
services.AddScoped<IBookRepository, BookRepository>();
services.AddEntityFrameworkSqlServer()
.AddDbContext<BookContext>(options =>
options.UseSqlServer(_configuration.GetConnectionString("DefaultConnection"))
);
}
- ConfigureDevelopment():由于 EF Core 尚未支持数据库初始化程序,我使用此方法进行数据库初始化和数据填充。
请注意,此 Bootstrapper 类也接受 IConfiguration 作为构造函数参数,以便它可以利用 ASP.NET MVC Core 配置系统来获取数据库连接字符串。
当应用程序启动时,ASP.NET MVC 将调用 BootstrapperLoaderDemo 中 Startup 类的方法。在这个类的构造函数中,除了正常的 ASP.NET MVC 配置设置外,它还使用 LoaderBuilder 配置并创建一个 BootstrapperLoader 实例
_bootstrapperLoader = new LoaderBuilder()
.Use(new FileSystemAssemblyProvider(PlatformServices.Default.Application.ApplicationBasePath, "BootstrapperLoaderDemo.*.dll"))
.ForClass()
.HasConstructorParameter(Configuration)
.When(env.IsDevelopment)
.AddMethodNameConvention("Development")
.Build();
这些代码行告诉 BootstrapperLoader 查找应用程序基路径,找到所有以“BootstrapperLoaderDemo”文本开头的 dll,并对于在这些 dll 中找到的所有 Bootstrapper 类,将 Configuration 对象作为构造函数参数传入。AddMethodNameConvention() 指示加载器除了默认的 ConfigureContainer() 和 Configure() 之外,还查找 ConfigureDevelopmentContainer() 和 ConfigureDevelopment()。
Startup.ConfigureServices() 是 ASP.NET MVC Core 用来配置整个应用程序依赖项的地方。在这里,我们使用之前创建的 BootstrapperLoader 触发 Repository Bootstrapper 类中的“ConfigureContainer()”方法。请注意,它还传入了 IServiceCollection 实例,以便 Repository Bootstrapper 可以将自己的注册添加到同一个集合中
_bootstrapperLoader.TriggerConfigureContainer(services);
Startup.Configure() 用于应用程序的其他配置。在这里,我们使用 BootstrapperLoader 触发 Repository Bootstrapper 中的“ConfigureDevelopment()”方法。此触发仅在当前环境为开发环境时才有效,如上所述。另请注意,我们传入了 GetService() 方法,以便库可以使用它来解析 ConfigureDevelopment() 的任何参数(如果有)。它的工作方式与 Startup.Configure() 相同,您可以在其中指定任意数量的参数,只要这些参数已在 IServiceCollection 中注册。此外,由于这里的问题(https://stackoverflow.com/a/45268690/1273147 - 在应用程序启动期间,我们无法访问请求范围,因此我们需要自己创建一个范围,以便访问 .NET Core 注册的服务)
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { _bootstrapperLoader.TriggerConfigure(scope.ServiceProvider.GetRequiredService); }
有一个注意事项是:由于我们不再从 UI 项目引用 Repository 项目,Visual Studio/msbuild 不会自动将 Repository dll 复制到 UI 项目的 bin 文件夹中。因此,为了解决这个问题,我在 UI 项目 BootstrapperLoaderDemo.csproj 中使用 PostBuild 目标,在构建后自动复制这些 dll。但是,为了使此 PostBuild 目标工作,我们需要确保 BootstrapperLoaderDemo 在其依赖项(BootstrapperLoaderDemo.Repository)完成构建后才进行构建。目前我不得不创建一个简单的 powershell/bash 脚本来单独构建项目,以确保维护该顺序。这是采用此库时最大的问题,但它只需要在开始时设置一次
<Target Name="PostPublish" AfterTargets="AfterPublish" > <ItemGroup> <ItemsToCopy Include="./../BootstrapperLoaderDemo.Repository/bin/$(Configuration)/$(TargetFramework)/publish/*" /> </ItemGroup> <Copy SourceFiles="@(ItemsToCopy)" DestinationFolder="./bin/$(Configuration)/$(TargetFramework)/publish"> <Output TaskParameter="CopiedFiles" ItemName="SuccessfullyCopiedFiles" /> </Copy> <Message Importance="High" Text="PostPublish Target successfully copied:%0a@(ItemsToCopy->'- %(fullpath)', '%0a')%0a -> %0a@(SuccessfullyCopiedFiles->'- %(fullpath)', '%0a')" /> </Target>
加载器配置
默认配置
默认情况下,BootstrapperLoader
具有以下设置
- 使用
FileSystemAssemblyProvider
在当前文件夹 (Directory.GetCurrentDirectory()
) 中查找所有*.dll
BootstrapperLoader.TriggerConfigure
在上述 dll 中查找任何名为Bootstrapper
的类中的Configure()
方法BootstrapperLoader.TriggerConfigureContainer
在上述 dll 中查找任何名为Bootstrapper
的类中的ConfigureContainer()
方法- Bootstrapper 类和 Configure()/ConfigureContainer() 可以是公共的或非公共的
LoaderBuilder 配置
加载器构建器通过 Fluent API 提供多种配置
-
WithName("SomeBootstrapper")
:查找名为SomeBootstrapper
的类,而不是Bootstrapper
示例
<code>_bootstrapperLoader = new LoaderBuilder() .ForClass() .WithName("SomeBootstrapper") .Build(); </code>
-
HasConstructorParameter<ISomeDependency>()
:创建Bootstrapper
实例时,使用接受ISomeDependency
参数的构造函数示例
<code>_bootstrapperLoader = new LoaderBuilder() .ForClass() .HasConstructorParameter<ISomeDependency>(new SomeDependency()) .Build(); </code>
-
When(condition).CallConfigure("SomeConfigure")
:调用BootstrapperLoader.TriggerConfigure()
时,如果condition
调用评估为 true,则除了Configure()
之外,还调用Bootstrapper
类中的SomeConfigure()
方法示例
<code>_bootstrapperLoader = new LoaderBuilder() .ForClass() .When(env.IsDevelopment) .CallConfigure("SomeConfigure") .Build(); </code>
-
When(condition).CallConfigureContainer("SomeConfigureContainer")
:调用BootstrapperLoader.TriggerConfigureContainer()
时,如果condition
调用评估为 true,则除了ConfigureContainer()
之外,还调用Bootstrapper
类中的SomeConfigureContainer()
方法示例
<code>_bootstrapperLoader = new LoaderBuilder() .ForClass() .When(env.IsDevelopment) .CallConfigureContainer("SomeConfigureContainer") .Build(); </code>
-
When(condition).AddMethodNameConvention("Development")
:当调用BootstrapperLoader.TriggerConfigure()
/BootstrapperLoader.TriggerConfigureContainer()
时,如果condition
调用评估为 true,则除了Configure()
/ConfigureContainer()
之外,还调用Bootstrapper
类中的SomeConfigure()
/SomeConfigureContainer()
方法示例
<code>_bootstrapperLoader = new LoaderBuilder() .ForClass() .When(env.IsDevelopment) .AddMethodNameConvention("Development") .Build(); </code>
-
Use()
:指定一个备用程序集提供程序示例
<code>_bootstrapperLoader = new LoaderBuilder() .Use(new FileSystemAssemblyProvider(Directory.GetCurrentDirectory(), "MyCoolProject*.dll")) //Look into current directory, grabs all dlls starting with MyCoolProject .Build(); </code>
您还可以创建新的 Assembly Provider 类,以自定义提供给加载器的程序集来源。目前,提供了两个类:FileSystemAssemblyProvider
和 InMemoryAssemblyProvider
从根项目触发引导程序
BootstrapperLoader
提供了 3 种方法来触发子项目 Bootstrapper
类中的方法
TriggerConfigureContainer<TArg>(TArg parameter)
当根项目进行 IoC 注册时,应使用此方法。其参数通常是 IoC 容器或容器构建器。此方法将在 Bootstrapper
类中查找 ConfigureContainer
方法并传入参数,允许 Bootstrapper
类将其子项目的依赖项注册到 IoC 容器中
TriggerConfigure(Func<Type, object> serviceLocator = null)
当执行任何非 IoC 配置/初始化(例如 AutoMapper 设置)的时机成熟时,应使用此方法。它可以带或不带 serviceLocator
参数调用
此方法接受 Func<Type, object>
作为其参数,以允许 Bootstrapper
类中的 Configure
方法接受任意数量的依赖项(只要这些依赖项可以使用 serviceLocator func 解析)。它的工作方式与 ASP.NET Core 中的 Startup.Configure
相同
此处使用 Func<Type, object>
是为了确保此库不依赖于任何特定的 IoC 容器。大多数 IoC 容器都应该支持具有此签名的 A 方法(例如,在 Autofac
中,它是 Resolve()
方法)
当不带 serviceLocator
参数调用时,它将仅在 Bootstrapper
类中查找 Configure()
方法(不带任何参数)
Trigger<TArg>(string methodName, TArg parameter)
当调用此方法时,它将在子项目中的 Bootstrapper
类中查找具有指定名称的方法并调用它们,传入提供的参数。此方法适用于您的项目无法使用上述两种方法的任何其他情况。
库设计
仅当您对我是如何实现该库感兴趣时才阅读此部分
该库的主要逻辑非常简单,完全位于 BootstrapperLoader 类中。
它依赖于 LoaderConfig 来保存加载和触发引导程序类所需的所有信息(例如,引导程序类名、要查找的方法等)
在初始化期间,它查找包含 Bootstrapper 类的程序集,创建这些引导程序类的实例并将其存储在一个列表中。
当调用 Trigger() 方法时,它会在存储的引导程序类列表中查找具有指定名称的方法并调用它们
public void Trigger<TArg>(string methodName, TArg parameter) { Bootstrappers.ForEach(bootstrapper => ExecuteIfNotNull( bootstrapper.GetType() .GetMethod(methodName, MethodBindingFlags, null, new[] {typeof(TArg)}, null), methodInfo => methodInfo.Invoke(bootstrapper, new object[] { parameter })) ); }
我需要编写一个自定义方法 ExecuteIfNotNull(),而不是使用 C# 6 安全导航运算符,因为我希望我的库针对 .NET 4.5.2 以获得更广泛的受众。
TriggerConfigureContainer() 类似。Config.ConfigureContainerMethods 是一个字典,其中键是要查找的方法名,值是一个 Func<bool>,用于决定何时调用该方法
public void TriggerConfigureContainer<TArg>(TArg parameter) { Bootstrappers.ForEach(bootstrapper => { Config.ConfigureContainerMethods .Where(c => c.Value()) .ToList() .ForEach(methodConfiguration => ExecuteIfNotNull( bootstrapper.GetType().GetMethod(methodConfiguration.Key, MethodBindingFlags, null, new[] {typeof(TArg)}, null), methodInfo => methodInfo.Invoke(bootstrapper, new object[] { parameter })) ); }); }
当调用 TriggerConfigure() 时,事情会变得更有趣一些
public void TriggerConfigure(Func<Type, object> serviceLocator = null) { Bootstrappers.ForEach(bootstrapper => { Config.ConfigureMethods .Where(c => c.Value()) .ToList() .ForEach(methodConfiguration => ExecuteIfNotNull( GetMethodInfoByName(bootstrapper.GetType(), methodConfiguration.Key, serviceLocator), methodInfo => methodInfo.InvokeWithDynamicallyResolvedParameters(bootstrapper, serviceLocator)) ); }); }
首先,它需要评估 Configure() 条件调用的条件,并且只选择条件评估为 true 的方法。其次,我有一个扩展方法 InvokeWithDynamicallyResolvedParameters() 用于调用这些方法
public static void InvokeWithDynamicallyResolvedParameters(this MethodInfo configureMethod, object bootstrapper, Func<Type, object> serviceLocator)
{
var parameterInfos = configureMethod.GetParameters();
var parameters = new object[parameterInfos.Length];
for (var index = 0; index < parameterInfos.Length; index++)
{
var parameterInfo = parameterInfos[index];
try
{
parameters[index] = serviceLocator(parameterInfo.ParameterType);
}
catch (Exception ex)
{
//Omitted for brevity
}
}
configureMethod.Invoke(bootstrapper, parameters);
}
此方法的作用是遍历 Bootstrapper Configure() 方法的参数列表,并尝试使用提供的 serviceLocator 解析这些参数。如果您查看 ASP.NET Core 源代码,您会发现这与 ASP.NET Core 用于触发 Startup.Configure() 的逻辑几乎完全相同。我最初无法弄清楚如何做到这一点,所以我不得不查看 ASP.NET Core 的实现并“借鉴”了这个想法。
库中剩余的类用于为加载器构建器提供 Fluent API 配置。加载器构建器的唯一目的是让客户端为 LoaderConfig 指定不同的选项,并最终将其传递给 BootstrapperLoader。
当创建 LoaderBuilder 实例时,它还会创建 LoaderConfig 类的实例。在构建器配置的整个过程中,LoaderConfig 实例在 LoaderBuilder、ForClassSyntax 和 MethodsSyntax 之间维护。当客户端调用 LoaderBuilder.Build() 时,它将创建一个 BootstrapperLoader 实例并传入最终的 LoaderConfig 实例
public BootstrapperLoader Build()
{
var loader = new BootstrapperLoader(Config);
loader.Initialize();
return loader;
}
这些类的实现相当简单。如果您感兴趣,请查看源代码。
诚然,如果我不提供 Fluent API 配置,而只是让客户端直接配置 LoaderConfig,事情会更简单。但是,我仍然觉得目前的方式更用户友好。
支持的框架
该库的目标是
- .NET Standard 2.0
- .NET Framework 4.5.2 及更高版本
- .NET Core 2.0
快速笔记
- 尽管演示应用程序是使用 ASP.NET MVC Core 完成的。但此库中没有任何特定于 Web 的内容,因此它可以在任何合适的应用程序中使用
- 演示项目在 master 分支中为 .NET Core 2.0 设置,在 net452 分支中为 .NET 4.5.2 设置
一些最后的想法
- 此库/方法确实存在一些限制(如上所述,需要使用编译后脚本将 dll 复制到正确的 bin 文件夹),但我认为它带来的优点(真正的层分离、更清晰的项目引用等)超过了这些限制。
- 此外,通过这种方法,依赖注入配置不再集中在一个地方,而是分散到不同的项目中。有些人可能认为这是一个缺点,但我喜欢每个项目负责自身初始化的想法,类似于面向对象设计中的数据封装。
历史
2017 年 11 月 11 日:更新文章,包含版本 2.0.0 的更改
2017 年 8 月 20 日:升级到 .NET Core 和 .NET Standard 2.0,添加了关于支持框架的部分
2016 年 12 月 28 日:初始版本