Skip to content

Add entry-point APIs for OpenAPI support #54789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1788,6 +1788,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10789,6 +10791,22 @@ Global
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|arm64.ActiveCfg = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|arm64.Build.0 = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x64.ActiveCfg = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x64.Build.0 = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x86.ActiveCfg = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Debug|x86.Build.0 = Debug|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|Any CPU.Build.0 = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|arm64.ActiveCfg = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|arm64.Build.0 = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x64.ActiveCfg = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x64.Build.0 = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.ActiveCfg = Release|Any CPU
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -11672,6 +11690,7 @@ Global
{15D08EA7-8C63-45FB-8B4D-C5F8E43B433E} = {05A169C7-4F20-4516-B10A-B13C5649D346}
{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995}
{6DEC24A8-A166-432F-8E3B-58FFCDA92F52} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
Expand Down
1 change: 1 addition & 0 deletions eng/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<LatestPackageReference Include="Microsoft.OpenApi" />
<LatestPackageReference Include="Microsoft.OpenApi.Readers" />
<LatestPackageReference Include="System.Buffers" />
<LatestPackageReference Include="System.CodeDom" />
<LatestPackageReference Include="System.CommandLine.Experimental" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@
<MicrosoftDataSqlClientVersion>4.0.5</MicrosoftDataSqlClientVersion>
<MicrosoftAspNetCoreAppVersion>6.0.0-preview.3.21167.1</MicrosoftAspNetCoreAppVersion>
<MicrosoftOpenApiVersion>1.6.13</MicrosoftOpenApiVersion>
<MicrosoftOpenApiReadersVersion>1.6.13</MicrosoftOpenApiReadersVersion>
<!-- dotnet tool versions (see also auto-updated DotnetEfVersion property). -->
<DotnetDumpVersion>6.0.322601</DotnetDumpVersion>
<DotnetServeVersion>1.10.93</DotnetServeVersion>
Expand Down
5 changes: 3 additions & 2 deletions src/OpenApi/OpenApi.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
"src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
"src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj",
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj"
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj",
"src\\OpenApi\\sample\\Sample.csproj"
]
}
}
}
46 changes: 46 additions & 0 deletions src/OpenApi/sample/EndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

internal static class OpenApiEndpointRouteBuilderExtensions
{
/// <summary>
/// Helper method to render Swagger UI view for testing.
/// </summary>
public static IEndpointConventionBuilder MapSwaggerUi(this IEndpointRouteBuilder endpoints)
{
return endpoints.MapGet("/swagger/{documentName}", (string documentName) => Results.Content($$"""
<html>
<head>
<meta charset="UTF-8">
<title>OpenAPI -- {{documentName}}</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>

<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>

<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/openapi/{{documentName}}.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
})
window.ui = ui
}
</script>
</body>
</html>
""", "text/html")).ExcludeFromDescription();
}
}
32 changes: 32 additions & 0 deletions src/OpenApi/sample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");

var app = builder.Build();

app.MapOpenApi();
if (app.Environment.IsDevelopment())
{
app.MapSwaggerUi();
}

var v1 = app.MapGroup("v1")
.WithMetadata(new ApiExplorerSettingsAttribute { GroupName = "v1" });

var v2 = app.MapGroup("v2")
.WithMetadata(new ApiExplorerSettingsAttribute { GroupName = "v2" });

v1.MapPost("/todos", (Todo todo) => Results.Created($"/todos/{todo.Id}", todo));
v1.MapGet("/todos/{id}", (int id) => new TodoWithDueDate(1, "Test todo", false, DateTime.Now.AddDays(1), DateTime.Now));

app.Run();

public record Todo(int Id, string Title, bool Completed, DateTime CreatedAt);
public record TodoWithDueDate(int Id, string Title, bool Completed, DateTime CreatedAt, DateTime DueDate) : Todo(Id, Title, Completed, CreatedAt);
38 changes: 38 additions & 0 deletions src/OpenApi/sample/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:43164",
"sslPort": 44391
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5051",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7174;http://localhost:5051",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
20 changes: 20 additions & 0 deletions src/OpenApi/sample/Sample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.OpenApi" />
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions src/OpenApi/sample/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions src/OpenApi/sample/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Writers;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// OpenAPI-related methods for <see cref="IEndpointRouteBuilder"/>.
/// </summary>
public static class OpenApiEndpointRouteBuilderExtensions
{
/// <summary>
/// Register an endpoint onto the current application for resolving the OpenAPI document associated
/// with the current application.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="pattern">The route to register the endpoint on. Must include the 'documentName' route parameter.</param>
/// <returns>An <see cref="IEndpointRouteBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = OpenApiConstants.DefaultOpenApiRoute)
{
var options = endpoints.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenApiOptions>>();
return endpoints.MapGet(pattern, async (HttpContext context, string documentName = OpenApiConstants.DefaultDocumentName) =>
{
// It would be ideal to use the `HttpResponseStreamWriter` to
// asynchronously write to the response stream here but Microsoft.OpenApi
// does not yet support async APIs on their writers.
// See https://github.com/microsoft/OpenAPI.NET/issues/421 for more info.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we pushed to get this resolved?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not aggressively -- it's on deck for their v2.1 release. I think if we're able to work around this with the buffered writer we should be fine.

var documentService = context.RequestServices.GetKeyedService<OpenApiDocumentService>(documentName);
if (documentService is null)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
context.Response.ContentType = "text/plain;charset=utf-8";
await context.Response.WriteAsync($"No OpenAPI document with the name '{documentName}' was found.");
}
else
{
var document = await documentService.GetOpenApiDocumentAsync();
var documentOptions = options.Get(documentName);
using var output = MemoryBufferWriter.Get();
using var writer = Utf8BufferTextWriter.Get(output);
try
{
document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
await context.Response.BodyWriter.WriteAsync(output.ToArray());
await context.Response.BodyWriter.FlushAsync();
}
finally
{
MemoryBufferWriter.Return(output);
Utf8BufferTextWriter.Return(writer);
}

}
}).ExcludeFromDescription();
}
}
72 changes: 72 additions & 0 deletions src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.ApiDescriptions;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// OpenAPI-related methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Adds OpenAPI services related to the given document name to the specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
/// <param name="documentName">The name of the OpenAPI document associated with registered services.</param>
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName)
{
ArgumentNullException.ThrowIfNull(services);

return services.AddOpenApi(documentName, _ => { });
}

/// <summary>
/// Adds OpenAPI services related to the given document name to the specified <see cref="IServiceCollection"/> with the specified options.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
/// <param name="documentName">The name of the OpenAPI document associated with registered services.</param>
/// <param name="configureOptions">A delegate used to configure the target <see cref="OpenApiOptions"/>.</param>
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName, Action<OpenApiOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);

services.AddOpenApiCore(documentName);
services.Configure<OpenApiOptions>(documentName, options =>
{
options.DocumentName = documentName;
configureOptions(options);
});
return services;
}

/// <summary>
/// Adds OpenAPI services related to the default document to the specified <see cref="IServiceCollection"/> with the specified options.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
/// <param name="configureOptions">A delegate used to configure the target <see cref="OpenApiOptions"/>.</param>
public static IServiceCollection AddOpenApi(this IServiceCollection services, Action<OpenApiOptions> configureOptions)
=> services.AddOpenApi(OpenApiConstants.DefaultDocumentName, configureOptions);

/// <summary>
/// Adds OpenAPI services related to the default document to the specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to register services onto.</param>
public static IServiceCollection AddOpenApi(this IServiceCollection services)
=> services.AddOpenApi(OpenApiConstants.DefaultDocumentName);

private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName)
{
services.AddEndpointsApiExplorer();
services.AddKeyedSingleton<OpenApiComponentService>(documentName);
services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
// Required for build-time generation
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
// Required to resolve document names for build-time generation
services.AddSingleton(new NamedService<OpenApiDocumentService>(documentName));
return services;
}
}
10 changes: 9 additions & 1 deletion src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;openapi</PackageTags>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>Provides APIs for annotating route handler endpoints in ASP.NET Core with OpenAPI annotations.</Description>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.OpenApi" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<Reference Include="Microsoft.AspNetCore.Routing" />
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.ApiExplorer" />
</ItemGroup>

<ItemGroup>
Expand All @@ -26,4 +29,9 @@
<InternalsVisibleTo Include="Microsoft.AspNetCore.OpenApi.Tests" />
</ItemGroup>

</Project>
<ItemGroup>
<Compile Include="$(RepoRoot)/src/SignalR/common/Shared/Utf8BufferTextWriter.cs" LinkBase="Shared" />
<Compile Include="$(RepoRoot)/src/SignalR/common/Shared/MemoryBufferWriter.cs" LinkBase="Shared" />
</ItemGroup>

</Project>
Loading
Loading