Web API Versionierung in .NET 8

Michael F. | Softwareentwickler
01/2025

Web API Versionierung in .NET 8

Web APIs werden mittlerweile an vielen Stellen eingesetzt. In der Regel wählt man einfach das Visual Studio-Template für ein Web-API-Projekt aus, aktiviert ggf. noch Swagger, und muss sich um nichts weiter kümmern.

Doch wie muss die Web-API-Anwendung konfiguriert werden, um neben der Standard-API-Version weitere Versionen bereitzustellen? Die dafür notwendigen Konfigurationsänderungen, die auch bei nur einer API-Version sinnvoll sein können, werden vom Visual Studio-Template leider nicht berücksichtigt. Dies wird im Folgenden exemplarisch mit verschiedenen Beispielen veranschaulicht. Zunächst betrachten wir die Standardkonfiguration, die mit dem Template in Visual Studio erstellt wird.

Standardkonfiguration

Wir erstellen ein neues ASP.NET Core-Web-API-Projekt mit dem Visual Studio-Template. Schauen wir uns an, was in der Program.cs zusätzlich zu einem normalen Web-Projekt hinzugefügt wurde.

Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddControllers();

        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        var app = builder.Build();

        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.MapControllers();
        app.Run();
    }
}

Controller.cs

[ApiController]
[Route("[controller]")]
public class PeopleController : ControllerBase
{
}

Dies ist bereits die gesamte Grundkonfiguration. Nun zu den entsprechenden Erweiterungen.

Konfiguration der Web-API-Anwendung für mehrere API-Versionen

Die Konfiguration erfolgt erneut in der Program.cs. Für die eigentliche API-Versionierung wird ApiVersioning mit Optionen zu den Services hinzugefügt und mit ApiExplorer erweitert.

Program.cs
Der Abschnitt AddApiVersioning(…) muss nach der Anweisung AddEndpointsApiExplorer() eingefügt und die AddSwaggerGen(…) entsprechend erweitert werden.

public class Program
{
  public static void Main(string[] args)
  {
    ...
    builder.Services.AddEndpointsApiExplorer();
    builder.Services
      .AddApiVersioning(setup =>
      {
        setup.DefaultApiVersion = new ApiVersion(3, 0);
        setup.AssumeDefaultVersionWhenUnspecified = true;
        setup.ReportApiVersions = true;
        setup.ApiVersionReader = ApiVersionReader.Combine(
            new UrlSegmentApiVersionReader(),
            //new QueryStringApiVersionReader(),
            new HeaderApiVersionReader("X-Api-Version"));
      })
      .AddApiExplorer(setup =>
      {
        //setup.GroupNameFormat = "'v'VV";    // major version, minor version         (1.0-Alpha -> 1.0)
        //setup.GroupNameFormat = "'v'VVV";   // major, optional minor, patch version (1.0-Alpha -> 1-Alpha)
        setup.GroupNameFormat = "'v'VVVV";    // major, minor, patch version      (1.0-Alpha -> 1.0-Alpha)
        setup.SubstituteApiVersionInUrl = true;
      });

    //builder.Services.AddSwaggerGen();
    builder.Services.AddSwaggerGen(options =>
    {
      options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
      {
          Version = "v1",
          Title = "Demo Web API Versioning",
          Description = "Web API für die Abfrage von Informationen zu Personen",
      });
    });
    ...
  }
}

Hier wird die DefaultApiVersion definiert und angegeben, dass diese Version verwendet werden soll, falls beim Aufruf der Methode keine Version spezifiziert wurde (AssumeDefaultVersionWhenUnspecified). Zusätzlich kann mit dem APIVersionReader festgelegt werden, wie die API-Version übergeben wird – z. B. per UrlSegment, QueryString oder Header-Info. Mit dem ApiExplorer wird außerdem das GroupNameFormat definiert, das bestimmt, wie die API-Version ausgegeben wird. Weitere Varianten sind in der Dokumentation von Visual Studio beschrieben.

Auslagerung der Swagger-Optionen

Für eine bessere Wartbarkeit der Anwendung sollten die Swagger-Optionen in eine separate Klasse ausgelagert werden. Bisher wurde nur eine API-Version definiert, jedoch können mehrere Versionen generisch definiert werden. Dafür sind folgende Änderungen notwendig:

public class Program
{
  public static void Main(string[] args)
  {
    ...
    builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();

    builder.Services.AddSwaggerGen(options =>
    {
      // durch builder.Services.ConfigureOptions() abgedeckt
      //-------------------------------------------------------
      //options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
      //{
      //    Version = "v1",
      //    Title = "Demo Web API Versioning",
      //    Description = "Web API für die Abfrage von Informationen zum Mitglied",
      //});
    });
    ...
  }
}

Die neue SwaggerOptions-Klasse:

public class ConfigureSwaggerOptions : IConfigureNamedOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        this.provider = provider;
    }

    public void Configure(SwaggerGenOptions options)
    {
        // add swagger document for every API version discovered
        foreach (var description in provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
        }
    }

    public void Configure(string name, SwaggerGenOptions options)
    {
        Configure(options);
    }

    private OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
    {
        var info = new OpenApiInfo()
        {
            Title = "Demo Web API Versioning",
            Version = description.ApiVersion.ToString()
        };

        if (description.IsDeprecated)
        {
            info.Description += "This API version has been deprecated.";
        }

        return info;
    }
}

Gleichnamige API-Controller-Methoden pro API-Version

Nun haben wir die Grundlagen für die Verwendung mehrerer API-Versionen geschaffen. Erstellen wir einen Controller, der direkt für mehrere API-Versionen vorbereitet wird.

