Repository navigation
Avoid enumerator allocations when evaluating authorization handlers - #70147
Conversation
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>
There was a problem hiding this comment.
🟢 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
|
/review |
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>
| /// Equivalent to <c>requirements.OfType<T>()</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> |
There was a problem hiding this comment.
restrict T to be of IAuthorizationRequirement?
There was a problem hiding this comment.
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)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DeagleGross
left a comment
There was a problem hiding this comment.
recalculate the perf benefit again please and update pr description to know the benefit based on the latest code changes
…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>
|
Re-measured on the latest head (e4166f6, with
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) |
|
✅ 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
|
Avoid enumerator allocations when evaluating authorization handlers
Avoid OfType iterators and boxed enumerators in authorization handlers.
Description
RequirementsOfType<T>struct enumerable.AuthorizationPolicy.RequirementsisList<T>.AsReadOnly(), so the requirements a handler sees are usually aReadOnlyCollection<IAuthorizationRequirement>; in that case it indexes directly and allocates nothing. Any other enumerable falls back toOfType<T>()unchanged. The type's doc comment explains the fast path, the fallback and whyTis unconstrained.AuthorizationHandler<T>,AuthorizationHandler<T, TResource>andPassThroughAuthorizationHandleruse it in place ofcontext.Requirements.OfType<T>(), keeping their originalforeachshape.DefaultAuthorizationServiceis unchanged from main (the earlier handler-array loop was reverted per review).AuthorizationServiceBenchmarkto src/Security/perf/Microbenchmarks.No public API or behavior change. Tests: Microsoft.AspNetCore.Authorization.Test 213/213 on net12.0.
RequirementsOfTypeTestscompares the enumerable againstOfType<T>()overReadOnlyCollection,List, array and lazy-enumerable inputs;DefaultAuthorizationServiceTestsruns 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.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
OfTypeiterator plus boxed enumerator removed per handler call.🤖 Posted by ASP.NET Core Perf 2 (AI Agent)