1
votes

.NET Core WebAPI version API de secours en cas de version mineure manquante

Après de nombreux essais et lecture d'articles, j'ai décidé de placer mon numéro ici. Ce que je veux, c'est ce qui suit: je travaille sur le contrôle de version d'une application. Un format de version pris en charge par .NET Core (package Microsoft.AspNetCore.Mvc.Versioning ) est Major.Minor, et c'est ce que je souhaite utiliser dans le projet sur lequel je travaille. Ce que je veux, c'est avoir une version de secours au cas où la version mineure ne serait pas spécifiée par le client. J'utilise .NET core 2.2, et j'utilise api-version spécifié dans l'en-tête. La configuration de version de l'API correspondante ressemble à ceci:

        app.MapWhen(x => x.GetRequestedApiVersion().Equals("1") && x.GetRequestedApiVersion().MinorVersion == null, builder =>
        {
            builder.UseMvc(routes =>
            {
                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new {controller = nameof(ValueControllerV11), action = "Collect"});
            });
        });

        app.UseMvc();

J'ai les deux contrôleurs suivants pour chaque version: (les contrôleurs sont simplifiés pour le bien de cette question SO):

[ApiVersion("1")]  
[ApiVersion("1.1")]  
[Route("api/[controller]")]  
public class ValueControllerV11 : Controller  
{  
    [HttpGet(Name = "collect")]
    [HttpRequestPriority]  
    public String Collect()  
    {  
        return "Version 1.1";  
    }  
}

Si le client spécifie api-version = 1.0 , alors ValueControllerV10 est utilisé. Et bien sûr, si le client spécifie api-version = 1.1 , alors ValueControllerV11 est utilisé, comme prévu.

Et maintenant vient mon problème. Si le client spécifie api-version = 1 (donc uniquement la version majeure sans la version mineure), alors ValueControllerV10 est utilisé. C'est parce que ApiVersion.Parse ("1") est égal à ApiVersion.Parse ("1.0") , si je ne me trompe pas. Mais ce que je veux dans ce cas, c'est invoquer la dernière version de la version majeure donnée, qui est la 1.1 dans mon exemple.

Mes tentatives:

D'abord: strong> Spécifier [ApiVersion ("1")] à ValueControllerV11

[AttributeUsage(AttributeTargets.Method)]
public class HttpRequestPriority : Attribute, IActionConstraint
{
    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        var requestedApiVersion = context.RouteContext.HttpContext.GetRequestedApiVersion();

        if (requestedApiVersion.MajorVersion.Equals(1) && !requestedApiVersion.MinorVersion.HasValue)
        {
            return true;
        }

        return false;
    }
}

Cela ne fonctionne pas, cela mène

AmbiguousMatchException: The request matched multiple endpoints

Pour résoudre ce problème, j'ai proposé la deuxième approche:

Deuxième : utiliser IActionConstraint . Pour cela, j'ai suivi ces articles:

J'ai ensuite créé la classe suivante:

    [ApiVersion("1")]  
    [ApiVersion("1.1")]  
    [Route("api/[controller]")]  
    public class ValueControllerV11 : Controller  
    {  
        [HttpGet(Name = "collect")]  
        public String Collect()  
        {  
            return "Version 1.1";  
        }  
    }  

Et utilisé à ValueControllerV11:

[ApiVersion("1.0")]  
[Route("api/[controller]")]  
public class ValueControllerV10 : Controller  
{  
    [HttpGet(Name = "collect")]  
    public String Collect()  
    {  
        return "Version 1.0";  
    }  
} 


[ApiVersion("1.1")]  
[Route("api/[controller]")]  
public class ValueControllerV11 : Controller  
{  
    [HttpGet(Name = "collect")]  
    public String Collect()  
    {  
        return "Version 1.1";  
    }  
}  

Eh bien, cela résout le AmbiguousMatchException , mais remplace le comportement par défaut du package Microsoft.AspNetCore.Mvc.Versioning donc si le client utilise api-version 1.1 , puis elle récupère un 404 Not Found, ce qui est compréhensible selon l'implémentation de HttpRequestPriority

Troisième : Utilisation de MapSpaFallbackRoute dans Startup.cs , sous condition:

    services.AddApiVersioning(options => { 
        options.ReportApiVersions = true;
        options.ApiVersionReader = new HeaderApiVersionReader("api-version");
        options.ErrorResponses = new ApiVersioningErrorResponseProvider();
    });

Cela ne fonctionne pas non plus, aucun impact. Le nom MapSpaFallbackRoute me donne également le sentiment que ce n'est pas ce que je dois utiliser ...

