使Web API支持namespace


問題描述

假設我有一個應用場景:Core Framework可以用於任何區域的站點,其中的CustomersController有個取customer的fullname的方法GetFullName(),可想而知,這個api在中國和美國的站點上,應該得到不同的返回值。如下圖所示:

web api namespace

這樣的設計可以帶來兩個好處:

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處理過程,如下圖所示:

web api handlers

從圖中我們可以看到,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 = request.GetRouteData();
        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));
至此,才算大功告成!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM