web-dev-qa-db-fra.com

Comment configurer Swashbuckle vs Microsoft.AspNetCore.Mvc.Versioning

Nous avons webapi de base asp.net. Nous avons ajouté Microsoft.AspNetCore.Mvc.Versioning et Swashbuckle pour avoir une interface utilisateur swagger. Nous avons spécifié des contrôleurs comme ceci:

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

Lorsque nous exécutons swagger ui, nous obtenons la version comme paramètre dans les routes: enter image description here

Comment configurer le "v1" par défaut pour l'itinéraire? Si la version 2 arrive à l'étape comment prendre en charge l'interface utilisateur swagger pour les deux versions?

25
Alezis

Pour le moment, Swashbuckle et Microsoft.AspNetCore.Mvc.Versioning sont amis. Ça marche bien. Je viens de créer un projet de test dans VS2017 et j'ai vérifié comment cela fonctionne.

Incluez d'abord ces deux packages de pépites:

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

Configurez tout dans Startup.cs (lire mes commentaires):

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();
    }

Il y a deux classes qui font l'affaire:

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
            );
    }
}

RemoveVersionFromParameter supprime de l'interface utilisateur de swagger cette zone de texte:

enter image description here

Le ReplaceVersionWithExactValueInPath change cela:

enter image description here

pour ça:

enter image description here

La classe de contrôleur se présente maintenant comme suit:

[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

28
Alezis

Lors de la mise à jour vers .net core 3, j'ai eu l'erreur suivante:

'Impossible de caster un objet de type' System.Collections.Generic.Dictionary`2 [System.String, Microsoft.OpenApi.Models.OpenApiPathItem] 'pour taper' Microsoft.OpenApi.Models.OpenApiPaths '.' =

Corrigé cela en modifiant le code pour:

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;
    }
}
6
Bas Slagter

@Alezis Belle approche, mais si vous utilisez la dernière version de la bibliothèque Microsoft.AspNetCore.Mvc.Versioning (2.3.0), ControllerAttributes() et ActionAttributes() sont obsolètes, vous pouvez mettre à jour DocInclusionPredicate comme suit:

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 le projet github m'aide beaucoup.

5
ArlanG

Si je travaille avec .Net Core 3, j'ai pris la solution de @ Alezis et l'ai mise à jour pour fonctionner avec .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
Yahya Hussein

Au lieu de modifier le document OpenAPI, vous pouvez utiliser la bibliothèque fournie par Microsoft qui ajoute des versions à l'API Explorer. De cette façon, les versions sont fournies avant que Swashbuckle (ou une autre chaîne d'outils) n'en ait besoin et vous permet d'éviter le code personnalisé.

Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

J'ai pu obtenir des versions correctement configurées après avoir ajouté le package et ce bloc de 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;
    }
);
2
kraihn

J'ai trouvé qu'en utilisant la méthode mise en évidence par ArlanG a pris {00:00:00.0001905} pour terminer lors de l'exécution

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

a pris {00:00:00.0000626}

Je sais que nous parlons de différences mineures mais quand même.

1
Tubs

@ArlanG cela m'a aidé, merci. Il fonctionne dans Asp.Net Core 3.1. Il y a une petite précision de mon point de vue. Si vous souhaitez obtenir un comportement plus similaire, comme la réponse principale, l'implémentation de la méthode @Alezis de DocInclusionPredicate () peut être:

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));
            });

Dans ce cas, lorsque vous choisissez une version sur la page SwaggerUi, elle affichera uniquement les méthodes de contrôleur mappées à cette version.

0
Komelkov Dmitriy