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.
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);
}