Skip to content

Commit

Permalink
Merge pull request #60 from Cratis:generalize-change-tracking
Browse files Browse the repository at this point in the history
Making the target object on Changeset generic
  • Loading branch information
einari authored Nov 10, 2021
2 parents b89bd78 + a21118c commit d363272
Show file tree
Hide file tree
Showing 57 changed files with 559 additions and 335 deletions.
6 changes: 2 additions & 4 deletions Source/Fundamentals/Changes/Change.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;

namespace Cratis.Changes
{
/// <summary>
/// Defines a change as part of a <see cref="Changeset{T}"/>.
/// Defines a change as part of a <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <param name="State">State after change applied.</param>
public record Change(ExpandoObject State);
public record Change(object State);
}
108 changes: 58 additions & 50 deletions Source/Fundamentals/Changes/Changes.cs
Original file line number Diff line number Diff line change
@@ -1,131 +1,139 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Dynamic;
using Cratis.Objects;
using Cratis.Properties;

namespace Cratis.Changes
{
/// <summary>
/// Represents changes that can be applied to a <see cref="Changeset{T}"/>.
/// Represents changes that can be applied to a <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
public static class Changes
{
/// <summary>
/// Applies properties to the <see cref="Changeset{T}"/>.
/// Applies properties to the <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to apply to.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{TSource, TTarget}">property mappers</see> that will manipulate properties on the target.</param>
/// <remarks>
/// This will run a diff against the initial state and only produce changes that are new.
/// </remarks>
public static void ApplyProperties<T>(this Changeset<T> changeset, IEnumerable<PropertyMapper<T>> propertyMappers)
public static void ApplyProperties<TSource, TTarget>(this Changeset<TSource, TTarget> changeset, IEnumerable<PropertyMapper<TSource, TTarget>> propertyMappers)
{
var workingState = changeset.InitialState.Clone();
var workingState = changeset.InitialState.Clone()!;
foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Incoming, workingState);
}

var comparer = new ObjectsComparer.Comparer<ExpandoObject>();
var comparer = new ObjectsComparer.Comparer<TTarget>();
if (!comparer.Compare(changeset.InitialState, workingState, out var differences))
{
changeset.Add(new PropertiesChanged(workingState, differences.Select(_ => new PropertyDifference(changeset.InitialState, workingState, _))));
changeset.Add(new PropertiesChanged<TTarget>(workingState, differences.Select(_ => new PropertyDifference<TTarget>(changeset.InitialState, workingState, _))));
}
}

/// <summary>
/// Applies properties for a child to the <see cref="Changeset{T}"/>.
/// Applies properties for a child to the <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <typeparam name="TChild">Type of child.</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to apply to.</param>
/// <param name="item">The item to add from.</param>
/// <param name="childrenProperty">The <see cref="Property"/> on the parent that holds the children.</param>
/// <param name="identifiedByProperty">The <see cref="Property"/> on the instance that identifies the child.</param>
/// <param name="childrenProperty">The <see cref="PropertyPath"/> on the parent that holds the children.</param>
/// <param name="identifiedByProperty">The <see cref="PropertyPath"/> on the instance that identifies the child.</param>
/// <param name="keyResolver">The <see cref="ValueProvider{T}"/> for resolving the key on the event.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{TSource, TTarget}">property mappers</see> that will manipulate properties on the target.</param>
/// <remarks>
/// This will run a diff against the initial state and only produce changes that are new.
/// </remarks>
public static void ApplyChildProperties<T>(
this Changeset<T> changeset,
ExpandoObject item,
Property childrenProperty,
Property identifiedByProperty,
ValueProvider<T> keyResolver,
IEnumerable<PropertyMapper<T>> propertyMappers)
public static void ApplyChildProperties<TSource, TTarget, TChild>(
this Changeset<TSource, TTarget> changeset,
TChild item,
PropertyPath childrenProperty,
PropertyPath identifiedByProperty,
ValueProvider<TSource> keyResolver,
IEnumerable<PropertyMapper<TSource, TChild>> propertyMappers)
{
var workingItem = item.Clone();
var workingItem = item.Clone()!;
foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Incoming, workingItem);
}

var comparer = new ObjectsComparer.Comparer<ExpandoObject>();
var comparer = new ObjectsComparer.Comparer();
if (!comparer.Compare(item, workingItem, out var differences))
{
changeset.Add(new ChildPropertiesChanged(
changeset.Add(new ChildPropertiesChanged<TChild>(
workingItem,
childrenProperty,
identifiedByProperty,
keyResolver(changeset.Incoming),
differences.Select(_ => new PropertyDifference(item, workingItem, _))));
differences.Select(_ => new PropertyDifference<TChild>(item, workingItem, _))));
}
}

/// <summary>
/// Applies properties to the child in the model to the <see cref="Changeset{T}"/>.
/// Applies properties to the child in the model to the <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <param name="childrenProperty"><see cref="Property"/> for accessing the children collection.</param>
/// <param name="identifiedByProperty"><see cref="Property"/> that identifies the child.</param>
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <typeparam name="TChild">Type of child.</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to apply to.</param>
/// <param name="childrenProperty"><see cref="PropertyPath"/> for accessing the children collection.</param>
/// <param name="identifiedByProperty"><see cref="PropertyPath"/> that identifies the child.</param>
/// <param name="key">Key value.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{TSource, TTarget}">property mappers</see> that will manipulate properties on the target.</param>
/// <exception cref="ChildrenPropertyIsNotEnumerable">Thrown when children property is not enumerable.</exception>
public static void ApplyAddChild<T>(
this Changeset<T> changeset,
Property childrenProperty,
Property identifiedByProperty,
public static void ApplyAddChild<TSource, TTarget, TChild>(
this Changeset<TSource, TTarget> changeset,
PropertyPath childrenProperty,
PropertyPath identifiedByProperty,
object key,
IEnumerable<PropertyMapper<T>> propertyMappers)
IEnumerable<PropertyMapper<TSource, TChild>> propertyMappers)
where TChild : new()
{
var workingState = changeset.InitialState.Clone();
var items = workingState.EnsureCollection(childrenProperty);
var workingState = changeset.InitialState.Clone()!;
var items = workingState.EnsureCollection<TTarget, TChild>(childrenProperty);

if (!items.Contains(identifiedByProperty, key))
{
var item = new ExpandoObject();
var item = new TChild();

foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Incoming, item);
}

identifiedByProperty.SetValue(item, key);
((IList<ExpandoObject>)items).Add(item);
((IList<TChild>)items).Add(item);

changeset.Add(new ChildAdded(item, childrenProperty, identifiedByProperty, key!));
}
}

/// <summary>
/// Apply a remove change to the <see cref="Changeset{T}"/>.
/// Apply a remove change to the <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
public static void ApplyRemove<T>(this Changeset<T> changeset)
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to apply to.</param>
public static void ApplyRemove<TSource, TTarget>(this Changeset<TSource, TTarget> changeset)
{
}

/// <summary>
/// Apply a remove child change to the <see cref="Changeset{T}"/>.
/// Apply a remove child change to the <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
public static void ApplyRemoveChild<T>(this Changeset<T> changeset)
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to apply to.</param>
public static void ApplyRemoveChild<TSource, TTarget>(this Changeset<TSource, TTarget> changeset)
{
}
}
Expand Down
19 changes: 9 additions & 10 deletions Source/Fundamentals/Changes/Changeset.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;

namespace Cratis.Changes
{
/// <summary>
/// Represents a changeset of changes that can occur to an object.
/// </summary>
/// <typeparam name="T">Type of object we're working on.</typeparam>
public class Changeset<T>
/// <typeparam name="TSource">Type of the source object we are working from.</typeparam>
/// <typeparam name="TTarget">Type of target object we are applying changes to.</typeparam>
public class Changeset<TSource, TTarget>
{
readonly List<Change> _changes = new();

/// <summary>
/// Initializes a new instance of <see cref="Changeset{T}"/>.
/// Initializes a new instance of <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
/// <param name="incoming"><see cref="Incoming"/> that the <see cref="Changeset{T}"/> is for.</param>
/// <param name="incoming"><see cref="Incoming"/> that the <see cref="Changeset{TSource, TTarget}"/> is for.</param>
/// <param name="initialState">The initial state before any changes are applied.</param>
public Changeset(T incoming, ExpandoObject initialState)
public Changeset(TSource incoming, TTarget initialState)
{
Incoming = incoming;
InitialState = initialState;
}

/// <summary>
/// Gets the <see cref="Incoming"/> the <see cref="Changeset{T}"/> is for.
/// Gets the <see cref="Incoming"/> the <see cref="Changeset{TSource, TTarget}"/> is for.
/// </summary>
public T Incoming { get; }
public TSource Incoming { get; }

/// <summary>
/// Gets the initial state of before changes in changeset occurred.
/// </summary>
public ExpandoObject InitialState { get; }
public TTarget InitialState { get; }

/// <summary>
/// Gets all the changes for the changeset.
Expand Down
27 changes: 14 additions & 13 deletions Source/Fundamentals/Changes/ChangesetExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Dynamic;
using Cratis.Properties;

namespace Cratis.Changes
{
/// <summary>
/// Extension methods for working with <see cref="Changeset{T}"/>.
/// Extension methods for working with <see cref="Changeset{TSource, TTarget}"/>.
/// </summary>
public static class ChangesetExtensions
{
/// <summary>
/// Check if changeset contains a <see cref="ChildAdded"/> to a collection with a specific key.
/// </summary>
/// <typeparam name="T">Type of object for the changeset.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to check.</param>
/// <param name="childrenProperty">The <see cref="Property"/> representing the collection.</param>
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to check.</param>
/// <param name="childrenProperty">The <see cref="PropertyPath"/> representing the collection.</param>
/// <param name="key">The key of the item.</param>
/// <returns>True if it has, false it not.</returns>
public static bool HasChildBeenAddedWithKey<T>(this Changeset<T> changeset, Property childrenProperty, object key)
public static bool HasChildBeenAddedWithKey<TSource, TTarget>(this Changeset<TSource, TTarget> changeset, PropertyPath childrenProperty, object key)
{
return changeset.Changes
.Select(_ => _ as ChildAdded)
Expand All @@ -30,15 +29,17 @@ public static bool HasChildBeenAddedWithKey<T>(this Changeset<T> changeset, Prop
/// <summary>
/// Get a specific child from
/// </summary>
/// <typeparam name="T">Type of object for the changeset.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to get from.</param>
/// <param name="childrenProperty">The <see cref="Property"/> representing the collection.</param>
/// <param name="identifiedByProperty">The <see cref="Property"/> that identifies the child</param>
/// <typeparam name="TSource">Source type for the changeset.</typeparam>
/// <typeparam name="TTarget">Target type for the changeset</typeparam>
/// <typeparam name="TChild">Type of child.</typeparam>
/// <param name="changeset"><see cref="Changeset{TSource, TTarget}"/> to get from.</param>
/// <param name="childrenProperty">The <see cref="PropertyPath"/> representing the collection.</param>
/// <param name="identifiedByProperty">The <see cref="PropertyPath"/> that identifies the child</param>
/// <param name="key">The key of the item.</param>
/// <returns>The added child.</returns>
public static ExpandoObject GetChildByKey<T>(this Changeset<T> changeset, Property childrenProperty, Property identifiedByProperty, object key)
public static TChild GetChildByKey<TSource, TTarget, TChild>(this Changeset<TSource, TTarget> changeset, PropertyPath childrenProperty, PropertyPath identifiedByProperty, object key)
{
var items = childrenProperty.GetValue(changeset.InitialState) as IEnumerable<ExpandoObject>;
var items = childrenProperty.GetValue(changeset.InitialState!) as IEnumerable<TChild>;
return items!.FindByKey(identifiedByProperty, key)!;
}
}
Expand Down
5 changes: 2 additions & 3 deletions Source/Fundamentals/Changes/ChildAdded.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Properties;

namespace Cratis.Changes
{
/// <summary>
/// Represents a child that has been added to a parent.
/// </summary>
/// <param name="State">State of the object being added.</param>
/// <param name="Child">State of the object being added.</param>
/// <param name="ChildrenProperty">The property holding the children in the parent object.</param>
/// <param name="IdentifiedByProperty">The property that identifies the key on the child object.</param>
/// <param name="Key">Key of the object.</param>
public record ChildAdded(ExpandoObject State, Property ChildrenProperty, Property IdentifiedByProperty, object Key) : Change(State);
public record ChildAdded(object Child, PropertyPath ChildrenProperty, PropertyPath IdentifiedByProperty, object Key) : Change(Child);
}
4 changes: 2 additions & 2 deletions Source/Fundamentals/Changes/ChildPropertiesChanged.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Properties;

namespace Cratis.Changes
{
/// <summary>
/// Represents properties that has been changed on a child.
/// </summary>
/// <typeparam name="TTarget">Target type.</typeparam>
/// <param name="State">State after change applied.</param>
/// <param name="ChildrenProperty">The property holding the children in the parent object.</param>
/// <param name="IdentifiedByProperty">The property that identifies the key on the child object.</param>
/// <param name="Key">Key of the object.</param>
/// <param name="Differences">The differences between initial state and a change.</param>
public record ChildPropertiesChanged(ExpandoObject State, Property ChildrenProperty, Property IdentifiedByProperty, object Key, IEnumerable<PropertyDifference> Differences) : Change(State);
public record ChildPropertiesChanged<TTarget>(object State, PropertyPath ChildrenProperty, PropertyPath IdentifiedByProperty, object Key, IEnumerable<PropertyDifference<TTarget>> Differences) : Change(State);
}
Loading

0 comments on commit d363272

Please sign in to comment.