前言
預設情況下,微軟提供的MVC架構模闆中,WebAPI路由是不支援Namespace參數的。這導緻一些比較大型的項目,無法把WebApi分離到單獨的類庫中。
本文将提供解決該問題的方案。
微軟官方曾經給出過一個關于WebAPI支援Namespace的擴充,其主要内容就是自定義實作了IHttpControllerSelector接口,通過路由配置時替換掉MVC中自帶的DefaultHttpControllerSelector達到WebAPI支援Namespace的目的。但是經過我的測試,效果并不好。這就就不貼微軟官方的源碼了。
解決方案
下面我介紹一下我的解決方案。
首先看一下我自定義的目錄結構,如下圖:

/// <summary>
/// 擴充自DefaultHttpControllerSelector類的控制器選擇器,目前在用
/// </summary>
public class ZhuSirNamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
private const string NamespaceRouteVariableName = "namespaces";
private readonly HttpConfiguration _configuration;
private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerCache;
public ZhuSirNamespaceHttpControllerSelector(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.Values.TryGetValue(NamespaceRouteVariableName, out namespaceName))
{
return from k in keys
where k.EndsWith(string.Format(".{0}{1}", controllerName,
DefaultHttpControllerSelector.ControllerSuffix), StringComparison.CurrentCultureIgnoreCase)
select k;
}
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("無法通過API路由比對到您所請求的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("無法通過API路由比對到您所請求的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("無法通過API路由比對到您所請求的URI '{0}'",
new object[] { request.RequestUri })));
}
}
第二步,需要我們配置WebAPI路由設定,添加{ namespaces }片段變量,同時也可以直接為其設定預設值,然後替換掉原MVC架構中的DefaultHttpControllerSelector選額器為我們之前擴充的ZhuSirNamespaceHttpControllerSelector選額器。這裡需要注意片段變量的變量名namespaces一定要與我們ZhuSirNamespaceHttpControllerSelector中定義的NamespaceRouteVariableName字元串常量的值一緻。下面貼出WebApiConfig的源碼:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//配置檢查Api控制字尾為ApiController
//var suffix = typeof(MicrosoftNamespaceHttpControllerSelector)
// .GetField("ControllerSuffix", BindingFlags.Static | BindingFlags.Public);
//if (suffix != null)
//{
// suffix.SetValue(null, "ApiController");
//}
// Web API 配置和服務
// 将 Web API 配置為僅使用不記名令牌身份驗證。
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// 對 JSON 資料使用混合大小寫。
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}/{namespaces}",
defaults: new { id = RouteParameter.Optional ,namespaces = new[] { "ZhuSir.HMS.WebApi.ApiControllers" } }
);
config.Services.Replace(typeof(IHttpControllerSelector), new ZhuSirNamespaceHttpControllerSelector(config));
}
}
如上源碼。我們替換了原MVC架構的DefaultHttpControllerSelector為ZhuSirNamespaceHttpControllerSelector,并且指定了預設的namespaces為ZhuSir.HMS.WebApi.ApiControllers。大體意思就是當URL為 http://XXX/api/testApi時,WebApi路由将在ZhuSir.HMS.WebApi.ApiControllers命名空間下尋找名稱為testApi的WebAPI控制器的Get方法。
當然,WebAPI控制器除了內建字ApiController意外還要注意命名規範,都需要以Controller結尾,為了不讓API控制器與MVC控制器重名,我都以ApiController結尾。下面貼出testAPI的源碼:
namespace ZhuSir.HMS.WebApi.ApiControllers
{
public class TestApiController : ApiController
{
[HttpGet]
public string Gettest()
{
return "測試資料";
}
}
}
測試
程式Debug,錄入URL: http://localhost:4541/api/TestApi,得到如下結果:
可以看出,WebAPI路由成功通路到了其他類庫中的WebAPI控制器。