Skip to content

Commit

Permalink
Updated BuildHistory to store a cache of build capabilities
Browse files Browse the repository at this point in the history
Updated DefaultExecuteStrategy to manage cached build capabilities
  • Loading branch information
roryprimrose committed Jun 15, 2020
1 parent bedcfda commit 1738a25
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 14 deletions.
38 changes: 38 additions & 0 deletions ModelBuilder.UnitTests/BuildHistoryItemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace ModelBuilder.UnitTests
{
using System;
using FluentAssertions;
using Xunit;

public class BuildHistoryItemTests
{
[Fact]
public void CapabilitiesReturnsEmptyByDefault()
{
var value = Guid.NewGuid().ToString();

var actual = new BuildHistoryItem(value);

actual.Capabilities.Should().BeEmpty();
}

[Fact]
public void ThrowsExceptionWithNullInstance()
{
// ReSharper disable once ObjectCreationAsStatement
Action action = () => new BuildHistoryItem(null!);

action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void ValueReturnsConstructorValue()
{
var value = Guid.NewGuid().ToString();

var actual = new BuildHistoryItem(value);

actual.Value.Should().Be(value);
}
}
}
116 changes: 116 additions & 0 deletions ModelBuilder.UnitTests/BuildHistoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,72 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using ModelBuilder.BuildActions;
using ModelBuilder.UnitTests.Models;
using NSubstitute;
using Xunit;

public class BuildHistoryTests
{
[Fact]
public void AddCapabilityDoesNotStoreCapabilityWhenHistoryIsEmpty()
{
var type = typeof(Person);

var capability = Substitute.For<IBuildCapability>();

var sut = new BuildHistory();

sut.AddCapability(type, capability);

var actual = sut.GetCapability(type);

actual.Should().BeNull();
}

[Fact]
public void AddCapabilityStoresCapability()
{
var type = typeof(Address);
var value = new Person();

var capability = Substitute.For<IBuildCapability>();

var sut = new BuildHistory();

sut.Push(value);

sut.AddCapability(type, capability);

var actual = sut.GetCapability(type);

actual.Should().Be(capability);
}

[Fact]
public void AddCapabilityThrowsExceptionWithNullCapability()
{
var type = typeof(Person);

var sut = new BuildHistory();

Action action = () => sut.AddCapability(type, null!);

action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void AddCapabilityThrowsExceptionWithNullType()
{
var capability = Substitute.For<IBuildCapability>();

var sut = new BuildHistory();

Action action = () => sut.AddCapability(null!, capability);

action.Should().Throw<ArgumentNullException>();
}

[Theory]
[InlineData(0)]
[InlineData(1)]
Expand Down Expand Up @@ -171,6 +232,61 @@ public void FirstReturnsSingleValueInHistory()
sut.First.Should().BeSameAs(item);
}

[Fact]
public void GetCapabilityReturnsNullWhenBuildHistoryIsEmpty()
{
var type = typeof(Address);

var sut = new BuildHistory();

var actual = sut.GetCapability(type);

actual.Should().BeNull();
}

[Fact]
public void GetCapabilityReturnsNullWhenNoCapabilityStoredForType()
{
var type = typeof(Address);
var value = new Person();

var sut = new BuildHistory();

sut.Push(value);

var actual = sut.GetCapability(type);

actual.Should().BeNull();
}

[Fact]
public void GetCapabilityReturnsStoredCapability()
{
var type = typeof(Address);
var value = new Person();

var capability = Substitute.For<IBuildCapability>();

var sut = new BuildHistory();

sut.Push(value);
sut.AddCapability(type, capability);

var actual = sut.GetCapability(type);

actual.Should().Be(capability);
}

[Fact]
public void GetCapabilityThrowsExceptionWithNullType()
{
var sut = new BuildHistory();

Action action = () => sut.GetCapability(null!);

action.Should().Throw<ArgumentNullException>();
}

[Theory]
[InlineData(1)]
[InlineData(2)]
Expand Down
64 changes: 57 additions & 7 deletions ModelBuilder/BuildHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ModelBuilder.BuildActions;

/// <summary>
/// The <see cref="BuildHistory" />
Expand All @@ -15,20 +17,66 @@
Justification = "The history is enumerable, but does not have the characteristics of a Collection.")]
public class BuildHistory : IBuildHistory
{
private readonly Stack<object> _buildHistory = new Stack<object>();
private readonly Stack<BuildHistoryItem> _buildHistory = new Stack<BuildHistoryItem>();

/// <inheritdoc />
public void AddCapability(Type type, IBuildCapability capability)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

if (capability == null)
{
throw new ArgumentNullException(nameof(capability));
}

if (_buildHistory.Count == 0)
{
return;
}

var historyItem = _buildHistory.Peek();

historyItem.Capabilities[type] = capability;
}

