Search code examples
c#linqexpression-trees

LINQ to SQL select property name by string on projection


How can I achieve the projection on the last select? I need the property defined by the string prop.Name to be selected into the SeriesProjection object.

public override IQueryable<SeriesProjection> FilterOn(string column)
{
    //Get metadata class property by defined Attributes and parameter column
    var prop = typeof(CommunicationMetaData)
                .GetProperties()
                .Single(p => p.GetCustomAttribute<FilterableAttribute>().ReferenceProperty == column);

    var attr = ((FilterableAttribute)prop.GetCustomAttribute(typeof(FilterableAttribute)));

    var param = Expression.Parameter(typeof(Communication));

    Expression conversion = Expression.Convert(Expression.Property(param, attr.ReferenceProperty), typeof(int));

    var condition = Expression.Lambda<Func<Communication, int>>(conversion, param); // for LINQ to SQl/Entities skip Compile() call

    var result = DbQuery.Include(prop.Name)
            //.GroupBy(c => c.GetType().GetProperty(attr.ReferenceProperty))
            .GroupBy(condition)
            .OrderByDescending(g => g.Count())
            .Select(group => new SeriesProjection()
            {
                Count = group.Count(),
                Id = group.Key,
                //set this navigation property dynamically 
                Name = group.FirstOrDefault().GetType().GetProperty(prop.Name)
            });

    return result;
}

For the GroupBy I used the fk property name that's always an int on the Communication entity, but for the select I can't figure out the expression.

[EDIT]

System.Data.Entity.Infrastructure.DbQuery<Communication> DbQuery;
---
[MetadataType(typeof(CommunicationMetaData))]
public partial class Communication
{
    public int CommunicationId { get; set; }
    public Nullable<int> TopicId { get; set; }
    public int CreateById { get; set; }
    public virtual Employee CreateByEmployee { get; set; }
    public virtual Topic Topic { get; set; }
}
---
public class CommunicationMetaData
{
    [Filterable("By Employee", nameof(Communication.CreateById))]
    public Employee CreateByEmployee { get; set; }
    [Filterable("By Topic", nameof(Communication.TopicId))]
    public Topic Topic { get; set; }
}
---
[AttributeUsage(AttributeTargets.Property)]
public class FilterableAttribute : System.Attribute
{

    public FilterableAttribute(string friendlyName, string referenceProperty)
    {
        FriendlyName = friendlyName;
        ReferenceProperty = referenceProperty;
    }

    public string FriendlyName { get; set; }

    public string ReferenceProperty { get; set; }
}
---
public class SeriesProjection
{
    public int Count { get; set; }
    public int Id { get; set; }
    public object Name { get; set; }
}

Solution

  • Without some expression helper library, you have to build the whole selector expression manually.

    The input of the selector will be a parameter of type IGrouping<int, Communication>, the result type - SeriesProjection, and the body will be MemberInit expression:

    var projectionParameter = Expression.Parameter(typeof(IGrouping<int, Communication>), "group");
    var projectionType = typeof(SeriesProjection);
    var projectionBody = Expression.MemberInit(
        // new SeriesProjection
        Expression.New(projectionType),
        // {
        //     Count = group.Count(),
        Expression.Bind(
            projectionType.GetProperty(nameof(SeriesProjection.Count)),
            Expression.Call(typeof(Enumerable), "Count", new[] { typeof(Communication) }, projectionParameter)),
        //     Id = group.Key
        Expression.Bind(
            projectionType.GetProperty(nameof(SeriesProjection.Id)),
            Expression.Property(projectionParameter, "Key")),
        //     Name = group.FirstOrDefault().Property
        Expression.Bind(
            projectionType.GetProperty(nameof(SeriesProjection.Name)),
            Expression.Property(
                Expression.Call(typeof(Enumerable), "FirstOrDefault", new[] { typeof(Communication) }, projectionParameter),
                prop.Name))
        // }
        );
    var projectionSelector = Expression.Lambda<Func<IGrouping<int, Communication>, SeriesProjection>>(projectionBody, projectionParameter);
    

    and then of course use simply:

    var result = DbQuery.Include(prop.Name)
            .GroupBy(condition)
            .OrderByDescending(g => g.Count())
            .Select(projectionSelector);