-
Notifications
You must be signed in to change notification settings - Fork 10k
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
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?
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'm wondering about your follow up on the above.
…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?
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, any follow-up on my question above?
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 don't think this is a backport candidate. This change is a feature, not fixing a major bug/regression introduced in 8.0.
src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs
Outdated
Show resolved
Hide resolved
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?
@@ -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.
The microbenchmark essentially duplicates the code in UseMiddleware. While that's fine for a one-off run, it's not great from a maintainability standpoint as we'd need to modify both the microbenchmark and the UseMiddleware code any time we make a change. I'm wondering if we can find a way to toggle whether we use reflection or not so that the microbenchmark just calls the UseMiddleware code directly. |
Hey @NicoBrabers we're starting to wrap up 9.0, would love for this change to make it but not a fan of merging the microbenchmark code as-is (see #55722 (comment)). Could we remove it and leave everything else? At a minimum we have this PR history to show the microbenchmarking 😃 |
Hi @BrennanConroy, I've bought a new house, and I've just been away on a holiday, hence I haven't been able to further improve on the microbenchmarks. Would love to continue that in the future. I agree that for now it's best to remove the benchmarks and wrap it all up! Will do so tomorrow 👍🏼. I'll make sure to tag you after the changes have been made and pushed 👍🏼 |
Awesome thanks!
And congrats 😃 |
Thank you @BrennanConroy! I've removed the microbenchmark code for now, the changes have been pushed. |
Looks like you didn't get around to this? Unless I just missed it 😆 |
Hi @BrennanConroy, might have missed that comment, or forgot about it later on. Will check it a.s.a.p. |
…pport unit-tests.
Hi @BrennanConroy, I've added additional unit-tests to test the constructor injection support. |
<data name="Exception_NoServiceRegistered" xml:space="preserve"> | ||
<value>No service for type '{0}' has been registered.</value> | ||
</data> | ||
<data name="Exception_ActivateMiddlewareNoService" xml:space="preserve"> |
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.
We shouldn't add resource strings that are only used by tests.
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.
Ah, agreed, I'll remove it from the resource. Shall I add it as a constant to the testmethod?
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.
Inline is fine
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've pushed the changes 👍
Co-authored-by: Brennan <brecon@microsoft.com>
…on check in test method..
<data name="Exception_KeyedServicesNotSupported" xml:space="preserve"> | ||
<value>This service provider doesn't support keyed services.</value> | ||
</data> | ||
<data name="Exception_NoServiceRegistered" xml:space="preserve"> |
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.
Sorry, there is this one too 😢
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.
Hehe, don't sweat it, should've checked it. Will run by all resources 👍
…on check in test method.
Thanks @NicoBrabers ! |
It's been a pleasure @BrennanConroy! |
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