How to set up Swashbuckle vs Microsoft.AspNetCore.Mvc.Versioning

丶灬走出姿态 提交于 2020-06-09 09:28:26

问题


We have asp.net core webapi. We added Microsoft.AspNetCore.Mvc.Versioning and Swashbuckle to have swagger UI. We specified controllers as this:

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ContactController : Controller
{

When we run swagger ui we get version as parameter in routes:

How to set-up default "v1" for route ? If version 2 come to the stage how support swagger ui for both versions ?


回答1:


At the moment Swashbuckle and Microsoft.AspNetCore.Mvc.Versioning are friends. It works good. I just created test project in VS2017 and checked how it works.

First include these two nuget packages:

<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />

Configure everything in Startup.cs (read my comments):

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();


        // Configure versions 
        services.AddApiVersioning(o =>
        {
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.DefaultApiVersion = new ApiVersion(1, 0);
        });

        // Configure swagger
        services.AddSwaggerGen(options =>
        {
            // Specify two versions 
            options.SwaggerDoc("v1", 
                new Info()
                {
                    Version = "v1",
                    Title = "v1 API",
                    Description = "v1 API Description",
                    TermsOfService = "Terms of usage v1"
                });

            options.SwaggerDoc("v2",
                new Info()
                {
                    Version = "v2",
                    Title = "v2 API",
                    Description = "v2 API Description",
                    TermsOfService = "Terms of usage v2"
                });

            // This call remove version from parameter, without it we will have version as parameter 
            // for all endpoints in swagger UI
            options.OperationFilter<RemoveVersionFromParameter>();

            // This make replacement of v{version:apiVersion} to real version of corresponding swagger doc.
            options.DocumentFilter<ReplaceVersionWithExactValueInPath>();

            // This on used to exclude endpoint mapped to not specified in swagger version.
            // In this particular example we exclude 'GET /api/v2/Values/otherget/three' endpoint,
            // because it was mapped to v3 with attribute: MapToApiVersion("3")
            options.DocInclusionPredicate((version, desc) =>
            {
                var versions = desc.ControllerAttributes()
                    .OfType<ApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions);

                var maps = desc.ActionAttributes()
                    .OfType<MapToApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions)
                    .ToArray();

                return versions.Any(v => $"v{v.ToString()}" == version) && (maps.Length == 0 || maps.Any(v => $"v{v.ToString()}" == version));
            });

        });

    }

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"v2");
            c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"v1");
        });
        app.UseMvc();
    }

There two classes that make the trick:

public class RemoveVersionFromParameter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters.Single(p => p.Name == "version");
        operation.Parameters.Remove(versionParameter);
    }
}

public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        swaggerDoc.Paths = swaggerDoc.Paths
            .ToDictionary(
                path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
                path => path.Value
            );
    }
}

The RemoveVersionFromParameter removes from swagger UI this textbox:

The ReplaceVersionWithExactValueInPath change this:

to this:

Controller class looks now as follows:

[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1")]
[ApiVersion("2")]
public class ValuesController : Controller
{
    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public string Get(int id)
    {
        return "value";
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody]string value)
    {
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }


    [HttpGet("otherget/one")]
    [MapToApiVersion("2")]
    public IEnumerable<string> Get2()
    {
        return new string[] { "value1", "value2" };
    }

    /// <summary>
    /// THIS ONE WILL BE EXCLUDED FROM SWAGGER Ui, BECAUSE v3 IS NOT SPECIFIED. 'DocInclusionPredicate' MAKES THE
    /// TRICK 
    /// </summary>
    /// <returns></returns>
    [HttpGet("otherget/three")]
    [MapToApiVersion("3")]
    public IEnumerable<string> Get3()
    {
        return new string[] { "value1", "value2" };
    }
}

Code: https://gist.github.com/Alezis/bab8b559d0d8800c994d065db03ab53e




回答2:


If working with .Net Core 3, Basically I have taken @Alezis's solution and updated it to work with .Net core 3:

public void ConfigureServices(IServiceCollection services)
    {
     ....
        services.AddSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo() { Title = "My API", Version = "v1" });
            options.OperationFilter<RemoveVersionFromParameter>();

            options.DocumentFilter<ReplaceVersionWithExactValueInPath>();

        });
      ...
    }

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
   ...
}

public class RemoveVersionFromParameter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters.Single(p => p.Name == "version");
        operation.Parameters.Remove(versionParameter);
    }
}

public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var paths = new OpenApiPaths();
        foreach (var path in swaggerDoc.Paths)
        {
            paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value);
        }
        swaggerDoc.Paths = paths;
    }
}



回答3:


@Alezis Nice approach, but if you are using the latest version of Microsoft.AspNetCore.Mvc.Versioning (2.3.0) library, ControllerAttributes() and ActionAttributes() are deprecated, you can update DocInclusionPredicate as follows:

options.DocInclusionPredicate((version, desc) =>
{
    if (!desc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
    var versions = methodInfo.DeclaringType
        .GetCustomAttributes(true)
        .OfType<ApiVersionAttribute>()
        .SelectMany(attr => attr.Versions);
     return versions.Any(v => $"v{v.ToString()}" == version);
});

Swashbuckle.AspNetCore github project helps me a lot.




回答4:


When updating to .net core 3 I got the following error:

'Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,Microsoft.OpenApi.Models.OpenApiPathItem]' to type 'Microsoft.OpenApi.Models.OpenApiPaths'.'

Fixed this by changes to code to:

public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        if (swaggerDoc == null)
            throw new ArgumentNullException(nameof(swaggerDoc));

        var replacements = new OpenApiPaths();

        foreach (var (key, value) in swaggerDoc.Paths)
        {
            replacements.Add(key.Replace("{version}", swaggerDoc.Info.Version, StringComparison.InvariantCulture), value);
        }

        swaggerDoc.Paths = replacements;
    }
}



回答5:


In Asp.core 2.+ Add this class:

public class ApiVersionOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var actionApiVersionModel = context.ApiDescription.ActionDescriptor?.GetApiVersion();
            if (actionApiVersionModel == null)
            {
                return;
            }

            if (actionApiVersionModel.DeclaredApiVersions.Any())
            {
                operation.Produces = operation.Produces
                    .SelectMany(p => actionApiVersionModel.DeclaredApiVersions
                        .Select(version => $"{p};v={version.ToString()}")).ToList();
            }
            else
            {
                operation.Produces = operation.Produces
                    .SelectMany(p => actionApiVersionModel.ImplementedApiVersions.OrderByDescending(v => v)
                        .Select(version => $"{p};v={version.ToString()}")).ToList();
            }
        }
   }

next add below codes in configureServices method in startup:

services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Versioned Api v1", Version = "v1" });

                c.OperationFilter<ApiVersionOperationFilter>();
        });

then add below codes in configure method in startup:

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {                
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned Api v1");
                    c.RoutePrefix = string.Empty;

in Asp.core 3.+ add these classes:

public class RemoveVersionFromParameter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
                if (!operation.Parameters.Any())
                    return;

                var versionParameter = operation.Parameters
                    .FirstOrDefault(p => p.Name.ToLower() == "version");

                if (versionParameter != null)
                    operation.Parameters.Remove(versionParameter);
        }
    }

 public class ReplaceVersionWithExactValueInPath : IDocumentFilter
    {
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            if (swaggerDoc == null)
                throw new ArgumentNullException(nameof(swaggerDoc));

            var replacements = new OpenApiPaths();

            foreach (var (key, value) in swaggerDoc.Paths)
            {
                replacements.Add(key.Replace("v{version}", swaggerDoc.Info.Version,
                        StringComparison.InvariantCulture), value);
            }

            swaggerDoc.Paths = replacements;
        }
    }

next add below codes in ConfigureServices method in startup:

protected virtual IEnumerable<int> Versions => new[] {1};

 services.AddSwaggerGen(options =>
            {
                Versions.ToList()
                    .ForEach(v =>
                        options.SwaggerDoc($"v{v}",
                            new OpenApiInfo
                            {
                                Title = $"Versioned Api:v{v}", Version = $"v{v}"
                            }));

                options.OperationFilter<RemoveVersionFromParameter>();
                options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
                options.RoutePrefix = string.Empty;
            });

then add below codes in configure method in startup:

            app.UseSwagger();

            app.UseSwaggerUI(options =>
           {
               Versions.ToList()
                   .ForEach(v => options.SwaggerEndpoint($"/swagger/v{v}/swagger.json", $"Versioned Api:v{v}"));

               options.RoutePrefix = string.Empty;
           });



回答6:


Instead of tweaking the OpenAPI document, you can use the library provided by Microsoft that adds versions to the API Explorer. That way the versions are provided before Swashbuckle (or another toolchain) needs it and allows you to avoid custom code.

Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

I was able to get versions configured correctly after adding the package and this block of code.

services.AddVersionedApiExplorer(
    options =>
    {
    // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
    // note: the specified format code will format the version as "'v'major[.minor][-status]"
    options.GroupNameFormat = "'v'VVV";

    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
    // can also be used to control the format of the API version in route templates
    options.SubstituteApiVersionInUrl = true;
    }
);



回答7:


I found that using the method ArlanG highlighted took {00:00:00.0001905} to complete whereas running

var versions = methodInfo.DeclaringType.GetConstructors().SelectMany(x =>
    x.DeclaringType.CustomAttributes.Where(y => 
        y.AttributeType == typeof(ApiVersionAttribute))
    .SelectMany(z => z.ConstructorArguments.Select(i=>i.Value)));

took {00:00:00.0000626}

I know we're talking about minor differences but still.




回答8:


@ArlanG it helped me, thanks. It works in Asp.Net Core 3.1. There is one small clarification from my point of view. If you want to get more similar behavior like main answer @Alezis method implementation of DocInclusionPredicate() can be:

options.DocInclusionPredicate((version, desc) =>
            {

                if (!desc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
                var versions = methodInfo.DeclaringType
                    .GetCustomAttributes(true)
                    .OfType<ApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions);


                var maps = methodInfo
                    .GetCustomAttributes(true)
                    .OfType<MapToApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions)
                    .ToArray();

                return versions.Any(v => $"v{v.ToString()}" == version)
                       && (!maps.Any() || maps.Any(v => $"v{v.ToString()}" == version));
            });

In this case when you choose a version on SwaggerUi page, it will show only controller methods that are mapped to this version.



来源:https://stackoverflow.com/questions/40929916/how-to-set-up-swashbuckle-vs-microsoft-aspnetcore-mvc-versioning

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!