Add support for IServiceProviderIsService (#54047)
authorDavid Fowler <davidfowl@gmail.com>
Fri, 11 Jun 2021 22:11:50 +0000 (15:11 -0700)
committerGitHub <noreply@github.com>
Fri, 11 Jun 2021 22:11:50 +0000 (15:11 -0700)
* 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 <tillig@paraesthesia.com>
13 files changed:
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs
src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceProviderIsService.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/ServiceProviderIsServiceSpecificationTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs
src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StashBox.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/StructureMap.cs
src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Unity.cs

index ca0fae3..5ba22b9 100644 (file)
@@ -36,6 +36,10 @@ namespace Microsoft.Extensions.DependencyInjection
         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 (file)
index 0000000..b24ab15
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Optional service used to determine if the specified type is available from the <see cref="IServiceProvider"/>.
+    /// </summary>
+    public interface IServiceProviderIsService
+    {
+        /// <summary>
+        /// Determines if the specified service type is available from the <see cref="IServiceProvider"/>.
+        /// </summary>
+        /// <param name="serviceType">An object that specifies the type of service object to test.</param>
+        /// <returns>true if the specified service is a available, false if it is not.</returns>
+        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 (file)
index 0000000..66ef77a
--- /dev/null
@@ -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<IServiceProviderIsService>();
+
+            // 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<IServiceProviderIsService>();
+
+            // Assert
+            Assert.NotNull(serviceProviderIsService);
+            Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<IFakeService>)));
+            Assert.False(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<>)));
+        }
+
+        [Fact]
+        public void ClosedGenericsWithIsService()
+        {
+            if (!SupportsIServiceProviderIsService)
+            {
+                return;
+            }
+
+            // Arrange
+            var collection = new TestServiceCollection();
+            collection.AddTransient(typeof(IFakeOpenGenericService<IFakeService>), typeof(FakeOpenGenericService<IFakeService>));
+            var provider = CreateServiceProvider(collection);
+
+            // Act
+            var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
+
+            // Assert
+            Assert.NotNull(serviceProviderIsService);
+            Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<IFakeService>)));
+        }
+
+        [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<IServiceProviderIsService>();
+
+            // Assert
+            Assert.NotNull(serviceProviderIsService);
+            Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable<IFakeService>)));
+            Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable<FakeService>)));
+            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<IServiceProviderIsService>();
+
+            // Assert
+            Assert.NotNull(serviceProviderIsService);
+            Assert.True(serviceProviderIsService.IsService(typeof(IServiceProvider)));
+            Assert.True(serviceProviderIsService.IsService(typeof(IServiceScopeFactory)));
+            Assert.True(serviceProviderIsService.IsService(typeof(IServiceProviderIsService)));
+        }
+    }
+}
index 47d7d02..da79113 100644 (file)
@@ -12,7 +12,7 @@ using Microsoft.Extensions.Internal;
 
 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 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
             _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;
index 228fb45..d21d980 100644 (file)
@@ -38,8 +38,11 @@ namespace Microsoft.Extensions.DependencyInjection
 
             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 @@ namespace Microsoft.Extensions.DependencyInjection
             Func<ServiceProviderEngineScope, object> 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)
index f80fd06..a18ea83 100644 (file)
@@ -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();
index 3de5ef1..2ab31bf 100644 (file)
@@ -7,8 +7,10 @@ using DryIoc.Microsoft.DependencyInjection;
 
 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()
index 1659974..7733aaa 100644 (file)
@@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
 {
     public class GraceDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests
     {
+        public override bool SupportsIServiceProviderIsService => false;
+
         public override string[] SkippedTests => new[]
         {
             "ResolvesMixedOpenClosedGenericsAsEnumerable",
index fecf0e4..795bc33 100644 (file)
@@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
 {
     public class LamarDependencyInjectionSpecificationTests : SkippableDependencyInjectionSpecificationTests
     {
+        public override bool SupportsIServiceProviderIsService => false;
+
         public override string[] SkippedTests => new[]
         {
             "DisposesInReverseOrderOfCreation",
index d586884..d9c8749 100644 (file)
@@ -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();
index 3deffe4..6d315d7 100644 (file)
@@ -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();
index 74e13fd..f73bf03 100644 (file)
@@ -8,6 +8,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
 {
     public class StructureMapDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests
     {
+        public override bool SupportsIServiceProviderIsService => false;
+
         public override string[] SkippedTests => new[]
         {
             "DisposesInReverseOrderOfCreation",
index affa7de..1987206 100644 (file)
@@ -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;