Skip to content

Commit cd344fa

Browse files
captainsafiamartincostelloRick-Anderson
committed
Add entry-point APIs for OpenAPI support (#54789)
* Add entry-point APIs for OpenAPI support * Apply suggestions from code review Co-authored-by: Martin Costello <[email protected]> Co-authored-by: Rick Anderson <[email protected]> * Update docs and pass cancellation token to WriteAsync * Address feedback * Remove trailing comma in slnf * Address more feedback * Seal OpenApiOptions --------- Co-authored-by: Martin Costello <[email protected]> Co-authored-by: Rick Anderson <[email protected]>
1 parent b6e09bd commit cd344fa

29 files changed

+960
-3
lines changed

AspNetCore.sln

+19
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack
17881788
EndProject
17891789
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}"
17901790
EndProject
1791+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}"
1792+
EndProject
17911793
Global
17921794
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17931795
Debug|Any CPU = Debug|Any CPU
@@ -10789,6 +10791,22 @@ Global
1078910791
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU
1079010792
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU
1079110793
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU
10794+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10795+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|Any CPU.Build.0 = Debug|Any CPU
10796+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|arm64.ActiveCfg = Debug|Any CPU
10797+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|arm64.Build.0 = Debug|Any CPU
10798+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x64.ActiveCfg = Debug|Any CPU
10799+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x64.Build.0 = Debug|Any CPU
10800+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x86.ActiveCfg = Debug|Any CPU
10801+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x86.Build.0 = Debug|Any CPU
10802+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|Any CPU.ActiveCfg = Release|Any CPU
10803+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|Any CPU.Build.0 = Release|Any CPU
10804+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|arm64.ActiveCfg = Release|Any CPU
10805+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|arm64.Build.0 = Release|Any CPU
10806+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x64.ActiveCfg = Release|Any CPU
10807+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x64.Build.0 = Release|Any CPU
10808+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.ActiveCfg = Release|Any CPU
10809+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.Build.0 = Release|Any CPU
1079210810
EndGlobalSection
1079310811
GlobalSection(SolutionProperties) = preSolution
1079410812
HideSolutionNode = FALSE
@@ -11672,6 +11690,7 @@ Global
1167211690
{15D08EA7-8C63-45FB-8B4D-C5F8E43B433E} = {05A169C7-4F20-4516-B10A-B13C5649D346}
1167311691
{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
1167411692
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995}
11693+
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}
1167511694
EndGlobalSection
1167611695
GlobalSection(ExtensibilityGlobals) = postSolution
1167711696
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/Dependencies.props

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ and are generated based on the last package release.
6969
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
7070
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
7171
<LatestPackageReference Include="Microsoft.OpenApi" />
72+
<LatestPackageReference Include="Microsoft.OpenApi.Readers" />
7273
<LatestPackageReference Include="System.Buffers" />
7374
<LatestPackageReference Include="System.CodeDom" />
7475
<LatestPackageReference Include="System.CommandLine.Experimental" />

eng/Versions.props

+1
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@
335335
<MicrosoftDataSqlClientVersion>4.0.5</MicrosoftDataSqlClientVersion>
336336
<MicrosoftAspNetCoreAppVersion>6.0.0-preview.3.21167.1</MicrosoftAspNetCoreAppVersion>
337337
<MicrosoftOpenApiVersion>1.6.13</MicrosoftOpenApiVersion>
338+
<MicrosoftOpenApiReadersVersion>1.6.13</MicrosoftOpenApiReadersVersion>
338339
<!-- dotnet tool versions (see also auto-updated DotnetEfVersion property). -->
339340
<DotnetDumpVersion>6.0.322601</DotnetDumpVersion>
340341
<DotnetServeVersion>1.10.93</DotnetServeVersion>

src/OpenApi/OpenApi.slnf

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
99
"src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
1010
"src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj",
11-
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj"
11+
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj",
12+
"src\\OpenApi\\sample\\Sample.csproj"
1213
]
1314
}
14-
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
internal static class OpenApiEndpointRouteBuilderExtensions
5+
{
6+
/// <summary>
7+
/// Helper method to render Swagger UI view for testing.
8+
/// </summary>
9+
public static IEndpointConventionBuilder MapSwaggerUi(this IEndpointRouteBuilder endpoints)
10+
{
11+
return endpoints.MapGet("/swagger/{documentName}", (string documentName) => Results.Content($$"""
12+
<html>
13+
<head>
14+
<meta charset="UTF-8">
15+
<title>OpenAPI -- {{documentName}}</title>
16+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
17+
</head>
18+
<body>
19+
<div id="swagger-ui"></div>
20+
21+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
22+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
23+
24+
<script>
25+
window.onload = function() {
26+
const ui = SwaggerUIBundle({
27+
url: "/openapi/{{documentName}}.json",
28+
dom_id: '#swagger-ui',
29+
deepLinking: true,
30+
presets: [
31+
SwaggerUIBundle.presets.apis,
32+
SwaggerUIStandalonePreset
33+
],
34+
plugins: [
35+
SwaggerUIBundle.plugins.DownloadUrl
36+
],
37+
layout: "StandaloneLayout",
38+
})
39+
window.ui = ui
40+
}
41+
</script>
42+
</body>
43+
</html>
44+
""", "text/html")).ExcludeFromDescription();
45+
}
46+
}

