首页 > 代码库 > 使Web API支持namespace

使Web API支持namespace

小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03


问题描述

假设我有一个应用场景:Core Framework可以用于任何区域的站点,其中的CustomersController有个取customer的fullname的方法GetFullName(),可想而知,这个api在中国和美国的站点上,应该得到不同的返回值。如下图所示:

技术分享

 

这样的设计可以带来两个好处:

1、利用了OO的思想,可以封装各个区域customer service相关的一些公共逻辑

2、使得client端可以一致的接口访问服务,如:http://hostname/api/customers 

这看上去不错,但是为了达到我的目的,就必须让web api支持在不同的namespace(或者area)中,存在相同名称的controller。但是web api默认情况下是不支持的,那么是否可以通过某种方法,使web api支持这种效果呢?答案是肯定的。

查找原因

为了让web api支持namespace(或者area),就必须找到为什么默认情况下web api不支持,在这个过程中,也许能找到切入点。为了能找到原因,我做了如下工作:

1、Server-Side Handlers

从mvc4官网,找到了server端的request处理过程,如下图所示:

技术分享

从图中我们可以看到,controller是通过HttpControllerDispatcher调度器,来处理的。

2、HttpControllerDispatcher

HttpControllerDispatcher 位于System.Web.Http.Dispatcher命名空间中,其源代码中有一个私有属性:

private IHttpControllerSelector ControllerSelector
{
    get
    {
        if (this._controllerSelector == null)
        {
            this._controllerSelector = this._configuration.Services.GetHttpControllerSelector();
        }
        return this._controllerSelector;
    }
}

从代码中可以看出,该属性,只是简单的从services容器中,得到IHttpControllerSelector类型的一个对象,所以问题现在转移到在这个controllerselector对象上。

3、DefaultHttpControllerSelector

在this._configuration.Services.GetHttpControllerSelector();这条语句中,_configuration其实就是System.Web.Http.HttpConfiguration,在其构造函数中,可以看到:

this.Services = new DefaultServices(this);

DefaultServices为ServicesContainer的一个子类,所以可以称之为服务容器,定义在System.Web.Http.Services命名空间下,在其构造函数中,有如下代码:

            ......
this.SetSingle<IHttpControllerActivator>(new DefaultHttpControllerActivator());
this.SetSingle<IHttpControllerSelector>(new DefaultHttpControllerSelector(configuration));
this.SetSingle<IHttpControllerTypeResolver>(new DefaultHttpControllerTypeResolver());
            ......

从红色部分这正是我所需要的找的controllerselector。

4、问题所在

从第2步中,如果通过源代码,可以发现:HttpControllerDispatcher 在处理request时,需要通过HttpControllerDescriptor对象的CreateController方法,才能最终实例化一个ApiController。而HttpControllerDescriptor是通过IHttpControllerSelector(默认就是DefaultHttpControllerSelector)的SelectController方法构造的。我进一步在DefaultHttpControllerSelector源码中,发现如下代码:

private ConcurrentDictionary<string, HttpControllerDescriptor> InitializeControllerInfoCache()
{
    ConcurrentDictionary<string, HttpControllerDescriptor> dictionary = new ConcurrentDictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
    HashSet<string> set = new HashSet<string>();
    foreach (KeyValuePair<string, ILookup<string, Type>> pair in this._controllerTypeCache.Cache)
    {
        string key = pair.Key;
        foreach (IGrouping<string, Type> grouping in pair.Value)
        {
            foreach (Type type in grouping)
            {
                if (dictionary.Keys.Contains(key))
                {
                    set.Add(key);
                    break;
                }
                dictionary.TryAdd(key, new HttpControllerDescriptor(this._configuration, key, type));
            }
        }
    }
    foreach (string str2 in set)
    {
        HttpControllerDescriptor descriptor;
        dictionary.TryRemove(str2, out descriptor);
    }
    return dictionary;
}

和HttpControllerTypeCache这样一个cache辅助类里的:

private Dictionary<string, ILookup<string, Type>> InitializeCache()
{
    IAssembliesResolver assembliesResolver = this._configuration.Services.GetAssembliesResolver();
    return this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(assembliesResolver).
        GroupBy<Type, string>(t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length), StringComparer.OrdinalIgnoreCase).
        ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>(g => g.Key, 
        g => g.ToLookup<Type, string>(t => (t.Namespace ?? string.Empty), StringComparer.OrdinalIgnoreCase), StringComparer.OrdinalIgnoreCase);
}