[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiVersion("3.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class PeopleController : ControllerBase
{
    [Route("FindPeople")]
    [MapToApiVersion("1.0")]
    public PeopleResponse FindV1(PersonRequest request)
    {
        ...
    }

    [Route("FindPeople")]
    [MapToApiVersion("2.0")]
    public PeopleResponse FindV2(PersonRequest request)
    {
        ...
    }

    [Route("FindPeople")]
    [MapToApiVersion("3.0")]
    public PeopleResponse FindV3(PersonRequest request)
    {
        ...
    }
}

Die Attribute für die API-Versionen werden am Controller hinzugefügt ([ApiVersion("x.x")]). Außerdem erweitern wir die Grund-Route des Controllers, sodass die API-Version automatisch in der URL verwendet wird. Da verschiedene API-Versionen einer Methode über dieselbe URL (abgesehen von der API-Version) aufgerufen werden sollen, erhält jede Methode ein eigenes Route-Attribut. Die Methoden werden mit [MapToApiVersion("x.x")] auf die jeweilige API-Version gemappt.

Alternativ könnte für jede API-Version ein separater Controller erstellt werden.

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/People")]
public class PeopleV1Controller : ControllerBase
{
    public PeopleResponse FindPeople(PersonRequest request)
    {
        ...
    }
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/People")]
public class PeopleV2Controller : ControllerBase
{
    public PeopleResponse FindPeople(PersonRequest request)
    {
        ...
    }
}

Beide Varianten liefern nach außen dasselbe Ergebnis. Welche bevorzugt wird, hängt von der Situation ab. Ein Controller pro API-Version kann sinnvoll sein, wenn ältere Versionen später als obsolet gekennzeichnet und entfernt werden sollen. Sollten hingegen einzelne Methoden über mehrere Versionen hinweg unverändert bleiben, ist es möglicherweise praktischer, alle Versionen in einem Controller zu vereinen. Diese Überlegungen werden im nächsten Abschnitt näher beleuchtet.

Eine API-Controller-Methode für mehrere Versionen

Für eine neue API-Version ist nicht zwingend eine neue Methode erforderlich. Bleibt der Code unverändert, kann die bestehende Methode weiterverwendet werden. In diesem Fall muss lediglich an der Controller-Klasse die zusätzliche API-Version per Attribut registriert werden. Natürlich kann die Methode weiterhin auf bestimmte API-Versionen eingeschränkt werden.

[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiVersion("3.0")]
public class PeopleController : ControllerBase
{
    [Route("FindPeople")]
    public PeopleResponse Find(PersonRequest request)
    {
        ...
    }

    [Route("GetPerson")]
    [MapToApiVersion("2.0")]
    [MapToApiVersion("3.0")]
    public PersonResponse Get(PersonRequest request)
    {
        ...
    }
}

Automatische Beschreibung der Methoden

Da Web APIs in der Regel Schnittstellen für externe Systeme bereitstellen, sollten die einzelnen Methoden selbsterklärend sein. Dazu empfiehlt es sich, jede Methode mit einer XML-Summary zu versehen. Diese kann per Compiler-Einstellung in eine XML-Datei zusammengefasst werden, welche anschließend für die Beschreibung der Web-API-Methoden genutzt wird. In Swagger (bzw. der generierten Swagger.json) werden diese Beschreibungen angezeigt, sofern die Anwendung entsprechend konfiguriert ist.

Generierung von XML-Kommentaren

Zunächst wird jede API-Methode mit einer XML-Summary versehen:

public class PeopleController : ControllerBase
{
    /// <summary>
    /// Find the first person that matches the search parameters
    /// </summary>
    /// <param name="request">Filter parameters for the request</param>
    /// <returns>The first person matching the filter parameters</returns>
    public PersonResponse FindPerson(PersonRequest request)
    {
        ...
    }
    ...
}

In der Projektdatei muss folgendes XML-Element hinzugefügt bzw. auf true gesetzt werden:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    ...
    <GenerateDocumentationFile>True</GenerateDocumentationFile>
  </PropertyGroup>

</Project>

Konfiguration der App:

public class Program
{
    Public static void Main(string[] args)
    {
        ...
        builder.Services.AddSwaggerGen(options =>
        {
            ...
            var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
        });
        ...
    }
    ...
}

Das Resultat:

No items found.
Michael F. | Softwareentwickler
Zurück zur Übersicht

Gemeinsam Großes schaffen

Wir freuen uns auf ein kostenloses Erstgespräch mit Ihnen!
Unser Geschäftsführer Tibor Csizmadia und unser Kundenbetreuer Jens Walter stehen Ihnen persönlich zur Verfügung. Profitieren Sie von unserer langjährigen Erfahrung und erhalten Sie eine kompetente Erstberatung in einem unverbindlichen Austausch.
Foto von Tibor

Tibor Csizmadia

Geschäftsführer
Foto von Jens

Jens Walter

Projektmanager
Devware GmbH verpflichtet sich, Ihre Privatsphäre zu schützen. Wir benötigen Ihre Kontaktinformationen, um Sie bezüglich unserer Produkte und Dienstleistungen zu kontaktieren. Mit Klick auf Absenden geben Sie sich damit einverstanden. Weitere Informationen finden Sie unter Datenschutz.
Vielen Dank für Ihre Nachricht!

Wir haben Ihre Anfrage erhalten und melden uns in Kürze bei Ihnen.

Falls Sie in der Zwischenzeit Fragen haben, können Sie uns jederzeit unter Kontaktanfrage@devware.de erreichen.

Wir freuen uns auf die Zusammenarbeit!
Oops! Something went wrong while submitting the form.
KontaktImpressumDatenschutz