src/OpenApi/sample/Program.cs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
builder.Services.AddOpenApi("v1");
10+
builder.Services.AddOpenApi("v2");
11+
12+
var app = builder.Build();
13+
14+
app.MapOpenApi();
15+
if (app.Environment.IsDevelopment())
16+
{
17+
app.MapSwaggerUi();
18+
}
19+
20+
var v1 = app.MapGroup("v1")
21+
.WithMetadata(new ApiExplorerSettingsAttribute { GroupName = "v1" });
22+
23+
var v2 = app.MapGroup("v2")
24+
.WithMetadata(new ApiExplorerSettingsAttribute { GroupName = "v2" });
25+
26+
v1.MapPost("/todos", (Todo todo) => Results.Created($"/todos/{todo.Id}", todo));
27+
v1.MapGet("/todos/{id}", (int id) => new TodoWithDueDate(1, "Test todo", false, DateTime.Now.AddDays(1), DateTime.Now));
28+
29+
app.Run();
30+
31+
public record Todo(int Id, string Title, bool Completed, DateTime CreatedAt);
32+
public record TodoWithDueDate(int Id, string Title, bool Completed, DateTime CreatedAt, DateTime DueDate) : Todo(Id, Title, Completed, CreatedAt);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "http://localhost:43164",
8+
"sslPort": 44391
9+
}
10+
},
11+
"profiles": {
12+
"http": {
13+
"commandName": "Project",
14+
"dotnetRunMessages": true,
15+
"launchBrowser": true,
16+
"applicationUrl": "http://localhost:5051",
17+
"environmentVariables": {
18+
"ASPNETCORE_ENVIRONMENT": "Development"
19+
}
20+
},
21+
"https": {
22+
"commandName": "Project",
23+
"dotnetRunMessages": true,
24+
"launchBrowser": true,
25+
"applicationUrl": "https://localhost:7174;http://localhost:5051",
26+
"environmentVariables": {
27+
"ASPNETCORE_ENVIRONMENT": "Development"
28+
}
29+
},
30+
"IIS Express": {
31+
"commandName": "IISExpress",
32+
"launchBrowser": true,
33+
"environmentVariables": {
34+
"ASPNETCORE_ENVIRONMENT": "Development"
35+
}
36+
}
37+
}
38+
}

src/OpenApi/sample/Sample.csproj

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Reference Include="Microsoft.AspNetCore" />
11+
<Reference Include="Microsoft.AspNetCore.Hosting" />
12+
<Reference Include="Microsoft.AspNetCore.OpenApi" />
13+
<Reference Include="Microsoft.AspNetCore.Http" />
14+
<Reference Include="Microsoft.AspNetCore.Http.Results" />
15+
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
16+
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
17+
<Reference Include="Microsoft.AspNetCore.Mvc" />
18+
</ItemGroup>
19+
20+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}

