Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom service providers like autofac #1233

Merged
merged 15 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ All notable changes to **bUnit** will be documented in this file. The project ad

## [Unreleased]

## Fixed
### Fixed

- 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.
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.

### Added

- 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).

## [1.23.9] - 2023-09-06

Expand Down
46 changes: 46 additions & 0 deletions docs/samples/tests/xunit/CustomServiceProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;

namespace Bunit.Docs.Samples;

public sealed class CustomServiceProvider : IServiceProvider, IServiceScopeFactory, IServiceScope {
private readonly IServiceProvider _serviceProvider;

public CustomServiceProvider(IServiceCollection serviceDescriptors)
=> _serviceProvider = serviceDescriptors.BuildServiceProvider();

public object GetService(Type serviceType) {
if (serviceType == typeof(IServiceScope) || serviceType == typeof(IServiceScopeFactory))
return this;

if (serviceType == typeof(DummyService))
return new DummyService();

return _serviceProvider.GetService(serviceType);
}

void IDisposable.Dispose() { }
public IServiceScope CreateScope() => this;
IServiceProvider IServiceScope.ServiceProvider => this;

}

public sealed class CustomServiceProviderFactoryContainerBuilder {
private readonly IServiceCollection _serviceDescriptors;

public CustomServiceProviderFactoryContainerBuilder(IServiceCollection serviceDescriptors)
=> this._serviceDescriptors = serviceDescriptors;

public IServiceProvider Build()
=> new CustomServiceProvider(_serviceDescriptors);
}

public sealed class CustomServiceProviderFactory : IServiceProviderFactory<CustomServiceProviderFactoryContainerBuilder> {
public CustomServiceProviderFactoryContainerBuilder CreateBuilder(IServiceCollection services)
=> new CustomServiceProviderFactoryContainerBuilder(services);

public IServiceProvider CreateServiceProvider(CustomServiceProviderFactoryContainerBuilder containerBuilder)
=> containerBuilder.Build();
}
82 changes: 82 additions & 0 deletions docs/samples/tests/xunit/CustomServiceProviderFactoryUsage.cs
egil marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;

namespace Bunit.Docs.Samples;
public class CustomServiceProviderFactoryUsage : TestContext {
[Fact]
public void CustomServiceProviderViaFactoryReturns() {
Services.UseServiceProviderFactory(new CustomServiceProviderFactory());

var dummyService = Services.GetService<DummyService>();

Assert.NotNull(dummyService);
}

[Fact]
public void CustomServiceProviderViaDelegateReturns() {
Services.UseServiceProviderFactory(x => new CustomServiceProvider(x));

var dummyService = Services.GetService<DummyService>();

Assert.NotNull(dummyService);
}

[Fact]
public void AutofacServiceProviderViaFactoryReturns() {
void ConfigureContainer(ContainerBuilder containerBuilder) {
containerBuilder
.RegisterType<DummyService>()
.AsSelf();
}

Services.UseServiceProviderFactory(new AutofacServiceProviderFactory(ConfigureContainer));

//get a service which was installed in the Autofac ContainerBuilder

var dummyService = Services.GetService<DummyService>();

Assert.NotNull(dummyService);

//get a service which was installed in the bUnit ServiceCollection

var testContextBase = Services.GetService<TestContextBase>();

Assert.NotNull(testContextBase);
Assert.Equal(this, testContextBase);
}

[Fact]
public void AutofacServiceProviderViaDelegateReturns() {
ILifetimeScope ConfigureContainer(IServiceCollection services) {
var containerBuilder = new ContainerBuilder();

containerBuilder
.RegisterType<DummyService>()
.AsSelf();

containerBuilder.Populate(services);

return containerBuilder.Build();
}

Services.UseServiceProviderFactory(x => new AutofacServiceProvider(ConfigureContainer(x)));

//get a service which was installed in the Autofac ContainerBuilder

var dummyService = Services.GetService<DummyService>();

Assert.NotNull(dummyService);

//get a service which was installed in the bUnit ServiceCollection

var testContextBase = Services.GetService<TestContextBase>();

Assert.NotNull(testContextBase);
Assert.Equal(this, testContextBase);
}
egil marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Autofac" Version="7.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
Expand Down
29 changes: 29 additions & 0 deletions docs/site/docs/providing-input/inject-services-into-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ Here is a test where the fallback service provider is used:

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

## Using a custom IServiceProvider implementation
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.

### Registering Autofac service provider factory
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.

Here is a test where the Autofac service provider factory is used:

[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=31&end=50)]

