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);
foreach (var item in DynamicModule.DefaultInstance.Modules)
item.Configuration = new NamedPluginConfiguration(item.Configuration);
var types = _configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(_configuration.Services.GetAssembliesResolver());
foreach (var type in types)
{
var moduleName = string.Empty;
var module = DynamicModule.DefaultInstance.Modules.FirstOrDefault(p => p.Assemblies.Contains(type.Assembly));
if (module != null)
moduleName = ((NamedPluginConfiguration)module.Configuration).Name;
var segments = type.Namespace.Split(Type.Delimiter);
var controllerName = type.Name.Remove(type.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName);
if (!string.IsNullOrWhiteSpace(moduleName))
key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", moduleName, key);
if (dictionary.Keys.Contains(key))
_duplicates.Add(key);
else
dictionary[key] = new HttpControllerDescriptor(_configuration, type.Name, type);
}
foreach (var item in _duplicates)
dictionary.Remove(item);
return dictionary;
}
需要注意的是 NamedPluginConfiguration 类:我采用了修饰模式对原始配置进行了扩展。真实代码中,上一节的 AppConfig 也使用了这种方式,好处是:后续在有扩展需要在配置文件中添加信息时,可以很方便的读取而不需要另开炉灶。推荐各位在配置文件的读取上也使用这种设计模式。
上面说完了填充映射关系,下面继续说 SelectController 方法,也就是获取映射关系。获取映射关系就是根据客户端传过来的路由变量,根据填充时的规则引擎,重新生成映射关系的键,找到对应的控制器,再进行下一步操作。真实代码如下:
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
var moduleName = routeData.GetRouteVariable(ModuleKey);
string namespaceName = routeData.GetRouteVariable(NamespaceKey);
if (namespaceName == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
string controllerName = routeData.GetRouteVariable(DefaultHttpControllerSelector.ControllerSuffix);
if (controllerName == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);
if (!string.IsNullOrWhiteSpace(moduleName))
key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", moduleName, key);
HttpControllerDescriptor controllerDescriptor;
if (_controllers.Value.TryGetValue(key, out controllerDescriptor))
return controllerDescriptor;
if (_duplicates.Contains(key))
throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.InternalServerError, "有多个控制器符合这个请求!"));
else
throw new HttpResponseException(HttpStatusCode.NotFound);
}
相信用心看到这里的人,心里已经隐隐明白了写什么。留个作业来检验下你的成果吧:如果要针对同一个功能,开发两个版本,此时该如何修改呢?
提示一下:在这两个方法里面加些东西就好了。实际并没有标准答案,功能实现了就行,下一篇文章我会写几种实现,需要的拿去就好。

浙公网安备 33010602011771号