7

武装你的WEBAPI-OData Versioning - 波多尔斯基

 1 year ago
source link: https://www.cnblogs.com/podolski/p/17375269.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

武装你的WEBAPI-OData Versioning

本文属于OData系列


对外提供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<DeviceInfo>("DeviceInfoes").EntityType.HasKey(p => p.DeviceId);
				break;
			case 2:
				builder.EntitySet<DeviceInfo>("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<IConfigureOptions<SwaggerGenOptions>, 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<SwaggerGenOptions>
{
  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了。

616093-20230505201121014-1646789618.png

旧系统升级

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<RedirectMiddlewareForV1>();
  • context.Request.Path.StartsWithSegments函数只能匹配完整的路径词汇,/api/v2去匹配/api/v会返回false。
  • 另外需要了解HTTP 301/302/307/308之间的区别,如果需要保留原来的请求body,需要使用307/308,308是永久移动。
  • Redirect并不保留原来的QueryString,需要手动拼接。
  1. 无法正确显示不同版本的Swagger,提示InvalidOperationException: Can't use schemaId "B"fortype"B"fortype"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内容。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK