Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/OpenApi/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext.OperationDescriptions.get -> System.Collections.Generic.IReadOnlyDictionary<Microsoft.OpenApi.OpenApiOperation!, Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!>!
Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext.OperationDescriptions.init -> void
20 changes: 15 additions & 5 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
Info = GetOpenApiInfo(),
Servers = GetOpenApiServers(httpRequest)
};
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
var operationDescriptions = new Dictionary<OpenApiOperation, ApiDescription>();
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, operationDescriptions, cancellationToken);
try
{
await ApplyTransformersAsync(document, scopedServiceProvider, schemaTransformers, cancellationToken);
await ApplyTransformersAsync(document, scopedServiceProvider, schemaTransformers, operationDescriptions, cancellationToken);
}

finally
Expand All @@ -97,15 +98,16 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
return document;
}

private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, IReadOnlyDictionary<OpenApiOperation, ApiDescription> operationDescriptions, CancellationToken cancellationToken)
{
var documentTransformerContext = new OpenApiDocumentTransformerContext
{
DocumentName = documentName,
ApplicationServices = scopedServiceProvider,
DescriptionGroups = apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items,
Document = document,
SchemaTransformers = schemaTransformers
SchemaTransformers = schemaTransformers,
OperationDescriptions = operationDescriptions
};
// Use index-based for loop to avoid allocating an enumerator with a foreach.
for (var i = 0; i < _options.DocumentTransformers.Count; i++)
Expand Down Expand Up @@ -246,6 +248,7 @@ private async Task<OpenApiPaths> GetOpenApiPathsAsync(
IServiceProvider scopedServiceProvider,
IOpenApiOperationTransformer[] operationTransformers,
IOpenApiSchemaTransformer[] schemaTransformers,
Dictionary<OpenApiOperation, ApiDescription> operationDescriptions,
CancellationToken cancellationToken)
{
var descriptionsByPath = apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items
Expand All @@ -256,7 +259,7 @@ private async Task<OpenApiPaths> GetOpenApiPathsAsync(
foreach (var descriptions in descriptionsByPath)
{
Debug.Assert(descriptions.Key != null, "Relative path mapped to OpenApiPath key cannot be null.");
var operations = await GetOperationsAsync(descriptions, document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
var operations = await GetOperationsAsync(descriptions, document, scopedServiceProvider, operationTransformers, schemaTransformers, operationDescriptions, cancellationToken);
if (operations.Count > 0)
{
paths.Add(descriptions.Key, new OpenApiPathItem { Operations = operations });
Expand All @@ -272,6 +275,7 @@ private async Task<Dictionary<HttpMethod, OpenApiOperation>> GetOperationsAsync(
IServiceProvider scopedServiceProvider,
IOpenApiOperationTransformer[] operationTransformers,
IOpenApiSchemaTransformer[] schemaTransformers,
Dictionary<OpenApiOperation, ApiDescription> operationDescriptions,
CancellationToken cancellationToken)
{
var operations = new Dictionary<HttpMethod, OpenApiOperation>();
Expand All @@ -298,7 +302,13 @@ private async Task<Dictionary<HttpMethod, OpenApiOperation>> GetOperationsAsync(
continue;
}

if (operations.TryGetValue(method, out var existingOperation))
{
operationDescriptions.Remove(existingOperation);
}

operations[method] = operation;
operationDescriptions[operation] = description;

// Use index-based for loop to avoid allocating an enumerator with a foreach.
for (var i = 0; i < operationTransformers.Length; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public sealed class OpenApiDocumentTransformerContext
/// </summary>
public required IServiceProvider ApplicationServices { get; init; }

/// <summary>
/// Gets a map of <see cref="OpenApiOperation"/> instances to their associated <see cref="ApiDescription"/>.
/// </summary>
public required IReadOnlyDictionary<OpenApiOperation, ApiDescription> OperationDescriptions { get; init; }
Comment thread
vvimjam marked this conversation as resolved.

internal IOpenApiSchemaTransformer[] SchemaTransformers { get; init; } = [];

// Internal because we expect users to interact with the `Document` provided in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,89 @@ internal void TestMethod() { }

internal static int InstantiationCount = 0;
}

[Fact]
public async Task DocumentTransformer_CanAccessOperationDescriptions()
{
var builder = CreateBuilder();

builder.MapGet("/todo", () => "todo");
builder.MapGet("/user", () => "user");
builder.MapPost("/todo", () => { });

var expectedPaths = new HashSet<string>(StringComparer.Ordinal)
{
"/todo",
"/user"
};

var mappedOperationsCount = 0;
var options = new OpenApiOptions();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
Assert.Equal(3, context.OperationDescriptions.Count);

foreach (var (operation, apiDescription) in context.OperationDescriptions)
{
var documentPath = "/" + apiDescription.RelativePath;
Assert.Contains(documentPath, expectedPaths);

Assert.True(document.Paths.TryGetValue(documentPath, out var pathItem));
Assert.Contains(operation, pathItem.Operations.Values);
mappedOperationsCount++;
}

return Task.CompletedTask;
});

await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Equal(2, document.Paths.Count);
});

Assert.Equal(3, mappedOperationsCount);
}

[Fact]
public async Task DocumentTransformer_CanAccessEndpointMetadataViaOperationDescriptions()
{
var builder = CreateBuilder();

builder.MapGet("/public", () => "public")
.WithMetadata(new OperationMarker());
builder.MapGet("/secret", () => "secret");

var options = new OpenApiOptions();
var publicEndpointHasMarker = false;
var secretEndpointHasMarker = false;

options.AddDocumentTransformer((document, context, cancellationToken) =>
{
Assert.Equal(2, context.OperationDescriptions.Count);

foreach (var (_, apiDescription) in context.OperationDescriptions)
{
var hasMarker = apiDescription.ActionDescriptor.EndpointMetadata
.Any(m => m is OperationMarker);

if (apiDescription.RelativePath == "public")
{
publicEndpointHasMarker = hasMarker;
}
else if (apiDescription.RelativePath == "secret")
{
secretEndpointHasMarker = hasMarker;
}
}

return Task.CompletedTask;
});

await VerifyOpenApiDocument(builder, options, document => { });

Assert.True(publicEndpointHasMarker);
Assert.False(secretEndpointHasMarker);
}

private sealed class OperationMarker;
}
Loading