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/src/Services/Schemas/OpenApiSchemaStore.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs index 88f1dd4633af..ced7395174b5 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); } /// 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..3f2ce1177aa3 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentConcurrentRequestTests.cs @@ -0,0 +1,26 @@ +// 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() + { + // Arrange + var client = fixture.CreateClient(); + + // Act + await Parallel.ForAsync(0, 150, async (_, ctx) => + { + var response = await client.GetAsync("/openapi/v1.json", ctx); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + }); + } +}