-
Notifications
You must be signed in to change notification settings - Fork 4.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
Add ability to detect if a service is registered in the DI container #53919
Comments
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue DetailsBackground and MotivationWe have a couple of scenarios where various components want to detect if a type is a registered/resolvable in the DI container without actually resolving the service (as this has side effects). See the following issues:
Proposed APInamespace Microsoft.Extensions.DependencyInjection
{
+ public interface ISupportServiceQuery
+ {
+ bool CanGetService(Type serviceType);
+ }
} Usage Examplesbool? HasServiceType<T>(IServiceProvider sp)
{
return sp.GetService<ISupportServiceQuery>()?.CanGetService(typeof(T));
}
void Tests(IServiceProvider sp)
{
// Assuming the container supports Func<T> and IEnumerable<T>
var supportsQuery = sp.GetRequiredService<ISupportServiceQuery>();
Assert.True(supportsQuery.CanGetService(typeof(Func<IFoo>)));
Assert.True(supportsQuery.CanGetService(typeof(IEnumerable<IFoo>)));
} Null means we can't tell if the service is registered since Risks
@alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper
|
Why? Because presumably most of the libraries here support the functionality (DryIoc does) and the libraries also support the generics. |
I think that was just meant to be a bullet point that clarified "resolvable" vs "registered". If the container natively supports |
I think, no. Funny enough that the |
This is something we need to resolve. If I ask about any
I hope not as that would lead to the side effects that caused this to be created in the first place. If you actually resolve the instance |
Ok, just to be on the same page. And I will talk just about DryIoc.
|
Great! That sounds good. I think this is a good distinction to flesh out:
Right, I think this is what the API needs to be. It doesn't actually check if the service will resolve if you call GetService as that might fail for other reasons. I like the idea that it'll tell you if you can resolve the service at all. Spec tests Basic Servicevar services = new ServiceCollection();
services.AddSingleton<IFoo, Foo>();
var serviceProvider = BuildServiceProvider(services); // Assume this builds a specific implementation
var supportsQuery = serviceProvider.GetRequiredService<ISupportServiceQuery>();
// This is a registered service type in the container, even if IFoo fails to resolve at runtime, it's still resolvable.
Assert.True(supportsQuery.IsService(typeof(IFoo)));
// Foo on the other hand is an implementation type and can't be resolved.
Assert.False(supportsQuery.IsService(typeof(Foo)));
interface IFoo { }
class Foo : IFoo { } IEnumerable<T>var services = new ServiceCollection();
services.AddSingleton<Foo>();
var serviceProvider = BuildServiceProvider(services); // Assume this builds a specific implementation
var supportsQuery = serviceProvider.GetRequiredService<ISupportServiceQuery>();
// True because Foo is a service
Assert.True(supportsQuery.IsService(typeof(IEnumerable<Foo>)));
// True because IEnumerable<Bar> would resolve to an empty list
Assert.True(supportsQuery.IsService(typeof(IEnumerable<Bar>)));
// Open generics don't work
Assert.False(supportsQuery.IsService(typeof(IEnumerable<>)));
class Foo { }
class Bar { } Open genericvar services = new ServiceCollection();
services.AddSingleton(typeof(IOptions<>), typeof(Options<>));
var serviceProvider = BuildServiceProvider(services); // Assume this builds a specific implementation
var supportsQuery = serviceProvider.GetRequiredService<ISupportServiceQuery>();
Assert.False(supportsQuery.IsService(typeof(IOptions<>))); Closed genericvar services = new ServiceCollection();
services.AddSingleton(typeof(IOptions<>), typeof(Options<>));
var serviceProvider = BuildServiceProvider(services); // Assume this builds a specific implementation
var supportsQuery = serviceProvider.GetRequiredService<ISupportServiceQuery>();
// Closed generic test with a wrapper
Assert.True(supportsQuery.IsService(typeof(IOptions<MyOptions>)));
class MyOptions { } Open question:For containers that support more wrapper types like var services = new ServiceCollection();
services.AddTransient<MyService>();
var serviceProvider = BuildServiceProvider(services); // Assume this builds a specific implementation
var supportsQuery = serviceProvider.GetRequiredService<ISupportServiceQuery>();
// Open generics return false, these don't make sense because they can't be resolved
Assert.False(supportsQuery.IsService(typeof(Func<>)));
// These are supported natively by the container and can be resolved even if there's no registration for Func<>
Assert.True(supportsQuery.IsService(typeof(Func<MyService>)));
Assert.True(supportsQuery.IsService(typeof(Lazy<MyService>)));
// These are not in the container so they return false.
Assert.False(supportsQuery.IsService(typeof(Func<MyService2>)));
Assert.False(supportsQuery.IsService(typeof(Lazy<MyService2>)));
class MyService { }
class MyService2 { } |
From an Autofac perspective, checking whether a service is registered does not instantiate the service so that requirement is fine, but there are a couple of things to consider about generics. Querying for Open GenericsWhen a user calls When someone interrogates the component registry to ask if a service is registered, we check whether we have anything (either a direct registration, or a registration source) that can provide that service. Nothing can provide an instance of an open generic, so a call to var builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(Options<>)).As(typeof(IOptions<>));
var container = builder.Build();
Assert.False(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IOptions<>))));
Assert.True(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IOptions<MyOptions>)))); I'd be interested to understand the use-case where someone needs to check for the open generic at runtime rather than the closed generic they actually may need to resolve. WrappersFor var builder = new ContainerBuilder();
// ExampleComponent registered, but not ExampleComponent2.
builder.RegisterInstance(new ExampleComponent());
var container = builder.Build();
// Factory for ExampleComponent? No problem, you registered ExampleComponent already.
Assert.True(container.ComponentRegistry.IsRegistered(new TypedService(typeof(Func<ExampleComponent>))));
// Not registered ExampleComponent2, can't give you a factory for it.
Assert.False(container.ComponentRegistry.IsRegistered(new TypedService(typeof(Func<ExampleComponent2>))));
// As already stated, you can't resolve an open generic, so IsRegistered will be false
Assert.False(container.ComponentRegistry.IsRegistered(new TypedService(typeof(Func<>))));
var builder = new ContainerBuilder();
builder.RegisterInstance(new ExampleComponent());
var container = builder.Build();
// Doesn't matter whether you registered a component or not.
Assert.True(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IEnumerable<ExampleComponent>))));
Assert.True(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IEnumerable<ExampleComponent2>))));
// Still can't check IsRegistered for something that isn't ever resolvable.
Assert.False(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IEnumerable<>)))); Constrained GenericsSomething that hasn't been mentioned yet is constrained generics; Autofac will respect generic constraints when checking if a service is registered: private class GenericComponent<T> : IGenericService<T>
where T : struct
{
}
private interface IGenericService<T>
{
}
var builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(GenericComponent<>)).As(typeof(IGenericService<>));
var container = builder.Build();
// Can provide, generic constraints are valid.
Assert.True(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IGenericService<int>))));
// Nothing provides IGenericService<T> that matches the constraints.
Assert.False(container.ComponentRegistry.IsRegistered(new TypedService(typeof(IGenericService<string>)))); |
You might want semantically different methods for IsRegistered(service type) and the “can resolve” method to differentiate between being explicitly registered and the container being able to “figure out” how to resolve the service type in some cases.
And rather than do this, can you instead use some kind of nullify service registration as a stand in instead of doing the branching logic based on container registrations? That’s always my recommendation to users over relying on magic constructor selection logic
…Sent from my iPhone
On Jun 9, 2021, at 2:25 AM, Maksim Volkau ***@***.***> wrote:
Ok, just to be on the same page. And I will talk just about DryIoc.
For open-generics you may only ask if they registered or not, because you cannot actually resolve something open.
The IEnumerable<> and Func<>, etc. are wrappers in the DryIoc terms, and you may check if the specific wrapper is registered.
Given the closed type X, you may ask if IsRegistered<X>() which does not automatically mean the X is resolvable (missing dependency, wrong lifestyle, etc.).
To check if X is resolvable without creating the instance the simplest robust way (closest to how container will actually resolve things) will be Resolve<Func<X>>(ifUnresolved.ReturnDefault) != null.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
Why this pattern vs. return (sp as ISupportServiceQuery)?.CanGetService(typeof(T)); It seems like the SerivceProvider should be the one answering the question - and not some service that I get from the ServiceProvider.
Lines 50 to 53 in 8671908
|
Where do you draw the line and define minimum criteria? I would strongly disagree with your risk assessment. If, for example, containers disagree on how deep dependencies are checked when calling |
sp.GetService<ISupportRequiredService>()?.CanGetService(typeof(T)); This pattern provides a better composability. I may not want to implement the feature services on the core container because they require additional package dependency. |
I would probably echo @eerhardt around adding the functionality in the same way as At a conceptual level, if a component other than the On the package dependency note, for Autofac we already have a separate library with the MS DI package dependency, and we could add an |
@alistairjevans Thanks for that, I didn't consider the constrained generics scenario. I will add that to the tests above. Also I don't have a concrete scenario for open generics, I'm fine if they return false. I wanted to put this to the group to see what the consensus was.
Maybe. I think the only difference is with respect to the wrappers/factories whatever you want to call the open generic "things" that containers can manifest on the fly. I'm not sure that difference is observable in the scenarios I mentioned above, at least, I don't have a scenario where the differentiation is needed.
This doesn't work for the scenarios listed above. They are from framework code that is trying to inspect the container without side effects. That's the key.
Yes but to @dadhi's point on composability. I might be wrapping the IServiceProvider now I've lost this feature because of that I need to re-implement these optional interfaces just in case the underlying provider supports it.
That's up to the people on this issue to define. I think usually we start with the minimum. I don't see anything fundamentally wrong with the questions you pose, we just need to make some decisions. |
From an Autofac standpoint, we can tell you if something is registered but we can't really tell you without trying to resolve whether you can actually get it. I think maybe it's just a naming thing - I would call it |
We could decide on the minimum, but what if a container performs more thorough magic than the baseline and succeeds where default container fails? Not an unreasonable scenario, I had one case like this with internal This will break expected behavior and create two different paths of execution. This type of errors are absolutely impossible for the regular user, without ASP.NET code, to detect and fix. |
Done. I renamed it to
I'm not sure how this would break based on the intended use cases. Supporting more than the default container is fine and what other containers do today (supporting less on the other hand is what is broken). This is what the specification tests are for. Can you clarify what the broken scenario is? |
Sounds like a missed test case. These happen but they don't mean we should stop and do nothing. They get fixed and we move on. |
It is more than that. |
I don't know why that matters. If you're using unity, then you can resolve structs. That feature doesn't need to be in the base feature set. |
To satisfy scenarios this type is intended for I think the semantics have to be similar to |
Agreed @pakrym. I like IsResolvable better than IsRegistered. I still think I like CanGetService best.
In addition to @dadhi's point on composability, there's also precedent here with IServiceScopeFactory. |
And there's also precedent the other way with |
From the original description of the use case, it was more about things like
With the dynamic nature of registration and resolution - from lambda expressions to registrations that can be added on the fly in child lifetime scopes - there's not a reliable way for Autofac to calculate whether something can be resolved without actually doing the resolution. If the goal is to determine if it's resolvable, that likely won't be something we can support. If the goal is to query whether the registration is present, that's something we can do. For example, consider a registration like this: var builder = new ContainerBuilder();
builder.Register(ctx => {
var config = ctx.Resolve<IConfiguration>();
var section = config.GetSection("data");
if(section == null) {
throw new InvalidOperationException("Missing configuration.");
}
return new Component(config["value"]);
}).As<IComponent>();
var container = builder.Build(); I can tell you that (It may appear to be a contrived use case, but it's actually pretty common to see stuff like this.) Point being - there's a significant difference between |
Hi guys and sorry for being late to to the party :) This feature has been in LightInject since the early days and it is called It is NOT a guarantee that the service actually can be resolved. It basically checks to see of the service is registered So given these services
The following example shows that
As we can see Going down the route of I think it is crucial to keep the behaviour of the feature as simple as possible, meaning that if it is registered it is considered "resolvable". As for
For This is how the feature basically works in LightInject and it is used to determine the "most resolvable constructor". |
@seesharper This is EXACTLY what we were thinking. |
I updated the spec tests above to specify the behavior of
|
Are you sure it's correct for MEDI? IIRC, you can resolve IEnumerable<> of non-registered service just fine. |
That's true, it'll just return an empty enumeration. OK this one is actually special. So I've updated the tests |
I updated the spec tests above to specify the behavior of IEnumerable<T> where T is a valid service type vs when it is not. Essentially, for the built-in wrapper types like IEnumerable<T> (the only one supported out of the box by the spec), Func<T>, Lazy<T>:
ISupportServiceQuery.IsService(IEnumerable<T>) is true if ISupportServiceQuery.IsService(T) is also true
Perhaps the same behavior should be added for the array?
|
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceProviderIsService
{
bool IsService(Type serviceType);
}
} |
|
Unrelated to the naming: I had mentioned during the chat that results from calling the At least in Autofac, we have the ability to register things into child lifetime scopes at scope creation time. While the container itself is immutable, this mechanism allows you to, for example, add the current var builder = new ContainerBuilder();
builder.RegisterType<Component1>().As<IService1>();
var container = builder.Build();
Assert.True(container.IsRegistered<IService1>());
Assert.False(container.IsRegistered<IService2>());
using var scope = container.BeginLifetimeScope(b => b.RegisterType<Component2>().As<IService2>())
{
Assert.True(scope.IsRegistered<IService1>());
Assert.True(scope.IsRegistered<IService2>());
} In Web API we ran into some challenges getting per-request action filters to work due to the aggressive caching of the filter instances. Pretty much all of that has been fixed with ASP.NET Core. However, if/when |
I opened a PR for this change, let me know if I missed any edge cases in the spec tests. |
@alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper @jeremydmiller @alistairjevans I assume you'll are gonna add support for this interface once .NET 6 ships? |
For Autofac, based on what we've done before, we may have a pre-release package against a 6 preview ready a little while before the release, and then yes, release a new version once .NET 6 drops. |
For DryIoc, yes. Will be tested first with the preview. |
Yes I plan to support it in Grace soon after .net 6 releases. |
LightInject will implement the interface in good time before the .Net 6 release 👍 |
It will be added in next major release |
Thought I'd mention that we've just pushed 7.2.0-preview.1 of Autofac.Extensions.DependencyInjection, which consumes .NET 6 Preview 6 and adds support for |
Woop! |
Background and Motivation
We have a couple of scenarios where various components want to detect if a type is a registered/resolvable in the DI container without actually resolving the service (as this has side effects). See the following issues:
This would be an optional service that could be implemented by a DI container implementation similar to ISupportRequiredService. Since it is optional, consumers need to decide what to do if the interface isn't implemented.
Proposed API
Usage Examples
Null means we can't tell if the service is registered since
ISupportServiceQuery
is optional.Risks
None
DI council: @alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper @jeremydmiller @alistairjevans
@halter73 @DamianEdwards
The text was updated successfully, but these errors were encountered: