Search code examples
linq-to-entitiesaggregatedynamictype

Change value of dynamic type in LINQ?


I have a problem with LINQ. I have a list of item and I want to change the Parent value of an orphan item to 0 with the following code:

void Main()
{
    var a = new List<Item> {
        new Item() { ID = 1, Name = "1", Parent = 0 },
        new Item() { ID = 2, Name = "1.1", Parent = 1 },
        new Item() { ID = 3, Name = "2", Parent = 0 },
        new Item() { ID = 4, Name = "3", Parent = 0 },
        new Item() { ID = 5, Name = "1.2", Parent = 1 },
        new Item() { ID = 6, Name = "2.1", Parent = 3 },
        new Item() { ID = 7, Name = "2.2", Parent = 3 },
        new Item() { ID = 8, Name = "3.1", Parent = 4 },
        new Item() { ID = 9, Name = "4", Parent = 0 },
        new Item() { ID = 10, Name = "5.1", Parent = 11 }
    };

    Test.Fix(a, x => x.ID, x => x.Parent).Dump();
}

public class Item
{
    public long ID {get;set;}
    public string Name {get;set;}
    public long Parent {get;set;}
}

public static class Test
{
    public static List<T> Fix<T>(this List<T> enumeration, Func<T, long> idProperty, Func<T, long> parentProperty)
    {
        enumeration = enumeration.Select(e =>
        {
            if (!enumeration.Any(x => idProperty(x) == parentProperty(x))) parentProperty(e) = 0;
            return e;
        }).ToList();

        return enumeration;
    }
}

I expected the lasted item with ID = 10 to have Parent = 0 (coz there is no item with ID = 11) but I got an error:

The left-hand side of an assignment must be a variable, property or indexer

I googled around but still find nothing useful.

Any helps would be appreciated!


Solution

  • Okay, firstly there's nothing dynamic going on here, but as you say, the problem is here:

    parentProperty(e) = 0;
    

    parentProperty is a function, that's all. It's not really a property. It allows you to get from a T to a long - and that's all. It could be anything - imagine a Func<string, long> which returned x.Length * 5. What would it mean to "set" that to (say) 3?

    If you want to be able to set a value, you'll need an action, e.g.

    public static List<T> Fix<T>(this List<T> enumeration,
                                 Func<T, long> idProperty,
                                 Func<T, long> parentGetter,
                                 Action<T, long> parentSetter)
    {
        enumeration = enumeration.Select(e =>
        {
            long parentId = parentGetter(e);
            if (!enumeration.Any(x => idProperty(x) == parentId))
            {
                parentSetter(e, 0);
            }
            return e;
        }).ToList();
    
        return enumeration;
    }
    

    and call it like this:

    Test.Fix(a, x => x.ID, x => x.Parent, (x, v) => x.Parent = v).Dump();
    

    Note that I've changed the logic in Any a bit because your current logic made no sense - it wasn't using e in the test.

    However, this is really not a good use of LINQ - LINQ is designed around not having side-effects. Given that you're only returning the same elements as before, you might as well not return anything, and just use a foreach loop:

    public static void Fix<T>(this List<T> enumeration,
                                 Func<T, long> idProperty,
                                 Func<T, long> parentGetter,
                                 Action<T, long> parentSetter)
    {
        foreach (T item in enumeration)
        {
            long parentId = parentGetter(e);
            if (!enumeration.Any(x => idProperty(x) == parentId))
            {
                parentSetter(e, 0);
            }
        }
    }
    

    I would personally then change this to:

    public static void Fix<T>(this List<T> enumeration,
                                 Func<T, long> idProperty,
                                 Func<T, long> parentGetter,
                                 Action<T, long> parentSetter)
    {
        HashSet<long> validIds = new HashSet<long>(enumeration.Select(idProperty));        
        foreach (T item in enumeration.Where(x => !validIds.Contains(parentGetter(x)))
        {
            parentSetter(e, 0);
        }
    }