0

I have a LINQ statement I want to convert it into Expression Tree

public class tblEmpLocation
{       
    public uint EmpLocationId { get; set; }            
    public uint? EmpId { get; set; }      
    public uint? LocationId { get; set; }      
    public DateTime EffectiveDt { get; set; } = DateTime.Now;      
}

We have employee location class basically have the location the Employee Id and Location Id.

 public class temptable
{
   public uint Id{ get; set; }      
   public DateTime EffectiveDt { get; set; } 
}

We have the temp class which basically contain the Id and Effective date that is a sample class. Now we have similar table like employee Department, employee salary etc. so we want to create an linq extension that basically take the class as input and get the desired result

List<tblEmpLocation> tbls = new List<tblEmpLocation>();


var data=tbls.GroupBy(p => p.EmpId).Select(q => new temptable{ Id=q.Key, EffectiveDt=q.Max(r => r.EffectiveDt) });

thanks for help

Prabhakar
  • 198
  • 2
  • 9
  • It is not clear which signature should have your extension method. – Svyatoslav Danyliv Jun 28 '22 at 07:37
  • signature of my extension method is public static IQueryable CurrentData(IQueryable source,string GroupColumnName= "EmpId", string DateColumnName= "EffectiveDt") where T : class – Prabhakar Jun 28 '22 at 07:51
  • What you want makes no sense. What you want to do is what Linq essentially is (selecting properties from data). LINQ by default does not support string params, you'd have to use Reflection for that or look into extension frameworks for LINQ. – CthenB Jun 28 '22 at 08:19
  • I would write separate queries. There is too much risk that if you want to check an extra criteria somewhere, you will end up with a large spaghetti mess. – Jeroen van Langen Jun 28 '22 at 10:24
  • Update question with signature. Actually I do not get idea of grouping. Do you want to select latest record from each group? – Svyatoslav Danyliv Jun 28 '22 at 10:31
  • @SvyatoslavDanyliv yes I want the latest record – Prabhakar Jun 28 '22 at 10:52
  • Well, last question. Why string names, why not lambda synax `.DistinctBy(x => x.EmpId, x => x. EffectiveDt)`? – Svyatoslav Danyliv Jun 28 '22 at 11:14
  • @SvyatoslavDanyliv , we have multiple table like tblEmpDepartment, tblEmpSalary, tblEmpDesignation etc. and each table have EmpId and EffectiveDate . and It is our businesss process that HR can set the Employee Desgination with Effective date. So there could be multiple Effective Date coresponding to same employee. and in the above query I only need single employee with their Last Effective date( Max) – Prabhakar Jun 28 '22 at 11:48

1 Answers1

0

This is universal implementation of DistinctBy method, which returns last record of the group.

Schematically when you make the following call:

query = query.DistinctBy(e => e.EmpId, e => e.EffectiveDt);
// or with complex keys
query = query.DistinctBy(e => new { e.EmpId, e.Other }, e => new { e.EffectiveDt, e.SomeOther});

Or fully dynamic

query = query.DistinctBy("EmpId", "EffectiveDt");

Function generates the following query:

query = 
   from d in query.Select(d => d.EmpId).Distinct()
   from e in query
        .Where(e => e.EmpId == d)
        .OrderByDescending(e => e.EffectiveDt)
        .Take(1)
    select e;

Or with complex keys:

query = 
   from d in query.Select(d => new { d.EmpId, d.Other }).Distinct()
   from e in query
        .Where(e => e.EmpId == d.EmpId && e.Other == d.Other)
        .OrderByDescending(e => e.EffectiveDt)
        .ThenByDescending(e => e.SomeOther)
        .Take(1)
    select e;

And realisation:

