• 武装你的WEBAPI-OData之API版本管理


    本文属于OData系列

    Intro

    对外提供WEBAPI时,如果遇上了版本升级,那么控制WEBAPI的版本也是非常必要的。OData官方提供了版本控制以及管理的解决方案,我个人是实践体会是不好用,好在社区提供了对应的nuget包,与.NET主版本同步更新。

    介绍

    ASP.NET API Versioning是一个提供ASP.NET WEBAPI版本管理的包,支持ASP.NET、ASP.NET CORE、ASP.NET CORE ODATA,作者以前是微软的员工,现在不在微软工作了,因此原先的命名空间不能继续用了。现在这个项目已经加入.NET Foundation,作者也非常活跃。

    版本管理

    首先对现有的项目安装这个包:

    Install-Package Asp.Versioning.OData
    

    在Program.cs文件中修改一下:

    var builder = WebApplication.CreateBuilder( args );
    
    builder.Services.AddControllers().AddOData();
    builder.Services.AddProblemDetails();
    builder.Services.AddApiVersioning().AddOData(
        options =>
        {
            options.AddRouteComponents();
        } );
    
    var app = builder.Build();
    
    app.MapControllers();
    app.Run();
    

    然后在需要控制版本的控制器上加上[ApiVersion]修饰就可以了。

    [ApiVersion( 1.0 )]
    public class PeopleController : ODataController
    {
        [EnableQuery]
        public IActionResult Get() => Ok( new[] { new Person() } );
    }
    

    注意,默认的版本是1.0,不过最好显式声明一下。

    EDM升级

    EDM根据版本不同也会有一些区别,需要分别进行配置,原来的GetEdm()模式显得有点麻烦,而EDM配置在这个库中变得非常灵活,使用的是Configuration模式。

    示例代码如下:

    public class DeviceInfoModelConfiguration : IModelConfiguration
    {
    	public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix)
    	{
    		switch (apiVersion.MajorVersion)
    		{
    			case 1:
    				builder.EntitySet("DeviceInfoes").EntityType.HasKey(p => p.DeviceId);
    				break;
    			case 2:
    				builder.EntitySet("DeviceInfos").EntityType.HasKey(p => p.DeviceId).Ignore(w => w.Layout);
    				break;
    			default:
    				break;
    		};
    	}
    }
    

    只需要实现IModelConfiguration接口,并在Apply函数中根据版本对实体或者DTO对象进行配置,不同版本的EDM可以不一样。

    一般实践是一个实体对象一个IModelConfiguration,方便后面管理。

    配置Swagger

    因为有重复配置的模型,直接使用默认的Swagger会报错,这个时候需要使用到Versioned API Explorer,对Swagger拓展版本信息。

    Install-Package Asp.Versioning.OData.ApiExplorer
    

    安装Asp.Versioning.OData.ApiExplorer,重新改造一下Program.cs文件:

    var builder = WebApplication.CreateBuilder( args );
    
    builder.Services.AddControllers().AddOData();
    builder.Services.AddProblemDetails();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddApiVersioning()
                    .AddOData( options => options.AddRouteComponents() )
                    .AddODataApiExplorer(
                         // format the version as "'v'major[.minor][-status]"
                         options => options.GroupNameFormat = "'v'VVV" );
    
    services.AddTransient, ConfigureSwaggerOptions>();
    services.AddSwaggerGen();
    
    var app = builder.Build();
    
    app.UseSwagger();
    app.UseSwaggerUI(
        options =>
        {
            foreach ( var description in app.DescribeApiVersions() )
            {
                var url = $"/swagger/{description.GroupName}/swagger.json";
                var name = description.GroupName.ToUpperInvariant();
                options.SwaggerEndpoint( url, name );
            }
        } );
    app.MapControllers();
    app.Run();
    

    还需要一个配置的类如下:

    public class ConfigureSwaggerOptions : IConfigureOptions
    {
      readonly IApiVersionDescriptionProvider provider;
    
      public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) =>
        this.provider = provider;
    
      public void Configure( SwaggerGenOptions options )
      {
        foreach ( var description in provider.ApiVersionDescriptions )
        {
          options.SwaggerDoc(
            description.GroupName,
              new OpenApiInfo()
              {
                Title = $"Example API {description.ApiVersion}",
                Version = description.ApiVersion.ToString(),
              } );
        }
      }
    }
    

    这样,swagger界面就可以下拉选择不同版本的API了。

    旧系统升级

    WEBAPI Versioning对这个内容有介绍,其中需要注意的是,基于路径的版本匹配并不支持默认版本的特性,对于以前系统直接使用api/开头的控制器,并不能直接默认转到到api/v1(参考介绍)。为了兼容旧系统,我们只能在ASP.NET CORE的管线上想想办法:插入一个中间件,对路径进行判断,如果是api开头的,就直接转到api/v1;如果是api/v开头的,那么就直接下一步。

        public class RedirectMiddlewareForV1
        {
            private readonly RequestDelegate _next;
    
            public RedirectMiddlewareForV1(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task InvokeAsync(HttpContext context)
            {
                if (context.Request.Path.StartsWithSegments("/api") && !context.Request.Path.Value.StartsWith("/api/v"))
                {
                    //千万小心,一定需要保留原来的QueryString
                    var newUrl = $"{context.Request.Path.Value.Replace("/api/", "/api/v1/")}{context.Request.QueryString}";
                    //permanent指示永久迁移,preserveMethod指示保留原来的谓词与body
                    context.Response.Redirect(newUrl, permanent: true, preserveMethod: true);
                }
                else
                {
                    await _next(context);
                }
            }
    
    
        }
    

    然后在configure函数中注册这个中间件就可以了。

    app.UseMiddleware();
    

    请注意:

    • context.Request.Path.StartsWithSegments函数只能匹配完整的路径词汇,/api/v2去匹配/api/v会返回false。
    • 另外需要了解HTTP 301/302/307/308之间的区别,如果需要保留原来的请求body,需要使用307/308,308是永久移动。
    • Redirect并不保留原来的QueryString,需要手动拼接。

    FAQ

    1. 无法正确显示不同版本的Swagger,提示InvalidOperationException: Can't use schemaId "\(B" for type "\)A.B". The same schemaId is already used for type "$A.B"
      这个问题是由多次对同一个类型Schema生成造成的。最常见的情况是你的控制器有方法不属于OData Routing的一部分(比如直接使用HttpGet指定),这样程序在扫描的过程中会重复对对象进行生成。解决办法有两种:
    1. 无法加载Swagger,提示System.MissingMethodException: Method not found: 'Microsoft.OData.ModelBuilder.Config.DefaultQuerySettings Microsoft.AspNetCore.OData.ODataOptions.get_QuerySettings()
      这个是版本问题,本人使用的OData版本在8.1.0,有一些破坏性更改,只要保持引用的OData版本<= 8.0.12就可以了。
      详细分析看这个MissingMethodException with OData v8.1.0 · Issue #980 · dotnet/aspnet-api-versioning (github.com)
    2. 找不到DescribeApiVersions()方法
      app找不到这个方法,大概率是在.NET 6的Minimal API之前的代码升级出现的,之前app是用IWebhostBuilder构建的,而现在的app是直接用过WebApplication构建得到的,含义不同,最简单的方法是改造一下,使用WebApplication重写一下Startup内容。

    参考

  • 相关阅读:
    使用python分组查询每组取前面10条数据
    九)Stable Diffussion使用教程:ControlNet
    Java同步与线程安全,同步方法、同步块和java.util.concurrent包的使用
    yoloV8训练自己的数据集合资源
    Go Context
    我的 Windows 文件管理哲学
    魔改YOLOv5/YOLOv7高阶版(魔法搭配+创新组合)
    CAD一键添加审图批注、AUTOCAD——图形界线怎么设置
    Jmeter进阶使用指南-使用断言
    《canvas》之第5章 文本操作
  • 原文地址:https://www.cnblogs.com/podolski/p/17375269.html