Skip to content

Avoid enumerator allocations when evaluating authorization handlers - #70147

Merged
DeagleGross merged 5 commits into
mainfrom
perf/authorization-handler-allocs
Nov 7, 2026
Merged

DeagleGross merged 5 commits into
mainfrom
perf/authorization-handler-allocs

Conversation

@davidfowl

@davidfowl davidfowl commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Avoid enumerator allocations when evaluating authorization handlers

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Avoid OfType iterators and boxed enumerators in authorization handlers.

Description

  • New internal RequirementsOfType<T> struct enumerable. AuthorizationPolicy.Requirements is List<T>.AsReadOnly(), so the requirements a handler sees are usually a ReadOnlyCollection<IAuthorizationRequirement>; in that case it indexes directly and allocates nothing. Any other enumerable falls back to OfType<T>() unchanged. The type's doc comment explains the fast path, the fallback and why T is unconstrained.
  • AuthorizationHandler<T>, AuthorizationHandler<T, TResource> and PassThroughAuthorizationHandler use it in place of context.Requirements.OfType<T>(), keeping their original foreach shape.
  • DefaultAuthorizationService is unchanged from main (the earlier handler-array loop was reverted per review).
  • Adds AuthorizationServiceBenchmark to src/Security/perf/Microbenchmarks.

No public API or behavior change. Tests: Microsoft.AspNetCore.Authorization.Test 213/213 on net12.0. RequirementsOfTypeTests compares the enumerable against OfType<T>() over ReadOnlyCollection, List, array and lazy-enumerable inputs; DefaultAuthorizationServiceTests runs typed, resource and pass-through handlers over the same collection kinds. net462, netstandard2.0 and net12.0 build with no warnings.

Benchmark

AuthorizationServiceBenchmark (added in this PR), measured on the latest head (e4166f6). In-process BenchmarkDotNet (--config perflab, [MemoryDiagnoser], Server GC) on .NET 12.0.0 (12.0.26.55606) X64 RyuJIT, SDK 12.0.100-alpha.1.26531.102, 2-core Linux VM. "main" is this PR's merge base (7816523) with identical benchmark source. 3 rounds, alternating main and PR.

Method main Mean (r1 / r2 / r3) PR Mean (r1 / r2 / r3) Δ time (avg) main Alloc PR Alloc
AuthorizeAsync_DefaultPolicy 196.2 / 192.2 / 200.0 ns 152.9 / 145.2 / 145.5 ns 196.1 → 147.9 ns (−25%) 632 B 456 B
AuthorizeAsync_ClaimsAndRolePolicy 363.8 / 369.2 / 366.4 ns 258.3 / 253.0 / 261.6 ns 366.5 → 257.6 ns (−30%) 792 B 528 B
Invoke_HasEndpoint_Authorize 472.6 / 476.7 / 478.6 ns 412.2 / 405.1 / 399.6 ns 476.0 → 405.6 ns (−15%) 1,032 B 856 B

The ranges don't overlap in any row, and allocations were identical in every round. The savings (176 B and 264 B) are 2× and 3× 88 B, consistent with one OfType iterator plus boxed enumerator removed per handler call.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

AuthorizationHandler<T>, AuthorizationHandler<T, TResource> and
PassThroughAuthorizationHandler index into the policy's ReadOnlyCollection of
requirements instead of allocating an OfType iterator per call, and
DefaultAuthorizationService iterates the DI-provided handler array directly.
Other requirement/handler collections keep using the existing enumeration.
Adds an authorization service/middleware microbenchmark.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 23:03
@github-actions github-actions Bot added the area-auth Includes: authentication, authorization, OAuth, OIDC, and access token validation label Oct 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.

🟢 Approval recommended

No active review issues are reported for the authorization iteration optimizations.

No open issues 🎉

What changed in this PR

This PR reduces iterator and enumerator allocations during authorization evaluation by adding fast paths for policy requirement collections and dependency-injected handler arrays, while preserving fallback enumeration.

Changes:

  • Optimize typed and pass-through requirement handling, plus service-level handler iteration.
  • Add tests for four requirement collection shapes, resource matching, and stopping after failure.
  • Add authorization service and middleware benchmarks.
File Description
src/​Security/​perf/​Microbenchmarks/​Microsoft.AspNetCore.Security.Microbenchmarks.csproj Adds the routing reference needed by the benchmarks.
src/​Security/​perf/​Microbenchmarks/​AuthorizationServiceBenchmark.cs Adds service and middleware authorization benchmarks.
src/​Security/​Authorization/​test/​DefaultAuthorizationServiceTests.cs Tests collection variants, typed handlers, and failure short-circuiting.
src/​Security/​Authorization/​Core/​src/​PassThroughAuthorizationHandler.cs Adds indexed iteration for read-only requirement collections.
src/​Security/​Authorization/​Core/​src/​DefaultAuthorizationService.cs Iterates handler arrays directly to avoid enumerator allocation.
src/​Security/​Authorization/​Core/​src/​AuthorizationHandler.cs Adds indexed requirement iteration to both generic handler variants.

Review effort: Balanced

@PureWeen

PureWeen commented Nov 1, 2026

Copy link
Copy Markdown
Member

/review

@davidfowl davidfowl added the agent:working An AI agent is actively working on this issue/PR label Nov 6, 2026 — with Copilot SWE Agent
Move the ReadOnlyCollection index loop used by AuthorizationHandler<T>,
AuthorizationHandler<T, TResource> and PassThroughAuthorizationHandler into
an internal RequirementsOfType<T> struct enumerable, so the call sites keep
their original single foreach. Also drop Arrange/Act/Assert comments from
the added tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl removed the agent:working An AI agent is actively working on this issue/PR label Nov 6, 2026
/// Equivalent to <c>requirements.OfType&lt;T&gt;()</c>, but indexes into the <see cref="ReadOnlyCollection{T}"/>
/// returned by <see cref="AuthorizationPolicy.Requirements"/> to avoid allocating an iterator.
/// </summary>
internal readonly struct RequirementsOfType<T>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

restrict T to be of IAuthorizationRequirement?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I tried where T : IAuthorizationRequirement and it doesn't compile. PassThroughAuthorizationHandler uses the same helper to find requirements that are also handlers:

foreach (var handler in new RequirementsOfType<IAuthorizationHandler>(context.Requirements))

IAuthorizationHandler doesn't derive from IAuthorizationRequirement, so every target fails with CS0311 ("no implicit reference conversion from 'IAuthorizationHandler' to 'IAuthorizationRequirement'"). That call site is a straight replacement for the original context.Requirements.OfType<IAuthorizationHandler>(), so I left T unconstrained and made no code change for this thread. If you'd prefer, I can constrain T to class, or drop the helper from PassThroughAuthorizationHandler and go back to OfType there so the constraint fits. The new tests in b86659d cover the IAuthorizationHandler filter case explicitly.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@davidfowl davidfowl added the agent:working An AI agent is actively working on this issue/PR label Nov 7, 2026 — with Copilot SWE Agent
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl removed the agent:working An AI agent is actively working on this issue/PR label Nov 7, 2026

@DeagleGross DeagleGross left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

recalculate the perf benefit again please and update pr description to know the benefit based on the latest code changes

@davidfowl davidfowl added the agent:working An AI agent is actively working on this issue/PR label Nov 7, 2026 — with Copilot SWE Agent
davidfowl and others added 2 commits November 7, 2026 11:23
…ndler-allocs

# Conflicts:
#	src/Security/perf/Microbenchmarks/Microsoft.AspNetCore.Security.Microbenchmarks.csproj
…entsOfType fast path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Member Author

Re-measured on the latest head (e4166f6, with DefaultAuthorizationService reverted) against the merge base, 3 interleaved local BenchmarkDotNet rounds on .NET 12. The win holds and the round ranges don't overlap:

Method main PR Alloc
AuthorizeAsync_DefaultPolicy 196.1 ns 147.9 ns (−25%) 632 → 456 B
AuthorizeAsync_ClaimsAndRolePolicy 366.5 ns 257.6 ns (−30%) 792 → 528 B
Invoke_HasEndpoint_Authorize 476.0 ns 405.6 ns (−15%) 1,032 → 856 B

Means are averaged over the 3 rounds. Per-round numbers and setup are in the updated PR description. This branch also merges current main, which resolves the conflict.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@davidfowl davidfowl removed the agent:working An AI agent is actively working on this issue/PR label Nov 7, 2026
@DeagleGross
DeagleGross merged commit 31ca3ac into main Nov 7, 2026
27 checks passed
@DeagleGross
DeagleGross deleted the perf/authorization-handler-allocs branch November 7, 2026 19:47
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:47 — with GitHub Actions Active
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:47 — with GitHub Actions Active
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:48 — with GitHub Actions Active
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:49 — with GitHub Actions Active
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:50 — with GitHub Actions Active
@DeagleGross
DeagleGross deployed to copilot-pat-pool November 7, 2026 19:50 — with GitHub Actions Active
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

✅ No documentation PR was created.

Confidence that documentation is needed: 3%

No docs needed: internal performance refactoring of authorization handler requirement enumeration with tests and a benchmark; no public or observable behavior change.

Documentation surfaces considered

  • Conceptual article: Not required — Internal struct is not public API (flagged signal is internal); allocation optimization only.
  • Migration guidance: Not required — No user action needed on upgrade.
  • Breaking change: Not required — No compatibility impact; PR states no public API or behavior change.

This branch was successfully deployed

1 active deployment
copilot-pat-pool — e4166f64 Deployed Nov 7, 2026 by DeagleGross via conclusion #235
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-auth Includes: authentication, authorization, OAuth, OIDC, and access token validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants