Skip to content

Commit 3a18e8c

Browse files
inf9144egil
andauthored
Allow custom service providers like autofac (#1233)
* Allow custom service providers like autofac * Allow custom service providers like autofac update CHANGELOG.md * Allow custom service providers like autofac Revert code changes that were done by automatic clean up. * Update CHANGELOG.md Co-authored-by: Egil Hansen <[email protected]> * Allow custom service providers like autofac Adding generic notnull constraint to UseServiceProviderFactory * Allow custom service providers like autofac InitializeProvider public => private * Allow custom service providers like autofac Documentation * Apply suggestions from code review Co-authored-by: Egil Hansen <[email protected]> * Allow custom service providers like autofac Fix code from code review applied changes * Allow custom service providers like autofac Fix code from code review applied changes * Allow custom service providers like autofac Documentation * Update src/bunit.core/TestServiceProvider.cs * Update docs/samples/tests/xunit/CustomServiceProviderFactoryUsage.cs --------- Co-authored-by: Egil Hansen <[email protected]>
1 parent 0a42fb9 commit 3a18e8c

File tree

7 files changed

+315
-10
lines changed

7 files changed

+315
-10
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9-
## Fixed
9+
### Fixed
1010

1111
- When the `TestContext` was disposed, it disposed of all services via the service provider. However, if there were ongoing renders happening, this could cause inconsistent state in the render tree, since the `TestRenderer` could try to access the service provider to instantiate components.
1212
This release changes the dispose phase such that the renderer gets disposed first, then the service provider. The disposal of any services that implement `IAsyncDisposable` is now also awaited. Fixed by [@egil](https://github.com/egil) and [@linkdotnet](https://github.com/linkdotnet). Reported by [@BenSchoen](https://github.com/BenSchoen) in https://github.com/bUnit-dev/bUnit/issues/1227.
13+
14+
### Added
15+
16+
- Support for custom service provider factories (`IServiceProviderFactory<TContainerBuilder>`). This enables the use of Autofac and other frameworks for dependency injection like on real-world ASP.NET Core / Blazor projects. By [@inf9144](https://github.com/inf9144).
1317

1418
## [1.23.9] - 2023-09-06
1519

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
6+
namespace Bunit.Docs.Samples;
7+
8+
public sealed class CustomServiceProvider : IServiceProvider, IServiceScopeFactory, IServiceScope {
9+
private readonly IServiceProvider _serviceProvider;
10+
11+
public CustomServiceProvider(IServiceCollection serviceDescriptors)
12+
=> _serviceProvider = serviceDescriptors.BuildServiceProvider();
13+
14+
public object GetService(Type serviceType) {
15+
if (serviceType == typeof(IServiceScope) || serviceType == typeof(IServiceScopeFactory))
16+
return this;
17+
18+
if (serviceType == typeof(DummyService))
19+
return new DummyService();
20+
21+
return _serviceProvider.GetService(serviceType);
22+
}
23+
24+
void IDisposable.Dispose() { }
25+
public IServiceScope CreateScope() => this;
26+
IServiceProvider IServiceScope.ServiceProvider => this;
27+
28+
}
29+
30+
public sealed class CustomServiceProviderFactoryContainerBuilder {
31+
private readonly IServiceCollection _serviceDescriptors;
32+
33+
public CustomServiceProviderFactoryContainerBuilder(IServiceCollection serviceDescriptors)
34+
=> this._serviceDescriptors = serviceDescriptors;
35+
36+
public IServiceProvider Build()
37+
=> new CustomServiceProvider(_serviceDescriptors);
38+
}
39+
40+
public sealed class CustomServiceProviderFactory : IServiceProviderFactory<CustomServiceProviderFactoryContainerBuilder> {
41+
public CustomServiceProviderFactoryContainerBuilder CreateBuilder(IServiceCollection services)
42+
=> new CustomServiceProviderFactoryContainerBuilder(services);
43+
44+
public IServiceProvider CreateServiceProvider(CustomServiceProviderFactoryContainerBuilder containerBuilder)
45+
=> containerBuilder.Build();
46+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Autofac;
2+
using Autofac.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using Xunit;
8+
9+
namespace Bunit.Docs.Samples;
10+
public class CustomServiceProviderFactoryUsage : TestContext
11+
{
12+
[Fact]
13+
public void CustomServiceProviderViaFactoryReturns()
14+
{
15+
Services.UseServiceProviderFactory(new CustomServiceProviderFactory());
16+
17+
var dummyService = Services.GetService<DummyService>();
18+
19+
Assert.NotNull(dummyService);
20+
}
21+
22+
[Fact]
23+
public void CustomServiceProviderViaDelegateReturns()
24+
{
25+
Services.UseServiceProviderFactory(x => new CustomServiceProvider(x));
26+
27+
var dummyService = Services.GetService<DummyService>();
28+
29+
Assert.NotNull(dummyService);
30+
}
31+
32+
[Fact]
33+
public void AutofacServiceProviderViaFactoryReturns()
34+
{
35+
void ConfigureContainer(ContainerBuilder containerBuilder)
36+
{
37+
containerBuilder
38+
.RegisterType<DummyService>()
39+
.AsSelf();
40+
}
41+
42+
Services.UseServiceProviderFactory(new AutofacServiceProviderFactory(ConfigureContainer));
43+
44+
//get a service which was installed in the Autofac ContainerBuilder
45+
46+
var dummyService = Services.GetService<DummyService>();
47+
48+
Assert.NotNull(dummyService);
49+
50+
//get a service which was installed in the bUnit ServiceCollection
51+
52+
var testContextBase = Services.GetService<TestContextBase>();
53+
54+
Assert.NotNull(testContextBase);
55+
Assert.Equal(this, testContextBase);
56+
}
57+
58+
[Fact]
59+
public void AutofacServiceProviderViaDelegateReturns()
60+
{
61+
ILifetimeScope ConfigureContainer(IServiceCollection services)
62+
{
63+
var containerBuilder = new ContainerBuilder();
64+
65+
containerBuilder
66+
.RegisterType<DummyService>()
67+
.AsSelf();
68+
69+
containerBuilder.Populate(services);
70+
71+
return containerBuilder.Build();
72+
}
73+
74+
Services.UseServiceProviderFactory(x => new AutofacServiceProvider(ConfigureContainer(x)));
75+
76+
//get a service which was installed in the Autofac ContainerBuilder
77+
78+
var dummyService = Services.GetService<DummyService>();
79+
80+
Assert.NotNull(dummyService);
81+
82+
//get a service which was installed in the bUnit ServiceCollection
83+
84+
var testContextBase = Services.GetService<TestContextBase>();
85+
86+
Assert.NotNull(testContextBase);
87+
Assert.Equal(this, testContextBase);
88+
}
89+
}

docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<PackageReference Include="Autofac" Version="7.1.0" />
9+
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
810
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
911
<PackageReference Include="Moq" Version="4.16.1" />
1012
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />

docs/site/docs/providing-input/inject-services-into-components.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,35 @@ Here is a test where the fallback service provider is used:
5151

5252
In this example, the `DummyService` is provided by the fallback service provider, since it is not registered in the default service provider.
5353

54+
## Using a custom IServiceProvider implementation
55+
A custom service provider factory can be registered with the built-in `TestServiceProvider`. It is used to create the underlying IServiceProvider. This enables a few interesting use cases, such as using an alternative IoC container (which should implement the `IServiceProvider` interface). This approach can be useful if the fallback service provider is not an option. For example, if you have dependencies in the fallback container, that rely on dependencies which are in the main container and vice versa.
56+
57+
### Registering Autofac service provider factory
58+
The example makes use of `AutofacServiceProviderFactory` and `AutofacServiceProvider` from the package `Autofac.Extensions.DependencyInjection` and shows how to use an Autofac dependency container with bUnit.
59+
60+
Here is a test where the Autofac service provider factory is used:
61+
62+
[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=31&end=50)]
63+
64+
Here is a test where the Autofac service provider is used via delegate:
65+
66+
[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=55&end=80)]
67+
68+
### Registering a custom service provider factory
69+
The examples contain dummy implementations of `IServiceProvider` and `IServiceProviderFactory<TContainerBuilder>`. Normally those implementations are supplied by the creator of your custom dependency injection solution (e.g. Autofac example above). This dummy implementations are not intended to use as is.
70+
71+
This is an example of how to implement and use a dummy custom service provider factory.
72+
73+
[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactory.cs?start=8&end=46)]
74+
75+
Here is a test where the custom service provider factory is used:
76+
77+
[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=13&end=17)]
78+
79+
Here is a test where the custom service provider is used via delegate:
80+
81+
[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=22&end=26)]
82+
5483
## Further reading
5584

5685
A closely related topic is mocking. To learn more about mocking in bUnit, go to the <xref:test-doubles> page.

src/bunit.core/TestServiceProvider.cs

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class TestServiceProvider : IServiceProvider, IServiceCollection,
1515
private IServiceProvider? serviceProvider;
1616
private IServiceProvider? fallbackServiceProvider;
1717
private ServiceProviderOptions options = DefaultServiceProviderOptions;
18+
private Func<IServiceProvider> serviceProviderFactory;
1819

1920
/// <summary>
2021
/// Gets a value indicating whether this <see cref="TestServiceProvider"/> has been initialized, and
@@ -60,8 +61,68 @@ public TestServiceProvider(IServiceCollection? initialServiceCollection = null)
6061
private TestServiceProvider(IServiceCollection initialServiceCollection, bool initializeProvider)
6162
{
6263
serviceCollection = initialServiceCollection;
64+
serviceProviderFactory = () => serviceCollection.BuildServiceProvider(Options);
65+
6366
if (initializeProvider)
64-
serviceProvider = serviceCollection.BuildServiceProvider();
67+
InitializeProvider();
68+
}
69+
70+
/// <summary>
71+
/// Use a custom service provider factory for creating the underlying IServiceProvider.
72+
/// </summary>
73+
/// <param name="serviceProviderFactory">custom service provider factory</param>
74+
public void UseServiceProviderFactory(Func<IServiceCollection, IServiceProvider> serviceProviderFactory)
75+
{
76+
if (serviceProviderFactory is null)
77+
{
78+
throw new ArgumentNullException(nameof(serviceProviderFactory));
79+
}
80+
81+
this.serviceProviderFactory = () => serviceProviderFactory(serviceCollection);
82+
}
83+
84+
/// <summary>
85+
/// Use a custom service provider factory for creating the underlying IServiceProvider.
86+
/// </summary>
87+
/// <typeparam name="TContainerBuilder">
88+
/// Type of the container builder.
89+
/// See <see cref="IServiceProviderFactory{TContainerBuilder}" />
90+
/// </typeparam>
91+
/// <param name="serviceProviderFactory">custom service provider factory</param>
92+
/// <param name="configure">builder configuration action</param>
93+
public void UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> serviceProviderFactory, Action<TContainerBuilder>? configure = null) where TContainerBuilder : notnull
94+
{
95+
if (serviceProviderFactory is null)
96+
{
97+
throw new ArgumentNullException(nameof(serviceProviderFactory));
98+
}
99+
100+
UseServiceProviderFactory(
101+
serviceCollection =>
102+
{
103+
var containerBuilder = serviceProviderFactory.CreateBuilder(serviceCollection);
104+
configure?.Invoke(containerBuilder);
105+
return serviceProviderFactory.CreateServiceProvider(containerBuilder);
106+
});
107+
}
108+
109+
/// <summary>
110+
/// Creates the underlying service provider. Throws if it was already build.
111+
/// Automatically called while getting a service if unitialized.
112+
/// No longer will accept calls to the <c>AddService</c>'s methods.
113+
/// See <see cref="IsProviderInitialized"/>
114+
/// </summary>
115+
#if !NETSTANDARD2_1
116+
[MemberNotNull(nameof(serviceProvider))]
117+
#endif
118+
private void InitializeProvider()
119+
{
120+
CheckInitializedAndThrow();
121+
122+
serviceCollection.AddSingleton<TestServiceProvider>(this);
123+
rootServiceProvider = serviceProviderFactory.Invoke();
124+
serviceScope = rootServiceProvider.CreateScope();
125+
serviceProvider = serviceScope.ServiceProvider;
65126
}
66127

67128
/// <summary>
@@ -92,14 +153,9 @@ public object GetService(Type serviceType)
92153
private object? GetServiceInternal(Type serviceType)
93154
{
94155
if (serviceProvider is null)
95-
{
96-
serviceCollection.AddSingleton<TestServiceProvider>(this);
97-
rootServiceProvider = serviceCollection.BuildServiceProvider(options);
98-
serviceScope = rootServiceProvider.CreateScope();
99-
serviceProvider = serviceScope.ServiceProvider;
100-
}
156+
InitializeProvider();
101157

102-
var result = serviceProvider.GetService(serviceType);
158+
var result = serviceProvider!.GetService(serviceType);
103159

104160
if (result is null && fallbackServiceProvider is not null)
105161
result = fallbackServiceProvider.GetService(serviceType);
@@ -113,7 +169,6 @@ public object GetService(Type serviceType)
113169
/// <inheritdoc/>
114170
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
115171

116-
117172
/// <inheritdoc/>
118173
public void Dispose()
119174
{

tests/bunit.core.tests/TestServiceProviderTest.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,39 @@ public void Test037()
264264
result.ShouldNotBeNull();
265265
}
266266

267+
[Fact(DisplayName = "Test custom service provider factory")]
268+
public void Test038()
269+
{
270+
using var sut = new TestServiceProvider();
271+
sut.AddSingleton<DummyService>();
272+
var dummyServiceProviderFactory = new DummyServiceProviderFactory();
273+
sut.UseServiceProviderFactory(dummyServiceProviderFactory);
274+
275+
var result = sut.GetRequiredService<DummyService>();
276+
277+
result.ShouldNotBeNull();
278+
dummyServiceProviderFactory.TestContainerBuilder.ShouldNotBeNull();
279+
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ShouldNotBeNull();
280+
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ResolvedTestServices.ShouldContain(result);
281+
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ResolvedTestServices.Count.ShouldBe(1);
282+
}
283+
284+
[Fact(DisplayName = "Test custom service provider factory as delegate")]
285+
public void Test039()
286+
{
287+
using var sut = new TestServiceProvider();
288+
sut.AddSingleton<DummyService>();
289+
DummyServiceProvider dummyServiceProvider = null;
290+
sut.UseServiceProviderFactory(x => dummyServiceProvider = new DummyServiceProvider(x));
291+
292+
var result = sut.GetRequiredService<DummyService>();
293+
294+
result.ShouldNotBeNull();
295+
dummyServiceProvider.ShouldNotBeNull();
296+
dummyServiceProvider.ResolvedTestServices.ShouldContain(result);
297+
dummyServiceProvider.ResolvedTestServices.Count.ShouldBe(1);
298+
}
299+
267300
private sealed class DummyService { }
268301

269302
private sealed class AnotherDummyService { }
@@ -301,4 +334,51 @@ public void Dispose()
301334
IsDisposed = true;
302335
}
303336
}
337+
338+
private sealed class DummyServiceProvider : IServiceProvider, IServiceScopeFactory, IServiceScope
339+
{
340+
private readonly IServiceCollection serviceDescriptors;
341+
342+
public readonly List<object?> ResolvedTestServices = new();
343+
344+
public DummyServiceProvider(IServiceCollection serviceDescriptors)
345+
=> this.serviceDescriptors = serviceDescriptors;
346+
347+
public object? GetService(Type serviceType)
348+
{
349+
if (serviceType == typeof(IServiceScope) || serviceType == typeof(IServiceScopeFactory))
350+
return this;
351+
352+
var result = Activator.CreateInstance(serviceDescriptors.Single(x => x.ServiceType == serviceType).ImplementationType);
353+
ResolvedTestServices.Add(result);
354+
return result;
355+
}
356+
357+
void IDisposable.Dispose() { }
358+
public IServiceScope CreateScope() => this;
359+
IServiceProvider IServiceScope.ServiceProvider => this;
360+
361+
}
362+
363+
private sealed class DummyServiceProviderFactoryContainerBuilder
364+
{
365+
private readonly IServiceCollection serviceDescriptors;
366+
367+
public DummyServiceProvider? TestServiceProvider { get; private set; }
368+
369+
public DummyServiceProviderFactoryContainerBuilder(IServiceCollection serviceDescriptors) => this.serviceDescriptors = serviceDescriptors;
370+
371+
public IServiceProvider Build() => TestServiceProvider = new DummyServiceProvider(serviceDescriptors);
372+
}
373+
374+
private sealed class DummyServiceProviderFactory : IServiceProviderFactory<DummyServiceProviderFactoryContainerBuilder>
375+
{
376+
public DummyServiceProviderFactoryContainerBuilder TestContainerBuilder { get; private set; }
377+
378+
public DummyServiceProviderFactoryContainerBuilder CreateBuilder(IServiceCollection services)
379+
=> TestContainerBuilder = new DummyServiceProviderFactoryContainerBuilder(services);
380+
381+
public IServiceProvider CreateServiceProvider(DummyServiceProviderFactoryContainerBuilder containerBuilder)
382+
=> containerBuilder.Build();
383+
}
304384
}

0 commit comments

Comments
 (0)