Add OpenAPI operation and schema transformer interfaces (#56395)

* Add operation and schema transformer interfaces

- Add `IOpenApiOperationTransformer`.
- Add `IOpenApiSchemaTransformer`.
- Rename `Use*Transfomer()` methods to `Add*Transformer()`.

Resolves #56022.

* Add operation and schema transform examples

Add examples for DI-activated operation and schema transformers.

* Fix rebase

Fix mistake during rebase.

* Run operation transforms before documents

Run operation transformers before any document transformers.

* Move transformer

Add type-based transformer for operations to its own class.

* Revert submodule

Revert accidental change to submodule during rebase.

* Apply review feedback

- Remove collection expression.
- Fix two typos.
- Add parameter to benchmark.
This commit is contained in:
Martin Costello 2024-07-16 20:43:35 +01:00 коммит произвёл GitHub
Родитель 13d056acf6
Коммит 2d47d49abc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
25 изменённых файлов: 1193 добавлений и 117 удалений

Просмотреть файл

@ -1788,7 +1788,7 @@ 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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hybrid", "Hybrid", "{2D64CA23-6E81-488E-A7D3-9BDF87240098}"
EndProject
@ -1802,7 +1802,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{9DC6B242-457B-4767-A84B-C3D23B76C642}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyManagementSimulator", "src\DataProtection\samples\KeyManagementSimulator\KeyManagementSimulator.csproj", "{5B5F86CC-3598-463C-9F9B-F78FBB6642F4}"
EndProject

Просмотреть файл

@ -2,15 +2,19 @@
"solution": {
"path": "..\\..\\AspNetCore.sln",
"projects": [
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
"src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
"src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj",
"src\\OpenApi\\sample\\Sample.csproj",
"src\\OpenApi\\src\\Microsoft.AspNetCore.OpenApi.csproj",
"src\\OpenApi\\test\\Microsoft.AspNetCore.OpenApi.Tests.csproj",
"src\\OpenApi\\sample\\Sample.csproj",
"src\\OpenApi\\perf\\Microbenchmarks\\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj"
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj"
]
}
}

Просмотреть файл

