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

Add ability to detect if a service is registered in the DI container #53919

Closed
davidfowl opened this issue Jun 9, 2021 · 59 comments · Fixed by #54047
Closed

Add ability to detect if a service is registered in the DI container #53919

davidfowl opened this issue Jun 9, 2021 · 59 comments · Fixed by #54047
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-DependencyInjection
Milestone

Comments

@davidfowl
Copy link
Member

davidfowl commented Jun 9, 2021

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

namespace Microsoft.Extensions.DependencyInjection
{
+    public interface ISupportServiceQuery
+    {
+        bool IsService(Type serviceType);
+    }
}

Usage Examples

bool? HasServiceType<T>(IServiceProvider sp)
{
    return sp.GetService<ISupportServiceQuery>()?.IsService(typeof(T));
}

HasServiceType<TodoDbContext>();
  • Open generics supported by the container will always return true. e.g.:
void Tests(IServiceProvider sp)
{
    // Assuming the container supports Func<T> and IEnumerable<T>
    var supportsQuery = sp.GetRequiredService<ISupportServiceQuery>();
    Assert.True(supportsQuery.IsService(typeof(Func<IFoo>)));
    Assert.True(supportsQuery.IsService(typeof(IEnumerable<IFoo>)));
}

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

@davidfowl davidfowl added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jun 9, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-Extensions-DependencyInjection untriaged New issue has not been triaged by the area owner labels Jun 9, 2021
@ghost
Copy link

ghost commented Jun 9, 2021

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

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:

Proposed API

namespace Microsoft.Extensions.DependencyInjection
{
+    public interface ISupportServiceQuery
+    {
+        bool CanGetService(Type serviceType);
+    }
}

Usage Examples

bool? HasServiceType<T>(IServiceProvider sp)
{
    return sp.GetService<ISupportServiceQuery>()?.CanGetService(typeof(T));
}
  • Open generics supported by the container will always return true. e.g.:
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 ISupportServiceQuery is optional.

Risks

  • Other DI implementations may not be able to support it

@alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper

Author: davidfowl
Assignees: -
Labels:

api-suggestion, area-Extensions-DependencyInjection, untriaged

Milestone: -

@davidfowl davidfowl added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jun 9, 2021
@dadhi
Copy link

dadhi commented Jun 9, 2021

@davidfowl

Open generics supported by the container will always return true.

Why? Because presumably most of the libraries here support the functionality (DryIoc does) and the libraries also support the generics.

@davidfowl
Copy link
Member Author

I think that was just meant to be a bullet point that clarified "resolvable" vs "registered". If the container natively supports Func<> then any Func<T> would return true right?

@dadhi
Copy link

dadhi commented Jun 9, 2021

Func would return true right?

I think, no. Funny enough that the
most containers may implement IsResolvable as TryResolve<Func<X>>() != null

@davidfowl
Copy link
Member Author

I think, no.

This is something we need to resolve. If I ask about any IEnumerable<T> I think it should return true for example. Otherwise it basically doesn't work for open generics. Same with IOptions<T> in the case of generic host based apps.

Funny enough that the
most containers may implement IsResolvable as TryResolve<Func>() != null

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
then you have to worry about disposal (and async disposal).

@dadhi
Copy link

dadhi commented Jun 9, 2021

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.
  • There are other DryIoc specific ways to check resolvability as well (e.g check if container can create the final expression for X), so there is no blockers here.

@davidfowl
Copy link
Member Author

davidfowl commented Jun 9, 2021

Great! That sounds good. I think this is a good distinction to flesh out:

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>(ifUnresolved.ReturnDefault) != null.

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 Service

var 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 generic

var 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 generic

var 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 Func<T>, Lazy<T> etc. I'd expect the following:

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 { } 

@alistairjevans
Copy link
Contributor

alistairjevans commented Jun 9, 2021

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 Generics

When a user calls RegisterGeneric on their container, we do not add an actual registration, we add a 'registration source' that may be able to provide a given service when it is resolved.

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 IsRegistered for the open generic IOptions<> will always return false, unlike querying the closed generic IOptions<MyOptions>, which will return true if Options<> was appropriately registered in the container.

 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.

Wrappers

For Func<> and other factory wrappers, similar rules are followed as for regular generics, in that we will only return true for IsRegistered checks if the registry knows that something can provide that service:

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<>))));

IEnumerable<> is different in that if there are no registrations for a service, you just get an empty set; so Autofac will return true for any closed IEnumerable<>:

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 Generics

Something 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>))));

@jeremydmiller
Copy link

jeremydmiller commented Jun 9, 2021 via email

@eerhardt
Copy link
Member

eerhardt commented Jun 9, 2021

return sp.GetService<ISupportServiceQuery>()?.CanGetService(typeof(T));

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.

This would be an optional service that could be implemented by a DI container implementation similar to ISupportRequiredService.