public static class QueryableExtensions
{
    public static IQueryable<T> DistinctBy<T>(
        this IQueryable<T> source,
        string distinctPropName,
        string maxPropName)
    {
        var entityParam = Expression.Parameter(typeof(T), "e");
        var distinctBy = Expression.Lambda(MakePropPath(entityParam, distinctPropName), entityParam);
        var maxBy = Expression.Lambda(MakePropPath(entityParam, maxPropName), entityParam);

        var queryExpression = Expression.Call(typeof(QueryableExtensions), nameof(QueryableExtensions.DistinctBy),
            new[] { typeof(T), distinctBy.Body.Type, maxBy.Body.Type },
            Expression.Constant(source),
            Expression.Quote(distinctBy),
            Expression.Quote(maxBy));

        var executionLambda = Expression.Lambda<Func<IQueryable<T>>>(queryExpression);
        return executionLambda.Compile()();
    }

    public static IQueryable<T> DistinctBy<T, TKey, TMax>(
        this IQueryable<T>        source, 
        Expression<Func<T, TKey>> distinctBy, 
        Expression<Func<T, TMax>> maxBy)
    {
        var distinctQuery = source.Select(distinctBy).Distinct();

        var distinctParam = Expression.Parameter(typeof(TKey), "d");
        var entityParam   = distinctBy.Parameters[0];

        var mapping = MapMembers(distinctBy.Body, distinctParam).ToList();

        var orderParam  = maxBy.Parameters[0];
        var oderMapping = CollectMembers(maxBy.Body).ToList();

        var whereExpr = mapping.Select(t => Expression.Equal(t.Item1, t.Item2))
            .Aggregate(Expression.AndAlso);
        var whereLambda = Expression.Lambda(whereExpr, entityParam);

        // d => query.Where(x => d.distinctBy == x.distinctBy).Take(1)
        Expression selectExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), new[] { typeof(T) },
            source.Expression,
            whereLambda);

        // prepare OrderByPart
        for (int i = 0; i < oderMapping.Count; i++)
        {
            var orderMethod = i == 0 ? nameof(Queryable.OrderByDescending) : nameof(Queryable.ThenByDescending);

            var orderItem = oderMapping[i];
            selectExpression = Expression.Call(typeof(Queryable), orderMethod, new[] { typeof(T), orderItem.Type },
                selectExpression, Expression.Lambda(orderItem, orderParam));
        }

        // Take(1)
        selectExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Take), new[] { typeof(T) },
            selectExpression,
            Expression.Constant(1));

        var selectManySelector =
            Expression.Lambda<Func<TKey, IEnumerable<T>>>(selectExpression, distinctParam);

        var selectManyQuery = Expression.Call(typeof(Queryable), nameof(Queryable.SelectMany),
            new[] { typeof(TKey), typeof(T) }, distinctQuery.Expression, selectManySelector);

        return source.Provider.CreateQuery<T>(selectManyQuery);
    }

    static Expression MakePropPath(Expression objExpression, string path)
    {
        return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
    }

    private static IEnumerable<Tuple<Expression, Expression>> MapMembers(Expression expr, Expression projectionPath)
    {
        switch (expr.NodeType)
        {
            case ExpressionType.New:
            {
                var ne = (NewExpression)expr;

                for (int i = 0; i < ne.Arguments.Count; i++)
                {
                    foreach (var e in MapMembers(ne.Arguments[i], Expression.MakeMemberAccess(projectionPath, ne.Members[i])))
                    {
                        yield return e;
                    }
                }
                break;
            }

            default:
                yield return Tuple.Create(projectionPath, expr);
                break;
        }
    }

    private static IEnumerable<Expression> CollectMembers(Expression expr)
    {
        switch (expr.NodeType)
        {
            case ExpressionType.New:
            {
                var ne = (NewExpression)expr;

                for (int i = 0; i < ne.Arguments.Count; i++)
                {
                    yield return ne.Arguments[i];
                }
                break;
            }

            default:
                yield return expr;
                break;
        }
    }
}
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • Thanks for your effort but above solution is not working for me and if I use simple query like HRMSContext.tblEmpLocation.Where(p=>p.EmpId==400).OrderBy(p=>p.EffectiveDt).DistinctBy(p=>p.EmpId); then still I get the error to generate as Query. I am using mysql database. – Prabhakar Jun 29 '22 at 07:36
  • Oh, man, you have used not my extension but standard .NET 6 `DistinctBy` my extension has "two" parameters. – Svyatoslav Danyliv Jun 29 '22 at 07:40