-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a853e95
commit 8558c67
Showing
6 changed files
with
333 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
ModelBuilder.UnitTests/TypeCreators/SingletonTypeCreatorTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |