ASP.NET Core MVC的"模块化"设计使我们可以构成应用的基本单元Controller定义在任意的模块(程序集)中,并在运行时动态加载和卸载。《设计篇》介绍了这种为"飞行中的飞机加油"的方案的实现原理?本篇我们将演示将介绍"分散定义Controller"的N种实现方案。源代码从这里下载。
一、标注ApplicationPartAttribute特性
二、标注RelatedAssemblyAttribute特性
三、注册ApplicationPartManager
四、添加ApplicationPart到现有ApplicationPartManager
一、标注ApplicationPartAttribute特性
接下来我们就通过几个简单的实例来演示如何将Controller类型定义在非入口应用所在的项目中。我们创建如图1所示的解决方案,其中App是一个MVC应用类型的项目,而Foo则是一个普通的类库项目,App具有针对Foo的项目引用。我们希望将部分Controller类型定义在Foo这个类库项目中。
图1 将部分Controller类型定义在Foo项目中
我们在App项目中定义了如下这个HomeController。如代码片段所示,我们在构造函数中注入了ApplicationPartManager对象,并利用它得到当前应用范围内所有有效Controller类型。在执行应用根路径的Action方法Index中,我们将得到的有效Controller类型名称呈现出来。如下所示的FooController类型是我们在Foo项目中定义的Controller类型。
public class HomeController : Controller
{
private readonly IEnumerable<Type> _controllerTypes;
public HomeController(ApplicationPartManager manager)
{
var feature = new ControllerFeature();
manager.PopulateFeature(feature);
_controllerTypes = feature.Controllers;
}
[HttpGet("/")]
public string Index()
{
var lines = _controllerTypes.Select(it => it.Name);
return string.Join(Environment.NewLine, lines.ToArray());
}
}
public class FooController
{
public void Index() => throw new NotImplementedException();
}
在启动这个演示程序后,如果利用浏览器通过根路径访问定义在HomeController类型中的Action方法Index,我们会得到如图2所示的输出结果。从输出结果可以看出,定义在非MVC应用项目Foo中的Controller类型在默认情况下是不会被解析的。
图2 默认只解析MVC应用所在项目定义的Controller
如果希望MVC应用在进行Controller类型解析的时候将项目Foo编译后的程序集(默认为Foo.dll)包括进来,我们可以在应用所在项目中标注ApplicationPartAttribute特性将程序集Foo作为应用的组成部分。所以我们在Program.cs中针对ApplicationPartAttribute特性进行了如下的标记。
[assembly:ApplicationPart("Foo")]
修改后的程序集启动之后,再次利用浏览器按照按照相同的路径对它发起请求,我们将得到如图3所示的输出结果。由于程序集Foo成为了当前应用的有效组成部分,定义在它里面的BarController自然也成为了当前应用有效的Controller类型。
图3 解析ApplicationPartAttribute特性指向程序集中的Controller类型
二、标注RelatedAssemblyAttribute特性
除了在入口程序集上标注ApplicationPartAttribute特性将某个程序集作为当前应用的有效组成部分之外,我们也可以通过标注RelatedAssemblyAttribute达到相同的目的。根据前面的介绍,我们知道RelatedAssemblyAttribute特性只能标注到入口程序集或者ApplicationPartAttribute特性指向的程序集中,所以我们可以将RelatedAssemblyAttribute特性标注到Foo项目中将另一个程序集包含进行。为此我们在解决方案中添加了另一个类库项目Bar(如图4所示),并为App添加针对Bar的项目引用,然后在Bar项目中定义一个类似于FooController的BarController类型。
图4 将部分Controller类型定义在Foo和Bar项目中
为了将项目Bar编译后生成的程序集(默认为Bar.dll)作为当前应用的组成部分,我们可以选择在App或者Foo项目中标注一个指向它的RelatedAssemblyAttribute特性。对于我们演示的实例来说,我们选择在FooController.cs中以如下形式标注一个指向程序集Bar的RelatedAssemblyAttribute特性。
[assembly: RelatedAssembly("Bar")]
修改后的程序集启动之后,再次利用浏览器按照按照相同的路径对它发起请求,我们将得到如图5所示的输出结果。由于程序集Bar成为了当前应用的有效组成部分,定义在它里面的BazController自然也成为了当前应用有效的Controller类型。
图5 解析RelatedAssemblyAttribute特性指向程序集中的Controller类型
三、注册ApplicationPartManager
由于针对有效Controller类型的解析是利用注册的ApplicationPartManager对象实现的,所以我们完全可以通过注册一个ApplicationPartManager对象的方式达到相同的目的。接下来我们将上一个演示实例中标注的ApplicationPartAttribute和RelatedAssemblyAttribute特性删除,并将承载程序修改为如下的形式。
var manager = new ApplicationPartManager();
var entry = Assembly.GetEntryAssembly()!;
var foo = Assembly.Load(new AssemblyName("Foo"));
var bar = Assembly.Load(new AssemblyName("Bar"));
manager.ApplicationParts.Add(new AssemblyPart(entry));
manager.ApplicationParts.Add(new AssemblyPart(foo));
manager.ApplicationParts.Add(new AssemblyPart(bar));
manager.FeatureProviders.Add(new ControllerFeatureProvider());
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton(manager)
.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
如上面的代码片段所示,我们创建了一个ApplicationPartManager对象,并在其ApplicationParts属性中显式添加了指向入口程序集以及Foo和Bar程序集的AssemblyPart对象。为了能够让这个ApplicationPartManager对象具有解析Controller类型的能力,我们在其FeatureProviders中添加了一个ControllerFeatureProvider对象。在后续的应用承载程序中,我们将这个ApplicationPartManager对象作为服务注册到依赖注入框架中。修改后的程序集启动之后,再次利用浏览器按照按照相同的路径对它发起请求,我们依然会得到如图5所示的输出结果。
四、添加ApplicationPart到现有ApplicationPartManager
其实我们没有必要注册一个新的,按照如下的方式将Foo、Bar程序集转换成AssemblyPart并将其添加到现有的ApplicationPartManager之中也可以达到相同的目的。
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddApplicationPart(Assembly.Load(new AssemblyName("Foo")))
.AddApplicationPart(Assembly.Load(new AssemblyName("Bar")));
var app = builder.Build();
app.MapControllers();
app.Run();