Skip to content

Instantly share code, notes, and snippets.

@scrocquesel
Created January 8, 2025 20:41
Show Gist options
  • Save scrocquesel/56e32c5c2a834f4061f2291c13707d12 to your computer and use it in GitHub Desktop.
Save scrocquesel/56e32c5c2a834f4061f2291c13707d12 to your computer and use it in GitHub Desktop.
dotnet minimal api versioning splitted openapi document
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.74" />
</ItemGroup>
</Project>
@openapi_HostAddress = http://localhost:5154
GET {{openapi_HostAddress}}/v1/weatherforecast/
Accept: application/json
###
GET {{openapi_HostAddress}}/openapi/v2.json
###
GET {{openapi_HostAddress}}/openapi/v1.json
using System.Text;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Primitives;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
internal static class OpenApiOptionsExtensions
{
public static OpenApiOptions ApplyApiVersionInfo(this OpenApiOptions options, string title, string description)
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
var versionedDescriptionProvider = context.ApplicationServices.GetService<IApiVersionDescriptionProvider>();
var apiDescription = versionedDescriptionProvider?.ApiVersionDescriptions
.SingleOrDefault(description => description.GroupName == context.DocumentName);
if (apiDescription is null)
{
return Task.CompletedTask;
}
document.Info.Version = apiDescription.ApiVersion.ToString();
document.Info.Title = title;
document.Info.Description = BuildDescription(apiDescription, description);
// SubstituteApiVersionInUrl helps us replace the version in the URL
// we just need to remove the version from the path
var paths = new OpenApiPaths();
var segment = $"/{apiDescription.GroupName}/";
foreach(var path in document.Paths){
paths.Add(path.Key.Replace(segment, "/"), path.Value);
}
document.Paths = paths;
return Task.CompletedTask;
});
return options;
}
private static string BuildDescription(ApiVersionDescription api, string description)
{
var text = new StringBuilder(description);
if (api.IsDeprecated)
{
if (text.Length > 0)
{
if (text[^1] != '.')
{
text.Append('.');
}
text.Append(' ');
}
text.Append("This API version has been deprecated.");
}
if (api.SunsetPolicy is { } policy)
{
if (policy.Date is { } when)
{
if (text.Length > 0)
{
text.Append(' ');
}
text.Append("The API will be sunset on ")
.Append(when.Date.ToShortDateString())
.Append('.');
}
if (policy.HasLinks)
{
text.AppendLine();
var rendered = false;
foreach (var link in policy.Links.Where(l => l.Type == "text/html"))
{
if (!rendered)
{
text.Append("<h4>Links</h4><ul>");
rendered = true;
}
text.Append("<li><a href=\"");
text.Append(link.LinkTarget.OriginalString);
text.Append("\">");
text.Append(
StringSegment.IsNullOrEmpty(link.Title)
? link.LinkTarget.OriginalString
: link.Title.ToString());
text.Append("</a></li>");
}
if (rendered)
{
text.Append("</ul>");
}
}
}
return text.ToString();
}
public static OpenApiOptions ApplyApiVersionDescription(this OpenApiOptions options)
{
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
// Find parameter named "api-version" and add a description to it
var apiVersionParameter = operation.Parameters.FirstOrDefault(p => p.Name == "api-version");
if (apiVersionParameter is not null)
{
apiVersionParameter.Description = "The API version, in the format 'major.minor'.";
apiVersionParameter.Schema.Example = new OpenApiString("1.0");
}
return Task.CompletedTask;
});
return options;
}
}
using Asp.Versioning;
using Microsoft.OpenApi.Models;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
var withApiVersioning = builder.Services.AddApiVersioning(setup =>
{
setup.ReportApiVersions = true;
setup.AssumeDefaultVersionWhenUnspecified = true;
setup.DefaultApiVersion = new ApiVersion(2, 0);
setup.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(
options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
}).EnableApiVersionBinding();
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi("v1", options => {
options.ApplyApiVersionInfo("weatherforecast API", "A simple example ASP.NET Core web API");
//options.ApplyApiVersionDescription();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Servers = [new OpenApiServer { Url = "/v1" }];
return Task.CompletedTask;
});
});
builder.Services.AddOpenApi("v2", options => {
options.ApplyApiVersionInfo("weatherforecast API", "A simple example ASP.NET Core web API");
//options.ApplyApiVersionDescription();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Servers = [new OpenApiServer { Url = "/v2" }];
return Task.CompletedTask;
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(2, 0))
.HasDeprecatedApiVersion(new ApiVersion(1, 0))
.Build();
app.MapGet("/v{version:apiVersion}/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecastV1")
.WithApiVersionSet(versionSet)
.MapToApiVersion(new ApiVersion(1, 0));
app.MapGet("/v{version:apiVersion}/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast2
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55)
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecastV2")
.WithApiVersionSet(versionSet)
.MapToApiVersion(new ApiVersion(2, 0));
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
record WeatherForecast2(DateOnly Date, int TemperatureC)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment