打造屬於本身的支持版本迭代的Asp.Net Web Api Route

 以Asp.Net Web Api 爲例,隨着業務的擴展,產品的迭代,咱們的web api也在隨之變化,不少時候會出現多個版本共存的現象,這個時候咱們就須要設計一個支持版本號的web api link,好比:web

原先:http://www.test.com/api/{controller}/{id}api

現在:http://www.test.com/api/{version}/{controller}/{id}瀏覽器

在咱們剛設計的時候,有可能沒有考慮版本的問題,我看到不少的項目都會在link後加入一個「?version=」的方式,這種方式確實可以解決問題,但對Asp.Net Web Api來講,進入的仍是同一個Controller,咱們須要在同一個Action中進行判斷版本號,例如:app

http://www.test.com/api/bolgs?version=v2[HttpGet]ide

複製代碼

public class BlogsController : ApiController
{    // GET api/<controller>
    public IEnumerable<string> Get([FromUri]string version = "")
    {        if (!String.IsNullOrEmpty(version))
        {            return new string[] { $"{version} blog1", $"{version} blog2" };
        }        return new string[] { "blog1", "blog2" };
    }
}

複製代碼

咱們看到咱們經過判斷url中的version參數進行對應的返回,爲了確保原先接口的可用,咱們須要對參數賦上默認值,雖然可以解決咱們的版本迭代問題,但隨着版本的不斷更新,你會發現這個Controller會愈來愈臃腫,維護愈來愈困難,由於這種修改已經嚴重違反了OCP(Open-Closed Principle),最好的方式是不修改原先的Controller,而是新建新的Controller,放在對應的目錄中(或者項目中),好比:post

p_w_picpath

爲了避免影響原先的項目,咱們儘可能不要改動原Controller的Namespace,除非你有十足的把握沒有影響,否則請儘可能只是移動到目錄。url

ok,爲了保持原接口的映射,咱們須要在WebApiConfig.Register中註冊支持版本號的Route映射:spa

config.Routes.MapHttpRoute(
    name: "DefaultVersionApi",
    routeTemplate: "api/{version}/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

打開瀏覽器或者postman,輸入原先的api url,你會發現這樣的錯誤:設計

p_w_picpath

那是由於web api 查找Controller的時候,只會根據ClassName進行查找的,當出現相同ClassName的時候,就會報這個錯誤,這時候咱們就須要打造本身的Controller Selector,好在微軟留了一個接口給到咱們:IHttpControllerSelector。不過爲了兼容原先的api(有些不在咱們權限範圍內的api,不加版本號的那種),咱們仍是直接集成DefaultHttpControllerSelector比較好,咱們給定一個規則,不負責咱們版本迭代的api,就讓它走原先的映射。orm

思路

一、項目啓動的時候,先把符合條件的Controller加入到一個字典中

二、判斷request,符合規則的,咱們返回咱們制定的controller。

p_w_picpath

p_w_picpath

打造屬於本身的Selector

思路有了,那改造起來也很是簡單,今天咱們先作一個簡單的,等有時間改爲可配置的。

第一步,咱們先建立一個Selector類,繼承自DefaultHttpControllerSelector,而後初始化的時候建立一個屬於咱們本身的字典:

複製代碼

public class VersionHttpControllerSelector : DefaultHttpControllerSelector
{    private readonly HttpConfiguration _configuration;    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _lazyMappingDictionary;    private const string DefaultVersion = "v1"; //默認版本號,由於以前的api咱們沒有版本號的概念
    private const string DefaultNamespaces = "WebApiVersions.Controllers"; //爲了演示方便,這裏就用到一個命名空間
    private const string RouteVersionKey = "version"; //路由規則中Version的字符串
    private const string DictKeyFormat = "{0}.{1}";    public VersionHttpControllerSelector(HttpConfiguration configuration):base(configuration)
    {
        _configuration = configuration;
        _lazyMappingDictionary = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDict);
    }    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDict()
    {        var result = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);        var assemblies = _configuration.Services.GetAssembliesResolver();        var controllerResolver = _configuration.Services.GetHttpControllerTypeResolver();        var controllerTypes = controllerResolver.GetControllerTypes(assemblies);        foreach(var t in controllerTypes)
        {            if (t.Namespace.Contains(DefaultNamespaces)) //符合NameSpace規則            {                var segments = t.Namespace.Split(Type.Delimiter);                var version = t.Namespace.Equals(DefaultNamespaces, StringComparison.OrdinalIgnoreCase) ?
                    DefaultVersion : segments[segments.Length - 1];                var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);                var key = string.Format(DictKeyFormat, version, controllerName);                if (!result.ContainsKey(key))
                {
                    result.Add(key, new HttpControllerDescriptor(_configuration, t.Name, t));
                }
            }
        }        return result;
    }
}

複製代碼


有了字典接下來就好辦了,只須要分析request就行了,符合咱們版本要求的,就從咱們的字典中查找對應的Descriptor,若是找不到,就走默認的,這裏咱們須要重寫SelectController方法:

複製代碼

public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    IHttpRouteData routeData = request.GetRouteData();    if (routeData == null)        throw new HttpResponseException(HttpStatusCode.NotFound);    var controllerName = GetControllerName(request);    if (String.IsNullOrEmpty(controllerName))        throw new HttpResponseException(HttpStatusCode.NotFound);    var version = DefaultVersion;    if (IsVersionRoute(routeData, out version))
    {        var key = String.Format(DictKeyFormat, version, controllerName);        if (_lazyMappingDictionary.Value.ContainsKey(key))
        {            return _lazyMappingDictionary.Value[key];
        }        throw new HttpResponseException(HttpStatusCode.NotFound);
    }    return base.SelectController(request);
}private bool IsVersionRoute(IHttpRouteData routeData, out string version)
{
    version = String.Empty;    var prevRouteTemplate = "api/{controller}/{id}";    object outVersion;    if(routeData.Values.TryGetValue(RouteVersionKey, out outVersion))   //先找符合新規則的路由版本    {
        version = outVersion.ToString();        return true;
    }    if (routeData.Route.RouteTemplate.Contains(prevRouteTemplate))  //不符合再比對是否符合原先的api路由    {
        version = DefaultVersion;        return true;
    }    return false;
}

複製代碼

完成這個類後,咱們去WebApiConfig.Register中進行替換操做:

config.Services.Replace(typeof(IHttpControllerSelector), new VersionHttpControllerSelector(config));

ok,再次打開瀏覽器,輸入http://www.xxx.com/api/blogs 和 http://www.xxx.com/api/v2/blogs ,這時應該能看到正確的執行:

p_w_picpath

p_w_picpath

相關文章
相關標籤/搜索