Here is a test where the Autofac service provider is used via delegate:

[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=55&end=80)]

### Registering a custom service provider factory
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.

This is an example of how to implement and use a dummy custom service provider factory.

[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactory.cs?start=8&end=46)]

Here is a test where the custom service provider factory is used:

[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=13&end=17)]

Here is a test where the custom service provider is used via delegate:

[!code-csharp[](../../../samples/tests/xunit/CustomServiceProviderFactoryUsage.cs?start=22&end=26)]

## Further reading

A closely related topic is mocking. To learn more about mocking in bUnit, go to the <xref:test-doubles> page.
73 changes: 64 additions & 9 deletions src/bunit.core/TestServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class TestServiceProvider : IServiceProvider, IServiceCollection,
private IServiceProvider? serviceProvider;
private IServiceProvider? fallbackServiceProvider;
private ServiceProviderOptions options = DefaultServiceProviderOptions;
private Func<IServiceProvider> serviceProviderFactory;

/// <summary>
/// Gets a value indicating whether this <see cref="TestServiceProvider"/> has been initialized, and
Expand Down Expand Up @@ -60,8 +61,68 @@ public TestServiceProvider(IServiceCollection? initialServiceCollection = null)
private TestServiceProvider(IServiceCollection initialServiceCollection, bool initializeProvider)
{
serviceCollection = initialServiceCollection;
serviceProviderFactory = () => serviceCollection.BuildServiceProvider(Options);

if (initializeProvider)
serviceProvider = serviceCollection.BuildServiceProvider();
InitializeProvider();
}

/// <summary>
/// Use a custom service provider factory for creating the underlying IServiceProvider.
/// </summary>
/// <param name="serviceProviderFactory">custom service provider factory</param>
public void UseServiceProviderFactory(Func<IServiceCollection, IServiceProvider> serviceProviderFactory)
{
if (serviceProviderFactory is null)
{
throw new ArgumentNullException(nameof(serviceProviderFactory));
}

this.serviceProviderFactory = () => serviceProviderFactory(serviceCollection);
}

/// <summary>
/// Use a custom service provider factory for creating the underlying IServiceProvider.
/// </summary>
/// <typeparam name="TContainerBuilder">
/// Type of the container builder.
/// See <see cref="IServiceProviderFactory{TContainerBuilder}" />
/// </typeparam>
/// <param name="serviceProviderFactory">custom service provider factory</param>
/// <param name="configure">builder configuration action</param>
public void UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> serviceProviderFactory, Action<TContainerBuilder>? configure = null) where TContainerBuilder : notnull
{
if (serviceProviderFactory is null)
{
throw new ArgumentNullException(nameof(serviceProviderFactory));
}

egil marked this conversation as resolved.
Show resolved Hide resolved
UseServiceProviderFactory(
serviceCollection =>
{
var containerBuilder = serviceProviderFactory.CreateBuilder(serviceCollection);
configure?.Invoke(containerBuilder);
return serviceProviderFactory.CreateServiceProvider(containerBuilder);
});
}

/// <summary>
/// Creates the underlying service provider. Throws if it was already build.
/// Automatically called while getting a service if unitialized.
/// No longer will accept calls to the <c>AddService</c>'s methods.
/// See <see cref="IsProviderInitialized"/>
/// </summary>
#if !NETSTANDARD2_1
[MemberNotNull(nameof(serviceProvider))]
#endif
Comment on lines +115 to +117
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: copy and make https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs internal and scoped to NETSTANDARD2_1 to bUnit so we can avoid having #if !NETSTANDARD2_1 in the code. That way we can drop the serviceProvider >>!<< .GetService(serviceType) below, even in netstandard2_1.

private void InitializeProvider()
{
CheckInitializedAndThrow();

serviceCollection.AddSingleton<TestServiceProvider>(this);
rootServiceProvider = serviceProviderFactory.Invoke();
serviceScope = rootServiceProvider.CreateScope();
serviceProvider = serviceScope.ServiceProvider;
}

