Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Versionierte WebAPI mit API Versioning (HTTP Header) und NSwag
fluxy
myCSharp.de - Member



Dabei seit:
Beiträge: 183
Herkunft: Herdecke

Themenstarter:

Versionierte WebAPI mit API Versioning (HTTP Header) und NSwag

beantworten | zitieren | melden

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:

Fehler
System.InvalidOperationException: The method 'get' on path 'api/test' is registered multiple times.
at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument document, Type controllerType, List`1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver)
at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateApiGroups(OpenApiDocument document, IGrouping`2[] 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();
            });
        }
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.757

beantworten | zitieren | melden

Zitat
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
Zitat
ü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".
private Nachricht | Beiträge des Benutzers
fluxy
myCSharp.de - Member



Dabei seit:
Beiträge: 183
Herkunft: Herdecke

Themenstarter:

beantworten | zitieren | melden

Zitat von Abt
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....
Zitat von Abt
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?
Zitat von Abt
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:
Zitat
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
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.757

beantworten | zitieren | melden

Zitat von fluxy
, 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.
Zitat von fluxy
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.
Zitat von Abt
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).
private Nachricht | Beiträge des Benutzers
fluxy
myCSharp.de - Member



Dabei seit:
Beiträge: 183
Herkunft: Herdecke

Themenstarter:

beantworten | zitieren | melden

Zitat von Abt
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
private Nachricht | Beiträge des Benutzers