Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any way to project "original plus a few changes" with a LINQ query?

I have code that is building up a collection of objects. I'm trying to reduce the number of .ToList() calls I make, so I'm trying to keep it as IEnumerable<T> for as long as possible.

I have it almost finished except for two properties which need to be set to a value which is passed into the calling method:

private IEnumerable<X> BuildCollection(int setMe){
    IEnumerable<Y> fromService = CallService();
    IEnumerable<X> mapped = Map(fromService);
    IEnumerable<X> filteredBySomething = FilterBySomething(mapped);

    IEnumerable<X> sorted = filteredBySomething
                                .OrderBy(x=>x.Property1)
                                .ThenBy(x=>x.Property2);

    // Here's the problem: return "sorted", but with each "Property3"
    //  and "Property4" set to "setMe". I don't want to do:

    var sortedList = sorted.ToList();
    sortedList.ForEach(s=>
    {
        s.Property3 = setMe;
        s.Property4 = setMe;
    };

    return sortedList;
}

If one could use sort of a wildcard in a select, then I could do something like:

return from f in filteredBySomething
    order by f.Property1, f.Property2
    select new {
        f.*,
        f.Property3 = setMe,
        f.Property4 = setMe
    };

That is, I would like to stream back the sorted objects, but with Property3 and Property4 set to the passed-in value.

Is there an elegant way to do this?

P.S. I don't think it matters, but the collection will eventually be sent to an ASP.NET view as a viewmodel. Clearly, .ToList() may have to be called before the View gets it, but I'd like that to be the only time.

P.P.S. I should have said that type X has about 30 properties! using

select new {
    x.Property1,
    x.Property2,
    Property3 = setMe,
    Property4 = setme,
    // ...
}

would not be useful, as the ... would be another 26 properties.

like image 398
John Saunders Avatar asked Jul 08 '14 22:07

John Saunders


2 Answers

Instead of this:

var sortedList = sorted.ToList();
sortedList.ForEach(s=>
{
    s.Property3 = setMe;
    s.Property4 = setMe;
};

do this:

sorted = sorted.Select(x =>
{
    x.Property3 = setMe;
    x.Property4 = setMe;
    return x;
});

If, however, you don't want to modify the objects, then you can instead do the following:

sorted = sorted.Select(x => new X()
{
    Property3 = setMe,
    Property4 = setMe,
    // set all other properties to what they were before
    // example: Property1 = x.Property1
});

I don't believe that there are any better ways than these two.

like image 186
Jashaszun Avatar answered Nov 16 '22 00:11

Jashaszun


Unless the object type you are projecting to provides a copy constructor and is mutable, then no, there is no elegant way to do this.

If a copy constructor was defined and the object was mutable, you could do this:

var updatedSorted = sorted.Select(x => new X(x)
    {
        Property3 = setMe,
        Property4 = setMe,
    });

However anonymous objects do not have accessible copy constructors nor are mutable so you'll have to do the work of copying the values over yourself. But with the help of some helper functions, it can be made less painful using a bit of reflection and some good ol' LINQ. Fortunately with anonymous objects, although we don't have access to the types at compile time, it doesn't mean we can't create new instances at run time.

public static class AnonExtensions
{
    public static TSource SetValues<TSource, TValue>(
        this TSource source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        var copier = copierExpr.Compile();
        return copier(source);
    }

    public static IEnumerable<TSource> UpdateValues<TSource, TValue>(
        this IEnumerable<TSource> source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        var copier = copierExpr.Compile();
        return source.Select(copier);
    }

    public static IQueryable<TSource> UpdateValues<TSource, TValue>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TValue>> setter)
    {
        var copierExpr = new Copier<TSource, TValue>().Rewrite(setter);
        return source.Select(copierExpr);
    }

    private class Copier<TSource, TValue> : ExpressionVisitor
    {
        private readonly ParameterExpression param =
            Expression.Parameter(typeof(TSource));
        public Expression<Func<TSource, TSource>> Rewrite(
            Expression<Func<TSource, TValue>> setter)
        {
            var newExpr = new SubstitutionVisitor(
                setter.Parameters.Single(), param).Visit(setter.Body);
            var body = this.Visit(newExpr);
            return Expression.Lambda<Func<TSource, TSource>>(body, param);
        }

        protected override Expression VisitNew(NewExpression node)
        {
            var type = typeof(TSource);
            var ctor = type.GetConstructors().Single();
            var arguments = new List<Expression>();
            var members = new List<MemberInfo>();
            var propMap = GetPropertyMap(node);
            foreach (var prop in type.GetProperties())
            {
                Expression arg;
                if (!propMap.TryGetValue(prop.Name, out arg))
                    arg = Expression.Property(param, prop);
                arguments.Add(arg);
                members.Add(prop);
            }
            return Expression.New(ctor, arguments, members);
        }

        private Dictionary<string, Expression> GetPropertyMap(
            NewExpression node)
        {
            return node.Members.Zip(node.Arguments, (m, a) => new { m, a })
                .ToDictionary(x => x.m.Name, x => x.a);
        }
    }

    private class SubstitutionVisitor : ExpressionVisitor
    {
        private Expression oldValue, newValue;
        public SubstitutionVisitor(Expression oldValue, Expression newValue)
        { this.oldValue = oldValue; this.newValue = newValue; }

        public override Expression Visit(Expression node)
        {
            return node == oldValue ? newValue : base.Visit(node);
        }
    }
}

This will allow you to do this:

var updatedSorted = sorted.UpdateValues(x => new
    {
        Property3 = setMe, // the types here should match the corresponding 
        Property4 = setMe, // property types
    });
like image 1
Jeff Mercado Avatar answered Nov 16 '22 00:11

Jeff Mercado