Laden...

Versionierte WebAPI mit API Versioning (HTTP Header) und NSwag

Erstellt von fluxy vor 2 Jahren Letzter Beitrag vor 2 Jahren 595 Views
F
fluxy Themenstarter:in
183 Beiträge seit 2009
vor 2 Jahren
Versionierte WebAPI mit API Versioning (HTTP Header) und NSwag

Grüsst Euch,

ich habe eine API, die ich gerne Versionieren möchte. Das klappt auch ganz gut, allerdings bekomme ich zwei Probleme. Zuvor möchte ich euch aber mitteilen, dass die Versionierung über den HTTP Header eine bewusste Entscheidung war und das nicht geändert werden soll (ich weiss das es dazu reichlich Diskussionen gibt...).

Dazu hier einmal ein Test Controller:


[Route("api/test")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValueController : Controller
{

    [HttpGet]
    [MapToApiVersion("1.0")]
    public ActionResult GetAllValues()
    {
        return Ok("1.0");
    }

    [HttpGet]
    [MapToApiVersion("1.1")]
    public ActionResult GetAllValues_V11()
    {
        return Ok("1.1");
    }
}

Problem Nummer 1 ist: Wenn ich das [MapToApiVersion] über GetAllValues (also Version 1.0) weglasse, dann kann das Swaggerdokument nicht erzeugt werden und ich bekomme eine Exception mit dem Hinweis, dass es für GET mehrere Actions für die selbe Resource gibt:

Fehlermeldung:
System.InvalidOperationException: The method 'get' on path 'api/test' is registered multiple times.
at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument document, Type controllerType, List1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver) at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateApiGroups(OpenApiDocument document, IGrouping2[] apiGroups, OpenApiSchemaResolver schemaResolver)
at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(ApiDescriptionGroupCollection apiDescriptionGroups)
at NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GenerateDocumentAsync(HttpContext context)
at NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext context)
at NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext context)
at NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.Invoke(HttpContext context)
at NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Das zweite Problem ist, dass ich zwar eine Auswahlliste für die Version in SwaggerUi bekomme, aber trotzdem das Header-Feld für die Eingabe der Version (try it out) angezeigt wird und dieses sogar required ist. Das macht aus meiner Sicht keinen Sinn, weil die Version ja durch die Auswahl schon bekannt sein sollte. Kann man das Feld in Swagger UI entfernen?

Anbei der wohl releante Startup code:


  public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddApiVersioning(options =>
            {
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.ReportApiVersions = true;
                options.ApiVersionReader = new HeaderApiVersionReader("service-version");
                options.AssumeDefaultVersionWhenUnspecified = true;
            });
            services.AddVersionedApiExplorer(options =>
            {
                options.ApiVersionParameterSource = new HeaderApiVersionReader("service-version");
            });
            
            services.AddSwaggerDocument(document =>
            {
                document.DocumentName = "1.0";
                document.ApiGroupNames = new[] { "1.0" };
            });

            services.AddSwaggerDocument(document =>
            {
                document.DocumentName = "1.1";
                document.ApiGroupNames = new[] { "1.1" };
            });
        }


 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime)
        {
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseOpenApi();
            app.UseSwaggerUi3();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

16.835 Beiträge seit 2008
vor 2 Jahren

dass die Versionierung über den HTTP Header eine bewusste Entscheidung war

Dann müsst ihr auch mit Workarounds leben, wie hier 🙂

Ich verwende Dein Vorgehen generell nirgends, weil es viele Probleme im Alltag macht. Die Grundidee ist ja, dass Versionen unabhängig voneinander betrieben und weiterentwickelt werden, zB. durch Legacy etc. Alles in eine Applikation zu packen, macht nur in ganz ganz ganz wenigen Mini-Szenarien sinn.
Und selbst in diesen würde man eher pro Version einen eigenen Namespace haben, statt alles über Mappings zu lösen. Ich hab das in freier Wildbahn, ausserhalb von Demos, noch nie gesehen.

Zum Problem: Implizite Versionierung ist im Framework nur dann möglich, wenn Du keinerlei andere Versionsinformationen setzt. Daher knallts.
In Deinem Fall dürfte es aber eh knallen, weil NSwag keine Versionierung über Header oder Query unterstützt. Nur URL Versionierung wird unterstützt.
D.h. Dein Header-Vorgehen mit NSwag so geht eh nicht.

Konzeptionell erstellt man in den aller meisten Fällen eigentlich erst das Swagger/OpenAPI Dokument, und implementiert dann die Anwendung (Contract First Architecture). Auch nen breites Team-Vorgehen geht nicht ohne Contract First.
Es gibt meines Wissens derzeit keinen OpenAPI Runtime Generator für ASP.NET, der Header unterstützt - nutzt halt auch fast niemand, wegen den enorm vielen Cons.
Die einzige Lib, die das meines Wissens zumindest teilweise kann, ist https://github.com/dotnet/aspnet-api-versioning

über den HTTP Header eine bewusste Entscheidung war

Tipp für die Zukunft: halte Dir den "Ich habs Dir gleich gesagt"-Satz schon mal parat 🙂

PS: auch in Deinem Startup sind paar Fehler, zB hast Du UseAuthorization ohne Authentication, sowie passen die Reihenfolgen nicht.
Siehe ASP.NET Core-Middleware
Swagger etc gilt als "Custom Middleware".

F
fluxy Themenstarter:in
183 Beiträge seit 2009
vor 2 Jahren

Ich verwende Dein Vorgehen generell nirgends, weil es viele Probleme im Alltag macht. Die Grundidee ist ja, dass Versionen unabhängig voneinander betrieben und weiterentwickelt werden, zB. durch Legacy etc. Alles in eine Applikation zu packen, macht nur in ganz ganz ganz wenigen Mini-Szenarien sinn.
Und selbst in diesen würde man eher pro Version einen eigenen Namespace haben, statt alles über Mappings zu lösen. Ich hab das in freier Wildbahn, ausserhalb von Demos, noch nie gesehen.

Genau das wollte ich eigentlich verhindern. Ich weiss, dass Versionierung über Urls beziehungsweise über verschiedene Endpunkte sehr pupulär ist. Wenn man sich aber mal mit dem Thema beschäftgt und ein paar Tutorials liest, wird man schnell merken, dass jedes Vorgehen seine Nachteile hat - auch die Vesionierung über Url Segmente. Aber wollen wir uns mal wieder auf das Problem fokussieren....

Zum Problem: Implizite Versionierung ist im Framework nur dann möglich, wenn Du keinerlei andere Versionsinformationen setzt. Daher knallts.

Was meinst Du mit impliziter Versionierung?

In Deinem Fall dürfte es aber eh knallen, weil NSwag keine Versionierung über Header oder Query unterstützt. Nur URL Versionierung wird unterstützt.
D.h. Dein Header-Vorgehen mit NSwag so geht eh nicht.

Bist Du Dir da sicher? Es gibt da in der Tat ein Issue zu, siehe hier. Dort steht folgender Lösungshinweis:

If you use header versioning then you probably want to manually add the global header parameter in PostProcess, apart from that the operations should correctly be reported by ASP.NET Core API explorer and generated in the spec...

Maybe even version filtering works - if the operation versions are correctly reported in API explorer groups.

Hat jemand da vielleicht ein Beispiel oder das schonmal gemacht? Mir ist auch nicht klar, was mit "global header parameter" gemeint ist. Vielleicht kann mir da jemand helfen, dass zu verstehen.

Lieben Gruß,
fluxy

16.835 Beiträge seit 2008
vor 2 Jahren

, dass jedes Vorgehen seine Nachteile hat - auch die Vesionierung über Url Segmente.

Nur um das nich ganz so stehen zu lassen: Ja, bei URL gibts einen einzigen Nachteil: eine Ressource kann mehrere URLs haben, was so nur Query/Header-Versioning erfüllt.
Dieser "größere" Nachteil hat aber nur ganz spezifische Anwendungsfälle, sodass es in 99,999% der APIs nicht relevant ist.
Die Nachteile der Header-Lösungen sind aber für 99,999% der Anwendungen so groß oder gar ein No-Go, dass es dafür absolut null Pros gibt, die das rechtfertigen würden.

Daher muss man die Pros/Cons gerade bei APIs gewichten. Man spricht daher auch i.d.R. davon, dass Header-Versionierungen nur theoretische Vorteile haben, aber keine praktischen. Dazu gibts auch einen Vortrag von Google, mit dem ich pers. auch jeden Kunden bisher überzeugen konnte, dass das keine gute Idee ist. Finde ihn aber gerade nicht. Aber das habt ihr sicher gut auf euren Anwendungsfall evaluiert.
Daher lass ichs nun auch dabei, wollte das mit der Gewichtung aber nicht unerwähnt lassen.

Was meinst Du mit impliziter Versionierung?

Deine Attribute.
ApiVersion und MapToApiVersion sind verschiedene Versionierungsattribute mit entsprechenden Auswirkungen in ASP.NET Core.
Ich habe nie eine Versionsverwaltung in der Applikation, wenn ich nicht dazu gezwungen werden.

Bist Du Dir da sicher?

Ja, und ich kenne auch den/die beiden Issues (Header/Query), wenn dann halt nur mit den nicht offiziell enthaltenen Workarounds.

Ich kenn auch Kunden-Bastellösungen, die das über nen eigenen Middleware-Resolver gemacht haben - alles bisher (von mir) gesehen aber war murks.
Kein Scherz: ich hab das für die Kunden dann so gelöst, dass es nur eine Methode gibt und die API Version als ENum über nen ModelBinder ein MethodenParameter ist, wenn es nur die Resource Versionierung betrifft.
Alles andere immer über mehrere Namespaces, ansonsten ist ein Evolutionsrisiko auch viel zu groß (meiner Meinung nach).

F
fluxy Themenstarter:in
183 Beiträge seit 2009
vor 2 Jahren

Ich kenn auch Kunden-Bastellösungen, die das über nen eigenen Middleware-Resolver gemacht haben - alles bisher (von mir) gesehen aber war murks.

Was sind denn die Workarounds? Ich würde die gerne einmal sehen.... Vielleicht kannst du sie ja mal beispielhaft skizzieren. Also wie du schon sagtest, unterstützt das aspnetcore-api-versioning package ja die Versionierung über alle vier gängigen Varianten (Url, Query Parameter, Request Parameter und Media Types). Gleiches gilt für en api-explorer. Zumindest kann man beim api-explorer über den Parameter ApiVersionParameterSource die Quelle der Versionierung (Url, Query Parameter, Request Parameter und Media Types) angeben.

So wie ich das verstanden habe, ist der API Versioning Explorer die Schnittstelle zwischen der API Versionierung und der OpenApi Schnittstelle (OpenAPI/Swagger). Entsprechend funktioniert die Versionierung über meinen Header ja grundsätzlich schon - und es sind auch Implementierungen vorhanden, die diese Variabilität erlauben. Wenn das Vorgehen so nicht unterstützt werden würde, dann macht die Schnittstelle ja so gar keinen Sinn.

Hast Du diesen "Workaround" denn mal gesehen?

Andere Frage: Siehst Du einen Weg, die Version in Swagger UI (es geht um Swagger UI, nicht um die Generierung der Swagger Dokumente) auszublenden bzw. die Version mit der Auswahl des Swagger Dokuments für die Version zu sezten? Das das HTTP Header Feld mit in der API Definition steht, würde ich ja als richtig ansehen 🙂

@ alle anderen: Auch wenn ich jetzt in die "Du" Form wechsle, weitere Meinungen / Anregungen anderer sind ausdrücklich erwünscht

Viele Grüße,
Fluxy