Created
January 8, 2025 20:41
-
-
Save scrocquesel/56e32c5c2a834f4061f2291c13707d12 to your computer and use it in GitHub Desktop.
dotnet minimal api versioning splitted openapi document
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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