Skip to content

Commit

Permalink
Added SingletonTypeCreator (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
roryprimrose committed Jun 14, 2020
1 parent a853e95 commit 8558c67
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 6 deletions.
5 changes: 4 additions & 1 deletion ModelBuilder.UnitTests/Models/Singleton.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
namespace ModelBuilder.UnitTests.Models
{
using System;

public class Singleton
{
private Singleton()
{
Value = Guid.NewGuid().ToString();
}

public static Singleton Instance { get; } = new Singleton();

public string? Value { get; set; }
public string Value { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

public class NonConstructorCreationTests
{
[Fact]
public void CanCreateViaSingletonProperty()
{
var actual = Model.Create<Singleton>();

actual.Value.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public void CanCreateViaStaticFactoryMethodWithCreatedParameters()
{
Expand Down
191 changes: 191 additions & 0 deletions ModelBuilder.UnitTests/TypeCreators/SingletonTypeCreatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
namespace ModelBuilder.UnitTests.TypeCreators
{
using System;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using ModelBuilder.TypeCreators;
using ModelBuilder.UnitTests.Models;
using NSubstitute;
using Xunit;

public class SingletonTypeCreatorTests
{
[Theory]
[InlineData(CacheLevel.Global)]
[InlineData(CacheLevel.PerInstance)]
[InlineData(CacheLevel.None)]
public void CacheLevelReturnsConstructorValue(CacheLevel cacheLevel)
{
var sut = new SingletonTypeCreator(cacheLevel);

sut.CacheLevel.Should().Be(cacheLevel);
}

[Fact]
public void CanCreateReturnsFalseForInterface()
{
var type = typeof(ITestItem);

var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var buildChain = Substitute.For<IBuildChain>();

configuration.TypeResolver.Returns(typeResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.CanCreate(configuration, buildChain, type);

actual.Should().BeFalse();
}

[Fact]
public void CanCreateReturnsFalseWhenConstructorWithArgumentsFound()
{
var type = typeof(SimpleConstructor);
var constructorInfo = type.GetConstructors().First();

var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var constructorResolver = Substitute.For<IConstructorResolver>();
var buildChain = Substitute.For<IBuildChain>();

configuration.TypeResolver.Returns(typeResolver);
configuration.ConstructorResolver.Returns(constructorResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);
constructorResolver.Resolve(type, Arg.Any<object?[]?>()).Returns(constructorInfo);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.CanCreate(configuration, buildChain, type);

actual.Should().BeFalse();
}

[Fact]
public void CanCreateReturnsFalseWhenDefaultConstructorFound()
{
var type = typeof(TestItem);
var constructorInfo = type.GetConstructors().First();

var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var constructorResolver = Substitute.For<IConstructorResolver>();
var buildChain = Substitute.For<IBuildChain>();

configuration.TypeResolver.Returns(typeResolver);
configuration.ConstructorResolver.Returns(constructorResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);
constructorResolver.Resolve(type, Arg.Any<object?[]?>()).Returns(constructorInfo);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.CanCreate(configuration, buildChain, type);

actual.Should().BeFalse();
}

[Fact]
public void CanCreateReturnsTrueWhenNoPublicConstructorAndStaticSingletonPropertyExists()
{
var type = typeof(Singleton);
ConstructorInfo? constructorInfo = null;

var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var constructorResolver = Substitute.For<IConstructorResolver>();
var buildChain = Substitute.For<IBuildChain>();

configuration.TypeResolver.Returns(typeResolver);
configuration.ConstructorResolver.Returns(constructorResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);
constructorResolver.Resolve(type, Arg.Any<object?[]?>()).Returns(constructorInfo);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.CanCreate(configuration, buildChain, type);

actual.Should().BeTrue();
}

[Fact]
public void CreateReturnsValueFromSingleton()
{
var type = typeof(Singleton);
ConstructorInfo? constructorInfo = null;

var executeStrategy = Substitute.For<IExecuteStrategy>();
var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var constructorResolver = Substitute.For<IConstructorResolver>();
var buildChain = Substitute.For<IBuildChain>();

executeStrategy.Configuration.Returns(configuration);
executeStrategy.BuildChain.Returns(buildChain);
configuration.TypeResolver.Returns(typeResolver);
configuration.ConstructorResolver.Returns(constructorResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);
constructorResolver.Resolve(type, Arg.Any<object?[]?>()).Returns(constructorInfo);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.Create(executeStrategy, type);

actual.Should().BeOfType<Singleton>();
actual.As<Singleton>().Value.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public void CreateReturnsValueWithoutCache()
{
var type = typeof(Singleton);
ConstructorInfo? constructorInfo = null;

var executeStrategy = Substitute.For<IExecuteStrategy>();
var configuration = Substitute.For<IBuildConfiguration>();
var typeResolver = Substitute.For<ITypeResolver>();
var constructorResolver = Substitute.For<IConstructorResolver>();
var buildChain = Substitute.For<IBuildChain>();

executeStrategy.Configuration.Returns(configuration);
executeStrategy.BuildChain.Returns(buildChain);
configuration.TypeResolver.Returns(typeResolver);
configuration.ConstructorResolver.Returns(constructorResolver);
typeResolver.GetBuildType(configuration, type).Returns(type);
constructorResolver.Resolve(type, Arg.Any<object?[]?>()).Returns(constructorInfo);

var sut = new SingletonTypeCreator(CacheLevel.None);

var actual = sut.Create(executeStrategy, type);

actual.Should().BeOfType<Singleton>();
actual.As<Singleton>().Value.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public void PopulateReturnsProvidedInstance()
{
var expected = Model.Create<Simple>()!;
var buildChain = new BuildHistory();
var constructorResolver = new DefaultConstructorResolver(CacheLevel.PerInstance);

var executeStrategy = Substitute.For<IExecuteStrategy>();
var typeResolver = Substitute.For<ITypeResolver>();
var configuration = Substitute.For<IBuildConfiguration>();

configuration.TypeResolver.Returns(typeResolver);
typeResolver.GetBuildType(configuration, Arg.Any<Type>()).Returns(x => x.Arg<Type>());
executeStrategy.BuildChain.Returns(buildChain);
executeStrategy.Configuration.Returns(configuration);
configuration.ConstructorResolver.Returns(constructorResolver);

var sut = new SingletonTypeCreator(CacheLevel.PerInstance);

var actual = sut.Populate(executeStrategy, expected);

actual.Should().Be(expected);
}
}
}
3 changes: 3 additions & 0 deletions ModelBuilder/DefaultConfigurationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ private static void AddTypeCreators(IBuildConfiguration configuration)
var factoryTypeCreator = new FactoryTypeCreator(CacheLevel.Global);
configuration.Add(factoryTypeCreator);

var singletonTypeCreator = new SingletonTypeCreator(CacheLevel.Global);
configuration.Add(singletonTypeCreator);

configuration.AddTypeCreator<ArrayTypeCreator>();
configuration.AddTypeCreator<EnumerableTypeCreator>();
configuration.AddTypeCreator<StructTypeCreator>();
Expand Down
10 changes: 5 additions & 5 deletions ModelBuilder/TypeCreators/FactoryTypeCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public FactoryTypeCreator(CacheLevel cacheLevel)
{
var buildType = ResolveBuildType(executeStrategy.Configuration, type);

// The base class has already validated CanCreate which ensures that the factory method is availabe
// The base class has already validated CanCreate which ensures that the factory method is available
var method = GetFactoryMethod(buildType)!;

if (args?.Length > 0)
Expand All @@ -87,10 +87,10 @@ protected override object PopulateInstance(IExecuteStrategy executeStrategy, obj

private static MethodInfo? CalculateFactoryMethod(Type type)
{
var bindingFlags = BindingFlags.Static
| BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.InvokeMethod;
const BindingFlags bindingFlags = BindingFlags.Static
| BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.InvokeMethod;

// Get all the public static methods that return the return type but do not have the return type as a parameter
// order by the the methods with the least amount of parameters
Expand Down
122 changes: 122 additions & 0 deletions ModelBuilder/TypeCreators/SingletonTypeCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
namespace ModelBuilder.TypeCreators
{
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;

/// <summary>
/// The <see cref="SingletonTypeCreator" />
/// class is used to create a value using a Singleton property found on the type.
/// </summary>
public class SingletonTypeCreator : TypeCreatorBase
{
private static readonly ConcurrentDictionary<Type, PropertyInfo?> _globalCache =
new ConcurrentDictionary<Type, PropertyInfo?>();

private readonly ConcurrentDictionary<Type, PropertyInfo?> _perInstanceCache =
new ConcurrentDictionary<Type, PropertyInfo?>();

/// <summary>
/// Initializes a new instance of the <see cref="SingletonTypeCreator" /> class.
/// </summary>
/// <param name="cacheLevel">The cache level to use for resolved methods.</param>
public SingletonTypeCreator(CacheLevel cacheLevel)
{
CacheLevel = cacheLevel;
}

/// <inheritdoc />
protected override bool CanCreate(IBuildConfiguration configuration, IBuildChain buildChain, Type type,
string? referenceName)
{
var buildType = ResolveBuildType(configuration, type);

var baseValue = base.CanCreate(configuration, buildChain, buildType, referenceName);

if (baseValue == false)
{
return false;
}

// Check if there is no constructor to use
var constructor = configuration.ConstructorResolver.Resolve(buildType);

if (constructor != null)
{
// There is a valid constructor to use so we don't need to search for a singleton property
return false;
}

var propertyInfo = GetSingletonProperty(buildType);

if (propertyInfo == null)
{
// There is no factory method that can be used
return false;
}

return true;
}

/// <inheritdoc />
protected override object? CreateInstance(IExecuteStrategy executeStrategy, Type type, string? referenceName,
params object?[]? args)
{
var buildType = ResolveBuildType(executeStrategy.Configuration, type);

// The base class has already validated CanCreate which ensures that the singleton property is available
var propertyInfo = GetSingletonProperty(buildType)!;

return propertyInfo.GetValue(null, args);
}

/// <inheritdoc />
protected override object PopulateInstance(IExecuteStrategy executeStrategy, object instance)
{
return instance;
}

private static PropertyInfo? CalculateSingletonProperty(Type type)
{
const BindingFlags bindingFlags = BindingFlags.Static
| BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.GetProperty;

// Get all the public static readonly properties that return the return type
var methods = from x in type.GetProperties(bindingFlags)
where x.GetIndexParameters().Length == 0
&& type.IsAssignableFrom(x.PropertyType)
select x;

return methods.FirstOrDefault();
}

private PropertyInfo? GetSingletonProperty(Type type)
{
if (CacheLevel == CacheLevel.Global)
{
return _globalCache.GetOrAdd(type,
x => CalculateSingletonProperty(type));
}

if (CacheLevel == CacheLevel.PerInstance)
{
return _perInstanceCache.GetOrAdd(type,
x => CalculateSingletonProperty(type));
}

return CalculateSingletonProperty(type);
}

/// <summary>
/// Gets or sets whether resolved singleton properties are cached.
/// </summary>
/// <returns>Returns the cache level to apply to properties.</returns>
public CacheLevel CacheLevel { get; set; }

/// <inheritdoc />
public override int Priority { get; } = 200;
}
}

0 comments on commit 8558c67

Please sign in to comment.