@ -21,16 +21,27 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
public int TransformerCount { get; set; }
private readonly IEndpointRouteBuilder _builder = CreateBuilder();
private readonly OpenApiOptions _options = new OpenApiOptions();
private readonly OpenApiOptions _options = new();
private OpenApiDocumentService _documentService;
[GlobalSetup(Target = nameof(ActivatedOperationTransformer))]
public void ActivatedOperationTransformer_Setup()
{
_builder.MapGet("/", () => { });
for (var i = 0; i <= TransformerCount; i++)
{
_options.AddOperationTransformer<OperationTransformer>();
}
_documentService = CreateDocumentService(_builder, _options);
}
[GlobalSetup(Target = nameof(OperationTransformerAsDelegate))]
public void OperationTransformerAsDelegate_Setup()
{
_builder.MapGet("/", () => { });
for (var i = 0; i <= TransformerCount; i++)
{
_options.UseOperationTransformer((operation, context, token) =>
_options.AddOperationTransformer((operation, context, token) =>
{
operation.Description = "New Description";
return Task.CompletedTask;
@ -45,7 +56,7 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
_builder.MapGet("/", () => { });
for (var i = 0; i <= TransformerCount; i++)
{
_options.UseTransformer<ActivatedTransformer>();
_options.AddDocumentTransformer<DocumentTransformer>();
}
_documentService = CreateDocumentService(_builder, _options);
}
@ -56,7 +67,7 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
_builder.MapGet("/", () => { });
for (var i = 0; i <= TransformerCount; i++)
{
_options.UseTransformer((document, context, token) =>
_options.AddDocumentTransformer((document, context, token) =>
{
document.Info.Description = "New Description";
return Task.CompletedTask;
@ -65,13 +76,24 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
_documentService = CreateDocumentService(_builder, _options);
}
[GlobalSetup(Target = nameof(SchemaTransformer))]
[GlobalSetup(Target = nameof(ActivatedSchemaTransformer))]
public void ActivatedSchemaTransformer_Setup()
{
_builder.MapPost("/", (Todo todo) => todo);
for (var i = 0; i <= TransformerCount; i++)
{
_options.AddSchemaTransformer<SchemaTransformer>();
}
_documentService = CreateDocumentService(_builder, _options);
}
[GlobalSetup(Target = nameof(SchemaTransformerAsDelegate))]
public void SchemaTransformer_Setup()
{
_builder.MapPost("/", (Todo todo) => todo);
for (var i = 0; i <= TransformerCount; i++)
{
_options.UseSchemaTransformer((schema, context, token) =>
_options.AddSchemaTransformer((schema, context, token) =>
{
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
{
@ -87,6 +109,12 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
_documentService = CreateDocumentService(_builder, _options);
}
[Benchmark]
public async Task ActivatedOperationTransformer()
{
await _documentService.GetOpenApiDocumentAsync();
}
[Benchmark]
public async Task OperationTransformerAsDelegate()
{
@ -106,12 +134,18 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
}
[Benchmark]
public async Task SchemaTransformer()
public async Task ActivatedSchemaTransformer()
{
await _documentService.GetOpenApiDocumentAsync();
}
private class ActivatedTransformer : IOpenApiDocumentTransformer
[Benchmark]
public async Task SchemaTransformerAsDelegate()
{
await _documentService.GetOpenApiDocumentAsync();
}
private class DocumentTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
@ -119,4 +153,29 @@ public class TransformersBenchmark : OpenApiDocumentServiceTestBase
return Task.CompletedTask;
}
}
private class OperationTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
operation.Description = "Operation Description";
return Task.CompletedTask;
}
}
private class SchemaTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.Type == typeof(Todo) && context.ParameterDescription != null)
{
schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
}
else
{
schema.Extensions["x-my-extension"] = new OpenApiString("response");
}
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.ComponentModel;
using Microsoft.AspNetCore.Http.HttpResults;
@ -19,11 +18,13 @@ builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddOpenApi("v1", options =>
{
options.AddHeader("X-Version", "1.0");
options.UseTransformer<BearerSecuritySchemeTransformer>();
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("v2", options => {
options.UseTransformer(new AddContactTransformer());
options.UseTransformer((document, context, token) => {
options.AddSchemaTransformer<AddExternalDocsTransformer>();
options.AddOperationTransformer<AddExternalDocsTransformer>();
options.AddDocumentTransformer(new AddContactTransformer());
options.AddDocumentTransformer((document, context, token) => {
document.Info.License = new OpenApiLicense { Name = "MIT" };
return Task.CompletedTask;
});
@ -82,7 +83,8 @@ v1.MapGet("/todos/{id}", (int id) => new TodoWithDueDate(1, "Test todo", false,
v2.MapGet("/users", () => new [] { "alice", "bob" })
.WithTags("users");
v2.MapPost("/users", () => Results.Created("/users/1", new { Id = 1, Name = "Test user" }));
v2.MapPost("/users", () => Results.Created("/users/1", new { Id = 1, Name = "Test user" }))
.WithName("CreateUser");
responses.MapGet("/200-add-xml", () => new TodoWithDueDate(1, "Test todo", false, DateTime.Now.AddDays(1), DateTime.Now))
.Produces<Todo>(additionalContentTypes: "text/xml");

Просмотреть файл

@ -0,0 +1,42 @@
// 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.OpenApi.Models;
namespace Sample.Transformers;
public sealed class AddExternalDocsTransformer(IConfiguration configuration) : IOpenApiOperationTransformer, IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
if (operation.OperationId is { Length: > 0 } id &&
Uri.TryCreate(configuration["DocumentationBaseUrl"], UriKind.Absolute, out var baseUri))
{
var url = new Uri(baseUri, $"/api/docs/operations/{Uri.EscapeDataString(id)}");
operation.ExternalDocs = new OpenApiExternalDocs
{
Description = "Documentation for this OpenAPI endpoint",
Url = url
};
}
return Task.CompletedTask;
}
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (Uri.TryCreate(configuration["DocumentationBaseUrl"], UriKind.Absolute, out var baseUri))
{
var url = new Uri(baseUri, $"/api/docs/schemas/{Uri.EscapeDataString(schema.Type)}");
schema.ExternalDocs = new OpenApiExternalDocs
{
Description = "Documentation for this OpenAPI schema",
Url = url
};
}
return Task.CompletedTask;
}
}

Просмотреть файл

@ -2,9 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
namespace Sample.Transformers;
@ -12,7 +12,7 @@ public static class OperationTransformers
{
public static OpenApiOptions AddHeader(this OpenApiOptions options, string headerName, string defaultValue)
{
return options.UseOperationTransformer((operation, context, cancellationToken) =>
return options.AddOperationTransformer((operation, context, cancellationToken) =>
{
var schema = OpenApiTypeMapper.MapTypeToOpenApiPrimitiveType(typeof(string));
schema.Default = new OpenApiString(defaultValue);

Просмотреть файл

@ -1,4 +1,5 @@
{
"DocumentationBaseUrl": "https://example.com",
"Logging": {
"LogLevel": {
"Default": "Information",

Просмотреть файл

@ -1,9 +1,17 @@
#nullable enable
Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiDocument! document, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer
Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiOperation! operation, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer
Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer.TransformAsync(Microsoft.OpenApi.Models.OpenApiSchema! schema, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.get -> Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Description.init -> void
Microsoft.AspNetCore.OpenApi.OpenApiOptions
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.get -> System.Func<System.Text.Json.Serialization.Metadata.JsonTypeInfo!, string?>!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.CreateSchemaReferenceId.set -> void
Microsoft.AspNetCore.OpenApi.OpenApiOptions.DocumentName.get -> string!
@ -12,11 +20,11 @@ Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiVersion.get -> Microsoft.Open
Microsoft.AspNetCore.OpenApi.OpenApiOptions.OpenApiVersion.set -> void
Microsoft.AspNetCore.OpenApi.OpenApiOptions.ShouldInclude.get -> System.Func<Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription!, bool>!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.ShouldInclude.set -> void
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseOperationTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseSchemaTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiSchema!, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiDocument!, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.UseTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddOperationTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddSchemaTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiSchema!, Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer(Microsoft.AspNetCore.OpenApi.IOpenApiDocumentTransformer! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer(System.Func<Microsoft.OpenApi.Models.OpenApiDocument!, Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiOptions.AddDocumentTransformer<TTransformerType>() -> Microsoft.AspNetCore.OpenApi.OpenApiOptions!
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices.get -> System.IServiceProvider!
Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext.ApplicationServices.init -> void

Просмотреть файл

@ -36,9 +36,9 @@ internal sealed class OpenApiDocumentService(
{
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
private readonly IOpenApiDocumentTransformer _schemaReferenceTransformer = new OpenApiSchemaReferenceTransformer();
private readonly OpenApiSchemaReferenceTransformer _schemaReferenceTransformer = new();
private static readonly OpenApiEncoding _defaultFormEncoding = new OpenApiEncoding { Style = ParameterStyle.Form, Explode = true };
private static readonly OpenApiEncoding _defaultFormEncoding = new() { Style = ParameterStyle.Form, Explode = true };
/// <summary>
/// Cache of <see cref="OpenApiOperationTransformerContext"/> instances keyed by the
@ -47,7 +47,7 @@ internal sealed class OpenApiDocumentService(
/// operations, API descriptions, and their respective transformer contexts.
/// </summary>
private readonly Dictionary<string, OpenApiOperationTransformerContext> _operationTransformerContextCache = new();
private static readonly ApiResponseType _defaultApiResponseType = new ApiResponseType { StatusCode = StatusCodes.Status200OK };
private static readonly ApiResponseType _defaultApiResponseType = new() { StatusCode = StatusCodes.Status200OK };
internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context)
=> _operationTransformerContextCache.TryGetValue(descriptionId, out context);
@ -86,6 +86,41 @@ internal sealed class OpenApiDocumentService(
await _schemaReferenceTransformer.TransformAsync(document, documentTransformerContext, cancellationToken);
}
internal async Task ForEachOperationAsync(
OpenApiDocument document,
Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> callback,
CancellationToken cancellationToken)
{
foreach (var pathItem in document.Paths.Values)
{
for (var i = 0; i < OpenApiConstants.OperationTypes.Length; i++)
{
var operationType = OpenApiConstants.OperationTypes[i];
if (!pathItem.Operations.TryGetValue(operationType, out var operation))
{
continue;
}
if (operation.Extensions.TryGetValue(OpenApiConstants.DescriptionId, out var descriptionIdExtension) &&
descriptionIdExtension is ScrubbedOpenApiAny { Value: string descriptionId } &&
TryGetCachedOperationTransformerContext(descriptionId, out var operationContext))
{
await callback(operation, operationContext, cancellationToken);
}
else
{
// If the cached operation transformer context was not found, throw an exception.
// This can occur if the `x-aspnetcore-id` extension attribute was removed by the
// user in another operation transformer or if the lookup for operation transformer
// context resulted in a cache miss. As an alternative here, we could just to implement
// the "slow-path" and look up the ApiDescription associated with the OpenApiOperation
// using the OperationType and given path, but we'll avoid this for now.
throw new InvalidOperationException("Cached operation transformer context not found. Please ensure that the operation contains the `x-aspnetcore-id` extension attribute.");
}
}
}
}
// Note: Internal for testing.
internal OpenApiInfo GetOpenApiInfo()
{
@ -137,13 +172,23 @@ internal sealed class OpenApiDocumentService(
{
var operation = await GetOperationAsync(description, capturedTags, cancellationToken);
operation.Extensions.Add(OpenApiConstants.DescriptionId, new ScrubbedOpenApiAny(description.ActionDescriptor.Id));
_operationTransformerContextCache.TryAdd(description.ActionDescriptor.Id, new OpenApiOperationTransformerContext
var operationContext = new OpenApiOperationTransformerContext
{
DocumentName = documentName,
Description = description,
ApplicationServices = serviceProvider,
});
};
_operationTransformerContextCache.TryAdd(description.ActionDescriptor.Id, operationContext);
operations[description.GetOperationType()] = operation;
// Use index-based for loop to avoid allocating an enumerator with a foreach.
for (var i = 0; i < _options.OperationTransformers.Count; i++)
{
var transformer = _options.OperationTransformers[i];
await transformer.TransformAsync(operation, operationContext, cancellationToken);
}
}
return operations;
}

Просмотреть файл

@ -15,7 +15,8 @@ namespace Microsoft.AspNetCore.OpenApi;
public sealed class OpenApiOptions
{
internal readonly List<IOpenApiDocumentTransformer> DocumentTransformers = [];
internal readonly List<Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>> SchemaTransformers = [];
internal readonly List<IOpenApiOperationTransformer> OperationTransformers = [];
internal readonly List<IOpenApiSchemaTransformer> SchemaTransformers = [];
/// <summary>
/// A default implementation for creating a schema reference ID for a given <see cref="JsonTypeInfo"/>.
@ -62,7 +63,7 @@ public sealed class OpenApiOptions
/// </summary>
/// <typeparam name="TTransformerType">The type of the <see cref="IOpenApiDocumentTransformer"/> to instantiate.</typeparam>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions UseTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
public OpenApiOptions AddDocumentTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiDocumentTransformer
{
DocumentTransformers.Add(new TypeBasedOpenApiDocumentTransformer(typeof(TTransformerType)));
@ -74,9 +75,9 @@ public sealed class OpenApiOptions
/// </summary>
/// <param name="transformer">The <see cref="IOpenApiDocumentTransformer"/> instance to use.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
public OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
{
ArgumentNullException.ThrowIfNull(transformer, nameof(transformer));
ArgumentNullException.ThrowIfNull(transformer);
DocumentTransformers.Add(transformer);
return this;
@ -87,24 +88,74 @@ public sealed class OpenApiOptions
/// </summary>
/// <param name="transformer">The delegate representing the document transformer.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
public OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
{
ArgumentNullException.ThrowIfNull(transformer, nameof(transformer));
ArgumentNullException.ThrowIfNull(transformer);
DocumentTransformers.Add(new DelegateOpenApiDocumentTransformer(transformer));
return this;
}
/// <summary>
/// Registers a new operation transformer on the current <see cref="OpenApiOptions"/> instance.
/// </summary>
/// <typeparam name="TTransformerType">The type of the <see cref="IOpenApiOperationTransformer"/> to instantiate.</typeparam>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions AddOperationTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiOperationTransformer
{
OperationTransformers.Add(new TypeBasedOpenApiOperationTransformer(typeof(TTransformerType)));
return this;
}
/// <summary>
/// Registers a given instance of <see cref="IOpenApiOperationTransformer"/> on the current <see cref="OpenApiOptions"/> instance.
/// </summary>
/// <param name="transformer">The <see cref="IOpenApiOperationTransformer"/> instance to use.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
{
ArgumentNullException.ThrowIfNull(transformer);
OperationTransformers.Add(transformer);
return this;
}
/// <summary>
/// Registers a given delegate as an operation transformer on the current <see cref="OpenApiOptions"/> instance.
/// </summary>
/// <param name="transformer">The delegate representing the operation transformer.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
public OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
{
ArgumentNullException.ThrowIfNull(transformer, nameof(transformer));
ArgumentNullException.ThrowIfNull(transformer);
DocumentTransformers.Add(new DelegateOpenApiDocumentTransformer(transformer));
OperationTransformers.Add(new DelegateOpenApiOperationTransformer(transformer));
return this;
}
/// <summary>
/// Registers a new schema transformer on the current <see cref="OpenApiOptions"/> instance.
/// </summary>
/// <typeparam name="TTransformerType">The type of the <see cref="IOpenApiSchemaTransformer"/> to instantiate.</typeparam>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions AddSchemaTransformer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TTransformerType>()
where TTransformerType : IOpenApiSchemaTransformer
{
SchemaTransformers.Add(new TypeBasedOpenApiSchemaTransformer(typeof(TTransformerType)));
return this;
}
/// <summary>
/// Registers a given instance of <see cref="IOpenApiOperationTransformer"/> on the current <see cref="OpenApiOptions"/> instance.
/// </summary>
/// <param name="transformer">The <see cref="IOpenApiOperationTransformer"/> instance to use.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
{
ArgumentNullException.ThrowIfNull(transformer);
SchemaTransformers.Add(transformer);
return this;
}
@ -113,11 +164,11 @@ public sealed class OpenApiOptions
/// </summary>
/// <param name="transformer">The delegate representing the schema transformer.</param>
/// <returns>The <see cref="OpenApiOptions"/> instance for further customization.</returns>
public OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
public OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
{
ArgumentNullException.ThrowIfNull(transformer, nameof(transformer));
ArgumentNullException.ThrowIfNull(transformer);
SchemaTransformers.Add(transformer);
SchemaTransformers.Add(new DelegateOpenApiSchemaTransformer(transformer));
return this;
}
}

Просмотреть файл

@ -154,7 +154,7 @@ internal sealed class OpenApiSchemaService(
for (var i = 0; i < _openApiOptions.SchemaTransformers.Count; i++)
{
var transformer = _openApiOptions.SchemaTransformers[i];
await transformer(schema, context, cancellationToken);
await transformer.TransformAsync(schema, context, cancellationToken);
}
}

Просмотреть файл

@ -31,34 +31,10 @@ internal sealed class DelegateOpenApiDocumentTransformer : IOpenApiDocumentTrans
if (_operationTransformer != null)
{
var documentService = context.ApplicationServices.GetRequiredKeyedService<OpenApiDocumentService>(context.DocumentName);
foreach (var pathItem in document.Paths.Values)
{
for (var i = 0; i < OpenApiConstants.OperationTypes.Length; i++)
{
var operationType = OpenApiConstants.OperationTypes[i];
if (!pathItem.Operations.TryGetValue(operationType, out var operation))
{
continue;
}
if (operation.Extensions.TryGetValue(OpenApiConstants.DescriptionId, out var descriptionIdExtension) &&
descriptionIdExtension is ScrubbedOpenApiAny { Value: string descriptionId } &&
documentService.TryGetCachedOperationTransformerContext(descriptionId, out var operationContext))
{
await _operationTransformer(operation, operationContext, cancellationToken);
}
else
{
// If the cached operation transformer context was not found, throw an exception.
// This can occur if the `x-aspnetcore-id` extension attribute was removed by the
// user in another operation transformer or if the lookup for operation transformer
// context resulted in a cache miss. As an alternative here, we could just to implement
// the "slow-path" and look up the ApiDescription associated with the OpenApiOperation
// using the OperationType and given path, but we'll avoid this for now.
throw new InvalidOperationException("Cached operation transformer context not found. Please ensure that the operation contains the `x-aspnetcore-id` extension attribute.");
}
}
}
await documentService.ForEachOperationAsync(
document,
async (operation, operationContext, token) => await _operationTransformer(operation, operationContext, token),
cancellationToken);
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
internal sealed class DelegateOpenApiOperationTransformer : IOpenApiOperationTransformer
{
private readonly Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> _transformer;
public DelegateOpenApiOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
{
_transformer = transformer;
}
public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
await _transformer(operation, context, cancellationToken);
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
internal sealed class DelegateOpenApiSchemaTransformer : IOpenApiSchemaTransformer
{
private readonly Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> _transformer;
public DelegateOpenApiSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
{
_transformer = transformer;
}
public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
await _transformer(schema, context, cancellationToken);
}
}

Просмотреть файл

@ -17,5 +17,5 @@ public interface IOpenApiDocumentTransformer
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <see paramref="document"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken);
Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken);
}

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
/// <summary>
/// Represents a transformer that can be used to modify an OpenAPI operation.
/// </summary>
public interface IOpenApiOperationTransformer
{
/// <summary>
/// Transforms the specified OpenAPI operation.
/// </summary>
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <see paramref="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken);
}

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
/// <summary>
/// Represents a transformer that can be used to modify an OpenAPI schema.
/// </summary>
public interface IOpenApiSchemaTransformer
{
/// <summary>
/// Transforms the specified OpenAPI schema.
/// </summary>
/// <param name="schema">The <see cref="OpenApiSchema"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiSchemaTransformerContext"/> associated with the <see paramref="schema"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken);
}

Просмотреть файл

@ -0,0 +1,44 @@
// 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;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
internal sealed class TypeBasedOpenApiOperationTransformer : IOpenApiOperationTransformer
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private readonly Type _transformerType;
private readonly ObjectFactory _transformerFactory;
internal TypeBasedOpenApiOperationTransformer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type transformerType)
{
_transformerType = transformerType;
_transformerFactory = ActivatorUtilities.CreateFactory(_transformerType, []);
}
public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
var transformer = _transformerFactory.Invoke(context.ApplicationServices, []) as IOpenApiOperationTransformer;
Debug.Assert(transformer is not null, $"The type {_transformerType} does not implement {nameof(IOpenApiOperationTransformer)}.");
try
{
await transformer.TransformAsync(operation, context, cancellationToken);
}
finally
{
if (transformer is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (transformer is IDisposable disposable)
{
disposable.Dispose();
}
}
}
}

Просмотреть файл

@ -0,0 +1,43 @@
// 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;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
namespace Microsoft.AspNetCore.OpenApi;
internal sealed class TypeBasedOpenApiSchemaTransformer : IOpenApiSchemaTransformer
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private readonly Type _transformerType;
private readonly ObjectFactory _transformerFactory;
internal TypeBasedOpenApiSchemaTransformer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type transformerType)
{
_transformerType = transformerType;
_transformerFactory = ActivatorUtilities.CreateFactory(_transformerType, []);
}
public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
var transformer = _transformerFactory.Invoke(context.ApplicationServices, []) as IOpenApiSchemaTransformer;
Debug.Assert(transformer != null, $"The type {_transformerType} does not implement {nameof(IOpenApiSchemaTransformer)}.");
try
{
await transformer.TransformAsync(schema, context, cancellationToken);
}
finally
{
if (transformer is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (transformer is IDisposable disposable)
{
disposable.Dispose();
}
}
}
}

Просмотреть файл

@ -34,6 +34,11 @@
"tags": [
"Sample"
],
"externalDocs": {
"description": "Documentation for this OpenAPI endpoint",
"url": "https://example.com/api/docs/operations/CreateUser"
},
"operationId": "CreateUser",
"responses": {
"200": {
"description": "OK"
@ -48,6 +53,10 @@
"type": "array",
"items": {
"type": "string"
},
"externalDocs": {
"description": "Documentation for this OpenAPI schema",
"url": "https://example.com/api/docs/schemas/array"
}
}
}

Просмотреть файл

@ -18,12 +18,12 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer((document, context, cancellationToken) =>
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info.Description = "1";
return Task.CompletedTask;
});
options.UseTransformer((document, context, cancellationToken) =>
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
Assert.Equal("1", document.Info.Description);
document.Info.Description = "2";
@ -45,7 +45,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer<ActivatedTransformer>();
options.AddDocumentTransformer<ActivatedTransformer>();
await VerifyOpenApiDocument(builder, options, document =>
{
@ -62,7 +62,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer(new ActivatedTransformer());
options.AddDocumentTransformer(new ActivatedTransformer());
await VerifyOpenApiDocument(builder, options, document =>
{
@ -79,7 +79,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => { });
var options = new OpenApiOptions();
options.UseTransformer<ActivatedTransformerWithDependency>();
options.AddDocumentTransformer<ActivatedTransformerWithDependency>();
// Assert that singleton dependency is only instantiated once
// regardless of the number of requests.
@ -105,7 +105,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => { });
var options = new OpenApiOptions();
options.UseTransformer<ActivatedTransformerWithDependency>();
options.AddDocumentTransformer<ActivatedTransformerWithDependency>();
// Assert that transient dependency is instantiated twice for each
// request to the OpenAPI document.
@ -131,7 +131,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer<DisposableTransformer>();
options.AddDocumentTransformer<DisposableTransformer>();
DisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>
@ -150,7 +150,7 @@ public class DocumentTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer<AsyncDisposableTransformer>();
options.AddDocumentTransformer<AsyncDisposableTransformer>();
AsyncDisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>

Просмотреть файл

@ -267,7 +267,7 @@ public class OpenApiSchemaReferenceTransformerTests : OpenApiDocumentServiceTest
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.Type == typeof(Todo) && context.ParameterDescription is not null)
{

Просмотреть файл

@ -7,7 +7,7 @@ using Microsoft.OpenApi.Models;
public class OpenApiOptionsTests
{
[Fact]
public void UseTransformer_WithDocumentTransformerDelegate()
public void AddDocumentTransformer_WithDocumentTransformerDelegate()
{
// Arrange
var options = new OpenApiOptions();
@ -18,47 +18,53 @@ public class OpenApiOptionsTests
});
// Act
var result = options.UseTransformer(transformer);
var result = options.AddDocumentTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.IsType<DelegateOpenApiDocumentTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void UseTransformer_WithDocumentTransformerInstance()
public void AddDocumentTransformer_WithDocumentTransformerInstance()
{
// Arrange
var options = new OpenApiOptions();
var transformer = new TestOpenApiDocumentTransformer();
// Act
var result = options.UseTransformer(transformer);
var result = options.AddDocumentTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.Same(transformer, insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void UseTransformer_WithDocumentTransformerType()
public void AddDocumentTransformer_WithDocumentTransformerType()
{
// Arrange
var options = new OpenApiOptions();
// Act
var result = options.UseTransformer<TestOpenApiDocumentTransformer>();
var result = options.AddDocumentTransformer<TestOpenApiDocumentTransformer>();
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.IsType<TypeBasedOpenApiDocumentTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.OperationTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void UseTransformer_WithOperationTransformerDelegate()
public void AddOperationTransformer_WithOperationTransformerDelegate()
{
// Arrange
var options = new OpenApiOptions();
@ -69,12 +75,106 @@ public class OpenApiOptionsTests
});
// Act
var result = options.UseOperationTransformer(transformer);
var result = options.AddOperationTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.DocumentTransformers);
Assert.IsType<DelegateOpenApiDocumentTransformer>(insertedTransformer);
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.IsType<DelegateOpenApiOperationTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void AddOperationTransformer_WithOperationTransformerInstance()
{
// Arrange
var options = new OpenApiOptions();
var transformer = new TestOpenApiOperationTransformer();
// Act
var result = options.AddOperationTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.Same(transformer, insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void AddOperationTransformer_WithOperationTransformerType()
{
// Arrange
var options = new OpenApiOptions();
// Act
var result = options.AddOperationTransformer<TestOpenApiOperationTransformer>();
// Assert
var insertedTransformer = Assert.Single(options.OperationTransformers);
Assert.IsType<TypeBasedOpenApiOperationTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.SchemaTransformers);
}
[Fact]
public void AddSchemaTransformer_WithSchemaTransformerDelegate()
{
// Arrange
var options = new OpenApiOptions();
var transformer = new Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>((schema, context, cancellationToken) =>
{
schema.Description = "New Description";
return Task.CompletedTask;
});
// Act
var result = options.AddSchemaTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.IsType<DelegateOpenApiSchemaTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
}
[Fact]
public void AddSchemaTransformer_WithSchemaTransformerInstance()
{
// Arrange
var options = new OpenApiOptions();
var transformer = new TestOpenApiSchemaTransformer();
// Act
var result = options.AddSchemaTransformer(transformer);
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.Same(transformer, insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
}
[Fact]
public void AddSchemaTransformer_WithSchemaTransformerType()
{
// Arrange
var options = new OpenApiOptions();
// Act
var result = options.AddSchemaTransformer<TestOpenApiSchemaTransformer>();
// Assert
var insertedTransformer = Assert.Single(options.SchemaTransformers);
Assert.IsType<TypeBasedOpenApiSchemaTransformer>(insertedTransformer);
Assert.IsType<OpenApiOptions>(result);
Assert.Empty(options.DocumentTransformers);
Assert.Empty(options.OperationTransformers);
}
private class TestOpenApiDocumentTransformer : IOpenApiDocumentTransformer
@ -84,4 +184,20 @@ public class OpenApiOptionsTests
return Task.CompletedTask;
}
}
private class TestOpenApiOperationTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
private class TestOpenApiSchemaTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -1,8 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
public class OperationTransformerTests : OpenApiDocumentServiceTestBase
{
@ -15,7 +18,7 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseOperationTransformer((operation, context, cancellationToken) =>
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
var apiDescription = context.Description;
operation.Description = apiDescription.RelativePath;
@ -41,7 +44,7 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
}
[Fact]
public async Task OperationTransformer_RunsInRegisteredOrder()
public async Task OperationTransformers_RunInRegisteredOrder()
{
var builder = CreateBuilder();
@ -49,21 +52,40 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseOperationTransformer((operation, context, cancellationToken) =>
// While added first, document transformers should run after the operation transformers
options.AddDocumentTransformer<MyDocumentationTransformer>();
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
Assert.All(document.Paths.Values.SelectMany(p => p.Operations).Select(p => p.Value), o => Assert.Equal("6", o.Description));
return Task.CompletedTask;
});
// Operation transforms should run FIFO regardless of which kind of transformer is used
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
Assert.Null(operation.Description);
operation.Description = "1";
return Task.CompletedTask;
});
options.UseOperationTransformer((operation, context, cancellationToken) =>
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
Assert.Equal("1", operation.Description);
operation.Description = "2";
return Task.CompletedTask;
});
options.UseOperationTransformer((operation, context, cancellationToken) =>
options.AddOperationTransformer<MyOperationTransformer3>();
options.AddOperationTransformer(new MyOperationTransformer4());
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
Assert.Equal("2", operation.Description);
operation.Description = "3";
Assert.Equal("4", operation.Description);
operation.Description = "5";
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
Assert.Equal("5", operation.Description);
operation.Description = "6";
return Task.CompletedTask;
});
@ -74,17 +96,46 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("3", operation.Description);
Assert.Equal("6", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("3", operation.Description);
Assert.Equal("6", operation.Description);
});
});
}
private sealed class MyDocumentationTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
Assert.All(document.Paths.Values.SelectMany(p => p.Operations).Select(p => p.Value), o => Assert.Equal("6", o.Description));
return Task.CompletedTask;
}
}
private sealed class MyOperationTransformer3 : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Assert.Equal("2", operation.Description);
operation.Description = "3";
return Task.CompletedTask;
}
}
private sealed class MyOperationTransformer4 : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Assert.Equal("3", operation.Description);
operation.Description = "4";
return Task.CompletedTask;
}
}
[Fact]
public async Task OperationTransformer_CanMutateOperationViaDocumentTransformer()
{
@ -94,7 +145,7 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseTransformer((document, context, cancellationToken) =>
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
foreach (var pathItem in document.Paths.Values)
{
@ -125,24 +176,320 @@ public class OperationTransformerTests : OpenApiDocumentServiceTestBase
}
[Fact]
public async Task OperationTransformer_ThrowsExceptionIfDescriptionIdNotFound()
public async Task OperationTransformer_CanMutateOperationViaOperationTransformer()
{
var builder = CreateBuilder();
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.UseOperationTransformer((operation, context, cancellationToken) =>
{
operation.Extensions.Remove("x-aspnetcore-id");
return Task.CompletedTask;
});
options.UseOperationTransformer((operation, context, cancellationToken) =>
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
operation.Description = "3";
return Task.CompletedTask;
});
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => VerifyOpenApiDocument(builder, options, _ => { }));
Assert.Equal("Cached operation transformer context not found. Please ensure that the operation contains the `x-aspnetcore-id` extension attribute.", exception.Message);
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("3", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("3", operation.Description);
});
});
}
[Fact]
public async Task OperationTransformer_SupportsActivatedTransformers()
{
var builder = CreateBuilder();
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer<ActivatedTransformer>();
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
});
});
}
[Fact]
public async Task OperationTransformer_SupportsInstanceTransformers()
{
var builder = CreateBuilder();
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer(new ActivatedTransformer());
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
});
});
}
[Fact]
public async Task OperationTransformer_SupportsActivatedTransformerWithSingletonDependency()
{
var serviceCollection = new ServiceCollection().AddSingleton<Dependency>();
var builder = CreateBuilder(serviceCollection);
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer<ActivatedTransformerWithDependency>();
// Assert that singleton dependency is only instantiated once
// regardless of the number of requests and operations.
string description = null;
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
description = operation.Description;
Assert.Equal(Dependency.InstantiationCount.ToString(CultureInfo.InvariantCulture), description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal(description, operation.Description);
});
});
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal(description, operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal(description, operation.Description);
});
});
}
[Fact]
public async Task OperationTransformer_SupportsActivatedTransformerWithTransientDependency()
{
var serviceCollection = new ServiceCollection().AddTransient<Dependency>();
var builder = CreateBuilder(serviceCollection);
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer<ActivatedTransformerWithDependency>();
// Assert that transient dependency is instantiated once for each operation.
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("1", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("2", operation.Description);
});
});
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("3", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("4", operation.Description);
});
});
}
[Fact]
public async Task OperationTransformer_SupportsDisposableActivatedTransformer()
{
var builder = CreateBuilder();
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer<DisposableTransformer>();
DisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
});
});
Assert.Equal(2, DisposableTransformer.DisposeCount);
}
[Fact]
public async Task OperationTransformer_SupportsAsyncDisposableActivatedTransformer()
{
var builder = CreateBuilder();
builder.MapGet("/todo", () => { });
builder.MapGet("/user", () => { });
var options = new OpenApiOptions();
options.AddOperationTransformer<AsyncDisposableTransformer>();
AsyncDisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>
{
Assert.Collection(document.Paths.OrderBy(p => p.Key),
path =>
{
Assert.Equal("/todo", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
},
path =>
{
Assert.Equal("/user", path.Key);
var operation = Assert.Single(path.Value.Operations.Values);
Assert.Equal("Operation Description", operation.Description);
});
});
Assert.Equal(2, AsyncDisposableTransformer.DisposeCount);
}
private class ActivatedTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
operation.Description = "Operation Description";
return Task.CompletedTask;
}
}
private class DisposableTransformer : IOpenApiOperationTransformer, IDisposable
{
internal bool Disposed = false;
internal static int DisposeCount = 0;
public void Dispose()
{
Disposed = true;
DisposeCount += 1;
}
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
operation.Description = "Operation Description";
return Task.CompletedTask;
}
}
private class AsyncDisposableTransformer : IOpenApiOperationTransformer, IAsyncDisposable
{
internal bool Disposed = false;
internal static int DisposeCount = 0;
public ValueTask DisposeAsync()
{
Disposed = true;
DisposeCount += 1;
return ValueTask.CompletedTask;
}
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
operation.Description = "Operation Description";
return Task.CompletedTask;
}
}
private class ActivatedTransformerWithDependency(Dependency dependency) : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
dependency.TestMethod();
operation.Description = Dependency.InstantiationCount.ToString(CultureInfo.InvariantCulture);
return Task.CompletedTask;
}
}
private class Dependency
{
public Dependency()
{
InstantiationCount += 1;
}
internal void TestMethod() { }
internal static int InstantiationCount = 0;
}
}