ISupportRequiredService follows the usage pattern I have above:

if (provider is ISupportRequiredService requiredServiceSupportingProvider)
{
return requiredServiceSupportingProvider.GetRequiredService(serviceType);
}

@ENikS
Copy link
Contributor

ENikS commented Jun 9, 2021

CanGetService(typeof(T)) can get complicated really quick. For example:

  • Is it enough for the container to check if it is a POCO type and assume it could be resolved?
  • Is it generic? Open, closed, constrained? (Each follows different rules)
  • Should it check if there are any public constructors?
  • Can any of the constructors be instantiated, in other words, satisfied with dependencies (how deep do you go into dependency graph )?
  • How about now, after something else has been registered?

Where do you draw the line and define minimum criteria?

@davidfowl

I would strongly disagree with your risk assessment. If, for example, containers disagree on how deep dependencies are checked when calling CanGetService(typeof(T)), it creates incompatibility that is very hard to fix.

@dadhi
Copy link

dadhi commented Jun 9, 2021

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.

@alistairjevans
Copy link
Contributor

I would probably echo @eerhardt around adding the functionality in the same way as ISupportRequiredService, where the IServiceProvider is extended with additional interfaces.

At a conceptual level, if a component other than the IServiceProvider itself is responsible for stating whether that IServiceProvider can provide a service, that feels like a strange disconnect to me. It would be possible for someone to register their own ISupportServiceQuery service override, and give very strange results to the caller that don't match up with the service provider at all.

On the package dependency note, for Autofac we already have a separate library with the MS DI package dependency, and we could add an ISupportServiceQuery to our existing AutofacServiceProvider implementation fairly trivially.

@davidfowl
Copy link
Member Author

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

@jeremydmiller

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.

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.

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

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.

@eerhardt

It seems like the SerivceProvider should be the one answering the question - and not some service that I get from the ServiceProvider.

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.

@ENikS

Where do you draw the line and define minimum criteria?

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.

@tillig
Copy link
Contributor

tillig commented Jun 9, 2021

You might want semantically different methods for IsRegistered(service type) and the “can resolve” method

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

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 IsRegistered or something like that rather than CanGetService simply because of the CanGetService -> GetService implied naming relationship. "I can get it, but when I tried to get it I got an exception!"

@ENikS
Copy link
Contributor

ENikS commented Jun 9, 2021

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.

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 CanResolve in the service provider. Another example.

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.

@davidfowl
Copy link
Member Author

davidfowl commented Jun 9, 2021

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 IsRegistered or something like that rather than CanGetService simply because of the CanGetService -> GetService implied naming relationship. "I can get it, but when I tried to get it I got an exception!"

Done. I renamed it to IsRegistered. As long as it has the intended semantics, I'm flexible on naming. Wrappers like IEnumerable<T> and Func<T> (when closed) are registered even if they weren't explicit added.

@ENikS

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 CanResolve in the service provider.

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?

@davidfowl
Copy link
Member Author

Not an unreasonable scenario, I had one case like this with internal CanResolve in the service provider.
Another example would be behavior like this

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.

@ENikS
Copy link
Contributor

ENikS commented Jun 9, 2021

@davidfowl

It is more than that.
For example, Unity can resolve structs (if they are not ref). It is the only container that does it. So, if constructor has struct parameter Unity will succeed, but default will fail. Other containers might have different peculiarities.

@davidfowl
Copy link
Member Author

For example, Unity can resolve structs (if they are not ref). It is the only container that does it. So, if constructor has struct parameter Unity will succeed, but default will fail. Other containers might have different peculiarities.

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.

@pakrym
Copy link
Contributor

pakrym commented Jun 9, 2021

Done. I renamed it to IsRegistered.

To satisfy scenarios this type is intended for I think the semantics have to be similar to GetService() != null but without actual instantiation. The IsRegistered just muddies the water as IEnumerable<IService> (and other patterns provided by 3rd party containers) are not explicitly registered.

@halter73
Copy link
Member

halter73 commented Jun 9, 2021

Agreed @pakrym. I like IsResolvable better than IsRegistered. I still think I like CanGetService best.

@eerhardt

It seems like the SerivceProvider should be the one answering the question - and not some service that I get from the ServiceProvider.

In addition to @dadhi's point on composability, there's also precedent here with IServiceScopeFactory.

@eerhardt
Copy link
Member

eerhardt commented Jun 9, 2021

there's also precedent here with IServiceScopeFactory.

And there's also precedent the other way with ISupportRequiredService. 😉

@tillig
Copy link
Contributor

tillig commented Jun 9, 2021

From the original description of the use case, it was more about things like ActivatorUtilities determining whether it should even try to resolve the thing; which indicates to me IsRegistered is the intent here, not IsResolvable.

