-
Notifications
You must be signed in to change notification settings - Fork 9.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix for issue #54500: Middleware keyed dependency injection #55722
base: main
Are you sure you want to change the base?
Fix for issue #54500: Middleware keyed dependency injection #55722
Conversation
…ction to Microsoft.AspNetCore.Http.Abstractions and utilized the ActivatorUtilities it provides to obtain a middleware instance. Also changed the ReflectionMiddlewareBinder to be able to handle keyed injection.
@dotnet-policy-service agree company="Progresity" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two small nits.
src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs
Outdated
Show resolved
Hide resolved
…new lines) based on pull-request feedback.
f01cba9
to
c4a279f
Compare
methodArguments[i] = GetService(serviceProvider, parameters[i].ParameterType, methodInfo.DeclaringType!); | ||
var parameter = parameters[i]; | ||
|
||
var hasServiceKey = TryGetServiceKey(parameter, out object? key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to happen on every single request, probably worth moving outside of the factory method since the data is the same for every single call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Brennan,
Thanks for your valuable feedback.
I've made some changes to reflect the feedback. I now precompute and cache the results for each parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy, I could eliminate the condition check (hasServiceKey ? GetKeyedService(...) : GetService(...)) as wel by compiling the method calls into delegates, and calling to the delegate directly within the factory. We'd probably be talking about a microsecond though. The code might become slightly less readable though.
What's your opinion on the matter?
… parameter within the ReflectionFallback.
@@ -209,19 +210,69 @@ public RequestDelegate CreateMiddleware(RequestDelegate next) | |||
} | |||
} | |||
|
|||
// Performance optimization: Precompute and cache the results for each parameter | |||
var parameterData = new (bool hasServiceKey, object? key, Type parameterType, Type declaringType)[parameters.Length]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You only really need to store an array of object? key
. The other properties are already being captured by the closure and the check for whether to get a keyed service is key == null ? GetService : GetKeyedService
.
https://github.com/dotnet/runtime/blob/26bcc1d12f895d654e24f7cbcfed466b6a34e65f/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ActivatorUtilities.cs#L381
@benjaminpetit should we be throwing if null
is passed in for key in FromKeyedServices
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy, I'll change the logic according to your advice.
The reason as to why I've precomputed and cached the parameter- and declaringType is due to the fact that these are actually method calls to get the property value which will happen every time the factory is used – twice since we'd be retrieving two values. As well as obtain the parameter from the parameters array. Since using the tuple provides direct memory access, I figured it would be performing better for high throughput applications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love improving perf, so appreciate you thinking about that 😃
But we probably shouldn't prematurely optimize without measuring. You can try it out in https://github.com/dotnet/aspnetcore/tree/main/src/Http/Http.Abstractions/perf/Microbenchmarks if you're up for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy, totally up for it! Tomorrow though, I need some sleep first 🥱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy, I've added some benchmarks, as well as the possible optimizations. It seems that precomputing and caching all the parameters is the most optimized option.
benchmarks.txt
I've also introduced a new optimization which would cache a delegate based on the GetService or GetKeyedService method call, however, calling to the cached delegate itself seems to have more overhead than the key check and direct access to the method.
Based on these benchmarks, should we go with the optimization to cache all the parameters rather than just the key? What's your opinion on the matter?
…ach parameter within the ReflectionFallback, removed the parameterType and declaringType from the precomputed cache.
{ | ||
if (parameterInfo.CustomAttributes != null) | ||
{ | ||
foreach (var attribute in parameterInfo.GetCustomAttributes(true)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parameterInfo.OfType<FromKeyedServicesAttribute>().FirstOrDefault()
might be more straightforward here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@captainsafia, I've taken your feedback into account and changed the method to be more straightforward. Also changed the Inherit parameter from true to false, to meet the ActivatorUtilities from the Microsoft.Extensions.DependencyInjections.Abstractions package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@captainsafia, @BrennanConroy, the changes within this pull-request target main, and thus the .NET 9.0 version. If I'd want the changes to be available in the latest .NET 8.x.x version as well, what would the appropriate steps for me to take?
} | ||
|
||
return (Task)methodInfo.Invoke(middleware, BindingFlags.DoNotWrapExceptions, binder: null, methodArguments, culture: null)!; | ||
}; | ||
} | ||
|
||
private static bool TryGetServiceKey(ParameterInfo parameterInfo, out object? key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static bool TryGetServiceKey(ParameterInfo parameterInfo, out object? key) | |
private static bool TryGetServiceKey(ParameterInfo parameterInfo, [MemberNotNullWhen(true)] out object? key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@captainsafia, thanks for the feedback. I've implemented it together with some other changes. Would've used the "Commit suggestion" otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue mentions adding support for constructor injection as well. Could you add a test for that?
@@ -293,13 +379,27 @@ private class MiddlewareInjectInvokeNoService | |||
public Task Invoke(HttpContext context, object value) => Task.CompletedTask; | |||
} | |||
|
|||
private class MiddlewareKeyedInjectInvokeNoService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem like this class is needed? Just use the other class and don't add services to DI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy, hmm, appears to be the case. I'll double check it in the morning and adjust it accordingly.
@@ -209,19 +210,69 @@ public RequestDelegate CreateMiddleware(RequestDelegate next) | |||
} | |||
} | |||
|
|||
// Performance optimization: Precompute and cache the results for each parameter | |||
var parameterData = new (bool hasServiceKey, object? key, Type parameterType, Type declaringType)[parameters.Length]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love improving perf, so appreciate you thinking about that 😃
But we probably shouldn't prematurely optimize without measuring. You can try it out in https://github.com/dotnet/aspnetcore/tree/main/src/Http/Http.Abstractions/perf/Microbenchmarks if you're up for it.
…d added the NotNullWhen(true) attribute. Also changed the Inherit param from true to false when obtaining the custom attribute FromKeyedServicesAttribute.
…test. Added Benchmark(s) for the ReflectionFallback method.
Middleware keyed dependency injection (fix for issue #54500)
Summary of the changes (Less than 80 chars)
Description
Added Microsoft.Extensions.DependencyInjection to Microsoft.AspNetCore.Http.Abstractions and utilized the ActivatorUtilities it provides to obtain a middleware instance. Also changed the ReflectionMiddlewareBinder to be able to handle keyed injection.
Fixes #54500