@@ -19,6 +19,10 @@ public static class RequestLimitExtensions
1919 /// </summary>
2020 public const string Policy = "hmac-client" ;
2121
22+ // Key used to stash the resolved policy name in HttpContext.Items so OnRejectedAsync
23+ // can look up the correct named RequestLimitOptions without knowing the policy name.
24+ private static readonly object _policyItemKey = new ( ) ;
25+
2226 /// <summary>Returns the resolved rate limiting policy name, lowercased.</summary>
2327 /// <param name="policyName">
2428 /// Optional suffix that scopes the policy name, enabling multiple independent limiters in
@@ -60,7 +64,7 @@ public static IServiceCollection AddHmacRateLimiter(
6064 /// </summary>
6165 /// <remarks>
6266 /// <typeparamref name="TProvider"/> is registered as a <see cref="IRequestLimitProvider"/> service.
63- /// On each request the provider's <see cref="IRequestLimitProvider.Get "/> is called; returning
67+ /// On each request the provider's <see cref="IRequestLimitProvider.GetAsync "/> is called; returning
6468 /// <see langword="null"/> falls back to <see cref="RequestLimitOptions.RequestsPerPeriod"/> and
6569 /// <see cref="RequestLimitOptions.BurstFactor"/>. Use <see cref="RequestLimitProvider"/> for
6670 /// configuration-backed per-client limits.
@@ -81,7 +85,9 @@ public static IServiceCollection AddHmacRateLimiter<TProvider>(
8185 Action < RequestLimitOptions > ? configure = null )
8286 where TProvider : class , IRequestLimitProvider
8387 {
84- services . TryAdd ( ServiceDescriptor . Describe ( typeof ( IRequestLimitProvider ) , typeof ( TProvider ) , lifetime ) ) ;
88+ var policy = PolicyName ( policyName ) ;
89+ services . TryAdd ( new ServiceDescriptor ( typeof ( IRequestLimitProvider ) , policy , typeof ( TProvider ) , lifetime ) ) ;
90+
8591 return AddHmacRateLimiterCore ( services , policyName , configure ) ;
8692 }
8793
@@ -108,16 +114,16 @@ private static IServiceCollection AddHmacRateLimiterCore(
108114 {
109115 var policy = PolicyName ( policyName ) ;
110116
111- services . AddOptions < RequestLimitOptions > ( ) ;
117+ services . AddOptions < RequestLimitOptions > ( policy ) ;
112118 if ( configure != null )
113- services . Configure ( configure ) ;
119+ services . Configure ( policy , configure ) ;
114120
115121 services . AddRateLimiter ( options =>
116122 {
117123 options . RejectionStatusCode = StatusCodes . Status429TooManyRequests ;
118124 options . OnRejected = OnRejectedAsync ;
119125
120- options . AddPolicy ( policy , Partition ) ;
126+ options . AddPolicy ( policy , httpContext => Partition ( httpContext , policy ) ) ;
121127 } ) ;
122128
123129 return services ;
@@ -127,7 +133,9 @@ private static async ValueTask OnRejectedAsync(OnRejectedContext context, Cancel
127133 {
128134 var httpContext = context . HttpContext ;
129135
130- var opts = httpContext . RequestServices . GetRequiredService < IOptions < RequestLimitOptions > > ( ) . Value ;
136+ // Look up the policy name and options to determine the appropriate Retry-After value for this request.
137+ var policyName = httpContext . Items [ _policyItemKey ] as string ?? Options . DefaultName ;
138+ var opts = httpContext . RequestServices . GetRequiredService < IOptionsMonitor < RequestLimitOptions > > ( ) . Get ( policyName ) ;
131139
132140 // Prefer the lease's own retry hint; token buckets replenish continuously so the lease
133141 // knows exactly when tokens will be available. Fall back to a short window within the period.
@@ -142,10 +150,14 @@ private static async ValueTask OnRejectedAsync(OnRejectedContext context, Cancel
142150 await httpContext . Response . WriteAsync ( $ "Rate limit exceeded. Retry after { retrySeconds } s.", token ) ;
143151 }
144152
145- private static RateLimitPartition < string > Partition ( HttpContext httpContext )
153+ private static RateLimitPartition < string > Partition ( HttpContext httpContext , string policy )
146154 {
147155 var opts = httpContext . RequestServices
148- . GetRequiredService < IOptions < RequestLimitOptions > > ( ) . Value ;
156+ . GetRequiredService < IOptionsMonitor < RequestLimitOptions > > ( )
157+ . Get ( policy ) ;
158+
159+ // Stash the policy name so OnRejectedAsync can look up the same named options.
160+ httpContext . Items [ _policyItemKey ] = policy ;
149161
150162 var authorizationHeader = httpContext . Request . Headers . Authorization . ToString ( ) ;
151163
@@ -161,9 +173,16 @@ private static RateLimitPartition<string> Partition(HttpContext httpContext)
161173 var endpoint = opts . EndpointSelector ( httpContext ) . ToLowerInvariant ( ) ;
162174
163175 // Per-client override: provider returns null → use options defaults.
164- // GetService returns null when no IRequestLimitProvider is registered (non-generic overload).
165- var provider = httpContext . RequestServices . GetService < IRequestLimitProvider > ( ) ;
166- var limit = provider ? . Get ( client ) ?? new RequestLimit ( opts . RequestsPerPeriod , opts . BurstFactor ) ;
176+ // Keyed lookup targets the provider registered for this specific policy;
177+ // non-keyed fallback preserves backward compat if no keyed registration exists.
178+ var provider = httpContext . RequestServices . GetKeyedService < IRequestLimitProvider > ( policy )
179+ ?? httpContext . RequestServices . GetService < IRequestLimitProvider > ( ) ;
180+
181+ // default to options if provider doesn't exist
182+ // ASP.NET Core's partition callback is synchronous; GetAwaiter().GetResult() is safe here
183+ // because ASP.NET Core has no SynchronizationContext.
184+ var limit = provider ? . GetAsync ( client , httpContext . RequestAborted ) . GetAwaiter ( ) . GetResult ( )
185+ ?? new RequestLimit ( opts . RequestsPerPeriod , opts . BurstFactor ) ;
167186
168187 // Include a content-derived version so that limit changes in configuration
169188 // cause new partition keys — and thus fresh token buckets — rather than
0 commit comments