Просмотреть файл

@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
@ -17,7 +18,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapPost("/todo", (Todo todo) => { });
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
Assert.Equal(typeof(Todo), context.Type);
Assert.Equal("todo", context.ParameterDescription.Name);
@ -35,7 +36,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
Assert.Equal(typeof(Todo), context.Type);
Assert.Null(context.ParameterDescription);
@ -53,7 +54,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
var service = context.ApplicationServices.GetKeyedService<OpenApiDocumentService>(context.DocumentName);
Assert.NotNull(service);
@ -74,7 +75,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
cts.Cancel();
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
Assert.Equal(cts.Token, cancellationToken);
Assert.True(cancellationToken.IsCancellationRequested);
@ -92,12 +93,12 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapPost("/todo", (Todo todo) => { });
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
schema.Extensions["x-my-extension"] = new OpenApiString("1");
return Task.CompletedTask;
});
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
Assert.Equal("1", ((OpenApiString)schema.Extensions["x-my-extension"]).Value);
schema.Extensions["x-my-extension"] = new OpenApiString("2");
@ -121,7 +122,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.Type == typeof(Todo))
{
@ -151,7 +152,7 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.UseSchemaTransformer((schema, context, cancellationToken) =>
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.Type == typeof(Todo) && context.ParameterDescription is not null)
{
@ -171,4 +172,248 @@ public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
Assert.False(responseSchema.Extensions.TryGetValue("x-my-extension", out var _));
});
}
[Fact]
public async Task SchemaTransformer_SupportsActivatedTransformers()
{
var builder = CreateBuilder();
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer<ActivatedTransformer>();
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.Equal("1", ((OpenApiString)requestSchema.Extensions["x-my-extension"]).Value);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal("1", ((OpenApiString)responseSchema.Extensions["x-my-extension"]).Value);
});
}
[Fact]
public async Task SchemaTransformer_SupportsInstanceTransformers()
{
var builder = CreateBuilder();
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer(new ActivatedTransformer());
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.Equal("1", ((OpenApiString)requestSchema.Extensions["x-my-extension"]).Value);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal("1", ((OpenApiString)responseSchema.Extensions["x-my-extension"]).Value);
});
}
[Fact]
public async Task SchemaTransformer_SupportsActivatedTransformerWithSingletonDependency()
{
var serviceCollection = new ServiceCollection().AddSingleton<Dependency>();
var builder = CreateBuilder(serviceCollection);
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer<ActivatedTransformerWithDependency>();
// Assert that singleton dependency is only instantiated once
// regardless of the number of requests, operations or schemas.
string value = null;
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
value = ((OpenApiString)requestSchema.Extensions["x-my-extension"]).Value;
Assert.Equal(Dependency.InstantiationCount.ToString(CultureInfo.InvariantCulture), value);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal(value, ((OpenApiString)responseSchema.Extensions["x-my-extension"]).Value);
});
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.Equal(value, ((OpenApiString)requestSchema.Extensions["x-my-extension"]).Value);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal(value, ((OpenApiString)responseSchema.Extensions["x-my-extension"]).Value);
});
}
[Fact]
public async Task SchemaTransformer_SupportsActivatedTransformerWithTransientDependency()
{
var serviceCollection = new ServiceCollection().AddTransient<Dependency>();
var builder = CreateBuilder(serviceCollection);
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer<ActivatedTransformerWithDependency>();
// Assert that transient dependency is instantiated once for each
// request to the OpenAPI document for each created schema.
var countBefore = Dependency.InstantiationCount;
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.True(requestSchema.Extensions.ContainsKey("x-my-extension"));
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.True(responseSchema.Extensions.ContainsKey("x-my-extension"));
});
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.True(requestSchema.Extensions.ContainsKey("x-my-extension"));
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.True(responseSchema.Extensions.ContainsKey("x-my-extension"));
});
var countAfter = Dependency.InstantiationCount;
Assert.Equal(countBefore + 4, countAfter);
}
[Fact]
public async Task SchemaTransformer_SupportsDisposableActivatedTransformer()
{
var builder = CreateBuilder();
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer<DisposableTransformer>();
DisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.Equal("Schema Description", requestSchema.Description);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal("Schema Description", responseSchema.Description);
});
Assert.Equal(2, DisposableTransformer.DisposeCount);
}
[Fact]
public async Task SchemaTransformer_SupportsAsyncDisposableActivatedTransformer()
{
var builder = CreateBuilder();
builder.MapPost("/todo", (Todo todo) => { });
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
var options = new OpenApiOptions();
options.AddSchemaTransformer<AsyncDisposableTransformer>();
AsyncDisposableTransformer.DisposeCount = 0;
await VerifyOpenApiDocument(builder, options, document =>
{
var path = Assert.Single(document.Paths.Values);
var postOperation = path.Operations[OperationType.Post];
var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
Assert.Equal("Schema Description", requestSchema.Description);
var getOperation = path.Operations[OperationType.Get];
var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
Assert.Equal("Schema Description", responseSchema.Description);
});
Assert.Equal(2, AsyncDisposableTransformer.DisposeCount);
}
private class ActivatedTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.Type == typeof(Todo))
{
schema.Extensions["x-my-extension"] = new OpenApiString("1");
}
return Task.CompletedTask;
}
}
private class DisposableTransformer : IOpenApiSchemaTransformer, IDisposable
{
internal bool Disposed = false;
internal static int DisposeCount = 0;
public void Dispose()
{
Disposed = true;
DisposeCount += 1;
}
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
schema.Description = "Schema Description";
return Task.CompletedTask;
}
}
private class AsyncDisposableTransformer : IOpenApiSchemaTransformer, IAsyncDisposable
{
internal bool Disposed = false;
internal static int DisposeCount = 0;
public ValueTask DisposeAsync()
{
Disposed = true;
DisposeCount += 1;
return ValueTask.CompletedTask;
}
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
schema.Description = "Schema Description";
return Task.CompletedTask;
}
}
private class ActivatedTransformerWithDependency(Dependency dependency) : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
dependency.TestMethod();
schema.Extensions["x-my-extension"] = new OpenApiString(Dependency.InstantiationCount.ToString(CultureInfo.InvariantCulture));
return Task.CompletedTask;
}
}
private class Dependency
{
public Dependency()
{
InstantiationCount += 1;
}
internal void TestMethod() { }
internal static int InstantiationCount = 0;
}
}