Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

VersionedOpenApiOptionsFactory race: concurrent document requests cause NullReferenceException or unconfigured document

Open
#1,228 0 comments 0 reactions 1 assignee View on GitHub

@commonsensesoftware is already working on this.

Since Sep 20, 2026.

Assessment

This issue has not been assessed yet.

Description

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
Dominant language
C#
Stars
3.2k
Forks
721
PR merge metrics
No merged PRs in 30d

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/aspnet-api-versioning

All issues in dotnet/aspnet-api-versioning

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.