From 89b48eecf7923bedc42882f2fab32af2b76f8f96 Mon Sep 17 00:00:00 2001 From: Justin Lampe Date: Thu, 19 Sep 2024 21:15:46 +0200 Subject: [PATCH 1/5] fix: Allow concurrent requests --- .../Services/Schemas/OpenApiSchemaStore.cs | 15 ++++------ .../OpenApiSchemaReferenceTransformer.cs | 3 +- .../OpenApiDocumentConcurrentRequestTests.cs | 30 +++++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs index 88f1dd4633af..b41a6fc124ec 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs @@ -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.Collections.Concurrent; using System.IO.Pipelines; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Http; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.OpenApi; /// internal sealed class OpenApiSchemaStore { - private readonly Dictionary _schemas = new() + private readonly ConcurrentDictionary _schemas = new() { // Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core. [new OpenApiSchemaKey(typeof(IFormFile), null)] = new JsonObject @@ -48,8 +49,8 @@ internal sealed class OpenApiSchemaStore }, }; - public readonly Dictionary SchemasByReference = new(OpenApiSchemaComparer.Instance); - private readonly Dictionary _referenceIdCounter = new(); + public readonly ConcurrentDictionary SchemasByReference = new(OpenApiSchemaComparer.Instance); + private readonly ConcurrentDictionary _referenceIdCounter = new(); /// /// Resolves the JSON schema for the given type and parameter description. @@ -59,13 +60,7 @@ internal sealed class OpenApiSchemaStore /// A representing the JSON schema associated with the key. public JsonNode GetOrAdd(OpenApiSchemaKey key, Func valueFactory) { - if (_schemas.TryGetValue(key, out var schema)) - { - return schema; - } - var targetSchema = valueFactory(key); - _schemas.Add(key, targetSchema); - return targetSchema; + return _schemas.GetOrAdd(key, valueFactory(key)); } /// diff --git a/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs b/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs index 07c76fe22974..35a0da7ff7cf 100644 --- a/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs +++ b/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs @@ -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.Collections.Concurrent; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; @@ -85,7 +86,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC /// The inline schema to replace with a reference. /// A cache of schemas and their associated reference IDs. /// When , will skip resolving references for the top-most schema provided. - internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, Dictionary schemasByReference, bool isTopLevel = false) + internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, ConcurrentDictionary schemasByReference, bool isTopLevel = false) { if (schema is null) { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs new file mode 100644 index 000000000000..2c35a27e6d14 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; + +namespace Microsoft.AspNetCore.OpenApi.Tests.Integration; + +public class OpenApiDocumentConcurrentRequestTests(SampleAppFixture fixture) : IClassFixture +{ + [Fact] + public async Task MapOpenApi_HandlesConcurrentRequests() + { + var client = fixture.CreateClient(); + Task[] requests = + [ + client.GetAsync("/openapi/v1.json"), + client.GetAsync("/openapi/v1.json"), + client.GetAsync("/openapi/v1.json") + ]; + + var results = await Task.WhenAll(requests); + + foreach (var result in results) + { + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + } + + } +} From 0a8373f036c151ae2773f4e5a1cb4aeccade6bce Mon Sep 17 00:00:00 2001 From: Justin Lampe Date: Thu, 19 Sep 2024 21:38:31 +0200 Subject: [PATCH 2/5] test: Update test --- .../OpenApiDocumentConcurrentRequestTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs index 2c35a27e6d14..e3a5e55d40f5 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs @@ -11,20 +11,22 @@ public class OpenApiDocumentConcurrentRequestTests(SampleAppFixture fixture) : I [Fact] public async Task MapOpenApi_HandlesConcurrentRequests() { + // Arrange var client = fixture.CreateClient(); - Task[] requests = - [ + var requests = new List> + { client.GetAsync("/openapi/v1.json"), client.GetAsync("/openapi/v1.json"), client.GetAsync("/openapi/v1.json") - ]; + }; + // Act var results = await Task.WhenAll(requests); + // Assert foreach (var result in results) { Assert.Equal(HttpStatusCode.OK, result.StatusCode); } - } } From cb01ee07234372356dbd64cffd575e0603c91a38 Mon Sep 17 00:00:00 2001 From: Justin Lampe Date: Fri, 20 Sep 2024 15:16:35 +0200 Subject: [PATCH 3/5] test: Use Parallel.ForEachAsync --- .../src/Services/OpenApiDocumentService.cs | 3 ++- .../OpenApiDocumentConcurrentRequestTests.cs | 18 ++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 2745d64770a7..007b6d8d326a 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -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.Collections.Concurrent; using System.Collections.Frozen; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -46,7 +47,7 @@ internal sealed class OpenApiDocumentService( /// are unique within the lifetime of an application and serve as helpful associators between /// operations, API descriptions, and their respective transformer contexts. /// - private readonly Dictionary _operationTransformerContextCache = new(); + private readonly ConcurrentDictionary _operationTransformerContextCache = new(); private static readonly ApiResponseType _defaultApiResponseType = new() { StatusCode = StatusCodes.Status200OK }; private static readonly FrozenSet _disallowedHeaderParameters = new[] { HeaderNames.Accept, HeaderNames.Authorization, HeaderNames.ContentType }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs index e3a5e55d40f5..3f2ce1177aa3 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs @@ -13,20 +13,14 @@ public async Task MapOpenApi_HandlesConcurrentRequests() { // Arrange var client = fixture.CreateClient(); - var requests = new List> - { - client.GetAsync("/openapi/v1.json"), - client.GetAsync("/openapi/v1.json"), - client.GetAsync("/openapi/v1.json") - }; // Act - var results = await Task.WhenAll(requests); - - // Assert - foreach (var result in results) + await Parallel.ForAsync(0, 150, async (_, ctx) => { - Assert.Equal(HttpStatusCode.OK, result.StatusCode); - } + var response = await client.GetAsync("/openapi/v1.json", ctx); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + }); } } From b02bdc5a7e0228116531e301ea71ea1f3c86a314 Mon Sep 17 00:00:00 2001 From: Justin Lampe Date: Sat, 21 Sep 2024 11:08:02 +0200 Subject: [PATCH 4/5] feat: Use valueFactory overload --- src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs index b41a6fc124ec..507aa77efd99 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs @@ -60,7 +60,7 @@ internal sealed class OpenApiSchemaStore /// A representing the JSON schema associated with the key. public JsonNode GetOrAdd(OpenApiSchemaKey key, Func valueFactory) { - return _schemas.GetOrAdd(key, valueFactory(key)); + return _schemas.GetOrAdd(key, _ => valueFactory(key)); } /// From 603b836186ec1baad3186eb4b84bf91c15c8ed10 Mon Sep 17 00:00:00 2001 From: Justin Lampe Date: Sat, 21 Sep 2024 11:30:42 +0200 Subject: [PATCH 5/5] feat: Pass valueFactory directly --- src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs index 507aa77efd99..ced7395174b5 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs @@ -60,7 +60,7 @@ internal sealed class OpenApiSchemaStore /// A representing the JSON schema associated with the key. public JsonNode GetOrAdd(OpenApiSchemaKey key, Func valueFactory) { - return _schemas.GetOrAdd(key, _ => valueFactory(key)); + return _schemas.GetOrAdd(key, valueFactory); } ///