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
47 changes: 41 additions & 6 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
Info = GetOpenApiInfo(),
Servers = GetOpenApiServers(httpRequest)
};
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
try
{
await ApplyTransformersAsync(document, scopedServiceProvider, schemaTransformers, cancellationToken);
await ApplyTransformersAndGeneratePathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
}

finally
Expand All @@ -101,7 +100,12 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
return document;
}

private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProvider scopedServiceProvider, IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken)
private async Task ApplyTransformersAndGeneratePathsAsync(
OpenApiDocument document,
IServiceProvider scopedServiceProvider,
IOpenApiOperationTransformer[] operationTransformers,
IOpenApiSchemaTransformer[] schemaTransformers,
CancellationToken cancellationToken)
{
var documentTransformerContext = new OpenApiDocumentTransformerContext
{
Expand All @@ -111,11 +115,42 @@ private async Task ApplyTransformersAsync(OpenApiDocument document, IServiceProv
Document = document,
SchemaTransformers = schemaTransformers
};
var pathsGenerated = false;
var hasGenerationTransformers = false;
for (var i = 0; i < _options.Transformers.Count; i++)
{
if (_options.Transformers[i].Kind is not OpenApiTransformerKind.Document)
{
hasGenerationTransformers = true;
break;
}
}
Comment on lines +119 to +127

if (!hasGenerationTransformers)
{
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
pathsGenerated = true;
}

// Use index-based for loop to avoid allocating an enumerator with a foreach.
for (var i = 0; i < _options.DocumentTransformers.Count; i++)
for (var i = 0; i < _options.Transformers.Count; i++)
{
var transformerRegistration = _options.Transformers[i];
if (transformerRegistration.Kind is OpenApiTransformerKind.Document)
{
var documentTransformer = (IOpenApiDocumentTransformer)transformerRegistration.Transformer;
await documentTransformer.TransformAsync(document, documentTransformerContext, cancellationToken);
}
else if (!pathsGenerated)
{
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
pathsGenerated = true;
}
}

if (!pathsGenerated)
{
var transformer = _options.DocumentTransformers[i];
await transformer.TransformAsync(document, documentTransformerContext, cancellationToken);
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
}
}

Expand Down
37 changes: 31 additions & 6 deletions src/OpenApi/src/Services/OpenApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.OpenApi;
/// </summary>
public sealed class OpenApiOptions
{
internal readonly List<OpenApiTransformerRegistration> Transformers = [];
internal readonly List<IOpenApiDocumentTransformer> DocumentTransformers = [];
internal readonly List<IOpenApiOperationTransformer> OperationTransformers = [];
internal readonly List<IOpenApiSchemaTransformer> SchemaTransformers = [];
Expand Down Expand Up @@ -64,7 +65,9 @@ public OpenApiOptions()
public OpenApiOptions AddDocumentTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiDocumentTransformer
{
DocumentTransformers.Add(new TypeBasedOpenApiDocumentTransformer(typeof(TTransformerType)));
var transformer = new TypeBasedOpenApiDocumentTransformer(typeof(TTransformerType));
DocumentTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Document, transformer));
return this;
}

Expand All @@ -78,6 +81,7 @@ public OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transfo
ArgumentNullException.ThrowIfNull(transformer);

DocumentTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Document, transformer));
return this;
}

Expand All @@ -90,7 +94,9 @@ public OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocume
{
ArgumentNullException.ThrowIfNull(transformer);

DocumentTransformers.Add(new DelegateOpenApiDocumentTransformer(transformer));
var transformerInstance = new DelegateOpenApiDocumentTransformer(transformer);
DocumentTransformers.Add(transformerInstance);
Transformers.Add(new(OpenApiTransformerKind.Document, transformerInstance));
return this;
}

Expand All @@ -102,7 +108,9 @@ public OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocume
public OpenApiOptions AddOperationTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiOperationTransformer
{
OperationTransformers.Add(new TypeBasedOpenApiOperationTransformer(typeof(TTransformerType)));
var transformer = new TypeBasedOpenApiOperationTransformer(typeof(TTransformerType));
OperationTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Operation, transformer));
return this;
}

Expand All @@ -116,6 +124,7 @@ public OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer trans
ArgumentNullException.ThrowIfNull(transformer);

OperationTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Operation, transformer));
return this;
}

Expand All @@ -128,7 +137,9 @@ public OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOper
{
ArgumentNullException.ThrowIfNull(transformer);

OperationTransformers.Add(new DelegateOpenApiOperationTransformer(transformer));
var transformerInstance = new DelegateOpenApiOperationTransformer(transformer);
OperationTransformers.Add(transformerInstance);
Transformers.Add(new(OpenApiTransformerKind.Operation, transformerInstance));
return this;
}

Expand All @@ -140,7 +151,9 @@ public OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOper
public OpenApiOptions AddSchemaTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiSchemaTransformer
{
SchemaTransformers.Add(new TypeBasedOpenApiSchemaTransformer(typeof(TTransformerType)));
var transformer = new TypeBasedOpenApiSchemaTransformer(typeof(TTransformerType));
SchemaTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Schema, transformer));
return this;
}

Expand All @@ -154,6 +167,7 @@ public OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer
ArgumentNullException.ThrowIfNull(transformer);

SchemaTransformers.Add(transformer);
Transformers.Add(new(OpenApiTransformerKind.Schema, transformer));
return this;
}

Expand All @@ -166,7 +180,18 @@ public OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTran
{
ArgumentNullException.ThrowIfNull(transformer);

SchemaTransformers.Add(new DelegateOpenApiSchemaTransformer(transformer));
var transformerInstance = new DelegateOpenApiSchemaTransformer(transformer);
SchemaTransformers.Add(transformerInstance);
Transformers.Add(new(OpenApiTransformerKind.Schema, transformerInstance));
return this;
}
}

internal readonly record struct OpenApiTransformerRegistration(OpenApiTransformerKind Kind, object Transformer);

internal enum OpenApiTransformerKind
{
Document,
Operation,
Schema
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -35,6 +37,96 @@ await VerifyOpenApiDocument(builder, options, document =>
});
}

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

builder.MapGet("/todo", () => { }).WithTags("todos", "v1");

var options = new OpenApiOptions();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Tags ??= new SortedSet<OpenApiTag>(Comparer<OpenApiTag>.Create(
static (left, right) => StringComparer.Ordinal.Compare(left.Name, right.Name)));
document.Tags.Add(new OpenApiTag
{
Name = "todos",
Description = "Operations for managing todo items."
});
document.Tags.Add(new OpenApiTag
{
Name = "v1",
Description = "Version 1 operations."
});
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, cancellationToken) => Task.CompletedTask);

await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Tags,
tag =>
{
Assert.Equal("todos", tag.Name);
Assert.Equal("Operations for managing todo items.", tag.Description);
},
tag =>
{
Assert.Equal("v1", tag.Name);
Assert.Equal("Version 1 operations.", tag.Description);
});
});
}

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

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

var options = new OpenApiOptions();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info.Description = "Document transformer ran.";
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
Assert.Equal("Document transformer ran.", context.Document.Info.Description);
operation.Description = "Operation transformer ran.";
return Task.CompletedTask;
});

await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Equal("Document transformer ran.", document.Info.Description);
Assert.Equal("Operation transformer ran.", document.Paths["/todo"].Operations[HttpMethod.Get].Description);
});
}

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

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

var transformer = new OperationAndDocumentTransformer();
var options = new OpenApiOptions();
options.AddOperationTransformer(transformer);

await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Null(document.Info.Description);
Assert.Equal("Operation transformer ran.", document.Paths["/todo"].Operations[HttpMethod.Get].Description);
});

Assert.Equal(0, transformer.DocumentTransformCount);
Assert.Equal(1, transformer.OperationTransformCount);
}

[Fact]
public async Task DocumentTransformer_SupportsActivatedTransformers()
{
Expand Down Expand Up @@ -322,6 +414,27 @@ public async Task DocumentTransformer_ExecutesAsynchronously()
Assert.Equal([1, 2, 3], transformerOrder);
}

private sealed class OperationAndDocumentTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
{
public int OperationTransformCount { get; private set; }

public int DocumentTransformCount { get; private set; }

public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
OperationTransformCount++;
operation.Description = "Operation transformer ran.";
return Task.CompletedTask;
}

public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
DocumentTransformCount++;
document.Info.Description = "Document transformer ran.";
return Task.CompletedTask;
}
}

private class ActivatedTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public void AddDocumentTransformer_WithDocumentTransformerDelegate()
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.IsType<DelegateOpenApiDocumentTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Document, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -40,6 +41,7 @@ public void AddDocumentTransformer_WithDocumentTransformerInstance()
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.Same(transformer, insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Document, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -57,6 +59,7 @@ public void AddDocumentTransformer_WithDocumentTransformerType()
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.IsType<TypeBasedOpenApiDocumentTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Document, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -79,6 +82,7 @@ public void AddOperationTransformer_WithOperationTransformerDelegate()
// Assert
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.IsType<DelegateOpenApiOperationTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Operation, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -97,6 +101,7 @@ public void AddOperationTransformer_WithOperationTransformerInstance()
// Assert
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.Same(transformer, insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Operation, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -114,6 +119,7 @@ public void AddOperationTransformer_WithOperationTransformerType()
// Assert
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.IsType<TypeBasedOpenApiOperationTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Operation, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
Expand All @@ -136,6 +142,7 @@ public void AddSchemaTransformer_WithSchemaTransformerDelegate()
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.IsType<DelegateOpenApiSchemaTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Schema, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
Expand All @@ -154,6 +161,7 @@ public void AddSchemaTransformer_WithSchemaTransformerInstance()
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.Same(transformer, insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Schema, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
Expand All @@ -171,11 +179,18 @@ public void AddSchemaTransformer_WithSchemaTransformerType()
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.IsType<TypeBasedOpenApiSchemaTransformer>(insertedTransformer);
AssertRegistration(insertedTransformer, OpenApiTransformerKind.Schema, Assert.Single(options.Transformers));
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
}

private static void AssertRegistration(object transformer, OpenApiTransformerKind kind, OpenApiTransformerRegistration registration)
{
Assert.Same(transformer, registration.Transformer);
Assert.Equal(kind, registration.Kind);
}

private class TestOpenApiDocumentTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
Expand Down
Loading
Loading