Gup3rSuR4c
Gup3rSuR4c

Reputation: 9488

LINQ: How to null out properties in a projection?

The title is pretty terrible, so let me try to explain in code. I have a an entity called Change and it has a collection of ChangeDetail entities. I want to project a collection of Change entities to a DTO called ChangeListItem. I've been messing around with LINQPad and I actually have the projection done and working as I want, but what I can't get to work is to null out all ChangeListItems in the group except for the first one. Here's my code:

Change class

public class Change : {
    public DateTime ChangedDateTime { get; set; }

    #region Navigation Properties
    public virtual ICollection<ChangeDetail> Details { get; private set; }
    #endregion

    public Change() {
        this.Details = new List<ChangeDetail>();
    }
}

ChangeDetail class

public class ChangeDetail {
    public string Details { get; set; }

    #region Navigation Properties
    public int ChangeId { get; set; }

    public virtual Change Change { get; set; }
    #endregion
}

My LINQPad attempt

var changes =
    Changes
        .SelectMany(c => c.Details)
        .OrderByDescending(c => c.Change.ChangedDateTime)
        .Select(c => new ChangeListItem
        {
            ChangedDateTime = c.Change.ChangedDateTime,
            Details = c.Details
        })
        .GroupBy(c => c.ChangedDateTime)
        .ForEach(g =>
        {
            g
                .Skip(1)
                .ForEach(c =>
                {
                    c.ChangedDateTime = null;
                });
        });

When I dump that into the results pane I still get a collection of groups of ChangeListItem, but the ChangedDateTime property is still filled out. I'm sure I'm missing a reassignment somewhere along the way, even though I thought the one I'm doing should be good enough. How can I get it to what I want?

I want to go from this:

enter image description here

To this:

enter image description here

Update As requested here's the code for the ForEach extension:

public static class IQueryableExtensions {
    public static IQueryable<T> ForEach<T>(
        this IEnumerable<T> source,
        Action<T> action) {
        if (source == null) {
            throw new ArgumentNullException("source");
        }

        if (action == null) {
            throw new ArgumentNullException("action");
        }

        foreach (T t in source) {
            action(t);
        }

        return source.AsQueryable<T>();
    }
}

Upvotes: 2

Views: 541

Answers (1)

Ani
Ani

Reputation: 113442

You're treating a LINQ query as though it were a collection. I assume your ForEach extension looks like this:

foreach(var item in source)
{
    SideEffect(item);
}

return source;

Here the results of the query are enumerated once, and each of its result items is mutated. But then when when you enumerate the query again, its results are recreated from scratch; consequently the mutations are 'lost'.

In theory, it would be possible to include some strategic ToList() calls into your code to get the desired effect. But this isn't in the spirit of LINQ. Don't mix LINQ and side-effects. If you want to conditionally project some items and leave others untouched, do it this way:

changeGroup.Select( (c, index) => index == 0 ? c : new ChangeListItem 
                    { 
                        ChangedDateTime = null,
                        Details = c.Details
                    } );

Btw, you may need to call AsEnumerable() before you perform this transformation as I don't think LINQ to Entities supports the overload of Select that includes the item index (can't recall off-hand, though).

Upvotes: 5

Related Questions