diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs index ca0fae35465455..5ba22b99dcbbd7 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs @@ -36,6 +36,10 @@ public partial interface IServiceProviderFactory where TConta TContainerBuilder CreateBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection services); System.IServiceProvider CreateServiceProvider(TContainerBuilder containerBuilder); } + public partial interface IServiceProviderIsService + { + bool IsService(System.Type serviceType); + } public partial interface IServiceScope : System.IDisposable { System.IServiceProvider ServiceProvider { get; } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceProviderIsService.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceProviderIsService.cs new file mode 100644 index 00000000000000..b24ab154d16e93 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceProviderIsService.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Optional service used to determine if the specified type is available from the . + /// + public interface IServiceProviderIsService + { + /// + /// Determines if the specified service type is available from the . + /// + /// An object that specifies the type of service object to test. + /// true if the specified service is a available, false if it is not. + bool IsService(Type serviceType); + } +} diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ServiceProviderIsServiceSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ServiceProviderIsServiceSpecificationTests.cs new file mode 100644 index 00000000000000..66ef77a01eb1e5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ServiceProviderIsServiceSpecificationTests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection.Specification.Fakes; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Specification +{ + public abstract partial class DependencyInjectionSpecificationTests + { + public virtual bool SupportsIServiceProviderIsService => true; + + [Fact] + public void ExplictServiceRegisterationWithIsService() + { + if (!SupportsIServiceProviderIsService) + { + return; + } + + // Arrange + var collection = new TestServiceCollection(); + collection.AddTransient(typeof(IFakeService), typeof(FakeService)); + var provider = CreateServiceProvider(collection); + + // Act + var serviceProviderIsService = provider.GetService(); + + // Assert + Assert.NotNull(serviceProviderIsService); + Assert.True(serviceProviderIsService.IsService(typeof(IFakeService))); + Assert.False(serviceProviderIsService.IsService(typeof(FakeService))); + } + + [Fact] + public void OpenGenericsWithIsService() + { + if (!SupportsIServiceProviderIsService) + { + return; + } + + // Arrange + var collection = new TestServiceCollection(); + collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>)); + var provider = CreateServiceProvider(collection); + + // Act + var serviceProviderIsService = provider.GetService(); + + // Assert + Assert.NotNull(serviceProviderIsService); + Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService))); + Assert.False(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<>))); + } + + [Fact] + public void ClosedGenericsWithIsService() + { + if (!SupportsIServiceProviderIsService) + { + return; + } + + // Arrange + var collection = new TestServiceCollection(); + collection.AddTransient(typeof(IFakeOpenGenericService), typeof(FakeOpenGenericService)); + var provider = CreateServiceProvider(collection); + + // Act + var serviceProviderIsService = provider.GetService(); + + // Assert + Assert.NotNull(serviceProviderIsService); + Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService))); + } + + [Fact] + public void IEnumerableWithIsServiceAlwaysReturnsTrue() + { + if (!SupportsIServiceProviderIsService) + { + return; + } + + // Arrange + var collection = new TestServiceCollection(); + collection.AddTransient(typeof(IFakeService), typeof(FakeService)); + var provider = CreateServiceProvider(collection); + + // Act + var serviceProviderIsService = provider.GetService(); + + // Assert + Assert.NotNull(serviceProviderIsService); + Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable))); + Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable))); + Assert.False(serviceProviderIsService.IsService(typeof(IEnumerable<>))); + } + + [Fact] + public void BuiltInServicesWithIsServiceReturnsTrue() + { + if (!SupportsIServiceProviderIsService) + { + return; + } + + // Arrange + var collection = new TestServiceCollection(); + collection.AddTransient(typeof(IFakeService), typeof(FakeService)); + var provider = CreateServiceProvider(collection); + + // Act + var serviceProviderIsService = provider.GetService(); + + // Assert + Assert.NotNull(serviceProviderIsService); + Assert.True(serviceProviderIsService.IsService(typeof(IServiceProvider))); + Assert.True(serviceProviderIsService.IsService(typeof(IServiceScopeFactory))); + Assert.True(serviceProviderIsService.IsService(typeof(IServiceProviderIsService))); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index 47d7d02f4db4b3..da791135ff0829 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup { - internal sealed class CallSiteFactory + internal sealed class CallSiteFactory : IServiceProviderIsService { private const int DefaultSlot = 0; private readonly ServiceDescriptor[] _descriptors; @@ -441,6 +441,38 @@ public void Add(Type type, ServiceCallSite serviceCallSite) _callSiteCache[new ServiceCacheKey(type, DefaultSlot)] = serviceCallSite; } + public bool IsService(Type serviceType) + { + if (serviceType is null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + // Querying for an open generic should return false (they aren't resolvable) + if (serviceType.IsGenericTypeDefinition) + { + return false; + } + + if (_descriptorLookup.ContainsKey(serviceType)) + { + return true; + } + + if (serviceType.IsConstructedGenericType && serviceType.GetGenericTypeDefinition() is Type genericDefinition) + { + // We special case IEnumerable since it isn't explicitly registered in the container + // yet we can manifest instances of it when requested. + return genericDefinition == typeof(IEnumerable<>) || _descriptorLookup.ContainsKey(genericDefinition); + } + + // These are the built in service types that aren't part of the list of service descriptors + // If you update these make sure to also update the code in ServiceProvider.ctor + return serviceType == typeof(IServiceProvider) || + serviceType == typeof(IServiceScopeFactory) || + serviceType == typeof(IServiceProviderIsService); + } + private struct ServiceDescriptorCacheItem { private ServiceDescriptor _item; diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs index 228fb45e3aa22f..d21d9807f5f771 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs @@ -38,8 +38,11 @@ internal ServiceProvider(IEnumerable serviceDescriptors, Serv Root = new ServiceProviderEngineScope(this); CallSiteFactory = new CallSiteFactory(serviceDescriptors); + // The list of built in services that aren't part of the list of service descriptors + // keep this in sync with CallSiteFactory.IsService CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite()); CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite(Root)); + CallSiteFactory.Add(typeof(IServiceProviderIsService), new ConstantCallSite(typeof(IServiceProviderIsService), CallSiteFactory)); if (options.ValidateScopes) { @@ -111,7 +114,9 @@ internal object GetService(Type serviceType, ServiceProviderEngineScope serviceP Func realizedService = _realizedServices.GetOrAdd(serviceType, _createServiceAccessor); OnResolve(serviceType, serviceProviderEngineScope); DependencyInjectionEventSource.Log.ServiceResolved(serviceType); - return realizedService.Invoke(serviceProviderEngineScope); + var result = realizedService.Invoke(serviceProviderEngineScope); + System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceType)); + return result; } private void ValidateService(ServiceDescriptor descriptor) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs index f80fd06d146aac..a18ea83ae7917f 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs @@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class AutofacDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { var builder = new ContainerBuilder(); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs index 3de5ef16d0b539..2ab31bf5f5fda1 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs @@ -7,8 +7,10 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { - public class DryIocDependencyInjectionSpecificationTests: DependencyInjectionSpecificationTests + public class DryIocDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { return new Container() diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs index 165997400bc457..7733aaa16587df 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs @@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class GraceDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + public override string[] SkippedTests => new[] { "ResolvesMixedOpenClosedGenericsAsEnumerable", diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs index fecf0e4d840346..795bc330c97754 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs @@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class LamarDependencyInjectionSpecificationTests : SkippableDependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + public override string[] SkippedTests => new[] { "DisposesInReverseOrderOfCreation", diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs index d586884b3bb3d3..d9c874958e63b7 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs @@ -10,6 +10,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class LightInjectDependencyInjectionSpecificationTests: DependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { var builder = new ContainerBuilder(); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StashBox.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StashBox.cs index 3deffe4767d7e6..6d315d7361f7d9 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StashBox.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StashBox.cs @@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class StashBoxDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) { return serviceCollection.UseStashbox(); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StructureMap.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StructureMap.cs index 74e13fd0a818a2..f73bf039220e8f 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StructureMap.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StructureMap.cs @@ -8,6 +8,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class StructureMapDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + public override string[] SkippedTests => new[] { "DisposesInReverseOrderOfCreation", diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Unity.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Unity.cs index affa7de4551afd..1987206fae02ba 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Unity.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Unity.cs @@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification { public class UnityDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests { + public override bool SupportsIServiceProviderIsService => false; + // See https://github.com/unitycontainer/microsoft-dependency-injection/issues/87 public override bool ExpectStructWithPublicDefaultConstructorInvoked => true;