Skip to content

Commit

Permalink
Fix concurrent request handling for OpenAPI documents (#57972)
Browse files Browse the repository at this point in the history
* fix: Allow concurrent requests

* test: Update test

* test: Use Parallel.ForEachAsync

* feat: Use valueFactory overload

* feat: Pass valueFactory directly
  • Loading branch information
xC0dex committed Sep 22, 2024
1 parent 7c73ecd commit cd7eec7
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 12 deletions.
3 changes: 2 additions & 1 deletion src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
/// </summary>
private readonly Dictionary<string, OpenApiOperationTransformerContext> _operationTransformerContextCache = new();
private readonly ConcurrentDictionary<string, OpenApiOperationTransformerContext> _operationTransformerContextCache = new();
private static readonly ApiResponseType _defaultApiResponseType = new() { StatusCode = StatusCodes.Status200OK };

private static readonly FrozenSet<string> _disallowedHeaderParameters = new[] { HeaderNames.Accept, HeaderNames.Authorization, HeaderNames.ContentType }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
Expand Down
15 changes: 5 additions & 10 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.OpenApi;
/// </summary>
internal sealed class OpenApiSchemaStore
{
private readonly Dictionary<OpenApiSchemaKey, JsonNode> _schemas = new()
private readonly ConcurrentDictionary<OpenApiSchemaKey, JsonNode> _schemas = new()
{
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
[new OpenApiSchemaKey(typeof(IFormFile), null)] = new JsonObject
Expand Down Expand Up @@ -48,8 +49,8 @@ internal sealed class OpenApiSchemaStore
},
};

public readonly Dictionary<OpenApiSchema, string?> SchemasByReference = new(OpenApiSchemaComparer.Instance);
private readonly Dictionary<string, int> _referenceIdCounter = new();
public readonly ConcurrentDictionary<OpenApiSchema, string?> SchemasByReference = new(OpenApiSchemaComparer.Instance);
private readonly ConcurrentDictionary<string, int> _referenceIdCounter = new();

/// <summary>
/// Resolves the JSON schema for the given type and parameter description.
Expand All @@ -59,13 +60,7 @@ internal sealed class OpenApiSchemaStore
/// <returns>A <see cref="JsonObject" /> representing the JSON schema associated with the key.</returns>
public JsonNode GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonNode> 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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -85,7 +86,7 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
/// <param name="schema">The inline schema to replace with a reference.</param>
/// <param name="schemasByReference">A cache of schemas and their associated reference IDs.</param>
/// <param name="isTopLevel">When <see langword="true" />, will skip resolving references for the top-most schema provided.</param>
internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, Dictionary<OpenApiSchema, string?> schemasByReference, bool isTopLevel = false)
internal static OpenApiSchema? ResolveReferenceForSchema(OpenApiSchema? schema, ConcurrentDictionary<OpenApiSchema, string?> schemasByReference, bool isTopLevel = false)
{
if (schema is null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SampleAppFixture>
{
[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);
});
}
}

0 comments on commit cd7eec7

Please sign in to comment.