Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

VersionedOpenApiOptionsFactory race: concurrent document requests cause NullReferenceException or unconfigured document

Aperta
#1,228 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

@commonsensesoftware ci sta già lavorando.

Dal 20/9/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

bug triage
Is there an existing issue for this?
  • I have searched the existing issues
Describe the bug

VersionedOpenApiOptionsFactory is registered as a singleton but keeps the context of the options being created in an instance field (private Context? context).
CreateAndConfigure sets it, calls Create, then clears it.
If the options for two documents are created at the same time, one thread can clear the field between another thread's null check and its use.

Expected Behavior

Every document is built with its own versioned configuration regardless of how many documents are requested concurrently.

Steps To Reproduce

Minimal project (two files). It starts a fresh host 20 times and requests /openapi/1.json and /openapi/2.json at the same moment. A run counts as failed if either response is not 200 or has no versioned paths.

Repro.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
    <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
    <PackageReference Include="Asp.Versioning.OpenApi" Version="10.2.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
</ItemGroup>
</Project>

Program.cs

using Asp.Versioning;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

// Starts the app repeatedly and requests both OpenAPI documents at the same moment.
var failures = 0;
const int runs = 20;

for (var i = 0; i < runs; i++)
{
    var builder = WebApplication.CreateBuilder();
    builder.WebHost.UseUrls("http://127.0.0.1:0");
    builder.Logging.ClearProviders();
    builder.Services.AddControllers();
    builder.Services.AddApiVersioning()
        .AddMvc()
        .AddApiExplorer(o => o.GroupNameFormat = "VVV")
        .AddOpenApi();

    var app = builder.Build();
    app.UseExceptionHandler(errorApp => errorApp.Run(context =>
        context.Response.WriteAsync(context.Features.Get<IExceptionHandlerFeature>()?.Error.ToString() ?? string.Empty)));
    app.MapControllers();
    app.MapOpenApi("/openapi/{documentName}.json").WithDocumentPerVersion();
    await app.StartAsync();

    using var client = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
    var gate = new TaskCompletionSource();

    async Task<(bool Ok, string Text)> Get(string name)
    {
        await gate.Task;
        var response = await client.GetAsync($"/openapi/{name}.json");
        var body = await response.Content.ReadAsStringAsync();

        // a correctly configured document lists the versioned paths; an unconfigured one has none
        var ok = response.IsSuccessStatusCode && body.Contains("/api/v");
        var text = $"{name}: {(int)response.StatusCode} length={body.Length} hasPaths={body.Contains("/api/v")}";
        return (ok, response.IsSuccessStatusCode ? text : text + Environment.NewLine + string.Join(Environment.NewLine, body.Split('\n').Take(4)));
    }

    var v1 = Get("1");
    var v2 = Get("2");
    gate.SetResult();
    var results = await Task.WhenAll(v1, v2);

    var ok = results.All(r => r.Ok);
    if (!ok) failures++;
    Console.WriteLine($"run {i,2}: {(ok ? "ok  " : "FAIL")} {string.Join(" | ", results.Select(r => r.Text))}");

    await app.StopAsync();
    await app.DisposeAsync();
}

Console.WriteLine($"{failures} of {runs} runs failed");

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/values")]
public class ValuesV1Controller : ControllerBase
{
    [HttpGet]
    public string Get() => "v1";
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/values")]
public class ValuesV2Controller : ControllerBase
{
    [HttpGet]
    public string Get() => "v2";
}

dotnet run -c Release

Result: between 1 and 4 of 20 runs fail per attempt, usually including the first (cold) run.

Exceptions (if any)
System.NullReferenceException: Object reference not set to an instance of an object.
    at Asp.Versioning.OpenApi.Configuration.VersionedOpenApiOptionsFactory.Create(String name)
    at Asp.Versioning.OpenApi.Configuration.VersionedOpenApiOptionsFactory.CreateAndConfigure(Context newContext)
    at Asp.Versioning.OpenApi.Configuration.ConfigureOpenApiOptions.PostConfigure(String name, OpenApiOptions options)
    at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
    at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
    at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd[TArg](String name, Func`3 createOptions, TArg factoryArgument)
    at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService..ctor(...)
    at Asp.Versioning.OpenApi.Reflection.Class.OpenApiDocumentService.OpenApiDocumentServiceCtor(...)
.NET Version

10.0.401

Anything else?
  • ASP.NET Core version: 10.0.12
  • Asp.Versioning.OpenApi 10.2.2 (the factory on main is unchanged: the mutable context field is still there)
  • OS: Windows 11
Lingua principale
C#
Stelle
3.2k
Fork
721
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/aspnet-api-versioning

Tutte le issue di dotnet/aspnet-api-versioning

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.