Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

.NET Core 9 Minimal Versioned API w/OpenAPI #1126

Discussion options

I've been following the examples outlined here for a .NET Core 9 minimal API with OpenAPI, but I'm running into several issues and wasn't sure if this was a bug or if I'm misunderstanding something.

My code:

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

    services.AddProblemDetails();
    services.AddEndpointsApiExplorer();
    services.AddApiVersioning(options =>
    {
        // reporting api versions will return the headers
        // "api-supported-versions" and "api-deprecated-versions"
        options.ReportApiVersions = true;

        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    });
    services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API v1", Version = "v1" });
        options.SwaggerDoc("v4", new OpenApiInfo { Title = $"Test API v4", Version = "v4" });

        options.CustomSchemaIds(type =>
        {
            var segment = type.Namespace?.Split('.').LastOrDefault();
            return $"{segment}_{type.Name}";
        });
    });

    var app = builder.Build();
    app.UseHttpsRedirection();
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedProto
    });
    app.UsePathBase("/api");
    app.UseSwagger();
    app.UseSwaggerUI();

    var health = app.NewVersionedApi("Health");

    // version 1
    var v1 = health.MapGroup("/health")
        .HasApiVersion(new ApiVersion(1.0));

    v1.MapGet("/", () =>
    {
        var responseObject = new V1.HealthResponse
        {
            ApiMode = builder.Configuration.GetValue<string>("API_MODE") ?? "LIVE",
            Environment = builder.Environment.EnvironmentName,
            Status = "Healthy",
            TimeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)
        };

        return Results.Json(responseObject, statusCode: StatusCodes.Status200OK);
    }).Produces<V1.HealthResponse>();


    // version 4
    var v4 = health.MapGroup("/v{version:apiVersion}/health")
        .HasApiVersion(new ApiVersion(4.0));

    v4.MapGet("/", () =>
    {
        var responseObject = new V4.HealthResponse
        {
            ApiMode = builder.Configuration.GetValue<string>("API_MODE") ?? "LIVE",
            Environment = builder.Environment.EnvironmentName,
            HealthStatus = "Healthy",
            TimeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture),
            TraceIdentifier = Guid.NewGuid().ToString()
        };

        return Results.Json(responseObject, statusCode: StatusCodes.Status200OK);
    }).Produces<V4.HealthResponse>();

    app.Run();
}

.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" />
  </ItemGroup>
</Project>
  1. AddApiExplorer has a compiler error that it cannot be resolved
  2. I tried adding the ConfigureSwaggerOptions, but when I do the application throws an exception on build that it cannot resolve IApiVersionDescriptionProvider.
  • "Unable to resolve service for type 'Asp.Versioning.ApiExplorer.IApiVersionDescriptionProvider' while attempting to activate 'TestApi.Swagger.ConfigureSwaggerOptions'."

Removing the ConfigureSwaggerOptions and manually defining the two API versions (as outlined in my code above) resolves the exception for IApiVersionDescriptionProvider, but only one version of the API appears in the dropdown of the generated Swagger page and both endpoints show up with both versions of the response schema:

image

I'm not really sure how to set this up so only the defined endpoints of the specified version show up with the correct schemas?

You must be logged in to vote

Unfortunately, I don't have a great story or example for this situation - yet. It's on my list, but I've just been incredibly backed up. The best example and guidance I have at the moment is to follow how the eShop reference application sets things up. It's less than ideal IMHO, but it seems several people have been following that. As soon as I can get something more streamlined, I'll update the examples.

Replies: 1 comment · 3 replies

Comment options

Which packages do you have referenced? It sounds like you are missing Asp.Versioning.Mvc.ApiExplorer.

You must be logged in to vote
3 replies
@BrandonSchreck
Comment options

Oops...forgot to add the .csproj 🤣...I updated the original post with the .csproj.

