Skip to content

Commit c14ac48

Browse files
davidfowltillig
andauthored
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 <tillig@paraesthesia.com>
1 parent 67b93b2 commit c14ac48

File tree

13 files changed

+207
-3
lines changed

13 files changed

+207
-3
lines changed

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public partial interface IServiceProviderFactory<TContainerBuilder> where TConta
3636
TContainerBuilder CreateBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection services);
3737
System.IServiceProvider CreateServiceProvider(TContainerBuilder containerBuilder);
3838
}
39+
public partial interface IServiceProviderIsService
40+
{
41+
bool IsService(System.Type serviceType);
42+
}
3943
public partial interface IServiceScope : System.IDisposable
4044
{
4145
System.IServiceProvider ServiceProvider { get; }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace Microsoft.Extensions.DependencyInjection
7+
{
8+
/// <summary>
9+
/// Optional service used to determine if the specified type is available from the <see cref="IServiceProvider"/>.
10+
/// </summary>
11+
public interface IServiceProviderIsService
12+
{
13+
/// <summary>
14+
/// Determines if the specified service type is available from the <see cref="IServiceProvider"/>.
15+
/// </summary>
16+
/// <param name="serviceType">An object that specifies the type of service object to test.</param>
17+
/// <returns>true if the specified service is a available, false if it is not.</returns>
18+
bool IsService(Type serviceType);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
8+
using Xunit;
9+
10+
namespace Microsoft.Extensions.DependencyInjection.Specification
11+
{
12+
public abstract partial class DependencyInjectionSpecificationTests
13+
{
14+
public virtual bool SupportsIServiceProviderIsService => true;
15+
16+
[Fact]
17+
public void ExplictServiceRegisterationWithIsService()
18+
{
19+
if (!SupportsIServiceProviderIsService)
20+
{
21+
return;
22+
}
23+
24+
// Arrange
25+
var collection = new TestServiceCollection();
26+
collection.AddTransient(typeof(IFakeService), typeof(FakeService));
27+
var provider = CreateServiceProvider(collection);
28+
29+
// Act
30+
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
31+
32+
// Assert
33+
Assert.NotNull(serviceProviderIsService);
34+
Assert.True(serviceProviderIsService.IsService(typeof(IFakeService)));
35+
Assert.False(serviceProviderIsService.IsService(typeof(FakeService)));
36+
}
37+
38+
[Fact]
39+
public void OpenGenericsWithIsService()
40+
{
41+
if (!SupportsIServiceProviderIsService)
42+
{
43+
return;
44+
}
45+
46+
// Arrange
47+
var collection = new TestServiceCollection();
48+
collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>));
49+
var provider = CreateServiceProvider(collection);
50+
51+
// Act
52+
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
53+
54+
// Assert
55+
Assert.NotNull(serviceProviderIsService);
56+
Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<IFakeService>)));
57+
Assert.False(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<>)));
58+
}
59+
60+
[Fact]
61+
public void ClosedGenericsWithIsService()
62+
{
63+
if (!SupportsIServiceProviderIsService)
64+
{
65+
return;
66+
}
67+
68+
// Arrange
69+
var collection = new TestServiceCollection();
70+
collection.AddTransient(typeof(IFakeOpenGenericService<IFakeService>), typeof(FakeOpenGenericService<IFakeService>));
71+
var provider = CreateServiceProvider(collection);
72+
73+
// Act
74+
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
75+
76+
// Assert
77+
Assert.NotNull(serviceProviderIsService);
78+
Assert.True(serviceProviderIsService.IsService(typeof(IFakeOpenGenericService<IFakeService>)));
79+
}
80+
81+
[Fact]
82+
public void IEnumerableWithIsServiceAlwaysReturnsTrue()
83+
{
84+
if (!SupportsIServiceProviderIsService)
85+
{
86+
return;
87+
}
88+
89+
// Arrange
90+
var collection = new TestServiceCollection();
91+
collection.AddTransient(typeof(IFakeService), typeof(FakeService));
92+
var provider = CreateServiceProvider(collection);
93+
94+
// Act
95+
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
96+
97+
// Assert
98+
Assert.NotNull(serviceProviderIsService);
99+
Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable<IFakeService>)));
100+
Assert.True(serviceProviderIsService.IsService(typeof(IEnumerable<FakeService>)));
101+
Assert.False(serviceProviderIsService.IsService(typeof(IEnumerable<>)));
102+
}
103+
104+
[Fact]
105+
public void BuiltInServicesWithIsServiceReturnsTrue()
106+
{
107+
if (!SupportsIServiceProviderIsService)
108+
{
109+
return;
110+
}
111+
112+
// Arrange
113+
var collection = new TestServiceCollection();
114+
collection.AddTransient(typeof(IFakeService), typeof(FakeService));
115+
var provider = CreateServiceProvider(collection);
116+
117+
// Act
118+
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();
119+
120+
// Assert
121+
Assert.NotNull(serviceProviderIsService);
122+
Assert.True(serviceProviderIsService.IsService(typeof(IServiceProvider)));
123+
Assert.True(serviceProviderIsService.IsService(typeof(IServiceScopeFactory)));
124+
Assert.True(serviceProviderIsService.IsService(typeof(IServiceProviderIsService)));
125+
}
126+
}
127+
}

src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
1414
{
15-
internal sealed class CallSiteFactory
15+
internal sealed class CallSiteFactory : IServiceProviderIsService
1616
{
1717
private const int DefaultSlot = 0;
1818
private readonly ServiceDescriptor[] _descriptors;
@@ -441,6 +441,38 @@ public void Add(Type type, ServiceCallSite serviceCallSite)
441441
_callSiteCache[new ServiceCacheKey(type, DefaultSlot)] = serviceCallSite;
442442
}
443443

444+
public bool IsService(Type serviceType)
445+
{
446+
if (serviceType is null)
447+
{
448+
throw new ArgumentNullException(nameof(serviceType));
449+
}
450+
451+
// Querying for an open generic should return false (they aren't resolvable)
452+
if (serviceType.IsGenericTypeDefinition)
453+
{
454+
return false;
455+
}
456+
457+
if (_descriptorLookup.ContainsKey(serviceType))
458+
{
459+
return true;
460+
}
461+
462+
if (serviceType.IsConstructedGenericType && serviceType.GetGenericTypeDefinition() is Type genericDefinition)
463+
{
464+
// We special case IEnumerable since it isn't explicitly registered in the container
465+
// yet we can manifest instances of it when requested.
466+
return genericDefinition == typeof(IEnumerable<>) || _descriptorLookup.ContainsKey(genericDefinition);
467+
}
468+
469+
// These are the built in service types that aren't part of the list of service descriptors
470+
// If you update these make sure to also update the code in ServiceProvider.ctor
471+
return serviceType == typeof(IServiceProvider) ||
472+
serviceType == typeof(IServiceScopeFactory) ||
473+
serviceType == typeof(IServiceProviderIsService);
474+
}
475+
444476
private struct ServiceDescriptorCacheItem
445477
{
446478
private ServiceDescriptor _item;

src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, Serv
3838

3939
Root = new ServiceProviderEngineScope(this);
4040
CallSiteFactory = new CallSiteFactory(serviceDescriptors);
41+
// The list of built in services that aren't part of the list of service descriptors
42+
// keep this in sync with CallSiteFactory.IsService
4143
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
4244
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite(Root));
45+
CallSiteFactory.Add(typeof(IServiceProviderIsService), new ConstantCallSite(typeof(IServiceProviderIsService), CallSiteFactory));
4346

4447
if (options.ValidateScopes)
4548
{
@@ -111,7 +114,9 @@ internal object GetService(Type serviceType, ServiceProviderEngineScope serviceP
111114
Func<ServiceProviderEngineScope, object> realizedService = _realizedServices.GetOrAdd(serviceType, _createServiceAccessor);
112115
OnResolve(serviceType, serviceProviderEngineScope);
113116
DependencyInjectionEventSource.Log.ServiceResolved(serviceType);
114-
return realizedService.Invoke(serviceProviderEngineScope);
117+
var result = realizedService.Invoke(serviceProviderEngineScope);
118+
System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceType));
119+
return result;
115120
}
116121

117122
private void ValidateService(ServiceDescriptor descriptor)

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Autofac.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
99
{
1010
public class AutofacDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests
1111
{
12+
public override bool SupportsIServiceProviderIsService => false;
13+
1214
protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection)
1315
{
1416
var builder = new ContainerBuilder();

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/DryIoc.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
namespace Microsoft.Extensions.DependencyInjection.Specification
99
{
10-
public class DryIocDependencyInjectionSpecificationTests: DependencyInjectionSpecificationTests
10+
public class DryIocDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests
1111
{
12+
public override bool SupportsIServiceProviderIsService => false;
13+
1214
protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection)
1315
{
1416
return new Container()

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Grace.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
99
{
1010
public class GraceDependencyInjectionSpecificationTests: SkippableDependencyInjectionSpecificationTests
1111
{
12+
public override bool SupportsIServiceProviderIsService => false;
13+
1214
public override string[] SkippedTests => new[]
1315
{
1416
"ResolvesMixedOpenClosedGenericsAsEnumerable",

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/Lamar.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
77
{
88
public class LamarDependencyInjectionSpecificationTests : SkippableDependencyInjectionSpecificationTests
99
{
10+
public override bool SupportsIServiceProviderIsService => false;
11+
1012
public override string[] SkippedTests => new[]
1113
{
1214
"DisposesInReverseOrderOfCreation",

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.External.Tests/LightInject.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
1010
{
1111
public class LightInjectDependencyInjectionSpecificationTests: DependencyInjectionSpecificationTests
1212
{
13+
public override bool SupportsIServiceProviderIsService => false;
14+
1315
protected override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection)
1416
{
1517
var builder = new ContainerBuilder();

0 commit comments

Comments
 (0)