Skip to content

Commit

Permalink
Adds registration helper for Custom Delegates as factories (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendankowitz authored Apr 13, 2020
1 parent cbf0175 commit f19a1fe
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects;
using NSubstitute;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class ComponentA : IComponent
{
public string Name { get; } = nameof(ComponentA);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class ComponentB : IComponent
{
public delegate IComponent Factory();

public string Name { get; } = nameof(ComponentB);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public interface IComponent
{
public string Name { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests
namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class TestComponent
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class TestDisposableObjectWithInterface : IEquatable<string>, IDisposable
{
public bool Equals(string other)
{
throw new NotImplementedException();
}

public void Dispose()
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests
namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class TestModule : IStartupModule
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects
{
public class TestScope : IScoped<IList<string>>
{
public TestScope(IList<string> value)
{
Value = value;
}

public IList<string> Value { get; }

public void Dispose()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Health.Extensions.DependencyInjection.UnitTests.TestObjects;
using Xunit;

namespace Microsoft.Health.Extensions.DependencyInjection.UnitTests
Expand Down Expand Up @@ -332,31 +333,56 @@ public void GivenFactory_WhenRegisteringAsSelfAsAService_ThenTheTypeAppearsAsMet
Assert.All(_collection, sd => Assert.Equal(typeof(List<string>), (sd as ServiceDescriptorWithMetadata)?.Metadata));
}

private class TestScope : IScoped<IList<string>>
[Fact]
public void GivenADelegate_WhenResolvingComponent_ThenResolverReturnsRegisteredService()
{
public TestScope(IList<string> value)
{
Value = value;
}
_collection.Add<ComponentA>()
.Transient()
.AsSelf()
.AsService<IComponent>();

_collection.Add<ComponentB>()
.Transient()
.AsSelf();

public IList<string> Value { get; }
_collection.AddDelegate<ComponentB.Factory, ComponentB>();

public void Dispose()
{
}
var provider = _collection.BuildServiceProvider();

var componentFactory = provider.GetService<ComponentB.Factory>();
IComponent instance = componentFactory.Invoke();

Assert.IsType<ComponentB>(instance);
}

private class TestDisposableObjectWithInterface : IEquatable<string>, IDisposable
[Fact]
public void GivenADelegateFromTypeBuilder_WhenResolvingComponent_ThenResolverReturnsRegisteredService()
{
public bool Equals(string other)
{
throw new NotImplementedException();
}
_collection.Add<ComponentA>()
.Transient()
.AsSelf()
.AsService<IComponent>();

public void Dispose()
{
throw new NotImplementedException();
}
_collection.Add<ComponentB>()
.Transient()
.AsSelf()
.AsService<IComponent>()
.AsDelegate<ComponentB.Factory>();

var provider = _collection.BuildServiceProvider();

// Using Func<IComponent> won't work here because there are 2 components that implement this interface.
// Using the delegate ComponentB.Factory works to resolve the desired instance while maintaining the interface
var componentFactory = provider.GetService<ComponentB.Factory>();
IComponent instance = componentFactory.Invoke();

Assert.IsType<ComponentB>(instance);
}

[Fact]
public void GivenADelegateWithIncompatibleType_WhenResolvingComponent_ThenExceptionIsThrown()
{
Assert.Throws<InvalidOperationException>(() => _collection.AddDelegate<ComponentB.Factory, int>());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Microsoft.Health.Extensions.DependencyInjection
public class TypeRegistrationBuilder
{
private readonly MethodInfo _factoryGenericMethod = typeof(TypeRegistrationExtensions).GetMethod(nameof(TypeRegistrationExtensions.AddFactory), BindingFlags.Public | BindingFlags.Static);
private readonly MethodInfo _factoryDelegateMethod = typeof(TypeRegistrationExtensions).GetMethod(nameof(TypeRegistrationExtensions.AddDelegate), BindingFlags.Public | BindingFlags.Static);
private readonly IServiceCollection _serviceCollection;
private readonly Type _type;
private readonly Func<IServiceProvider, object> _delegateRegistration;
Expand Down Expand Up @@ -86,6 +87,21 @@ public TypeRegistrationBuilder AsFactory()
return this;
}

/// <summary>
/// Creates a service registration for the specified interface that can be resolved with a custom delegate
/// </summary>
/// <typeparam name="TDelegate">Custom delegate that will resolve the service</typeparam>
/// <returns>The registration builder</returns>
public TypeRegistrationBuilder AsDelegate<TDelegate>()
where TDelegate : Delegate
{
var factoryMethod = _factoryDelegateMethod.MakeGenericMethod(typeof(TDelegate), _type);

factoryMethod.Invoke(null, new object[] { _serviceCollection });

return this;
}

/// <summary>
/// Replaces a service registration for the specified interface
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ public static TypeRegistration Add(this IServiceCollection serviceCollection, Ty
return new TypeRegistration(serviceCollection, type);
}

/// <summary>
/// Adds a service that allows a factory to be injected that resolves the specified type (MyType CustomDelegate()).
/// This is useful where the type being resolved should be as-needed, or multiple instances need to be created
/// </summary>
/// <typeparam name="TDelegate">Custom delegate that will resolve the service</typeparam>
/// <typeparam name="TImplementation">Type of service to be resolved</typeparam>
/// <param name="serviceCollection">The service collection.</param>
public static void AddDelegate<TDelegate, TImplementation>(this IServiceCollection serviceCollection)
where TDelegate : Delegate
{
EnsureArg.IsNotNull(serviceCollection, nameof(serviceCollection));

Type implementationType = typeof(TImplementation);
Type delegateType = typeof(TDelegate);
Type serviceType = delegateType.GetMethod("Invoke")?.ReturnType;
MethodInfo factoryMethod = typeof(TypeRegistrationExtensions).GetMethod(nameof(FactoryDelegate), BindingFlags.NonPublic | BindingFlags.Static);

Debug.Assert(factoryMethod != null, $"{nameof(Factory)} was not found.");

if (!serviceType.IsAssignableFrom(implementationType))
{
throw new InvalidOperationException($"Delegate '{serviceType.Name} {delegateType.Name}()' cannot be used for resolving implementation type '{implementationType.Name}'");
}

MethodInfo implFactoryMethod = factoryMethod.MakeGenericMethod(delegateType, implementationType, serviceType);
Delegate implDelegate = implFactoryMethod.CreateDelegate(typeof(Func<IServiceProvider, object>), null);
serviceCollection.AddTransient(delegateType, (Func<IServiceProvider, object>)implDelegate);
}

/// <summary>
/// Adds a service that allows a factory to be injected that resolves the specified type (Func{T}).
/// This is useful where the type being resolved should be as-needed, or multiple instances need to be created
Expand Down Expand Up @@ -97,10 +126,24 @@ public static void AddScoped(this IServiceCollection serviceCollection)

private static object Factory<T>(IServiceProvider provider)
{
EnsureArg.IsNotNull(provider, nameof(provider));

Func<T> factory = provider.GetService<T>;
return factory;
}

private static object FactoryDelegate<TDelegate, TImplementation, TService>(IServiceProvider provider)
where TDelegate : Delegate
where TImplementation : TService
{
EnsureArg.IsNotNull(provider, nameof(provider));

var delegateType = typeof(TDelegate);
Func<TService> factory = () => (TService)provider.GetService<TImplementation>();
var invokeMethod = factory.GetType().GetMethod(nameof(factory.Invoke));
return invokeMethod.CreateDelegate(delegateType, factory);
}

private static IEnumerable<T> Do<T>(this IEnumerable<T> registrations, Action<T> action)
{
EnsureArg.IsNotNull(registrations, nameof(registrations));
Expand Down

0 comments on commit f19a1fe

Please sign in to comment.