WebApi 插件式构建方案:重写的控制器获取工厂
模块化的 WebApi 服务,最核心的东西就是这货了:负责请求 URL 和控制器类型的映射 —— 简单来说就是红娘,不认识的话,小伙子你别想讨到媳妇儿了。
系统内置的缺省 WebApi 控制器发现工厂,只能从路由信息中获得控制器和动作,要获取自定义的路由信息,只能通过重写控制器获取工厂 IHttpControllerSelector
来解决。按照第一章中定下的规则,使用 {module}/{controller}/{action}/{id}
路由规则,我们需要为路由参数额外读取一个 module 参数。
注:id 变量并不由控制器获取工厂使用,在实际使用中由 Action 相关类映射:一个很明显的例子就是 WebApi 2 的 Attribute 映射。
话说回来,也不一定必须使用我定下这套规则,确保你能访问控制器即可。在这里简要说一下 IHttpControllerSelector
接口两大方法的作用:
-
GetControllerMapping
用于获取路由规则和控制器类型的关系映射列表。 -
SelectController
用于根据路由规则获取对应的控制器类型。
首先说一下 GetControllerMapping
方法:从当前加载的所有程序集中,获取得到符合条件的所有控制器类型,然后依据 {module}/{controller}/{action}/{id}
路由规则,从请求 URL 地址中获取对应变量的值,拼接形成缓存键后,添加到字典中。
根据《WebApi 插件式构建方案:发现并加载程序集》这一章定下的配置文件,是不包含 name 属性的(即 module 路由变量),我们需要为其扩展,扩展后的结果如下(这里只考虑在基础配置上扩展):
<?xml version="1.0" encoding="UTF-8"?>
<configuration enabled="true">
<name>Authorization</name>
<description>授权支持插件</description>
<assemblies>
<add type="relative">bin/Intime.AuthorizationService.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Services.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Data.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Data.Repository.dll</add>
</assembiles>
</configuration>
在填充路由规则和控制器类型的关系映射时,读取 name 到路由变量 module 中,生成缓存项的键 Key。下面是填充逻辑真实代码:
注:下面这段代码,返回的字典中,键(string 类型)必须是不区分大小写的,否则 Http 请求地址大小写不同时找不到控制器。
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
<span class="pl-k">foreach</span> (<span class="pl-k">var</span> item <span class="pl-k">in</span> DynamicModule.DefaultInstance.Modules)
item.Configuration = <span class="pl-s">new</span> NamedPluginConfiguration(item.Configuration);
<span class="pl-k">var</span> types = _configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(_configuration.Services.GetAssembliesResolver());
<span class="pl-k">foreach</span> (<span class="pl-k">var</span> type <span class="pl-k">in</span> types)
{
<span class="pl-k">var</span> moduleName = <span class="pl-st">string</span>.Empty;
<span class="pl-k">var</span> module = DynamicModule.DefaultInstance.Modules.FirstOrDefault(p => p.Assemblies.Contains(type.Assembly));
<span class="pl-k">if</span> (module != <span class="pl-c1">null</span>)
moduleName = ((NamedPluginConfiguration)module.Configuration).Name;
<span class="pl-k">var</span> segments = type.Namespace.Split(Type.Delimiter);
<span class="pl-k">var</span> controllerName = type.Name.Remove(type.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
<span class="pl-k">var</span> key = String.Format(CultureInfo.InvariantCulture, <span class="pl-s1"><span class="pl-pds">"</span>{0}.{1}<span class="pl-pds">"</span></span>, segments[segments.Length - <span class="pl-c1">1</span>], controllerName);
<span class="pl-k">if</span> (!<span class="pl-st">string</span>.IsNullOrWhiteSpace(moduleName))
key = String.Format(CultureInfo.InvariantCulture, <span class="pl-s1"><span class="pl-pds">"</span>{0}.{1}<span class="pl-pds">"</span></span>, moduleName, key);
<span class="pl-k">if</span> (dictionary.Keys.Contains(key))
_duplicates.Add(key);
<span class="pl-k">else</span>
dictionary[key] = <span class="pl-s">new</span> HttpControllerDescriptor(_configuration, type.Name, type);
}
<span class="pl-k">foreach</span> (<span class="pl-k">var</span> item <span class="pl-k">in</span> _duplicates)
dictionary.Remove(item);
<span class="pl-k">return</span> dictionary;
}
需要注意的是 NamedPluginConfiguration
类:我采用了修饰模式对原始配置进行了扩展。真实代码中,上一节的 AppConfig
也使用了这种方式,好处是:后续在有扩展需要在配置文件中添加信息时,可以很方便的读取而不需要另开炉灶。推荐各位在配置文件的读取上也使用这种设计模式。
上面说完了填充映射关系,下面继续说 SelectController
方法,也就是获取映射关系。获取映射关系就是根据客户端传过来的路由变量,根据填充时的规则引擎,重新生成映射关系的键,找到对应的控制器,再进行下一步操作。真实代码如下:
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
<span class="pl-k">var</span> moduleName = routeData.GetRouteVariable(ModuleKey);
<span class="pl-st">string</span> namespaceName = routeData.GetRouteVariable(NamespaceKey);
<span class="pl-k">if</span> (namespaceName == <span class="pl-c1">null</span>)
<span class="pl-k">throw</span> <span class="pl-s">new</span> HttpResponseException(HttpStatusCode.NotFound);
<span class="pl-st">string</span> controllerName = routeData.GetRouteVariable(DefaultHttpControllerSelector.ControllerSuffix);
<span class="pl-k">if</span> (controllerName == <span class="pl-c1">null</span>)
<span class="pl-k">throw</span> <span class="pl-s">new</span> HttpResponseException(HttpStatusCode.NotFound);
<span class="pl-st">string</span> key = String.Format(CultureInfo.InvariantCulture, <span class="pl-s1"><span class="pl-pds">"</span>{0}.{1}<span class="pl-pds">"</span></span>, namespaceName, controllerName);
<span class="pl-k">if</span> (!<span class="pl-st">string</span>.IsNullOrWhiteSpace(moduleName))
key = String.Format(CultureInfo.InvariantCulture, <span class="pl-s1"><span class="pl-pds">"</span>{0}.{1}<span class="pl-pds">"</span></span>, moduleName, key);
HttpControllerDescriptor controllerDescriptor;
<span class="pl-k">if</span> (_controllers.Value.TryGetValue(key, out controllerDescriptor))
<span class="pl-k">return</span> controllerDescriptor;
<span class="pl-k">if</span> (_duplicates.Contains(key))
<span class="pl-k">throw</span> <span class="pl-s">new</span> HttpResponseException(request.CreateErrorResponse(HttpStatusCode.InternalServerError, <span class="pl-s1"><span class="pl-pds">"</span>有多个控制器符合这个请求!<span class="pl-pds">"</span></span>));
<span class="pl-k">else</span>
<span class="pl-k">throw</span> <span class="pl-s">new</span> HttpResponseException(HttpStatusCode.NotFound);
}
相信用心看到这里的人,心里已经隐隐明白了写什么。留个作业来检验下你的成果吧:如果要针对同一个功能,开发两个版本,此时该如何修改呢?
提示一下:在这两个方法里面加些东西就好了。实际并没有标准答案,功能实现了就行,下一篇文章我会写几种实现,需要的拿去就好。