From a9eb1fb9709d46d5f12453d19b38934f38ca5d68 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sun, 23 Feb 2025 10:40:22 -0800 Subject: [PATCH 1/6] Add support for AddOpenApiOperationTransformer API --- ...nApiEndpointConventionBuilderExtensions.cs | 13 +++++ src/OpenApi/src/PublicAPI.Unshipped.txt | 1 + .../src/Services/OpenApiDocumentService.cs | 10 ++++ .../Transformers/OperationTransformerTests.cs | 55 +++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointConventionBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointConventionBuilderExtensions.cs index 3e325362f6bf..936eebf24cd4 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointConventionBuilderExtensions.cs @@ -121,4 +121,17 @@ private static void AddAndConfigureOperationForEndpoint(EndpointBuilder endpoint } } } + + /// + /// Adds an OpenAPI operation transformer to the associated + /// with the current endpoint. + /// + /// The . + /// The that modifies the operation in the . + /// A that can be used to further customize the endpoint. + public static TBuilder AddOpenApiOperationTransformer(this TBuilder builder, Func transformer) where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new DelegateOpenApiOperationTransformer(transformer)); + return builder; + } } diff --git a/src/OpenApi/src/PublicAPI.Unshipped.txt b/src/OpenApi/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..e84200ba1e60 100644 --- a/src/OpenApi/src/PublicAPI.Unshipped.txt +++ b/src/OpenApi/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.AspNetCore.Builder.OpenApiEndpointConventionBuilderExtensions.AddOpenApiOperationTransformer(this TBuilder builder, System.Func! transformer) -> TBuilder diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 45fa4fa3f9f0..e778e807eaaf 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -268,6 +268,16 @@ private async Task> GetOperationsAsy var transformer = operationTransformers[i]; await transformer.TransformAsync(operation, operationContext, cancellationToken); } + + // Apply any endpoint-specific operation transformers registered via + // the AddOpenApiOperationTransformer extension method. + var endpointOperationTransformer = description.ActionDescriptor.EndpointMetadata + .OfType() + .LastOrDefault(); + if (endpointOperationTransformer is not null) + { + await endpointOperationTransformer.TransformAsync(operation, operationContext, cancellationToken); + } } return operations; } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs index c85c2b2ec8e8..84d63f8e7473 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs @@ -478,6 +478,61 @@ public async Task OperationTransformer_CanAccessTransientServiceFromContextAppli Assert.Equal(4, Dependency.InstantiationCount); } + [Fact] + public async Task WithOpenApi_CanApplyTransformer() + { + var builder = CreateBuilder(); + + builder.MapGet("/", () => { }) + .AddOpenApiOperationTransformer((operation, context, cancellationToken) => + { + operation.Description = "Operation Description"; + return Task.CompletedTask; + }); + + await VerifyOpenApiDocument(builder, document => + { + Assert.Collection(document.Paths.OrderBy(p => p.Key), + path => + { + Assert.Equal("/", path.Key); + var operation = Assert.Single(path.Value.Operations.Values); + Assert.Equal("Operation Description", operation.Description); + }); + }); + } + + [Fact] + public async Task WithOpenApi_TransformerRunsAfterOtherTransformers() + { + var builder = CreateBuilder(); + + builder.MapGet("/", () => { }) + .AddOpenApiOperationTransformer((operation, context, cancellationToken) => + { + operation.Description = "Operation Description"; + return Task.CompletedTask; + }); + + var options = new OpenApiOptions(); + options.AddOperationTransformer((operation, context, cancellationToken) => + { + operation.Description = "Operation Description 2"; + return Task.CompletedTask; + }); + + await VerifyOpenApiDocument(builder, document => + { + Assert.Collection(document.Paths.OrderBy(p => p.Key), + path => + { + Assert.Equal("/", path.Key); + var operation = Assert.Single(path.Value.Operations.Values); + Assert.Equal("Operation Description", operation.Description); + }); + }); + } + private class ActivatedTransformer : IOpenApiOperationTransformer { public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) From becb36a8c64082b752bdf7eab69d225885e2964c Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sun, 23 Feb 2025 17:54:31 -0800 Subject: [PATCH 2/6] Apply all transformers in order instead of last-one-wins --- .../src/Services/OpenApiDocumentService.cs | 7 ++-- .../Transformers/OperationTransformerTests.cs | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index e778e807eaaf..73ebd9541cb9 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -271,10 +271,9 @@ private async Task> GetOperationsAsy // Apply any endpoint-specific operation transformers registered via // the AddOpenApiOperationTransformer extension method. - var endpointOperationTransformer = description.ActionDescriptor.EndpointMetadata - .OfType() - .LastOrDefault(); - if (endpointOperationTransformer is not null) + var endpointOperationTransformers = description.ActionDescriptor.EndpointMetadata + .OfType(); + foreach (var endpointOperationTransformer in endpointOperationTransformers) { await endpointOperationTransformer.TransformAsync(operation, operationContext, cancellationToken); } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs index 84d63f8e7473..ce8847a22ef1 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs @@ -479,7 +479,7 @@ public async Task OperationTransformer_CanAccessTransientServiceFromContextAppli } [Fact] - public async Task WithOpenApi_CanApplyTransformer() + public async Task AddOpenApiOperationTransformer_CanApplyTransformer() { var builder = CreateBuilder(); @@ -503,7 +503,7 @@ await VerifyOpenApiDocument(builder, document => } [Fact] - public async Task WithOpenApi_TransformerRunsAfterOtherTransformers() + public async Task AddOpenApiOperationTransformer_TransformerRunsAfterOtherTransformers() { var builder = CreateBuilder(); @@ -533,6 +533,44 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task AddOpenApiOperationTransformer_SupportsMultipleTransformers() + { + var builder = CreateBuilder(); + + builder.MapGet("/", () => { }) + .AddOpenApiOperationTransformer((operation, context, cancellationToken) => + { + operation.Description = "Operation Description"; + return Task.CompletedTask; + }) + .AddOpenApiOperationTransformer((operation, context, cancellationToken) => + { + operation.Description += " 2"; + operation.Deprecated = true; + return Task.CompletedTask; + }) + .AddOpenApiOperationTransformer((operation, context, cancellationToken) => + { + operation.Description += " 3"; + operation.OperationId = "OperationId"; + return Task.CompletedTask; + }); + + await VerifyOpenApiDocument(builder, document => + { + Assert.Collection(document.Paths.OrderBy(p => p.Key), + path => + { + Assert.Equal("/", path.Key); + var operation = Assert.Single(path.Value.Operations.Values); + Assert.Equal("Operation Description 2 3", operation.Description); + Assert.True(operation.Deprecated); + Assert.Equal("OperationId", operation.OperationId); + }); + }); + } + private class ActivatedTransformer : IOpenApiOperationTransformer { public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) From 32f88823b10bd2313213ec22ffd3e352e7395df6 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 24 Feb 2025 16:41:47 -0800 Subject: [PATCH 3/6] Add cancellation and async tests to transformers --- .../OpenApiDocumentServiceTestsBase.cs | 10 +-- .../Transformers/DocumentTransformerTests.cs | 88 +++++++++++++++++++ .../Transformers/OperationTransformerTests.cs | 86 ++++++++++++++++++ .../Transformers/SchemaTransformerTests.cs | 88 +++++++++++++++++++ 4 files changed, 266 insertions(+), 6 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs index 00aeaef2e6d5..857325fa0cd3 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs @@ -3,12 +3,10 @@ using System.Reflection; using System.Text; -using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; @@ -29,8 +27,8 @@ public abstract class OpenApiDocumentServiceTestBase { - public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Action verifyOpenApiDocument, CancellationToken cancellationToken = default) - => await VerifyOpenApiDocument(builder, new OpenApiOptions(), verifyOpenApiDocument, cancellationToken); + public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Action verifyOpenApiDocument) + => await VerifyOpenApiDocument(builder, new OpenApiOptions(), verifyOpenApiDocument); public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, OpenApiOptions openApiOptions, Action verifyOpenApiDocument, CancellationToken cancellationToken = default) { @@ -40,12 +38,12 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op verifyOpenApiDocument(document); } - public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument) + public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument, CancellationToken cancellationToken = default) { var builder = CreateBuilder(); var documentService = CreateDocumentService(builder, action); var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope(); - var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken); verifyOpenApiDocument(document); } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs index 30bc6f82f7ca..60064497b8ef 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs @@ -240,6 +240,94 @@ public async Task DocumentTransformer_CanAccessTransientServiceFromContextApplic Assert.Equal(2, Dependency.InstantiationCount); } + [Fact] + public async Task DocumentTransformer_RespectsOperationCancellation() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => { }); + + var options = new OpenApiOptions(); + var transformerCalled = false; + var exceptionThrown = false; + + options.AddDocumentTransformer(async (document, context, cancellationToken) => + { + transformerCalled = true; + try + { + await Task.Delay(5000, cancellationToken); + document.Info.Description = "Should not be set"; + } + catch (OperationCanceledException) + { + exceptionThrown = true; + throw; + } + }); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(100); + + await Assert.ThrowsAsync(async () => + { + await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token); + }); + + Assert.True(transformerCalled); + Assert.True(exceptionThrown); + } + + [Fact] + public async Task DocumentTransformer_ExecutesAsynchronously() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => { }); + + var options = new OpenApiOptions(); + var transformerOrder = new List(); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var tcs2Called = false; + + options.AddDocumentTransformer(async (document, context, cancellationToken) => + { + await tcs1.Task; + transformerOrder.Add(1); + document.Info.Title = "First"; + }); + + options.AddDocumentTransformer((document, context, cancellationToken) => + { + transformerOrder.Add(2); + document.Info.Title += " Second"; + if (!tcs2Called) + { + tcs2Called = true; + tcs2.TrySetResult(); + } + return Task.CompletedTask; + }); + + options.AddDocumentTransformer(async (document, context, cancellationToken) => + { + await tcs2.Task; + transformerOrder.Add(3); + document.Info.Title += " Third"; + }); + + var documentTask = VerifyOpenApiDocument(builder, options, document => + { + Assert.Equal("First Second Third", document.Info.Title); + }); + + await Task.Delay(100); + tcs1.TrySetResult(); + + await documentTask; + + Assert.Equal([1, 2, 3], transformerOrder); + } + private class ActivatedTransformer : IOpenApiDocumentTransformer { public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs index ce8847a22ef1..294912889fe3 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs @@ -571,6 +571,92 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task OperationTransformer_RespectsOperationCancellation() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => { }); + + var options = new OpenApiOptions(); + var transformerCalled = false; + var exceptionThrown = false; + + options.AddOperationTransformer(async (operation, context, cancellationToken) => + { + transformerCalled = true; + try + { + await Task.Delay(5000, cancellationToken); + operation.Description = "Should not be set"; + } + catch (OperationCanceledException) + { + exceptionThrown = true; + throw; + } + }); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(100); + + await Assert.ThrowsAsync(async () => + { + await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token); + }); + + Assert.True(transformerCalled); + Assert.True(exceptionThrown); + } + + [Fact] + public async Task OperationTransformer_ExecutesAsynchronously() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => { }); + + var options = new OpenApiOptions(); + var transformerOrder = new List(); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + + options.AddOperationTransformer(async (operation, context, cancellationToken) => + { + await tcs1.Task; + transformerOrder.Add(1); + operation.Description = "First"; + }); + + options.AddOperationTransformer((operation, context, cancellationToken) => + { + transformerOrder.Add(2); + operation.Description += " Second"; + tcs2.TrySetResult(); + return Task.CompletedTask; + }); + + options.AddOperationTransformer(async (operation, context, cancellationToken) => + { + await tcs2.Task; + transformerOrder.Add(3); + operation.Description += " Third"; + }); + + var documentTask = VerifyOpenApiDocument(builder, options, document => + { + var operation = Assert.Single(document.Paths["/todo"].Operations.Values); + Assert.Equal("First Second Third", operation.Description); + }); + + await Task.Delay(100); + tcs1.TrySetResult(); + + await documentTask; + + // Verify transformers executed in the correct order, once for each transformer + // since there is a single operation in the document. + Assert.Equal([1, 2, 3], transformerOrder); + } + private class ActivatedTransformer : IOpenApiOperationTransformer { public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs index 699512213905..be75489bdb54 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs @@ -848,6 +848,94 @@ public async Task SchemaTransformer_CanAccessTransientServiceFromContextApplicat Assert.Equal(10, Dependency.InstantiationCount); } + [Fact] + public async Task SchemaTransformer_RespectsOperationCancellation() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now)); + + var options = new OpenApiOptions(); + var transformerCalled = false; + var exceptionThrown = false; + + options.AddSchemaTransformer(async (schema, context, cancellationToken) => + { + transformerCalled = true; + try + { + await Task.Delay(5000, cancellationToken); + schema.Description = "Should not be set"; + } + catch (OperationCanceledException) + { + exceptionThrown = true; + throw; + } + }); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(100); + + await Assert.ThrowsAsync(async () => + { + await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token); + }); + + Assert.True(transformerCalled); + Assert.True(exceptionThrown); + } + + [Fact] + public async Task SchemaTransformer_ExecutesAsynchronously() + { + var builder = CreateBuilder(); + builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now)); + + var options = new OpenApiOptions(); + var transformerOrder = new List(); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + + // Assert that transformers wait for completion signal from sibling tasks before running + options.AddSchemaTransformer(async (schema, context, cancellationToken) => + { + await tcs1.Task; + transformerOrder.Add(1); + schema.Description = "First"; + }); + + options.AddSchemaTransformer((schema, context, cancellationToken) => + { + transformerOrder.Add(2); + schema.Description += " Second"; + tcs2.TrySetResult(); + return Task.CompletedTask; + }); + + options.AddSchemaTransformer(async (schema, context, cancellationToken) => + { + await tcs2.Task; + transformerOrder.Add(3); + schema.Description += " Third"; + }); + + var documentTask = VerifyOpenApiDocument(builder, options, document => + { + var operation = Assert.Single(document.Paths["/todo"].Operations.Values); + var schema = operation.Responses["200"].Content["application/json"].Schema; + Assert.Equal("First Second Third", schema.Description); + }); + + await Task.Delay(100); + tcs1.TrySetResult(); + + await documentTask; + + // Each transformer is called a total of 5 times, once for the top-level schema + // and one for each of the four properties within the Todo type. + Assert.Equal([1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], transformerOrder); + } + private class PolymorphicContainer { public string Name { get; } From 4a74ec91a6342c753208f57a43afebde281e982c Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 24 Feb 2025 16:42:26 -0800 Subject: [PATCH 4/6] Fix Timespan declaration --- .../GenerateAdditionalXmlFilesForOpenApiTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs index 9f066e5cab77..f230d7186e5d 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Build.Tests/GenerateAdditionalXmlFilesForOpenApiTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OpenApi.Build.Tests; public class GenerateAdditionalXmlFilesForOpenApiTests { - private static readonly TimeSpan _defaultProcessTimeout = TimeSpan.FromSeconds(120); + private static readonly TimeSpan _defaultProcessTimeout = TimeSpan.FromMinutes(2); [Fact] public void VerifiesTargetGeneratesXmlFiles() From a4557f5daf50325713033201e9590554e9022075 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 25 Feb 2025 11:17:22 -0800 Subject: [PATCH 5/6] Avoid delays in async and cancellation tests --- .../Transformers/DocumentTransformerTests.cs | 14 +++++--------- .../Transformers/OperationTransformerTests.cs | 7 ++++--- .../Transformers/SchemaTransformerTests.cs | 9 +++++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs index 60064497b8ef..600b7f8fadcb 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs @@ -249,13 +249,14 @@ public async Task DocumentTransformer_RespectsOperationCancellation() var options = new OpenApiOptions(); var transformerCalled = false; var exceptionThrown = false; + var tcs = new TaskCompletionSource(); options.AddDocumentTransformer(async (document, context, cancellationToken) => { transformerCalled = true; try { - await Task.Delay(5000, cancellationToken); + await tcs.Task.WaitAsync(cancellationToken); document.Info.Description = "Should not be set"; } catch (OperationCanceledException) @@ -266,7 +267,7 @@ public async Task DocumentTransformer_RespectsOperationCancellation() }); using var cts = new CancellationTokenSource(); - cts.CancelAfter(100); + cts.CancelAfter(1); await Assert.ThrowsAsync(async () => { @@ -287,7 +288,6 @@ public async Task DocumentTransformer_ExecutesAsynchronously() var transformerOrder = new List(); var tcs1 = new TaskCompletionSource(); var tcs2 = new TaskCompletionSource(); - var tcs2Called = false; options.AddDocumentTransformer(async (document, context, cancellationToken) => { @@ -300,11 +300,7 @@ public async Task DocumentTransformer_ExecutesAsynchronously() { transformerOrder.Add(2); document.Info.Title += " Second"; - if (!tcs2Called) - { - tcs2Called = true; - tcs2.TrySetResult(); - } + tcs2.TrySetResult(); return Task.CompletedTask; }); @@ -320,7 +316,7 @@ public async Task DocumentTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", document.Info.Title); }); - await Task.Delay(100); + await Task.Yield(); tcs1.TrySetResult(); await documentTask; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs index 294912889fe3..242d8c67e7ad 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs @@ -580,13 +580,14 @@ public async Task OperationTransformer_RespectsOperationCancellation() var options = new OpenApiOptions(); var transformerCalled = false; var exceptionThrown = false; + var tcs = new TaskCompletionSource(); options.AddOperationTransformer(async (operation, context, cancellationToken) => { transformerCalled = true; try { - await Task.Delay(5000, cancellationToken); + await tcs.Task.WaitAsync(cancellationToken); operation.Description = "Should not be set"; } catch (OperationCanceledException) @@ -597,7 +598,7 @@ public async Task OperationTransformer_RespectsOperationCancellation() }); using var cts = new CancellationTokenSource(); - cts.CancelAfter(100); + cts.CancelAfter(1); await Assert.ThrowsAsync(async () => { @@ -647,7 +648,7 @@ public async Task OperationTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", operation.Description); }); - await Task.Delay(100); + await Task.Yield(); tcs1.TrySetResult(); await documentTask; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs index be75489bdb54..ca99331260b0 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs @@ -857,13 +857,15 @@ public async Task SchemaTransformer_RespectsOperationCancellation() var options = new OpenApiOptions(); var transformerCalled = false; var exceptionThrown = false; + var tcs = new TaskCompletionSource(); + //Assert that transformers wait for completion signal from sibling tasks before running options.AddSchemaTransformer(async (schema, context, cancellationToken) => { transformerCalled = true; try { - await Task.Delay(5000, cancellationToken); + await tcs.Task.WaitAsync(cancellationToken); schema.Description = "Should not be set"; } catch (OperationCanceledException) @@ -874,7 +876,7 @@ public async Task SchemaTransformer_RespectsOperationCancellation() }); using var cts = new CancellationTokenSource(); - cts.CancelAfter(100); + cts.CancelAfter(1); await Assert.ThrowsAsync(async () => { @@ -896,7 +898,6 @@ public async Task SchemaTransformer_ExecutesAsynchronously() var tcs1 = new TaskCompletionSource(); var tcs2 = new TaskCompletionSource(); - // Assert that transformers wait for completion signal from sibling tasks before running options.AddSchemaTransformer(async (schema, context, cancellationToken) => { await tcs1.Task; @@ -926,7 +927,7 @@ public async Task SchemaTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", schema.Description); }); - await Task.Delay(100); + await Task.Yield(); tcs1.TrySetResult(); await documentTask; From 447fac9abcff8f225e0fe6d892cea8e60b26d94f Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 25 Feb 2025 11:20:58 -0800 Subject: [PATCH 6/6] Avoid delays in async and cancellation tests --- .../Transformers/DocumentTransformerTests.cs | 1 - .../Transformers/OperationTransformerTests.cs | 1 - .../Transformers/SchemaTransformerTests.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs index 600b7f8fadcb..27741a0afc4d 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs @@ -316,7 +316,6 @@ public async Task DocumentTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", document.Info.Title); }); - await Task.Yield(); tcs1.TrySetResult(); await documentTask; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs index 242d8c67e7ad..f172ac364f55 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs @@ -648,7 +648,6 @@ public async Task OperationTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", operation.Description); }); - await Task.Yield(); tcs1.TrySetResult(); await documentTask; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs index ca99331260b0..9cec9411254e 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs @@ -927,7 +927,6 @@ public async Task SchemaTransformer_ExecutesAsynchronously() Assert.Equal("First Second Third", schema.Description); }); - await Task.Yield(); tcs1.TrySetResult(); await documentTask;