/// <summary>
Expand Down Expand Up @@ -92,14 +153,9 @@ public object GetService(Type serviceType)
private object? GetServiceInternal(Type serviceType)
{
if (serviceProvider is null)
{
serviceCollection.AddSingleton<TestServiceProvider>(this);
rootServiceProvider = serviceCollection.BuildServiceProvider(options);
serviceScope = rootServiceProvider.CreateScope();
serviceProvider = serviceScope.ServiceProvider;
}
InitializeProvider();

var result = serviceProvider.GetService(serviceType);
var result = serviceProvider!.GetService(serviceType);

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


/// <inheritdoc/>
public void Dispose()
{
Expand Down
80 changes: 80 additions & 0 deletions tests/bunit.core.tests/TestServiceProviderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,39 @@ public void Test037()
result.ShouldNotBeNull();
}

[Fact(DisplayName = "Test custom service provider factory")]
public void Test038()
{
using var sut = new TestServiceProvider();
sut.AddSingleton<DummyService>();
var dummyServiceProviderFactory = new DummyServiceProviderFactory();
sut.UseServiceProviderFactory(dummyServiceProviderFactory);

var result = sut.GetRequiredService<DummyService>();

result.ShouldNotBeNull();
dummyServiceProviderFactory.TestContainerBuilder.ShouldNotBeNull();
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ShouldNotBeNull();
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ResolvedTestServices.ShouldContain(result);
dummyServiceProviderFactory.TestContainerBuilder.TestServiceProvider.ResolvedTestServices.Count.ShouldBe(1);
}

[Fact(DisplayName = "Test custom service provider factory as delegate")]
public void Test039()
{
using var sut = new TestServiceProvider();
sut.AddSingleton<DummyService>();
DummyServiceProvider dummyServiceProvider = null;
sut.UseServiceProviderFactory(x => dummyServiceProvider = new DummyServiceProvider(x));

var result = sut.GetRequiredService<DummyService>();

result.ShouldNotBeNull();
dummyServiceProvider.ShouldNotBeNull();
dummyServiceProvider.ResolvedTestServices.ShouldContain(result);
dummyServiceProvider.ResolvedTestServices.Count.ShouldBe(1);
}

private sealed class DummyService { }

private sealed class AnotherDummyService { }
Expand Down Expand Up @@ -301,4 +334,51 @@ public void Dispose()
IsDisposed = true;
}
}

private sealed class DummyServiceProvider : IServiceProvider, IServiceScopeFactory, IServiceScope
{
private readonly IServiceCollection serviceDescriptors;

public readonly List<object?> ResolvedTestServices = new();

public DummyServiceProvider(IServiceCollection serviceDescriptors)
=> this.serviceDescriptors = serviceDescriptors;

public object? GetService(Type serviceType)
{
if (serviceType == typeof(IServiceScope) || serviceType == typeof(IServiceScopeFactory))
return this;

var result = Activator.CreateInstance(serviceDescriptors.Single(x => x.ServiceType == serviceType).ImplementationType);
ResolvedTestServices.Add(result);
return result;
}

void IDisposable.Dispose() { }
public IServiceScope CreateScope() => this;
IServiceProvider IServiceScope.ServiceProvider => this;

}

private sealed class DummyServiceProviderFactoryContainerBuilder
{
private readonly IServiceCollection serviceDescriptors;

public DummyServiceProvider? TestServiceProvider { get; private set; }

public DummyServiceProviderFactoryContainerBuilder(IServiceCollection serviceDescriptors) => this.serviceDescriptors = serviceDescriptors;

public IServiceProvider Build() => TestServiceProvider = new DummyServiceProvider(serviceDescriptors);
}

private sealed class DummyServiceProviderFactory : IServiceProviderFactory<DummyServiceProviderFactoryContainerBuilder>
{
public DummyServiceProviderFactoryContainerBuilder TestContainerBuilder { get; private set; }

public DummyServiceProviderFactoryContainerBuilder CreateBuilder(IServiceCollection services)
=> TestContainerBuilder = new DummyServiceProviderFactoryContainerBuilder(services);

public IServiceProvider CreateServiceProvider(DummyServiceProviderFactoryContainerBuilder containerBuilder)
=> containerBuilder.Build();
}
}
Loading