src/OpenApi/sample/appsettings.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Internal;
7+
using Microsoft.AspNetCore.OpenApi;
8+
using Microsoft.AspNetCore.Routing;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
11+
using Microsoft.OpenApi.Extensions;
12+
using Microsoft.OpenApi.Writers;
13+
14+
namespace Microsoft.AspNetCore.Builder;
15+
16+
/// <summary>
17+
/// OpenAPI-related methods for <see cref="IEndpointRouteBuilder"/>.
18+
/// </summary>
19+
public static class OpenApiEndpointRouteBuilderExtensions
20+
{
21+
/// <summary>
22+
/// Register an endpoint onto the current application for resolving the OpenAPI document associated
23+
/// with the current application.
24+
/// </summary>
25+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
26+
/// <param name="pattern">The route to register the endpoint on. Must include the 'documentName' route parameter.</param>
27+
/// <returns>An <see cref="IEndpointRouteBuilder"/> that can be used to further customize the endpoint.</returns>
28+
public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = OpenApiConstants.DefaultOpenApiRoute)
29+
{
30+
var options = endpoints.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenApiOptions>>();
31+
return endpoints.MapGet(pattern, async (HttpContext context, string documentName = OpenApiConstants.DefaultDocumentName) =>
32+
{
33+
// It would be ideal to use the `HttpResponseStreamWriter` to
34+
// asynchronously write to the response stream here but Microsoft.OpenApi
35+
// does not yet support async APIs on their writers.
36+
// See https://github.com/microsoft/OpenAPI.NET/issues/421 for more info.
37+
var documentService = context.RequestServices.GetKeyedService<OpenApiDocumentService>(documentName);
38+
if (documentService is null)
39+
{
40+
context.Response.StatusCode = StatusCodes.Status404NotFound;
41+
context.Response.ContentType = "text/plain;charset=utf-8";
42+
await context.Response.WriteAsync($"No OpenAPI document with the name '{documentName}' was found.");
43+
}
44+
else
45+
{
46+
var document = await documentService.GetOpenApiDocumentAsync();
47+
var documentOptions = options.Get(documentName);
48+
using var output = MemoryBufferWriter.Get();
49+
using var writer = Utf8BufferTextWriter.Get(output);
50+
try
51+
{
52+
document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
53+
await context.Response.BodyWriter.WriteAsync(output.ToArray());
54+
await context.Response.BodyWriter.FlushAsync();
55+
}
56+
finally
57+
{
58+
MemoryBufferWriter.Return(output);
59+
Utf8BufferTextWriter.Return(writer);
60+
}
61+
62+
}
63+
}).ExcludeFromDescription();
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.OpenApi;
5+
using Microsoft.Extensions.ApiDescriptions;
6+
7+
namespace Microsoft.Extensions.DependencyInjection;
8+
9+
/// <summary>
10+
/// OpenAPI-related methods for <see cref="IServiceCollection"/>.
11+
/// </summary>
12+
public static class OpenApiServiceCollectionExtensions
13+
{
14+
/// <summary>
15+
/// Adds OpenAPI services related to the given document name to the specified <see cref="IServiceCollection"/>.
16+
/// </summary>
17+
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
18+
/// <param name="documentName">The name of the OpenAPI document associated with registered services.</param>
19+
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName)
20+
{
21+
ArgumentNullException.ThrowIfNull(services);
22+
23+
return services.AddOpenApi(documentName, _ => { });
24+
}
25+
26+
/// <summary>
27+
/// Adds OpenAPI services related to the given document name to the specified <see cref="IServiceCollection"/> with the specified options.
28+
/// </summary>
29+
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
30+
/// <param name="documentName">The name of the OpenAPI document associated with registered services.</param>
31+
/// <param name="configureOptions">A delegate used to configure the target <see cref="OpenApiOptions"/>.</param>
32+
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName, Action<OpenApiOptions> configureOptions)
33+
{
34+
ArgumentNullException.ThrowIfNull(services);
35+
ArgumentNullException.ThrowIfNull(configureOptions);
36+
37+
services.AddOpenApiCore(documentName);
38+
services.Configure<OpenApiOptions>(documentName, options =>
39+
{
40+
options.DocumentName = documentName;
41+
configureOptions(options);
42+
});
43+
return services;
44+
}
45+
46+
/// <summary>
47+
/// Adds OpenAPI services related to the default document to the specified <see cref="IServiceCollection"/> with the specified options.
48+
/// </summary>
49+
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
50+
/// <param name="configureOptions">A delegate used to configure the target <see cref="OpenApiOptions"/>.</param>
51+
public static IServiceCollection AddOpenApi(this IServiceCollection services, Action<OpenApiOptions> configureOptions)
52+
=> services.AddOpenApi(OpenApiConstants.DefaultDocumentName, configureOptions);
53+
54+
/// <summary>
55+
/// Adds OpenAPI services related to the default document to the specified <see cref="IServiceCollection"/>.
56+
/// </summary>
57+
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
58+
public static IServiceCollection AddOpenApi(this IServiceCollection services)
59+
=> services.AddOpenApi(OpenApiConstants.DefaultDocumentName);
60+
61+
private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName)
62+
{
63+
services.AddEndpointsApiExplorer();
64+
services.AddKeyedSingleton<OpenApiComponentService>(documentName);
65+
services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
66+
// Required for build-time generation
67+
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
68+
// Required to resolve document names for build-time generation
69+
services.AddSingleton(new NamedService<OpenApiDocumentService>(documentName));
70+
return services;
71+
}
72+
}

src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<PackageTags>aspnetcore;openapi</PackageTags>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
78
<Description>Provides APIs for annotating route handler endpoints in ASP.NET Core with OpenAPI annotations.</Description>
89
</PropertyGroup>
910

1011
<ItemGroup>
1112
<Reference Include="Microsoft.OpenApi" />
1213
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
14+
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1315
<Reference Include="Microsoft.AspNetCore.Routing" />
1416
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
17+
<Reference Include="Microsoft.AspNetCore.Mvc.ApiExplorer" />
1518
</ItemGroup>
1619

1720
<ItemGroup>
@@ -26,4 +29,9 @@
2629
<InternalsVisibleTo Include="Microsoft.AspNetCore.OpenApi.Tests" />
2730
</ItemGroup>
2831

29-
</Project>
32+
<ItemGroup>
33+
<Compile Include="$(RepoRoot)/src/SignalR/common/Shared/Utf8BufferTextWriter.cs" LinkBase="Shared" />
34+
<Compile Include="$(RepoRoot)/src/SignalR/common/Shared/MemoryBufferWriter.cs" LinkBase="Shared" />
35+
</ItemGroup>
36+
37+
</Project>

0 commit comments

Comments
 (0)