Ma question est donc: Comment puis-je introduire un comportement de repli 'utiliser le dernier' pour le cas où la version mineure n'est pas spécifiée dans api-version ? Merci d'avance!


0 commentaires

3 Réponses :


3
votes

Ceci n'est intrinsèquement pas pris en charge par défaut. Les versions flottantes, les plages, etc. sont contraires aux principes de la gestion des versions d'API. Une version d'API n'implique pas et ne peut pas impliquer de compatibilité descendante. À moins que vous ne contrôliez les deux côtés dans un système fermé, supposer qu'un client peut gérer n'importe quel changement de contrat, même si vous n'ajoutez qu'un seul nouveau membre, est une erreur. En fin de compte, si un client demande 1 / 1.0, c'est ce qu'il devrait obtenir ou le serveur devrait dire que ce n'est pas pris en charge.

Mis à part mon opinion, certaines personnes veulent toujours ce type de comportement. Ce n'est pas particulièrement simple, mais vous devriez être en mesure d'atteindre votre objectif en utilisant un IApiVersionRoutePolicy ou custom endpoint matcher - cela dépend du style de routage que vous utilisez.

Si vous utilisez toujours le ancien routage, cela peut être le plus simple car vous créez simplement une nouvelle stratégie ou étendez la DefaultApiVersionRoutePolicy existante en remplaçant OnSingleMatch et enregistrez-le dans la configuration de votre service. Vous saurez que c'est le scénario que vous recherchez car la version de l'API entrante n'aura pas la version mineure. Vous avez raison de dire que 1 et 1.0 équivaudront à la même chose, mais la version mineure n'est pas fusionnée; par conséquent, ApiVersion.MinorVersion sera null dans ce scénario.

Si vous utilisez Endpoint Routing , vous ' ll faudra remplacer ApiVersionMatcherPolicy . Ce qui suit devrait être proche de ce que vous voulez réaliser:

[ApiController]
[ApiVersion( "2.0" )]
[ApiVersion( "2.1" )]
[ApiVersion( "2.2" )]
[Route( "api/values" )]
public class Values2Controller : ControllerBase
{
    [HttpGet]
    public string Get( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";

    [HttpGet]
    [MapToApiVersion( "2.1" )]
    public string Get2_1( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";

    [HttpGet]
    [MapToApiVersion( "2.2" )]
    public string Get2_2( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";
}

Ensuite, vous devrez mettre à jour la configuration de votre service comme ceci:

// IMPORTANT: must be configured after AddApiVersioning
services.Remove( services.Single( s => s.ImplementationType == typeof( ApiVersionMatcherPolicy ) ) );
services.TryAddEnumerable( ServiceDescriptor.Singleton<MatcherPolicy, MinorApiVersionMatcherPolicy>() );


3 commentaires

merci beaucoup pour votre réponse détaillée, j'en ai beaucoup appris! (et désolé pour la réponse tardive, a eu des jours très occupés ces derniers temps ...). Eh bien, il semble que ce soit la bonne façon de le faire, mais auriez-vous l'amabilité de vérifier ma réponse à cette même question? Je viens de la publier ... Je pourrais notamment trouver une autre façon de résoudre mon problème et je me demande si c'est aussi une solution correcte ou je pourrais me tirer une balle dans le pied avec. Merci encore beaucoup!


L'OP est-il déjà passé à .NET Core 3.1? Je serais intéressé de voir la mise à jour de l'implémentation de ce qui précède car j'ai vraiment du mal à faire fonctionner notre versioning dans 3.1


Toutes nos excuses pour ne pas savoir ce qu'est l'OP , mais la gestion des versions d'API est prise en charge sur .NET Core 3.1 Pour ce scénario particulier, la configuration serait en grande partie la même. Avec Endpoint Routing , vous pouvez probablement utiliser le matcher comme indiqué ci-dessus et simplement l'enregistrer après le contrôle de version de l'API. Vous marqueriez à nouveau un candidat invalide comme valide; cependant, il est probablement plus sûr de simplement remplacer l'implémentation enregistrée.



0
votes

Eh bien, le mérite d'avoir répondu à la question revient à @Chris Martinez, d'un autre côté, je pourrais trouver un autre moyen de résoudre mon problème: J'ai notamment créé une extension pour RouteAttribute , implémentant IActionConstraintFactory:

[ApiVersion("1")]
[ApiVersion("1.1")]
[Route("collect", "1", "1.1")]
public class ValueControllerV11 : Controller
{
    [HttpRequestPriority]
    public String Collect()
    {
        return "Version 1.1";
    }
}

Où le IActionContraint ressemble au suivant:

    public class ApiVersionHeaderConstraint : IActionConstraint
{
    private const bool AllowRouteToBeHit = true;
    private const bool NotAllowRouteToBeHit = false;

    private readonly string[] _allowedApiVersions;

    public ApiVersionHeaderConstraint(params string[] allowedApiVersions)
    {
        _allowedApiVersions = allowedApiVersions;
    }

    public int Order => 0;

    public bool Accept(ActionConstraintContext context)
    {
        var requestApiVersion = GetApiVersionFromRequest(context);

        if (_allowedApiVersions.Contains(requestApiVersion))
        {
            return AllowRouteToBeHit;
        }

        return NotAllowRouteToBeHit;
    }

    private static string GetApiVersionFromRequest(ActionConstraintContext context)
    {
        return context.RouteContext.HttpContext.Request.GetTypedHeaders().Headers[CollectApiVersion.HeaderKey];
    }
}

Ensuite, je peux utiliser ensemble ApiVersionAttribute et mon RouteWithVersionAttribute personnalisé, comme suit:

public class RouteWithVersionAttribute : RouteAttribute, IActionConstraintFactory
{
    private readonly IActionConstraint _constraint;

    public bool IsReusable => true;

    public RouteWithVersionAttribute(string template, params string[] apiVersions) : base(template)
    {
        Order = -10; //Minus value means that the api-version specific route to be processed before other routes
        _constraint = new ApiVersionHeaderConstraint(apiVersions);
    }

    public IActionConstraint CreateInstance(IServiceProvider services)
    {
        return _constraint;
    }
}

Cheers !


3 commentaires

Il semble que cela pourrait fonctionner. Le plus gros inconvénient de cette approche serait que vous devez l'appliquer à tous vos contrôleurs, ce qui n'est probablement pas souhaitable. Vous ne devriez pas avoir à remettre en vente les versions d'API autorisées , car vous les avez toutes déclarées via [ApiVersion] . Dans votre contrainte d'itinéraire, vous pouvez y accéder via context.CurrentCandidate.Action.GetApiVersionModel () , qui est calculé au démarrage de l'application. Vous pouvez ensuite obtenir la version d'API actuellement demandée via context.RouteContext.HttpContext.GetRequestedApiVersion () .


Il ne reste plus qu'à comparer la version d'API demandée à l'ensemble implémenté. Ceci pourrait être fait avec quelque chose comme: var max = model.ImplementedApiVersions.Where (v => v.MajorVersion == requiredVersion.MajorVersion) .Max () puis < code> context.CurrentCandidate.Action.MappingTo (max)! = ApiVersioningMapping.None . Je dois également mentionner que si vous autorisez tout type de gestion de version implicite, cela tombera car il n'y a aucune valeur à obtenir du pipeline de requêtes. J'espère que cela vous aidera et vous donnera quelques idées supplémentaires.


@ChrisMartinez Lovely, bons points, je vais les traiter! Merci encore pour votre aide détaillée, je souhaite le meilleur! ;)



0
votes

Qu'en est-il de l'option CurrentImplementationApiVersionSelector , lors de l'enregistrement du service? voir ici: https://github.com/microsoft/aspnet -api-versioning / wiki / API-Version-Selector

Le CurrentImplementationApiVersionSelector sélectionne la version d'API maximale disponible qui n'a pas de statut de version. Si aucune correspondance n'est trouvée, il revient à la DefaultApiVersion configurée. Par exemple, si les versions "1.0", "2.0" et "3.0-Alpha" sont disponibles, alors "2.0" sera sélectionné car il s'agit de la version d'API la plus élevée, implémentée ou publiée.

services.AddApiVersioning(
    options => options.ApiVersionSelector =
        new CurrentImplementationApiVersionSelector( options ) );


2 commentaires

Bienvenue dans stackoverflow. Les réponses courtes avec un lien externe sont considérées comme de mauvaise qualité car le contenu du lien externe peut changer. La meilleure pratique consiste à inclure les détails clés dans votre réponse.


J'ai juste remarqué cela, mais j'ai pensé que je répondrais. Bien que ce soit une question à poser, c'est une bonne question. La raison pour laquelle vous ne pouvez pas utiliser CurrentImplementationApiVersionSelector ici est que si vous aviez 2.0 , 2.2 et 3.0 , le désir est uniquement pour aller à 2.2 , mais le sélecteur choisira 3.0 . Vous pouvez également implémenter un IApiVersionSelector personnalisé, mais il ne sera pas appelé - de par sa conception - lorsqu'un client spécifie une version explicite. Il n'y a pas de remplacement simple de ce comportement sans changer certains bits de routage principaux.