Skip to content

Skip HttpContext.Items allocation in CsrfProtectionMiddleware hot path#67488

Open
DeagleGross wants to merge 4 commits into
dotnet:mainfrom
DeagleGross:deaglegross-dmkorolev-csrf-perf-no-items
Open

Skip HttpContext.Items allocation in CsrfProtectionMiddleware hot path#67488
DeagleGross wants to merge 4 commits into
dotnet:mainfrom
DeagleGross:deaglegross-dmkorolev-csrf-perf-no-items

Conversation

@DeagleGross

Copy link
Copy Markdown
Member

Fixes degradation noticed by @BrennanConroy (thanks!) #67119 (comment)

CsrfProtectionMiddleware was unconditionally writing MiddlewareInvokedKeys.CsrfProtection to context.Items. I did not run the crank-based tests to confirm allocation improvement, but used a local test (see source code below). The results show that CsrfProtectionMiddleware allocates 0 bytes compared to 264 bytes without the fix.

[Fact]
public async Task CsrfProtection_HotPath_NoEndpointMetadata_DoesNotAllocateItemsDictionary()
{
    var middleware = new Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware(
        next: _ => Task.CompletedTask,
        csrfProtection: new AlwaysAllowCsrfProtection(),
        logger: NullLoggerFactory.Instance.CreateLogger<Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware>());

    var endpoint = new Endpoint(
        requestDelegate: _ => Task.CompletedTask,
        metadata: new EndpointMetadataCollection(),
        displayName: "plaintext");

    static DefaultHttpContext NewContext(Endpoint endpoint)
    {
        var ctx = new DefaultHttpContext();
        ctx.SetEndpoint(endpoint);
        return ctx;
    }

    for (var i = 0; i < 200; i++)
    {
        await middleware.InvokeAsync(NewContext(endpoint));
    }

    const int Iterations = 5_000;

    GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
    var before = GC.GetAllocatedBytesForCurrentThread();
    for (var i = 0; i < Iterations; i++)
    {
        await middleware.InvokeAsync(NewContext(endpoint));
    }
    var bytesPerRequest = (double)(GC.GetAllocatedBytesForCurrentThread() - before) / Iterations;

    GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
    var ctxBefore = GC.GetAllocatedBytesForCurrentThread();
    for (var i = 0; i < Iterations; i++)
    {
        _ = NewContext(endpoint);
    }
    var ctxOnlyPerRequest = (double)(GC.GetAllocatedBytesForCurrentThread() - ctxBefore) / Iterations;

    var middlewareDelta = bytesPerRequest - ctxOnlyPerRequest;
    Assert.True(
        middlewareDelta < 10,
        $"Hot-path allocation regression: middleware added {middlewareDelta:F1} bytes/request " +
        $"on top of DefaultHttpContext baseline ({ctxOnlyPerRequest:F1} bytes).");
}

private sealed class AlwaysAllowCsrfProtection : ICsrfProtection
{
    public ValueTask<CsrfProtectionResult> ValidateAsync(HttpContext context)
        => new(CsrfProtectionResult.Allowed());
}
DeagleGross and others added 3 commits June 30, 2026 14:08
PR dotnet#67119 introduced an unconditional
context.Items[MiddlewareInvokedKeys.CsrfProtection] = ...
write in CsrfProtectionMiddleware.InvokeAsync, which forces the lazy
HttpContext.Items dictionary to allocate on every request. This blew up
allocations on hot non-antiforgery paths (~264 B/request extra; ~1.3 GB/s
at TechEmpower plaintext throughput). See PR dotnet#67119 comment 4835979504.

Gate the marker write on either endpoint == null (re-execute into an
antiforgery-required page must still find the marker) or the matched
endpoint carrying any IAntiforgeryMetadata (both DisableAntiforgery()
for re-execute and the FormFeature backstop for RequiresValidation:true).
For endpoints with no antiforgery metadata at all - the hot non-antiforgery
path - the marker has no consumer and is now skipped.

Adds CsrfProtection_HotPath_NoEndpointMetadata_DoesNotAllocateItemsDictionary
as a regression guard using GC.GetAllocatedBytesForCurrentThread().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@DeagleGross DeagleGross self-assigned this Jun 30, 2026
Copilot AI review requested due to automatic review settings June 30, 2026 12:43
@DeagleGross DeagleGross requested a review from halter73 as a code owner June 30, 2026 12:43
@DeagleGross DeagleGross added area-perf Performance infrastructure issues feature-antiforgery labels Jun 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes the auto-injected CsrfProtectionMiddleware hot path by avoiding unnecessary HttpContext.Items initialization when an endpoint has no antiforgery metadata, while preserving the marker behavior needed for reroute/re-execute scenarios and antiforgery/disable-antiforgery endpoints.

Changes:

  • Conditionally stamps the MiddlewareInvokedKeys.CsrfProtection marker only when endpoint is null or when IAntiforgeryMetadata is present.
  • Updates integration tests to assert the marker is not set for endpoints without antiforgery metadata.
Show a summary per file
File Description
src/DefaultBuilder/src/Internal/CsrfProtectionMiddleware.cs Avoids HttpContext.Items allocation by not stamping the marker on the no-antiforgery-metadata path.
src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/CsrfProtectionIntegrationTests.cs Adjusts assertions to reflect that the marker is not set when the endpoint has no antiforgery metadata.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
Comment thread src/DefaultBuilder/src/Internal/CsrfProtectionMiddleware.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-perf Performance infrastructure issues feature-antiforgery

2 participants