使Web API支持namespace
作者:互联网
问题描述
假设我有一个应用场景: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<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));至此,才算大功告成! posted on 2013-01-03 10:15 Xavier Zhang 阅读(...) 评论(...) 编辑 收藏
转载于:https://www.cnblogs.com/champoin/archive/2013/01/03/2842714.html
标签:Web,string,request,controllerName,namespace,API,._,new,configuration 来源: https://blog.csdn.net/weixin_34315189/article/details/94344336