重写的控制器获取工厂

模块化的 WebApi 服务,最核心的东西就是这货了:负责请求 URL 和控制器类型的映射 —— 简单来说就是红娘,不认识的话,小伙子你别想讨到媳妇儿了。

系统内置的缺省 WebApi 控制器发现工厂,只能从路由信息中获得控制器和动作,要获取自定义的路由信息,只能通过重写控制器获取工厂 IHttpControllerSelector 来解决。按照第一章中定下的规则,使用 {module}/{controller}/{action}/{id} 路由规则,我们需要为路由参数额外读取一个 module 参数。

注:id 变量并不由控制器获取工厂使用,在实际使用中由 Action 相关类映射:一个很明显的例子就是 WebApi 2 的 Attribute 映射。

话说回来,也不一定必须使用我定下这套规则,确保你能访问控制器即可。在这里简要说一下 IHttpControllerSelector 接口两大方法的作用:

  1. GetControllerMapping 用于获取路由规则和控制器类型的关系映射列表。

  2. 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);
}

相信用心看到这里的人,心里已经隐隐明白了写什么。留个作业来检验下你的成果吧:如果要针对同一个功能,开发两个版本,此时该如何修改呢?

提示一下:在这两个方法里面加些东西就好了。实际并没有标准答案,功能实现了就行,下一篇文章我会写几种实现,需要的拿去就好。