ehorrent / With.Extensions

Extension methods used to copy and update immutable classes
http://ehorrent.github.io/With.Extensions/
MIT License
2 stars 1 forks source link

Supporting more complex objects #2

Open nzbart opened 7 years ago

nzbart commented 7 years ago

Would it be possible to support more complex scenarios, such as collections?

For example, could I "modify" one of the values in Wrapper.Collection?

class Value
{
    public Value(string someString)
    {
        SomeString = someString;
    }

    public string SomeString { get; }
}

class Wrapper
{
    public Wrapper(int value, IEnumerable<Value> collection)
    {
        Value = value;
        Collection = collection.ToArray();
    }

    public int Value { get; }
    public IReadOnlyCollection<Value> Collection { get; }
}
        var value = new Wrapper(42, new[] { new Value("Hello"), new Value("World") });

        //This works well
        var newValue = value.With(w => w.Value, 21).Create();

        //How can I change one of the values in the collection?
        //new Wrapper(42, new[] { new Value("Hello"), new Value("Foo") });
nzbart commented 7 years ago

If you're interested, I've written a feature I'm using to allow access to deep properties. Sample tests:

using FluentAssertions;
using NUnit.Framework;
using With;

class SampleOuterOuter
{
    public SampleOuterOuter(int value, SampleOuterOuter recursiveInner)
    {
        Value = value;
        RecursiveInner = recursiveInner;
    }

    public int Value { get; }
    public SampleOuterOuter RecursiveInner { get; }
}

class SampleOuter
{
    public SampleOuter(int value, SampleInner inner)
    {
        Value = value;
        Inner = inner;
    }

    public int Value { get; }
    public SampleInner Inner { get; }
}

class SampleInner
{
    public SampleInner(string text)
    {
        Text = text;
    }

    public string Text { get; }
}

public class WithDepthExtensionsTests
{
    [Test]
    public void Given_an_object_with_a_nested_object_When_updating_the_nested_object_Then_the_original_is_unchanged_And_the_new_object_is_correct()
    {
        var value = new SampleOuter(42, new SampleInner("Hello, World!"));

        var modified = value.CopyWithDeep(v => v.Inner, v => v.Text, "Changed");

        value.Value.Should().Be(42);
        value.Inner.Text.Should().Be("Hello, World!");
        modified.Value.Should().Be(42);
        modified.Inner.Text.Should().Be("Changed");
    }

    [Test]
    public void Given_a_deeply_nested_object_When_updating_the_deepest_object_Then_the_original_is_unchanged_And_the_new_object_is_correct()
    {
        var value = new SampleOuterOuter(1, new SampleOuterOuter(2, new SampleOuterOuter(3, new SampleOuterOuter(4, null))));

        var modified = value.CopyWithDeep(v => v.RecursiveInner, v => v.RecursiveInner, v => v.RecursiveInner, v => v.Value, 1000);

        value.Value.Should().Be(1);
        value.RecursiveInner.RecursiveInner.RecursiveInner.Value.Should().Be(4);
        modified.Value.Should().Be(1);
        modified.RecursiveInner.RecursiveInner.RecursiveInner.Value.Should().Be(1000);
    }

    [Test]
    public void Given_an_object_with_a_nested_object_When_updating_the_outer_object_Then_the_value_is_changed_And_the_original_reference_to_the_inner_object_is_unchanged()
    {
        var value = new SampleOuter(42, new SampleInner("Hello, World!"));

        var modified = value.CopyWith(v => v.Value, 21);

        value.Value.Should().Be(42);
        modified.Value.Should().Be(21);
        modified.Inner.Should().BeSameAs(value.Inner);
    }
}

I had to codegen the implementation:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System;
using System.Linq.Expressions;
using With;

public static class WithDepthExtensions
{
    public static TSource CopyWithDeep<TSource, TPropertyOrField>(
        this TSource source,
        Expression<Func<TSource, TPropertyOrField>> propertyOrFieldSelector,
        TPropertyOrField value)
        where TSource : class
    {
        return source.With(propertyOrFieldSelector, value).Create();
    }

<# Func<int, string> BuildTemplateParameters = (int number) => string.Join(", ", Enumerable.Range(1, number).Select(n => "TPropertyOrField" + n)); #>
<# Func<int, string> BuildMethodParameters = (int number) => string.Join("\r\n      ", Enumerable.Range(1, number).Select(n => "Expression<Func<TPropertyOrField" + n + ", TPropertyOrField" + (n + 1) + ">> propertyOrFieldSelector" + (n + 1) + ",")); #>
<# Func<int, string> BuildWhereClause = (int number) => string.Join("\r\n       ", Enumerable.Range(1, number).Select(n => "where TPropertyOrField" + n + ": class")); #>
<# Func<int, string> BuildRecursionArguments = (int number) => string.Join(", ", Enumerable.Range(2, number).Select(n => "propertyOrFieldSelector" + n)); #>

<# for(int i = 2; i <= 10; ++i) { #>
    public static TSource CopyWithDeep<TSource, <#= BuildTemplateParameters(i) #>>(
        this TSource source,
        Expression<Func<TSource, TPropertyOrField1>> propertyOrFieldSelector1,
        <#= BuildMethodParameters(i - 1) #>
        TPropertyOrField<#= i #> value)
        where TSource : class
        <#= BuildWhereClause(i - 1) #>
    {

        var innerValue = propertyOrFieldSelector1.Compile().Invoke(source).CopyWithDeep(<#= BuildRecursionArguments(i - 1) #>, value);
        return source.CopyWithDeep(propertyOrFieldSelector1, innerValue);
    }

<# } #>
}

If you're interested, I could submit this code as a pull request. I'm also looking at supporting modification of collections as in my original comment. The code and tests can be simplified and tidied before I submit it - this is mostly a proof of concept.

nzbart commented 7 years ago

Another update: I have some code that will allow manipulation of collections in addition to the deep manipulation code above. I've got this in my codebase for now, but am willing to submit a pull request if you're interested.

ehorrent commented 7 years ago

Hi, sorry for the late response (holidays here). Yes i'm interested, you can submit a pull request !

wallymathieu commented 6 years ago

Look into what can be reused from: https://github.com/wallymathieu/with

For instance: https://github.com/wallymathieu/with/blob/master/src/Tests/With/Manipulation_of_readonly_dictionary.cs