Skip to content

Commit

Permalink
Add support for IServiceProviderIsService (#54047)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
davidfowl and tillig committed Jun 11, 2021
1 parent 67b93b2 commit c14ac48
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public partial interface IServiceProviderFactory<TContainerBuilder> 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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ internal ServiceProvider(IEnumerable<ServiceDescriptor> 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)
{
Expand Down Expand Up @@ -111,7 +114,9 @@ internal object GetService(Type serviceType, ServiceProviderEngineScope serviceP
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
{
public class GraceDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests
{
public override bool SupportsIServiceProviderIsService => false;

public override string[] SkippedTests => new[]
{
"ResolvesMixedOpenClosedGenericsAsEnumerable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
{
public class LamarDependencyInjectionSpecificationTests : SkippableDependencyInjectionSpecificationTests
{
public override bool SupportsIServiceProviderIsService => false;

public override string[] SkippedTests => new[]
{
"DisposesInReverseOrderOfCreation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
{
public class StructureMapDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests
{
public override bool SupportsIServiceProviderIsService => false;

public override string[] SkippedTests => new[]
{
"DisposesInReverseOrderOfCreation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit c14ac48

Please sign in to comment.