Skip to content
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

Merged

Conversation

NicoBrabers
Copy link
Contributor

@NicoBrabers NicoBrabers commented May 14, 2024

Middleware keyed dependency injection (fix for issue #54500)

  • 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.

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

…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-issue-labeler dotnet-issue-labeler bot added the area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions label May 14, 2024
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label May 14, 2024
@NicoBrabers
Copy link
Contributor Author

@NicoBrabers please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"

Contributor License Agreement

@dotnet-policy-service agree company="Progresity"

@gfoidl gfoidl added area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlesware and removed area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions labels May 15, 2024
Copy link
Member

@gfoidl gfoidl left a comment

Choose a reason for hiding this comment

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

Two small nits.

@NicoBrabers NicoBrabers requested a review from gfoidl May 15, 2024 12:41
…new lines) based on pull-request feedback.
@NicoBrabers NicoBrabers force-pushed the 54500-keyed-service-middleware-injection branch from f01cba9 to c4a279f Compare May 15, 2024 13:08
@amcasey
Copy link
Member

amcasey commented May 15, 2024

FYI @benjaminpetit @captainsafia

methodArguments[i] = GetService(serviceProvider, parameters[i].ParameterType, methodInfo.DeclaringType!);
var parameter = parameters[i];

var hasServiceKey = TryGetServiceKey(parameter, out object? key);
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@NicoBrabers NicoBrabers May 17, 2024

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];
Copy link
Member

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?

Copy link
Contributor Author

@NicoBrabers NicoBrabers May 17, 2024

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.

Copy link
Member

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.

Copy link
Contributor Author

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 🥱

Copy link
Contributor Author

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?

Copy link
Contributor Author

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))
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

@BrennanConroy BrennanConroy left a 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?

src/Http/Http.Abstractions/test/UseMiddlewareTest.cs Outdated Show resolved Hide resolved
@@ -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];
Copy link
Member

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.

NicoBrabers added 2 commits May 18, 2024 01:47
…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.
@BrennanConroy
Copy link
Member

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.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jun 11, 2024
@BrennanConroy
Copy link
Member

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 😃

@NicoBrabers
Copy link
Contributor Author

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 👍🏼

@BrennanConroy
Copy link
Member

Awesome thanks!

I've bought a new house

And congrats 😃

@NicoBrabers
Copy link
Contributor Author

Awesome thanks!

I've bought a new house

And congrats 😃

Thank you @BrennanConroy!

I've removed the microbenchmark code for now, the changes have been pushed.

@BrennanConroy
Copy link
Member

The issue mentions adding support for constructor injection as well. Could you add a test for that?

Looks like you didn't get around to this? Unless I just missed it 😆

@NicoBrabers
Copy link
Contributor Author

NicoBrabers commented Jul 24, 2024

The issue mentions adding support for constructor injection as well. Could you add a test for that?

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.

@NicoBrabers
Copy link
Contributor Author

The issue mentions adding support for constructor injection as well. Could you add a test for that?

Looks like you didn't get around to this? Unless I just missed it 😆

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">
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

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

Inline is fine

Copy link
Contributor Author

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 👍

src/Http/Http.Abstractions/test/UseMiddlewareTest.cs Outdated Show resolved Hide resolved
<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">
Copy link
Member

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 😢

Copy link
Contributor Author

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 👍

@BrennanConroy BrennanConroy merged commit e29ea10 into dotnet:main Jul 25, 2024
22 of 25 checks passed
@BrennanConroy
Copy link
Member

Thanks @NicoBrabers !

@dotnet-policy-service dotnet-policy-service bot added this to the 9.0-rc1 milestone Jul 25, 2024
@BrennanConroy BrennanConroy removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jul 25, 2024
@NicoBrabers
Copy link
Contributor Author

Thanks @NicoBrabers !

It's been a pleasure @BrennanConroy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlesware community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Keyed services fail to dependency inject into middleware
6 participants