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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

NicoBrabers
Copy link

@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
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
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
Author

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
Author

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
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
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?

…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
Author

Choose a reason for hiding this comment

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

Copy link
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?

}

return (Task)methodInfo.Invoke(middleware, BindingFlags.DoNotWrapExceptions, binder: null, methodArguments, culture: null)!;
};
}

private static bool TryGetServiceKey(ParameterInfo parameterInfo, 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.

Suggested change
private static bool TryGetServiceKey(ParameterInfo parameterInfo, out object? key)
private static bool TryGetServiceKey(ParameterInfo parameterInfo, [MemberNotNullWhen(true)] out object? key)

Copy link
Author

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.

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?

@@ -293,13 +379,27 @@ private class MiddlewareInjectInvokeNoService
public Task Invoke(HttpContext context, object value) => Task.CompletedTask;
}

private class MiddlewareKeyedInjectInvokeNoService
Copy link
Member

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?

Copy link
Author

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];
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.
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