武装你的WEBAPI-OData Versioning

科技资讯 投稿 6200 0 评论

武装你的WEBAPI-OData Versioning

本文属于OData系列

    武装你的WEBAPI-OData入门
  • 武装你的WEBAPI-OData便捷查询
  • 武装你的WEBAPI-OData分页查询
  • 武装你的WEBAPI-OData资源更新Delta
  • 武装你的WEBAPI-OData之EDM
  • 武装你的WEBAPI-OData常见问题
  • 武装你的WEBAPI-OData使用Endpoint
  • 武装你的WEBAPI-OData聚合查询
  • 武装你的WEBAPI-OData Versioning

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

介绍

版本管理

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

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升级

示例代码如下:

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可以不一样。

配置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了。

旧系统升级

    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"
            {
                context.Response.Redirect(context.Request.Path.Value.Replace("/api/", "/api/v1/";
            }
            else
            {
                await _next(context;
            }
        }


    }

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

app.UseMiddleware<RedirectMiddlewareForV1>(;

请注意,context.Request.Path.StartsWithSegments函数只能匹配完整的路径词汇,/api/v2去匹配/api/v会返回false。

FAQ

    无法正确显示不同版本的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指定),这样程序在扫描的过程中会重复对对象进行生成。解决办法有两种:
    在EDM正确配置控制器中的Action或者Function,不要有不在OData路由的路径。(UseODataRouteDebug(可以启用$odata的路径,这样可以查看哪些路径不被OData解析)
  • 为每一个对象使用唯一名称:在配置中增加options.CustomSchemaIds(type => type.AssemblyQualifiedName这个项目,所有的swagger对外名称会变成随机生成的值,也可以规避这个问题。
    详细分析看这个:Ignoring property on EDM causes InvalidOperationException: Can't use schemaId "\(B" for type "\A.B". The same schemaId is already used for type "$A.B" · Issue #772 · dotnet/aspnet-api-versioning (github.com
  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内容。

参考

    API Documentation · dotnet/aspnet-api-versioning Wiki (github.com
    API versioning extension with ASP.NET Core OData 8 - OData (microsoft.com

编程笔记 » 武装你的WEBAPI-OData Versioning

赞同 (27) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