File: Transformers\SchemaTransformerTests.cs
Web Access
Project: src\src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests\Microsoft.AspNetCore.OpenApi.Tests.csproj (Microsoft.AspNetCore.OpenApi.Tests)
// 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.Mvc;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
 
public class SchemaTransformerTests : OpenApiDocumentServiceTestBase
{
    [Fact]
    public async Task SchemaTransformer_CanAccessTypeAndParameterDescriptionForParameter()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", (Todo todo) => { });
 
        var options = new OpenApiOptions();
        var firstInvocationOnSecondTransformer = true;
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            ValidateContext(context);
            return Task.CompletedTask;
        })
        .AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            // Coverage for https://github.com/dotnet/aspnetcore/issues/56899
            if (firstInvocationOnSecondTransformer)
            {
                Assert.Equal(typeof(Todo), context.JsonTypeInfo.Type);
                firstInvocationOnSecondTransformer = false;
            }
            // Rest of the state is still consistent
            ValidateContext(context);
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
 
        static void ValidateContext(OpenApiSchemaTransformerContext context)
        {
            if (context.JsonPropertyInfo == null)
            {
                Assert.Equal(typeof(Todo), context.JsonTypeInfo.Type);
                Assert.Equal("todo", context.ParameterDescription.Name);
            }
            if (context.JsonPropertyInfo?.Name == "id")
            {
                Assert.Equal(typeof(int), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "name")
            {
                Assert.Equal(typeof(string), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "isComplete")
            {
                Assert.Equal(typeof(bool), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "dueDate")
            {
                Assert.Equal(typeof(DateTime), context.JsonTypeInfo.Type);
            }
        }
    }
 
    [Fact]
    public async Task SchemaTransformer_CanAccessTypeForResponse()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonPropertyInfo == null)
            {
                Assert.Equal(typeof(Todo), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "id")
            {
                Assert.Equal(typeof(int), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "name")
            {
                Assert.Equal(typeof(string), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "isComplete")
            {
                Assert.Equal(typeof(bool), context.JsonTypeInfo.Type);
            }
            if (context.JsonPropertyInfo?.Name == "dueDate")
            {
                Assert.Equal(typeof(DateTime), context.JsonTypeInfo.Type);
            }
            Assert.Null(context.ParameterDescription);
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanAccessApplicationServicesAndDocumentName()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            var service = context.ApplicationServices.GetKeyedService<OpenApiDocumentService>(context.DocumentName);
            Assert.NotNull(service);
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
    }
 
    [Fact]
    public async Task SchemaTransformer_RespectsCancellationToken()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var cts = new CancellationTokenSource();
        cts.Cancel();
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            Assert.Equal(cts.Token, cancellationToken);
            Assert.True(cancellationToken.IsCancellationRequested);
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { }, cts.Token);
    }
 
    [Fact]
    public async Task SchemaTransformer_RunsInRegisteredOrder()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", (Todo todo) => { });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            schema.Extensions["x-my-extension"] = new OpenApiString("1");
            return Task.CompletedTask;
        });
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            Assert.Equal("1", ((OpenApiString)schema.Extensions["x-my-extension"]).Value);
            schema.Extensions["x-my-extension"] = new OpenApiString("2");
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            var operation = Assert.Single(document.Paths.Values).Operations.Values.Single();
            var schema = operation.RequestBody.Content["application/json"].Schema.GetEffective(document);
            Assert.Equal("2", ((OpenApiString)schema.Extensions["x-my-extension"]).Value);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_OnTypeModifiesBothRequestAndResponse()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", (Todo todo) => { });
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Todo))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("1");
            }
            return Task.CompletedTask;
        });
 
        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_WithDescriptionOnlyModifiesParameter()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", (Todo todo) => { });
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Todo) && context.ParameterDescription is not null)
            {
                schema.Extensions["x-my-extension"] = new OpenApiString(context.ParameterDescription.Name);
            }
            return Task.CompletedTask;
        });
 
        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("todo", ((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.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>();
 
        Dependency.InstantiationCount = 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.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"));
        });
        // Assert that the transient dependency has a "scoped" lifetime within
        // the context of the transformer and is called twice, once for each request.
        Assert.Equal(2, Dependency.InstantiationCount);
    }
 
    [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 that the transformer is disposed once for the entire document.
        Assert.Equal(1, 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 that the transformer is disposed once for the entire document.
        Assert.Equal(1, AsyncDisposableTransformer.DisposeCount);
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyAllTypesInADocument()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", (Todo todo) => { });
        builder.MapGet("/todo", (int id) => { });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(int))
            {
                schema.Format = "modified-number-format";
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that parameter schema has been update
            var path = Assert.Single(document.Paths.Values);
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Parameters[0].Schema;
            Assert.Equal("modified-number-format", responseSchema.Format);
 
            // Assert that property in request body schema has been updated
            var postOperation = path.Operations[OperationType.Post];
            var requestSchema = postOperation.RequestBody.Content["application/json"].Schema;
            Assert.Equal("modified-number-format", requestSchema.GetEffective(document).Properties["id"].Format);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyItemTypesInADocument()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/list", () => new List<int> { 1, 2, 3, 4 });
        builder.MapGet("/single", () => 1);
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(int))
            {
                schema.Format = "modified-number-format";
            }
            schema = new OpenApiSchema { Type = "array", Items = schema };
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the schema represent list elements has been modified
            var path = document.Paths["/list"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
            Assert.Equal("modified-number-format", responseSchema.Items.Format);
 
            // Assert that top-level schema associated with the standalone integer has been updated
            path = document.Paths["/single"];
            getOperation = path.Operations[OperationType.Get];
            responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
            Assert.Equal("modified-number-format", responseSchema.Format);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyPolymorphicChildSchemas()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/shape", (Shape todo) => { });
        builder.MapPost("/triangle", (Triangle todo) => { });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Triangle))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-triangle");
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the polymorphic sub-type `Triangle` has been updated
            var path = document.Paths["/shape"];
            var postOperation = path.Operations[OperationType.Post];
            var requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
            var triangleSubschema = Assert.Single(requestSchema.AnyOf.Where(s => s.Reference.Id == "ShapeTriangle"));
            Assert.True(triangleSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var _));
 
            // Assert that the standalone `Triangle` type has been updated
            path = document.Paths["/triangle"];
            postOperation = path.Operations[OperationType.Post];
            requestSchema = postOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
            Assert.Equal("this-is-a-triangle", ((OpenApiString)requestSchema.Extensions["x-my-extension"]).Value);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyPropertiesInAnItemsType()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/list-of-todo", () => new List<Todo> { new Todo(1, "Item1", false, DateTime.Now) });
        builder.MapGet("/list-of-int", () => new List<int> { 1, 2, 3, 4 });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(int))
            {
                schema.Format = "modified-number-format";
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the `id` property in the `Todo` within the array has been updated
            var path = document.Paths["/list-of-todo"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema;
            var itemSchema = responseSchema.GetEffective(document).Items.GetEffective(document);
            Assert.Equal("modified-number-format", itemSchema.Properties["id"].Format);
 
            // Assert that the integer type within the list has been updated
            var otherPath = document.Paths["/list-of-int"];
            var otherGetOperation = otherPath.Operations[OperationType.Get];
            var otherResponseSchema = otherGetOperation.Responses["200"].Content["application/json"].Schema;
            var otherItemSchema = otherResponseSchema.GetEffective(document).Items.GetEffective(document);
            Assert.Equal("modified-number-format", otherItemSchema.Format);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyListOfPolymorphicTypes()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/list", () => new List<Shape> { new Triangle { Hypotenuse = 12, Color = "blue", Sides = 3 }, new Square { Area = 24, Color = "red ", Sides = 4 } });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Triangle))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-triangle");
            }
            if (context.JsonTypeInfo.Type == typeof(Square))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-square");
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the `Triangle` type within the list has been updated
            var path = document.Paths["/list"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema;
            var itemSchema = responseSchema.GetEffective(document).Items.GetEffective(document);
            var triangleSubschema = Assert.Single(itemSchema.AnyOf.Where(s => s.Reference.Id == "ShapeTriangle"));
            // Assert that the x-my-extension type is set to this-is-a-triangle
            Assert.True(triangleSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var triangleExtension));
            Assert.Equal("this-is-a-triangle", ((OpenApiString)triangleExtension).Value);
 
            // Assert that the `Square` type within the polymorphic type list has been updated
            var squareSubschema = Assert.Single(itemSchema.AnyOf.Where(s => s.Reference.Id == "ShapeSquare"));
            // Assert that the x-my-extension type is set to this-is-a-square
            Assert.True(squareSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var squareExtension));
            Assert.Equal("this-is-a-square", ((OpenApiString)squareExtension).Value);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyPolymorphicTypesInProperties()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/list", () => new PolymorphicContainer());
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Triangle))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-triangle");
            }
            if (context.JsonTypeInfo.Type == typeof(Square))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-square");
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the `Triangle` type within the list has been updated
            var path = document.Paths["/list"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema;
            var someShapeSchema = responseSchema.GetEffective(document).Properties["someShape"].GetEffective(document);
            var triangleSubschema = Assert.Single(someShapeSchema.AnyOf.Where(s => s.Reference.Id == "ShapeTriangle"));
            // Assert that the x-my-extension type is set to this-is-a-triangle
            Assert.True(triangleSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var triangleExtension));
            Assert.Equal("this-is-a-triangle", ((OpenApiString)triangleExtension).Value);
 
            // Assert that the `Square` type within the polymorphic type list has been updated
            var squareSubschema = Assert.Single(someShapeSchema.AnyOf.Where(s => s.Reference.Id == "ShapeSquare"));
            // Assert that the x-my-extension type is set to this-is-a-square
            Assert.True(squareSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var squareExtension));
            Assert.Equal("this-is-a-square", ((OpenApiString)squareExtension).Value);
        });
    }
 
    [Fact]
    public async Task SchemaTransformer_CanModifyDeeplyNestedPolymorphicTypesInProperties()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/list", () => new List<PolymorphicContainer>());
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Triangle))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-triangle");
            }
            if (context.JsonTypeInfo.Type == typeof(Square))
            {
                schema.Extensions["x-my-extension"] = new OpenApiString("this-is-a-square");
            }
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document =>
        {
            // Assert that the `Triangle` type within the list has been updated
            var path = document.Paths["/list"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema;
            var someShapeSchema = responseSchema.GetEffective(document).Items.GetEffective(document).Properties["someShape"].GetEffective(document);
            var triangleSubschema = Assert.Single(someShapeSchema.AnyOf.Where(s => s.Reference.Id == "ShapeTriangle"));
            // Assert that the x-my-extension type is set to this-is-a-triangle
            Assert.True(triangleSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var triangleExtension));
            Assert.Equal("this-is-a-triangle", ((OpenApiString)triangleExtension).Value);
 
            // Assert that the `Square` type within the polymorphic type list has been updated
            var squareSubschema = Assert.Single(someShapeSchema.AnyOf.Where(s => s.Reference.Id == "ShapeSquare"));
            // Assert that the x-my-extension type is set to this-is-a-square
            Assert.True(squareSubschema.GetEffective(document).Extensions.TryGetValue("x-my-extension", out var squareExtension));
            Assert.Equal("this-is-a-square", ((OpenApiString)squareExtension).Value);
        });
    }
 
    [Fact]
    public async Task SchemaTransformers_CanModifyMultipleFormParameters()
    {
        var builder = CreateBuilder();
 
        builder.MapPost("/todo", ([FromForm] Todo todo, [FromForm] Error error) => { });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(int))
            {
                schema.Format = "modified-number-format";
            }
            return Task.CompletedTask;
        });
 
        // We use `allOf` for multiple form parameters to ensure that they should be aggregated
        // appropriately in the request body schema. Although we don't handle `AllOf` when we apply
        // schema transformers, these modifications still work because the wrapping of these schemas into
        // allOf definitions happens after all transformers have been applied.
        await VerifyOpenApiDocument(builder, options, document =>
        {
            var path = document.Paths["/todo"];
            var postOperation = path.Operations[OperationType.Post];
            var requestSchema = postOperation.RequestBody.Content["application/x-www-form-urlencoded"].Schema;
            Assert.Equal(2, requestSchema.AllOf.Count);
            var todoSchema = requestSchema.AllOf[0];
            var errorSchema = requestSchema.AllOf[1];
            Assert.Equal("modified-number-format", todoSchema.Properties["id"].Format);
            Assert.Equal("modified-number-format", errorSchema.Properties["code"].Format);
        });
    }
 
    [Fact]
    public async Task SchemaTransformers_CanImplementNotSchemaIndependently()
    {
        var builder = CreateBuilder();
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
        builder.MapPost("/shape", (Shape shape) => { });
 
        var options = new OpenApiOptions();
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            if (context.JsonTypeInfo.Type == typeof(Todo))
            {
                schema.Not = new OpenApiSchema { Type = "string" };
            }
            if (context.JsonTypeInfo.Type == typeof(Triangle))
            {
                schema.Not = new OpenApiSchema { Type = "string" };
            }
            return Task.CompletedTask;
        });
        UseNotSchemaTransformer(options, (schema, context, cancellationToken) =>
        {
            schema.Extensions["modified-by-not-schema-transformer"] = new OpenApiBoolean(true);
            return Task.CompletedTask;
        });
 
        // Assert that not schemas have been modified for both `Todo` and `Triangle` types.
        await VerifyOpenApiDocument(builder, options, document =>
        {
            var path = document.Paths["/todo"];
            var getOperation = path.Operations[OperationType.Get];
            var responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
            Assert.True(((OpenApiBoolean)responseSchema.Not.Extensions["modified-by-not-schema-transformer"]).Value);
 
            var shapePath = document.Paths["/shape"];
            var shapeOperation = shapePath.Operations[OperationType.Post];
            var shapeRequestSchema = shapeOperation.RequestBody.Content["application/json"].Schema.GetEffective(document);
            var triangleSchema = Assert.Single(shapeRequestSchema.AnyOf.Where(s => s.Reference.Id == "ShapeTriangle")).GetEffective(document);
            Assert.True(((OpenApiBoolean)triangleSchema.Not.Extensions["modified-by-not-schema-transformer"]).Value);
        });
 
        static void UseNotSchemaTransformer(OpenApiOptions options, Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> func)
        {
            options.AddSchemaTransformer(async (schema, context, cancellationToken) =>
            {
                if (schema.Not != null)
                {
                    await func(schema.Not, context, cancellationToken);
                }
                return;
            });
        }
    }
 
    [Fact]
    public async Task SchemaTransformer_CanAccessSingletonServiceFromContextApplicationServices()
    {
        var serviceCollection = new ServiceCollection().AddSingleton<Dependency>();
        var builder = CreateBuilder(serviceCollection);
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        Dependency.InstantiationCount = 0;
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            var service = context.ApplicationServices.GetRequiredService<Dependency>();
            var sameServiceAgain = context.ApplicationServices.GetRequiredService<Dependency>();
            service.TestMethod();
            sameServiceAgain.TestMethod();
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
        await VerifyOpenApiDocument(builder, options, document => { });
 
        // Assert that the singleton dependency is instantiated only once
        // for the entire lifetime of the application.
        Assert.Equal(1, Dependency.InstantiationCount);
    }
 
    [Fact]
    public async Task SchemaTransformer_CanAccessScopedServiceFromContextApplicationServices()
    {
        var serviceCollection = new ServiceCollection().AddScoped<Dependency>();
        var builder = CreateBuilder(serviceCollection);
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        Dependency.InstantiationCount = 0;
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            var service = context.ApplicationServices.GetRequiredService<Dependency>();
            var sameServiceAgain = context.ApplicationServices.GetRequiredService<Dependency>();
            service.TestMethod();
            sameServiceAgain.TestMethod();
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
        await VerifyOpenApiDocument(builder, options, document => { });
 
        // Assert that the scoped dependency is instantiated twice. Once for
        // each request to the document.
        Assert.Equal(2, Dependency.InstantiationCount);
    }
 
    [Fact]
    public async Task SchemaTransformer_CanAccessTransientServiceFromContextApplicationServices()
    {
        var serviceCollection = new ServiceCollection().AddTransient<Dependency>();
        var builder = CreateBuilder(serviceCollection);
 
        builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
 
        var options = new OpenApiOptions();
        Dependency.InstantiationCount = 0;
        options.AddSchemaTransformer((schema, context, cancellationToken) =>
        {
            var service = context.ApplicationServices.GetRequiredService<Dependency>();
            var sameServiceAgain = context.ApplicationServices.GetRequiredService<Dependency>();
            service.TestMethod();
            sameServiceAgain.TestMethod();
            return Task.CompletedTask;
        });
 
        await VerifyOpenApiDocument(builder, options, document => { });
        // Assert that the transient dependency is invoked for each schema
        // in the document. In this case, we have five total schemas in the document.
        // One for the top-level `Todo` type and four for the properties of the `Todo` type.
        // Since we call GetRequiredService twice in the transformer, the total number of
        // instantiations should be 10.
        Assert.Equal(10, Dependency.InstantiationCount);
    }
 
    private class PolymorphicContainer
    {
        public string Name { get; }
        public Shape SomeShape { get; }
    }
 
    private class ActivatedTransformer : IOpenApiSchemaTransformer
    {
        public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
        {
            if (context.JsonTypeInfo.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;
    }
}