Skip to content

Commit fc647b7

Browse files
committed
添加文章:ApplicationPartManager 使用指南
1 parent 745b824 commit fc647b7

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed
Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
---
2+
title: ApplicationPartManager 使用指南
3+
title_en: ApplicationPartManager Usage Guide
4+
date: 2024-05-20 10:00:00
5+
updated: 2024-05-20 10:00:00
6+
tags:
7+
- ASP.NET Core
8+
- Testing
9+
- MVC
10+
- Integration Testing
11+
---
12+
13+
> `ApplicationPartManager` 是 ASP.NET Core 中用于管理应用程序部分(Application Parts)的核心组件,它控制框架在哪些程序集中发现和加载 MVC 组件。在集成测试场景中,当测试控制器位于测试程序集中时,需要使用 `ApplicationPartManager` 将其注册到测试服务器中。本文详细介绍 `ApplicationPartManager` 的使用方法、常见场景和最佳实践。
14+
15+
# 一、概述
16+
17+
`ApplicationPartManager` 是 ASP.NET Core 中用于管理应用程序部分(Application Parts)的核心组件。它控制框架在哪些程序集中发现和加载 MVC 组件,如控制器、视图、Tag Helpers 等。
18+
19+
## 1.1 什么是 Application Parts?
20+
21+
Application Parts 是 ASP.NET Core MVC 用来发现和加载应用程序组件的机制。每个 Application Part 代表一个程序集或程序集的一部分,框架会扫描这些部分来查找:
22+
23+
- 控制器(Controllers)
24+
- 视图组件(View Components)
25+
- Tag Helpers
26+
- 模型绑定器(Model Binders)
27+
- 其他 MVC 相关组件
28+
29+
# 二、使用场景
30+
31+
## 2.1 集成测试中注册测试控制器
32+
33+
**最常见场景**:在集成测试中,测试控制器位于测试程序集中,而不是主应用程序程序集。默认情况下,ASP.NET Core 只会扫描主应用程序程序集中的控制器。
34+
35+
**问题示例**
36+
```C#
37+
// 测试程序集中的控制器
38+
namespace Tests.HttpConcurrency.Fixtures;
39+
40+
[ApiController]
41+
[Route("/api/test")]
42+
public class TestController : ControllerBase
43+
{
44+
// ...
45+
}
46+
```
47+
48+
如果直接使用 `WebApplicationFactory<Program>`,测试服务器无法发现 `TestController`,因为它在不同的程序集中。
49+
50+
**解决方案**:使用 `ApplicationPartManager` 将测试程序集添加到应用程序部分。
51+
52+
## 2.2 插件式架构
53+
54+
在需要支持插件或模块化架构的应用中,可以动态加载外部程序集中的控制器。
55+
56+
## 2.3 共享控制器库
57+
58+
当控制器定义在共享类库中,需要在多个应用程序中使用时。
59+
60+
## 2.4 条件性加载控制器
61+
62+
根据配置或环境动态决定加载哪些程序集中的控制器。
63+
64+
# 三、如何正确使用
65+
66+
## 3.1 基本用法
67+
68+
### 3.1.1 获取 ApplicationPartManager
69+
70+
`ApplicationPartManager` 通常在服务配置阶段就已经注册。可以通过以下方式获取:
71+
72+
```C#
73+
var partManager = services
74+
.Last(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
75+
.ImplementationInstance as ApplicationPartManager;
76+
```
77+
78+
或者更安全的方式:
79+
80+
```C#
81+
var partManager = services
82+
.Where(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
83+
.Select(descriptor => descriptor.ImplementationInstance)
84+
.OfType<ApplicationPartManager>()
85+
.FirstOrDefault();
86+
87+
if (partManager != null)
88+
{
89+
// 使用 partManager
90+
}
91+
```
92+
93+
### 3.1.2 添加程序集部分
94+
95+
使用 `AssemblyPart` 将程序集添加到应用程序部分:
96+
97+
```C#
98+
using System.Reflection;
99+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
100+
101+
// 方式 1: 通过类型获取程序集
102+
var assembly = Assembly.GetAssembly(typeof(TestController));
103+
partManager.ApplicationParts.Add(new AssemblyPart(assembly!));
104+
105+
// 方式 2: 直接指定程序集
106+
partManager.ApplicationParts.Add(new AssemblyPart(typeof(TestController).Assembly));
107+
108+
// 方式 3: 通过程序集名称加载
109+
var assembly = Assembly.LoadFrom("path/to/assembly.dll");
110+
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
111+
```
112+
113+
## 3.2 完整示例:集成测试场景
114+
115+
以下示例展示了在集成测试中如何正确使用 `ApplicationPartManager`
116+
117+
```C#
118+
using System.Net.Http.Json;
119+
using System.Reflection;
120+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
121+
using Microsoft.Extensions.DependencyInjection;
122+
using Xunit;
123+
124+
public class HttpConcurrencyTest : TestBase
125+
{
126+
[Fact]
127+
public async Task should_return_different_user_when_use_different_request()
128+
{
129+
// Arrange
130+
var factory = new TestWebApplicationFactory(services =>
131+
{
132+
// 配置其他服务...
133+
services.AddTransient<TestDelegatingHandler>();
134+
services.AddHttpClient<ITestHttpClient, TestHttpClient>()
135+
.AddHttpMessageHandler<TestDelegatingHandler>();
136+
137+
// 获取 ApplicationPartManager 并添加测试程序集
138+
var partManager = (ApplicationPartManager)services
139+
.Last(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
140+
.ImplementationInstance!;
141+
142+
// 将包含 TestController 的程序集添加为 Application Part
143+
partManager.ApplicationParts.Add(
144+
new AssemblyPart(Assembly.GetAssembly(typeof(TestController))!)
145+
);
146+
});
147+
148+
// Act & Assert
149+
var httpClient = factory.CreateClient();
150+
var response = await httpClient.GetAsync("/api/test");
151+
response.EnsureSuccessStatusCode();
152+
}
153+
}
154+
```
155+
156+
## 3.3 在 Startup/Program.cs 中使用
157+
158+
如果需要在应用程序启动时配置,可以在 `Program.cs``Startup.cs` 中:
159+
160+
```C#
161+
var builder = WebApplication.CreateBuilder(args);
162+
163+
// 获取 ApplicationPartManager
164+
var partManager = builder.Services
165+
.Where(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
166+
.Select(descriptor => descriptor.ImplementationInstance)
167+
.OfType<ApplicationPartManager>()
168+
.FirstOrDefault();
169+
170+
if (partManager != null)
171+
{
172+
// 添加外部程序集
173+
var pluginAssembly = Assembly.LoadFrom("path/to/plugin.dll");
174+
partManager.ApplicationParts.Add(new AssemblyPart(pluginAssembly));
175+
}
176+
177+
var app = builder.Build();
178+
// ...
179+
```
180+
181+
## 3.4 使用 CompiledRazorAssemblyPart(适用于 Razor 视图)
182+
183+
如果程序集包含预编译的 Razor 视图,使用 `CompiledRazorAssemblyPart`
184+
185+
```C#
186+
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
187+
188+
var assembly = Assembly.GetAssembly(typeof(SomeView));
189+
partManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly!));
190+
```
191+
192+
# 四、注意事项
193+
194+
## 4.1 时机很重要
195+
196+
必须在 `WebApplicationFactory``ConfigureWebHost``ConfigureServices` 阶段添加 Application Parts,**在服务构建完成之前**
197+
198+
```C#
199+
// ✅ 正确:在 ConfigureWebHost 中
200+
var factory = new TestWebApplicationFactory(services =>
201+
{
202+
var partManager = GetApplicationPartManager(services);
203+
partManager.ApplicationParts.Add(new AssemblyPart(...));
204+
});
205+
206+
// ❌ 错误:在服务构建之后
207+
var factory = new TestWebApplicationFactory();
208+
var partManager = factory.Services.GetRequiredService<ApplicationPartManager>();
209+
// 此时已经太晚了,控制器发现已经完成
210+
```
211+
212+
## 4.2 空值检查
213+
214+
始终检查 `ApplicationPartManager` 是否为 null:
215+
216+
```C#
217+
var partManager = services
218+
.Where(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
219+
.Select(descriptor => descriptor.ImplementationInstance)
220+
.OfType<ApplicationPartManager>()
221+
.FirstOrDefault();
222+
223+
if (partManager == null)
224+
{
225+
throw new InvalidOperationException(
226+
"ApplicationPartManager not found in service collection");
227+
}
228+
229+
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
230+
```
231+
232+
## 4.3 程序集引用
233+
234+
确保目标程序集已被正确引用。在测试项目中,确保测试程序集引用了包含控制器的程序集。
235+
236+
## 4.4 避免重复添加
237+
238+
虽然重复添加同一个程序集通常不会导致错误,但最好检查是否已存在:
239+
240+
```C#
241+
var assembly = typeof(TestController).Assembly;
242+
var assemblyName = assembly.GetName().Name;
243+
244+
if (!partManager.ApplicationParts
245+
.OfType<AssemblyPart>()
246+
.Any(ap => ap.Assembly.GetName().Name == assemblyName))
247+
{
248+
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
249+
}
250+
```
251+
252+
## 4.5 性能考虑
253+
254+
添加大量程序集可能会影响应用程序启动时间,因为框架需要扫描所有这些程序集来发现组件。
255+
256+
# 五、常见问题排查
257+
258+
## 5.1 控制器无法被发现
259+
260+
**症状**:路由返回 404,即使控制器已定义。
261+
262+
**可能原因**
263+
- Application Part 添加时机太晚
264+
- 程序集未正确引用
265+
- 控制器未正确标记(缺少 `[ApiController]``[Controller]`
266+
267+
**解决方案**
268+
```C#
269+
// 确保在 ConfigureServices 阶段添加
270+
var factory = new TestWebApplicationFactory(services =>
271+
{
272+
var partManager = GetApplicationPartManager(services);
273+
var assembly = Assembly.GetAssembly(typeof(YourController));
274+
partManager.ApplicationParts.Add(new AssemblyPart(assembly!));
275+
});
276+
```
277+
278+
## 5.2 ApplicationPartManager 为 null
279+
280+
**症状**:无法从服务集合中获取 `ApplicationPartManager`
281+
282+
**可能原因**
283+
-`AddControllers()``AddMvc()` 之前尝试获取
284+
- 服务集合配置不正确
285+
286+
**解决方案**
287+
```C#
288+
// 确保先添加 MVC 服务
289+
builder.Services.AddControllers();
290+
// 然后再获取 ApplicationPartManager
291+
var partManager = GetApplicationPartManager(builder.Services);
292+
```
293+
294+
## 5.3 多个程序集需要添加
295+
296+
**解决方案**:遍历并添加多个程序集:
297+
298+
```C#
299+
var assemblies = new[]
300+
{
301+
typeof(TestController1).Assembly,
302+
typeof(TestController2).Assembly,
303+
typeof(TestController3).Assembly
304+
};
305+
306+
foreach (var assembly in assemblies)
307+
{
308+
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
309+
}
310+
```
311+
312+
# 六、最佳实践
313+
314+
## 6.1 封装获取逻辑
315+
316+
创建一个辅助方法来获取 `ApplicationPartManager`
317+
318+
```C#
319+
private static ApplicationPartManager? GetApplicationPartManager(IServiceCollection services)
320+
{
321+
return services
322+
.Where(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
323+
.Select(descriptor => descriptor.ImplementationInstance)
324+
.OfType<ApplicationPartManager>()
325+
.FirstOrDefault();
326+
}
327+
```
328+
329+
## 6.2 在测试基类中提供帮助方法
330+
331+
```C#
332+
public abstract class TestBase
333+
{
334+
protected static void AddApplicationPart<T>(IServiceCollection services)
335+
{
336+
var partManager = GetApplicationPartManager(services);
337+
if (partManager != null)
338+
{
339+
partManager.ApplicationParts.Add(
340+
new AssemblyPart(typeof(T).Assembly)
341+
);
342+
}
343+
}
344+
}
345+
```
346+
347+
## 6.3 使用扩展方法
348+
349+
创建扩展方法简化使用:
350+
351+
```C#
352+
public static class ApplicationPartManagerExtensions
353+
{
354+
public static IServiceCollection AddApplicationPart<T>(
355+
this IServiceCollection services)
356+
{
357+
var partManager = services
358+
.Where(descriptor => descriptor.ServiceType == typeof(ApplicationPartManager))
359+
.Select(descriptor => descriptor.ImplementationInstance)
360+
.OfType<ApplicationPartManager>()
361+
.FirstOrDefault();
362+
363+
if (partManager != null)
364+
{
365+
partManager.ApplicationParts.Add(
366+
new AssemblyPart(typeof(T).Assembly)
367+
);
368+
}
369+
370+
return services;
371+
}
372+
}
373+
374+
// 使用
375+
services.AddApplicationPart<TestController>();
376+
```
377+
378+
# 七、总结
379+
380+
`ApplicationPartManager` 是 ASP.NET Core 中扩展控制器发现机制的关键组件。在集成测试场景中,它允许你将测试程序集中的控制器注册到测试服务器中。正确使用时需要注意:
381+
382+
- ✅ 在服务构建之前添加 Application Parts
383+
- ✅ 进行空值检查
384+
- ✅ 确保程序集正确引用
385+
- ✅ 封装重复逻辑以提高代码可维护性
386+
387+
通过遵循这些指南,你可以有效地在集成测试和其他场景中使用 `ApplicationPartManager`
388+
389+
# 相关参考
390+
391+
- [ApplicationPartManager Class - Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.applicationpartmanager)
392+
- [Application Parts in ASP.NET Core - Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts)
393+
- [Integration tests in ASP.NET Core - Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)
394+
- [WebApplicationFactory<TEntryPoint> Class - Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1)

0 commit comments

Comments
 (0)