#46132 - This would allow the logic in ActivatorUtilities to pick the constructor based on what services are registered in the container.

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 IComponent is registered. I can't really tell you that it's not actually resolvable because there's logic involved doing service location in an imperative fashion. Even if it were, I can't tell you if the configuration that's required is there and would cause the resolution to fail.

(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 IsRegistered and IsResolvable.

@seesharper
Copy link

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

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

public interface IFoo { }

public class Foo : IFoo
{
    public Foo(IBar bar) { }
}

public interface IBar { }

public class Bar : IBar { }

The following example shows that IFoo is registered, but the dependency IBar is not.

var container = new ServiceContainer();
container.Register<IFoo, Foo>();
Assert.True(container.CanGetInstance(typeof(IFoo), string.Empty));

Note: Never mind the empty string being passed in here. LightInject support named services which is not really relevant here

As we can see IFoo is considered to be "resolvable" just by being registered in the container.

Going down the route of GetService() != null semantics without actually creating an instance would be far more complex and there will be edge cases where it might still not be true.

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 Func<T>, LightInject will check to see if T is registered

var container = new ServiceContainer();
container.Register<IFoo, Foo>();
Assert.True(container.CanGetInstance(typeof(Func<IFoo>), string.Empty));
Assert.False(container.CanGetInstance(typeof(Func<IBar>), string.Empty));

For IEnumerable<T> it will always return true since an empty list is also considered valid

This is how the feature basically works in LightInject and it is used to determine the "most resolvable constructor".

@davidfowl
Copy link
Member Author

@seesharper This is EXACTLY what we were thinking.

@davidfowl
Copy link
Member Author

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

@pakrym
Copy link
Contributor

pakrym commented Jun 10, 2021

ISupportServiceQuery.IsService(IEnumerable) is true if ISupportServiceQuery.IsService(T) is also true

Are you sure it's correct for MEDI? IIRC, you can resolve IEnumerable<> of non-registered service just fine.

@davidfowl
Copy link
Member Author

davidfowl commented Jun 10, 2021

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 IsService(typeof(IEnumerable<Foo>)) where Foo isn't registered should return true.

I've updated the tests

@ENikS
Copy link
Contributor

ENikS commented Jun 10, 2021 via email

@bartonjs
Copy link
Member

bartonjs commented Jun 10, 2021

Video

  • The existing ISupportRequiredService is used with an as-cast from IServiceProvider, so this shouldn't start with ISupport if it's used in a different way.
    • Either a different naming pattern, or change the usage to be as-based instead of GetService-based.
    • Last minute proposal was "IServiceProvider[PrimaryMethodName]" for this new kind of thing.
namespace Microsoft.Extensions.DependencyInjection
{
     public interface IServiceProviderIsService
     {
         bool IsService(Type serviceType);
     }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jun 10, 2021
@davidfowl
Copy link
Member Author

IServiceProviderIsService what is this name 😄

@tillig
Copy link
Contributor

tillig commented Jun 10, 2021

Unrelated to the naming: I had mentioned during the chat that results from calling the IsService method shouldn't be too aggressively cached. I thought I'd expand on that a bit.

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 HttpRequestMessage into the web request lifetime scope in Web API.

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 IServiceProviderIsService.IsService() becomes available (or whatever the name of it becomes), I'd caution against being too aggressive about caching whether the service provider has the service in question registered. Depending on whether the query is against ApplicationServices or RequestServices and the context of the query, the result of the query may actually be different. I'm thinking particularly about RequestServices since that's the point at which new lifetime scopes are spawned, but multitenant application-level container support could also return a different value at the ApplicationServices level based on which tenant is accessing at the given time.

@davidfowl davidfowl removed the blocking Marks issues that we want to fast track in order to unblock other important work label Jun 11, 2021
@davidfowl davidfowl self-assigned this Jun 11, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 11, 2021
@davidfowl
Copy link
Member Author

I opened a PR for this change, let me know if I missed any edge cases in the spec tests.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jun 11, 2021
@davidfowl
Copy link
Member Author

@alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper @jeremydmiller @alistairjevans I assume you'll are gonna add support for this interface once .NET 6 ships?

@alistairjevans
Copy link
Contributor

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.

@dadhi
Copy link

dadhi commented Jul 6, 2021

For DryIoc, yes. Will be tested first with the preview.

@ipjohnson
Copy link

Yes I plan to support it in Grace soon after .net 6 releases.

@seesharper
Copy link

LightInject will implement the interface in good time before the .Net 6 release 👍

@ENikS
Copy link
Contributor

ENikS commented Jul 6, 2021

It will be added in next major release

@alistairjevans
Copy link
Contributor

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 IServiceProviderIsService; all the new spec tests pass fine. 👍

@davidfowl
Copy link
Member Author

Woop!

@ghost ghost locked as resolved and limited conversation to collaborators Aug 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-DependencyInjection
Projects
None yet
Development

Successfully merging a pull request may close this issue.