Skip to content

Commit f047d2a

Browse files
xC0dexcaptainsafia
authored andcommitted
Fix concurrent request handling for OpenAPI documents (#57972)
* fix: Allow concurrent requests * test: Update test * test: Use Parallel.ForEachAsync * feat: Use valueFactory overload * feat: Pass valueFactory directly
1 parent 0cf9021 commit f047d2a

File tree

4 files changed

+35
-12
lines changed

4 files changed

+35
-12
lines changed

src/OpenApi/src/Services/OpenApiDocumentService.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Collections.Frozen;
56
using System.ComponentModel;
67
using System.ComponentModel.DataAnnotations;
@@ -46,7 +47,7 @@ internal sealed class OpenApiDocumentService(
4647
/// are unique within the lifetime of an application and serve as helpful associators between
4748
/// operations, API descriptions, and their respective transformer contexts.
4849
/// </summary>
49-
private readonly Dictionary<string, OpenApiOperationTransformerContext> _operationTransformerContextCache = new();
50+
private readonly ConcurrentDictionary<string, OpenApiOperationTransformerContext> _operationTransformerContextCache = new();
5051
private static readonly ApiResponseType _defaultApiResponseType = new() { StatusCode = StatusCodes.Status200OK };
5152

5253
private static readonly FrozenSet<string> _disallowedHeaderParameters = new[] { HeaderNames.Accept, HeaderNames.Authorization, HeaderNames.ContentType }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);

src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.IO.Pipelines;
56
using System.Text.Json.Nodes;
67
using Microsoft.AspNetCore.Http;
@@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.OpenApi;
1415
/// </summary>
1516
internal sealed class OpenApiSchemaStore
1617
{
17-
private readonly Dictionary<OpenApiSchemaKey, JsonNode> _schemas = new()
18+
private readonly ConcurrentDictionary<OpenApiSchemaKey, JsonNode> _schemas = new()
1819
{
1920
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
2021
[new OpenApiSchemaKey(typeof(IFormFile), null)] = new JsonObject
@@ -48,8 +49,8 @@ internal sealed class OpenApiSchemaStore
4849
},
4950
};
5051

51-
public readonly Dictionary<OpenApiSchema, string?> SchemasByReference = new(OpenApiSchemaComparer.Instance);
52-
private readonly Dictionary<string, int> _referenceIdCounter = new();
52+
public readonly ConcurrentDictionary<OpenApiSchema, string?> SchemasByReference = new(OpenApiSchemaComparer.Instance);
53+
private readonly ConcurrentDictionary<string, int> _referenceIdCounter = new();
5354

5455
/// <summary>
5556
/// Resolves the JSON schema for the given type and parameter description.
@@ -59,13 +60,7 @@ internal sealed class OpenApiSchemaStore
5960
/// <returns>A <see cref="JsonObject" /> representing the JSON schema associated with the key.</returns>
6061
public JsonNode GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonNode> valueFactory)
6162
{
62-
if (_schemas.TryGetValue(key, out var schema))
63-
{
64-
return schema;
65-
}
66-
var targetSchema = valueFactory(key);
67-
_schemas.Add(key, targetSchema);
68-
return targetSchema;
63+
return _schemas.GetOrAdd(key, valueFactory);
6964
}
7065

7166
/// <summary>

src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Linq;
56
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.OpenApi.Models;
@@ -85,7 +86,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
8586
/// <param name="schema">The inline schema to replace with a reference.</param>
8687
/// <param name="schemasByReference">A cache of schemas and their associated reference IDs.</param>
8788
/// <param name="isTopLevel">When <see langword="true" />, will skip resolving references for the top-most schema provided.</param>
88-
internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, Dictionary<OpenApiSchema, string?> schemasByReference, bool isTopLevel = false)
89+
internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, ConcurrentDictionary<OpenApiSchema, string?> schemasByReference, bool isTopLevel = false)
8990
{
9091
if (schema is null)
9192
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net;
5+
using System.Net.Http;
6+
7+
namespace Microsoft.AspNetCore.OpenApi.Tests.Integration;
8+
9+
public class OpenApiDocumentConcurrentRequestTests(SampleAppFixture fixture) : IClassFixture<SampleAppFixture>
10+
{
11+
[Fact]
12+
public async Task MapOpenApi_HandlesConcurrentRequests()
13+
{
14+
// Arrange
15+
var client = fixture.CreateClient();
16+
17+
// Act
18+
await Parallel.ForAsync(0, 150, async (_, ctx) =>
19+
{
20+
var response = await client.GetAsync("/openapi/v1.json", ctx);
21+
22+
// Assert
23+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
24+
});
25+
}
26+
}

0 commit comments

Comments
 (0)