From c14ac488cc5f7d2ad71e7f81d6a05a1da8c76237 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 11 Jun 2021 15:11:50 -0700 Subject: [PATCH] Add support for IServiceProviderIsService (#54047) * Add support for IServiceProviderIsService - This optional service lets consumers query to see if a service is resolvable without side effects (not having to explicitly resolve the service). - Added new spec tests to verify the baseline behavior based on IServiceCollection features. - Handle built in services as part of IsServce - Special case built in services as part of the IsService check - Make the tests part of the core DI tests and enable skipping via a property Co-authored-by: Travis Illig --- ...nsions.DependencyInjection.Abstractions.cs | 4 + .../src/IServiceProviderIsService.cs | 20 +++ ...viceProviderIsServiceSpecificationTests.cs | 127 ++++++++++++++++++ .../src/ServiceLookup/CallSiteFactory.cs | 34 ++++- .../src/ServiceProvider.cs | 7 +- .../tests/DI.External.Tests/Autofac.cs | 2 + .../tests/DI.External.Tests/DryIoc.cs | 4 +- .../tests/DI.External.Tests/Grace.cs | 2 + .../tests/DI.External.Tests/Lamar.cs | 2 + .../tests/DI.External.Tests/LightInject.cs | 2 + .../tests/DI.External.Tests/StashBox.cs | 2 + .../tests/DI.External.Tests/StructureMap.cs | 2 + .../tests/DI.External.Tests/Unity.cs | 2 + 13 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceProviderIsService.cs create mode 100644 src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ServiceProviderIsServiceSpecificationTests.cs 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 ca0fae3546545..5ba22b99dcbbd 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 0000000000000..b24ab154d16e9 --- /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 0000000000000..66ef77a01eb1e --- /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 47d7d02f4db4b..da791135ff082 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 228fb45e3aa22..d21d9807f5f77 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 f80fd06d146aa..a18ea83ae7917 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 3de5ef16d0b53..2ab31bf5f5fda 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 165997400bc45..7733aaa16587d 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 fecf0e4d84034..795bc330c9775 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 d586884b3bb3d..d9c874958e63b 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 3deffe4767d7e..6d315d7361f7d 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 74e13fd0a818a..f73bf039220e8 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 affa7de4551af..1987206fae02b 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;