这就是问题所在,导致在同一个assembly中,不能有两个相同名字的api controller。否则就会执行:

ICollection<Type> controllerTypes = this._controllerTypeCache.GetControllerTypes(controllerName);
if (controllerTypes.Count == 0)
{
    throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound, System.Web.Http.Error.Format(SRResources.ResourceNotFound, new object[] { request.RequestUri }), System.Web.Http.Error.Format(SRResources.DefaultControllerFactory_ControllerNameNotFound, new object[] { controllerName })));
}
throw CreateAmbiguousControllerException(request.GetRouteData().Route, controllerName, controllerTypes);

controllerTypes.Count大于0,导致抛出“Multiple types were found that match the controller named…”异常。

解决问题

到现在为止,其实解决办法已经出来了:就是不用上面的两个方法,来构造HttpControllerDispatcher。那么我们就只能自定义IHttpControllerSelector了。所以我自定义了一个NamespaceHttpControllerSelector用于支持namespace,源代码如下:

public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
    private const string NamespaceRouteVariableName = "Namespace";
    private readonly HttpConfiguration _configuration;
    private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerCache;

    public NamespaceHttpControllerSelector(HttpConfiguration configuration)
        : base(configuration)
    {
        _configuration = configuration;
        _apiControllerCache = new Lazy<ConcurrentDictionary<string, Type>>(new Func<ConcurrentDictionary<string,Type>>(InitializeApiControllerCache));
    }

    private ConcurrentDictionary<string, Type> InitializeApiControllerCache()
    {
        IAssembliesResolver assembliesResolver = this._configuration.Services.GetAssembliesResolver();
        var types = this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(assembliesResolver).ToDictionary(t => t.FullName, t => t);

        return new ConcurrentDictionary<string, Type>(types);
    }

    public IEnumerable<string> GetControllerFullName(HttpRequestMessage request, string controllerName)
    {
        object namespaceName;
        var data = http://www.mamicode.com/request.GetRouteData();"color: #2b91af">IEnumerable<string> keys = _apiControllerCache.Value.ToDictionary<KeyValuePair<string, Type>, string, Type>(t => t.Key, 
                t => t.Value, StringComparer.CurrentCultureIgnoreCase).Keys.ToList();
        if (data.Route.DataTokens == null ||
            !data.Route.DataTokens.TryGetValue(NamespaceRouteVariableName, out namespaceName))
        {
            return from k in keys
                    where k.EndsWith(string.Format(".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix), StringComparison.CurrentCultureIgnoreCase)
                    select k;
        }

        //get the defined namespace
        string[] namespaces = (string[])namespaceName;
        return from n in namespaces
                join k in keys on string.Format("{0}.{1}{2}", n, controllerName, DefaultHttpControllerSelector.ControllerSuffix).ToLower() equals k.ToLower()
                select k;
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        Type type;
        if (request == null)
        {
            throw new ArgumentNullException("request");
        }
        string controllerName = this.GetControllerName(request);
        if (string.IsNullOrEmpty(controllerName))
        {
            throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                string.Format("No route providing a controller name was found to match request URI ‘{0}‘", new object[] { request.RequestUri })));
        }
        IEnumerable<string> fullNames = GetControllerFullName(request, controllerName);
        if (fullNames.Count() == 0)
        {
            throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                    string.Format("No route providing a controller name was found to match request URI ‘{0}‘", new object[] { request.RequestUri })));
        }

        if (this._apiControllerCache.Value.TryGetValue(fullNames.First(), out type))
        {
            return new HttpControllerDescriptor(_configuration, controllerName, type);
        }
        throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
            string.Format("No route providing a controller name was found to match request URI ‘{0}‘", new object[] { request.RequestUri })));
    }
}

其实代码并不难懂,核心部分就是:

1、InitializeApiControllerCache方法,用于通过fullname为key,构造一个controller type的一个集合

2、GetControllerFullName方法,从namespace数组和_apiControllerCache集合中,取到符合条件的controller的fullname

到目前为止,我们的工作还剩下最后一步,就是用NamespaceHttpControllerSelector替换DefaultHttpControllerSelector,使其生效。通过以上的分析,其实也很明显了,只需要在Application_Start方法中,用DefaultServices继承下来的Replace即可:

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), 
    new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration));

至此,才算大功告成!

参考页面:http://qingqingquege.cnblogs.com/p/5933752.html

使Web API支持namespace