I found a blog post that mentioned .NET 9 removed Swagger and was playing around with the Microsoft.AspNetCore.OpenApi and Swashbuckle.AspNetCore nuget packages. It's a bit hacky, but I found a way to setup OpenApi/Swagger with versioning since I cannot access IApiVersionDescriptionProvider to get the defined API versions:

public static void Main(string[] args)
{
    var apiVersions = new[] { "v1", "v4" }; // not ideal...I would rather grab from IApiVersionDescriptionProvider.ApiVersionDescriptions
    
    var builder = WebApplication.CreateBuilder(args);
    var services = builder.Services;
    
    // setup versioning
    services.AddEndpointsApiExplorer();
    services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;

        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    }).EnableApiVersionBinding();
    
    // add each ApiVersion
    foreach (var apiVersion in apiVersions)
    {
        services.AddOpenApiVersion(apiVersion);
    }

    var app = builder.Build();
    app.UseHttpsRedirection();
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedProto
    });
    app.MapOpenApi();

    var health = app.NewVersionedApi("Health");

    // version 1
    var v1 = health.MapGroup("/api/health").HasApiVersion(new ApiVersion(1, 0)).WithGroupName("v1"); // group v1
    v1.MapGet("/", async () =>
    {
        var responseObject = new V1.HealthResponse
        {
            ApiMode = builder.Configuration.GetValue<string>("API_MODE") ?? "LIVE",
            Environment = builder.Environment.EnvironmentName,
            Status = "Healthy",
            TimeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)
        };

        return await Task.FromResult(Results.Json(responseObject, statusCode: StatusCodes.Status200OK));
    }).Produces<V1.HealthResponse>();


    // version 4
    var v4 = health.MapGroup("/api/v{version:apiVersion}/health").HasApiVersion(new ApiVersion(4, 0)).WithGroupName("v4"); // group v4
    v4.MapGet("/", async () =>
    {
    var responseObject = new V4.HealthResponse
    {
        ApiMode = builder.Configuration.GetValue<string>("API_MODE") ?? "LIVE",
        Environment = builder.Environment.EnvironmentName,
        HealthStatus = "Healthy",
        TimeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture),
        TraceIdentifier = Guid.NewGuid().ToString()
    };

    return await Task.FromResult(Results.Json(responseObject, statusCode: StatusCodes.Status200OK));
    }).Produces<V4.HealthResponse>();

    app.UseSwaggerUI(options =>
    {
        // create different Swagger version pages
        foreach (var version in apiVersions)
        {
            options.SwaggerEndpoint($"/openapi/{version}.json", $"Test Api {version}");
        }
    });

    app.Run();
}
    private static void AddOpenApiVersion(this IServiceCollection services, string apiVersion) => services.AddOpenApi(apiVersion, options =>
   {
       // include group name
       options.ShouldInclude = description => description.GroupName == apiVersion;
       
       // change `v{version}` Swagger path to `vX`
       options.AddDocumentTransformer((doc, _, _) =>
       {
           var newPaths = new OpenApiPaths();

           foreach (var (key, value) in doc.Paths)
           {
               var fixedKey = key.Replace("v{version}", apiVersion);
               newPaths[fixedKey] = value;
           }

           doc.Paths = newPaths;
           return Task.CompletedTask;
       });
   });

[IApiVersionDescriptionProvider.ApiVersionDescriptions}(https://github.com/dotnet/aspnet-api-versioning/blob/main/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs#L31C19-L31C69)

@commonsensesoftware
Comment options

Unfortunately, I don't have a great story or example for this situation - yet. It's on my list, but I've just been incredibly backed up. The best example and guidance I have at the moment is to follow how the eShop reference application sets things up. It's less than ideal IMHO, but it seems several people have been following that. As soon as I can get something more streamlined, I'll update the examples.

Answer selected by BrandonSchreck
@BrandonSchreck
Comment options

No worries at all, I'm sure Microsoft has everyone stretched thin like most companies are stretching their workforce. I've read over several of your posts and piecemealed together something that works for me. I appreciate the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.