/// <inheritdoc />
public IBuildCapability? GetCapability(Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

if (_buildHistory.Count == 0)
{
return null;
}

var historyItem = _buildHistory.Peek();

if (historyItem.Capabilities.ContainsKey(type))
{
return historyItem.Capabilities[type];
}

return null;
}

/// <inheritdoc />
public IEnumerator GetEnumerator()
{
return _buildHistory.GetEnumerator();
return _buildHistory.Select(x => x.Value).GetEnumerator();
}

/// <inheritdoc />
public void Pop()
{
var instance = _buildHistory.Pop();
var historyItem = _buildHistory.Pop();

if (First == instance)
if (First == historyItem.Value)
{
First = null;
}
Expand All @@ -43,7 +91,9 @@ public void Push(object instance)
throw new ArgumentNullException(nameof(instance));
}

_buildHistory.Push(instance);
var historyItem = new BuildHistoryItem(instance);

_buildHistory.Push(historyItem);

if (First == null)
{
Expand All @@ -54,7 +104,7 @@ public void Push(object instance)
/// <inheritdoc />
IEnumerator<object> IEnumerable<object>.GetEnumerator()
{
return _buildHistory.GetEnumerator();
return _buildHistory.Select(x => x.Value).GetEnumerator();
}

/// <inheritdoc />
Expand All @@ -73,7 +123,7 @@ IEnumerator<object> IEnumerable<object>.GetEnumerator()
return null;
}

return _buildHistory.Peek();
return _buildHistory.Peek().Value;
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions ModelBuilder/BuildHistoryItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace ModelBuilder
{
using System;
using System.Collections.Generic;
using ModelBuilder.BuildActions;

/// <summary>
/// The <see cref="BuildHistoryItem" />
/// class is used to track items built and related build capabilities.
/// </summary>
public class BuildHistoryItem
{
/// <summary>
/// Initializes a new instance of the <see cref="BuildHistoryItem" /> class.
/// </summary>
/// <param name="value">The value created.</param>
public BuildHistoryItem(object value)
{
Value = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// The cache of build capabilities by requested types.
/// </summary>
public Dictionary<Type, IBuildCapability> Capabilities { get; } = new Dictionary<Type, IBuildCapability>();

/// <summary>
/// Gets the value created.
/// </summary>
public object Value { get; }
}
}
30 changes: 23 additions & 7 deletions ModelBuilder/DefaultExecuteStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,14 @@ protected virtual object Build(Type type, params object?[]? args)

try
{
IBuildCapability GetCapability()
{
return _buildProcessor.GetBuildCapability(this, BuildRequirement.Create, type);
}

var instance = Build(
GetCapability,
() => GetCreateTypeCapability(type),
(capability, items) => capability.CreateType(this, type, items), type, args)!;

if (instance == null)
{
// The Build method above would have thrown an exception if the build capability could not be identified
var capability = GetCapability()!;
var capability = GetCreateTypeCapability(type)!;

var message = string.Format(CultureInfo.CurrentCulture,
"The type '{0}' failed to create a non-null value of type '{1}'",
Expand Down Expand Up @@ -392,6 +387,27 @@ private void AutoPopulateInstance(object instance, object?[]? args)
}
}

private IBuildCapability GetCreateTypeCapability(Type type)
{
// As an internal implementation, we know that implementations like EnumerableTypeCreator and ArrayTypeCreator
// will make many calls to get a build capability for the same type and state of the build chain.
// We can increase performance if we can cache the build capabilities for the same requested type for the same item
// at the end of the build chain
var capability = _buildHistory.GetCapability(type);

if (capability != null)
{
// We have already calculated this capability
return capability;
}

capability = _buildProcessor.GetBuildCapability(this, BuildRequirement.Create, type);

_buildHistory.AddCapability(type, capability);

return capability;
}

private object Populate(IBuildCapability capability, object instance, params object?[]? args)
{
if (capability.SupportsPopulate == false)
Expand Down
16 changes: 16 additions & 0 deletions ModelBuilder/IBuildHistory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace ModelBuilder
{
using System;
using System.Diagnostics.CodeAnalysis;
using ModelBuilder.BuildActions;

/// <summary>
/// The <see cref="IBuildHistory" />
Expand All @@ -12,6 +14,20 @@
Justification = "The history is enumerable, but does not have the characteristics of a Collection.")]
public interface IBuildHistory : IBuildChain
{
/// <summary>
/// Adds a build capability for the specified type.
/// </summary>
/// <param name="type">The type being created.</param>
/// <param name="capability">The build capability.</param>
void AddCapability(Type type, IBuildCapability capability);

/// <summary>
/// Gets the build capability for the specified type.
/// </summary>
/// <param name="type">The type to build.</param>
/// <returns>The build capability or <c>null</c> if no capability exists for the type.</returns>
IBuildCapability? GetCapability(Type type);

/// <summary>
/// Removes the last item added to the build chain.
/// </summary>
Expand Down

0 comments on commit 1738a25

Please sign in to comment.