Search code examples
c#linqentity-framework-6expression-trees

Accessing expression bodied members to build expression trees


Trying to build an order by expression using expression trees. But I am unable to access an expression bodied property of the query result's class. This is the class structure:

public class AssetFileRecord : IAuditable, IEntity, INavigateToCustomValues
{
    public AssetFileRecord()
    {
        this.UpdatedTimeStamp = DateTime.UtcNow;
    }

    public AssetFileRecord GetRecord()
    {
        return this;
    }

    public Guid Id { get; set; }
    public int DisplayId { get; set; }
    public string AssetTagNumber { get; set; }
    [JObjectIgnore]
    public virtual Account Account { get; set; }
    public string AccountNumber => Account?.AccountNumber;
    public string AuditTrail { get; set; }
    public string OldTagNumber { get; set; }
    public ActivityCode ActivityCode { get; set; }

    [JObjectIgnore]
    public virtual ICollection<AssetFileRecordDepreciation> AssetFileRecordDepreciations { get; set; }
    // Depreciation Records
    public double? AccumulatedDepreciation => Depreciation()?.AccumulatedDepreciation;
    public DateTime? DepreciationAsOfDate => Depreciation()?.DepreciationAsOfDate;
    public double? LifeMonths => Depreciation()?.LifeMonths;
    public double? DepreciationBasis => Depreciation()?.DepreciationBasis;
    public double? PeriodDepreciation => Depreciation()?.PeriodDepreciation;

    private AssetFileRecordDepreciation Depreciation()
    {
        return AssetFileRecordDepreciations?.AsQueryable()?.OrderBy(d => d.AssetFileDepreciationBook.BookNo)?.FirstOrDefault();
    }
}

I am unable to get to the property AccountNumber which is a property of a virtual property of AssetFileRecord.

Below is the current code that works fine for any other non-expression bodied properties.

var type = typeof(T);
var property = type.GetProperty(sortProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
var parameter = Expression.Parameter(type, "p");
var propertyAccess = Expression.MakeMemberAccess(parameter, property);
var orderByExp = Expression.Lambda(propertyAccess, parameter);
var typeArguments = new[] { type, property.PropertyType };
var methodBase = isFirstOrderTerm ? "OrderBy" : "ThenBy";
var methodName = sortOrder == ListSortDirection.Ascending ? methodBase : $"{methodBase}Descending";
var resultExp = Expression.Call(typeof(Queryable), methodName, typeArguments, source.Expression, Expression.Quote(orderByExp));

return source.Provider.CreateQuery<T>(resultExp);

Expression.Call does not evaluate to a valid SQL query and rather throws an exception.

((System.Data.Entity.Infrastructure.DbQuery<AssetFileRecord>)records).Sql = '((System.Data.Entity.Infrastructure.DbQuery<AssetFileRecord>)records).Sql' threw an exception of type 'System.NotSupportedException'

Intended result: It should append an order by expression to the expression tree generated in the end; while it's failing to do so, when tried to order by an expression bodied property member.

Can someone please help me get this working.


Solution

  • You have two problems with your approach. First is that you can't use a null propagating operator in Linq Expressions. Test this code:

    var account = new Account();
    // will cause "error CS8072: An expression tree lambda may not contain a null propagating operator"    
    Expression<Func<string>> accountNumber = () => account?.AccountNumber;
    

    Second and main problem is that your AccountNumber will be compiled into get_AccountNumber method and you can't invoke arbitrary methods with Linq to SQL. You may test this code:

    public class AssetFileRecord
    {
      //...
      public string AccountNumber => Account != null ? Account.AccountNumber : null;
    }
    

    while this can be compiled, it produce same runtime exception.

    One possible way to workaround this problem is to create a map with expressions for complex properties:

    var map = new Dictionary<string, Expression>
    {
        {
            "AssetFileRecord.AccountNumber", // type and property
            (Expression<Func<AssetFileRecord, string>>) (
                afr => afr.Account != null ? afr.Account.AccountNumber : null
            )
        }
    };
    

    Now you can rewrite your method that builds dynamic OrderBy with respect to this map:

    private static IQueryable<T> DynamicOrderBy<T>(
        IQueryable<T> source,
        string sortProperty,
        Dictionary<string, Expression> map)
    {
        var type = typeof(T);
        var parameter = Expression.Parameter(type, "p");
        var property = type.GetProperty(sortProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
    
        Expression whereLambda;
        if (!map.TryGetValue($"{type.Name}.{sortProperty}", out whereLambda))
        {
            var propertyAccess = Expression.MakeMemberAccess(parameter, property);
            whereLambda = Expression.Lambda(propertyAccess, parameter);
        }
        // else we just using a lambda from map
    
        // call OrderBy
        var query = Expression.Call(
            typeof(Queryable),
            "OrderBy",
            new[] {type, property.PropertyType},
            source.Expression,
            whereLambda
        );
    
        return source.Provider.CreateQuery<T>(query);
    }