問題描述
假設我有一個應用場景: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 = 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));至此,才算大功告成!