How to avoid duplicate code in linq queries

139 views Asked by At

I have the code which can be considered as repetetive:

private IEnumerable<decimal?> GetWidth(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Width != null)
    .Select(s => s.Width);

private IEnumerable<decimal?> GetHeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Height != null)
        .Select(s => s.Height);

private IEnumerable<decimal?> GetDiameter(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Diameter != null)
    .Select(s => s.Diameter);

private IEnumerable<decimal?> GetWeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Weight != null)
        .Select(s => s.Weight);
        

The only difference between these methods is just field names such as Width, Height, Diameter, Weight. Is it possible to replace these names with a string property name and create just one method without using any third party libraries?

4

There are 4 answers

0
Svyatoslav Danyliv On BEST ANSWER

You can create simple extension for that:

public static class EnumerableExtensions
{
    public static IEnumerable<TValue> ExtractValues<TEntity, TValue>(
        this IEnumerable<TEntity> items,
        Func<TEntity, TValue?>    selector)
    {
        return items.Where(i => selector(i) != null)
            .Select(i => selector(i)!);
    }
}

And use it like this:

var heights   = items.ExtractValues(i => i.Height);
var diameters = items.ExtractValues(i => i.Diameter);
0
JonasH On

In this specific example I would introduce a separate method to do the null check. Something like :

public static T NotNull<T>(this IEnumerable<T?> list) where T : struct 
    => list.Where(t => t.HasValue).Select( t => t.Value);

private IEnumerable<decimal> GetWeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Select(s => s.Weight).NotNull();
0
Panagiotis Kanavos On

Since you use IEnumerable and the arguments simply select and check value, you can reverse the operations to select the value and filter out nulls. You're still performing the same simple operations - checking all values but allowing only the non-null ones:

var heights=items.Select(s =>s.Height).Where(x=>x!=null);
var diameters=items.Select(s =>s.Diameter).Where(x=>x!=null);
var weights=items.Select(s =>s.Weight).Where(x=>x!=null);

You can convert that to an expression method :

IEnumerable<TResult> SelectNonNull<TSource,TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource,TResult?> selector) 
{
    return source.Select(selector).Where(x=>x!=null);
}

...
var heights=items.SelectNotNull(s =>s.Height);
var diameters=items.SelectNotNull(s =>s.Diameter);
var weights=items.SelectNotNull(s =>s.Weight);
6
phuzi On

If you really want to use a string to indicate which property to get you can build up a dynamic select as an extension method.

Also, by switching around Where().Select() to Select().Where() allows you to only need a single dynamic expression too. i.e.

rectangles
    .Where(s => s.Width != null)
    .Select(s => s.Width);

// becomes

rectangles
    .Select(s => s.Width)
    .Where(s => s != null);

Right, so how do you build the dynamic filter?

You could do something like this - based on my answer to a similar question

public static class ExtensionMethods
{
    public static IEnumerable<decimal?> GetProperty(
        this IEnumerable<Rectangle> rectangles,
        string property)
    {
        var queryableRectangles = rectangles.AsQueryable();
        
        // w =>
        var param = Expression.Parameter(typeof(Rectangle), "w");
    
        var propertyInfo = typeof(Rectangle).GetProperty(property);
        if (propertyInfo == null)
            throw new Exception($@"Property ""{property}"" was not found");
    
        // w.[property]
        var selector = Expression.Property(param, propertyInfo);
    
        // Bring it all together
        // Select(w => w.[property])
        var selectExpression = Expression.Call(
            typeof(Queryable),
            nameof(System.Linq.Enumerable.Select),
            new Type[] { typeof(Rectangle), typeof(decimal?) },
            queryableRectangles.Expression,
            Expression.Lambda<Func<Rectangle, decimal?>>(selector, new ParameterExpression[] { param })
        );
    
        // Query the collection
        var filteredItems = queryableRectangles.Provider.CreateQuery<decimal?>(selectExpression);
    
        // return the list of values removing null values.
        return filteredItems
            .Where(x => x.HasValue) // Use HasValue instead of != null for Nullable<t>
            .ToList();
    }
}

You could then use this in your code

var heights = rectangles.GetProperty("Height");

UPDATE - Completely generic version

To make this completely generic is fairly easy to replace Rectangle with Tanddecimal?with TOut, of course you need to update the method signature a little with the generic type parameters to<T, TOut>`

Note the change from .Where(x => x.HasValue) back to `.Where(x => x != null)

public static IEnumerable<TOut> GetProperty<T, TOut>(this IEnumerable<T> source, string property)
{
    var queryable = source.AsQueryable();

    // w =>
    var param = Expression.Parameter(typeof(T), "w");

    var propertyInfo = typeof(T).GetProperty(property);
    if (propertyInfo == null)
        throw new Exception($@"Property ""{property}"" was not found");

    // w.[property]
    var selector = Expression.Property(param, propertyInfo);

    // Bring it all together
    // Select(w => w.[property])
    var selectExpression = Expression.Call(
        typeof(Queryable),
        nameof(System.Linq.Enumerable.Select),
        new Type[] { typeof(T), typeof(TOut) },
        queryable.Expression,
        Expression.Lambda<Func<T, TOut>>(selector, new ParameterExpression[] { param })
    );

    // Run query against the database
    var filteredItems = queryable.Provider.CreateQuery<TOut>(selectExpression);

    return filteredItems
        .Where(x => x != null) // revert to != null for non Nullable<T> return types
        .ToList();
}

This requires a slight tweak to the way it's called too:

var heights = rectangles.GetProperty<Rectangle, decimal?>("Height");