打造属于自己的支持版本迭代的Asp.Net Web Api Route

简介: 在目前的主流架构中,我们越来越多的看到web Api的存在,小巧,灵活,基于Http协议,使它在越来越多的微服务项目或者移动项目充当很好的service endpoint。问题    以Asp.

    在目前的主流架构中,我们越来越多的看到web Api的存在,小巧,灵活,基于Http协议,使它在越来越多的微服务项目或者移动项目充当很好的service endpoint。

问题

    以Asp.Net Web Api 为例,随着业务的扩展,产品的迭代,我们的web api也在随之变化,很多时候会出现多个版本共存的现象,这个时候我们就需要设计一个支持版本号的web api link,比如:

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

如今:http://www.test.com/api/{version}/{controller}/{id}

在我们刚设计的时候,有可能没有考虑版本的问题,我看到很多的项目都会在link后加入一个“?version=”的方式,这种方式确实能够解决问题,但对Asp.Net Web Api来说,进入的还是同一个Controller,我们需要在同一个Action中进行判断版本号,例如:

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

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,放在对应的目录中(或者项目中),比如:

image

为了不影响原先的项目,我们尽量不要改动原Controller的Namespace,除非你有十足的把握没有影响,不然请尽量只是移动到目录。

ok,为了保持原接口的映射,我们需要在WebApiConfig.Register中注册支持版本号的Route映射:

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

打开浏览器或者postman,输入原先的api url,你会发现这样的错误:

image

那是因为web api 查找Controller的时候,只会根据ClassName进行查找的,当出现相同ClassName的时候,就会报这个错误,这时候我们就需要打造自己的Controller Selector,好在微软留了一个接口给到我们:IHttpControllerSelector。不过为了兼容原先的api(有些不在我们权限范围内的api,不加版本号的那种),我们还是直接集成DefaultHttpControllerSelector比较好,我们给定一个规则,不负责我们版本迭代的api,就让它走原先的映射。

思路

1、项目启动的时候,先把符合条件的Controller加入到一个字典中

2、判断request,符合规则的,我们返回我们制定的controller。

image

image

打造属于自己的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 ,这时应该能看到正确的执行:

image

image

写在最后

今天我们打造了一个简单符合webapi版本号更新迭代的ControllerSelector,不过还不是很完善,因为很多都是hard code,后面我会做一个支持配置的ControllerSelector放到github上。

之前一直在研究eShopOnContrainers,最近也在研究,不过工作确实有点忙,见谅见谅,如果大家.Net有什么问题或者喜欢技术交友的,都可以加QQ群:376248054

相关文章
|
8天前
|
开发框架 监控 .NET
Visual Basic的Web服务和REST API开发指南
【4月更文挑战第27天】本文探讨了使用Visual Basic(VB.NET)构建Web服务和RESTful API的方法。首先介绍了Web服务的基础和REST API的概念,然后阐述了.NET Framework与.NET Core/.NET 5+对VB.NET的支持,以及ASP.NET Core在Web开发中的作用。接着,详细讲解了创建RESTful API的步骤,包括控制器与路由设置、模型绑定与验证,以及返回响应。此外,还讨论了安全措施、测试方法、部署选项和监控策略。最后强调,VB.NET开发者可以通过ASP.NET Core涉足现代Web服务开发,拓宽技术领域。
|
5天前
|
弹性计算 JSON Shell
基于Web API的自动化信息收集和整理
【4月更文挑战第30天】
13 0
|
8天前
|
开发框架 缓存 前端开发
利用Visual Basic构建高效的ASP.NET Web应用
【4月更文挑战第27天】本文探讨使用Visual Basic与ASP.NET创建高效Web应用的策略,包括了解两者基础、项目规划、MVC架构、数据访问与缓存、代码优化、异步编程、安全性、测试及部署维护。通过这些步骤,开发者能构建出快速、可靠且安全的Web应用,适应不断进步的技术环境。
|
7天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
21 1
|
9天前
|
缓存 前端开发 API
toapi,一个强大的 Python Web API库!
toapi,一个强大的 Python Web API库!
24 5
|
17天前
|
安全 编译器 PHP
PHP 8.1版本发布:引领Web开发新潮流
PHP编程语言一直是Web开发的主力军,而最新发布的PHP 8.1版本则为开发者们带来了更多创新和便利。本文将介绍PHP 8.1版本的主要特性,包括更快的性能、新的语言功能和增强的安全性,以及如何利用这些功能来提升Web应用程序的质量和效率。
|
17天前
|
JSON API 数据库
解释如何在 Python 中实现 Web 服务(RESTful API)。
在Python中实现Web服务(RESTful API)涉及选择框架(如Flask、Django、FastAPI),定义路由及处理函数(对应HTTP请求方法),处理请求,构建响应和启动服务器。以下是一个使用Flask的简单示例:定义用户列表,通过`@app.route`装饰器设置GET和POST请求处理函数,返回JSON响应,并用`app.run()`启动服务器。实际API会包含更复杂的逻辑和错误处理。
15 1
|
2月前
|
XML JSON API
通过Flask框架创建灵活的、可扩展的Web Restful API服务
通过Flask框架创建灵活的、可扩展的Web Restful API服务
|
2月前
|
缓存 监控 API
Python Web框架FastAPI——一个比Flask和Tornada更高性能的API框架
Python Web框架FastAPI——一个比Flask和Tornada更高性能的API框架
60 0
|
2月前
|
Windows
Windows Server 各版本搭建 Web 服务器实现访问本地 Web 网站(03~19)
Windows Server 各版本搭建 Web 服务器实现访问本地 Web 网站(03~19)