As the title says, "How to cast TResult
which can be an object
or List<object>
inside an Expression<Func<T,TResult?>
?"
public Task ExplicitLoadAsync<TProperty>(T entity, Expression<Func<T, TProperty?>> propertyExpression) where TProperty : class
{
var typeOfProp = typeof(TProperty);
if (typeOfProp.IsInterface && typeOfProp.GetGenericTypeDefinition() == typeof(ICollection<>))
{
var collectionExpression = propertyExpression as Expression<Func<T, IEnumerable<TProperty>>>;
if (collectionExpression is not null)
{
return _dbContext.Entry(entity).Collection(collectionExpression).LoadAsync();
}
}
return _dbContext.Entry(entity).Reference(propertyExpression).LoadAsync();
}
collectionExpression
will always be null, the issue is that Reference()
only accept IEnumerable<TProperty>
and overload is not possible because it will always use the method with TProperty
instead of Expression<Func<T, IEnumerable<TProperty>>> propertyExpression
The reason is the cast is wrong because it will be IEnumerable<IEnumerable<...>>
How to solve it?
One way is to have two methods with different names, but I would like to skip that as that would require more refactoring in the whole code base.
CodePudding user response:
This solution is based on Guru Stron's deleted answer
public async Task ExplicitLoadAsync<T, TProperty>(T entity, Expression<Func<T, IEnumerable<TProperty>>> propertiesExpression) where T : class where TProperty : class
=> await _dbContext.Entry(entity).Collection(propertiesExpression).LoadAsync();
public async Task ExplicitLoadAsync<T, TProperty>(T entity, Expression<Func<T, TProperty?>> propertyExpression) where T : class where TProperty : class
=> await _dbContext.Entry(entity).Reference(propertyExpression).LoadAsync();
Here the trick is to use different parameter names:
Expression<Func<T, IEnumerable<TProperty>>> propertiesExpression
Expression<Func<T, TProperty?>> propertyExpression
which allows you to be able to call both overloads
ExplicitLoadAsync<Entity, string>(enity, e => e.SingleString);
ExplicitLoadAsync<Entity, string>(enity, propertiesExpression: e => e.CollectionOfStrings);
CodePudding user response:
I just want to post another answer to just solve my case without calling the method with parameter name.
public Task ExplicitLoadAsync<TProperty>(T entity, Expression<Func<T, TProperty?>> propertyExpression) where TProperty : class
{
var typeOfProp = typeof(TProperty);
if (typeOfProp.IsInterface && typeOfProp.GetGenericTypeDefinition() == typeof(ICollection<>))
{
var pathName = PropertyPath<T>.GetAsStr(propertyExpression);
return _dbContext.Entry(entity).Collection(pathName).LoadAsync();
}
return _dbContext.Entry(entity).Reference(propertyExpression).LoadAsync();
}
Here is the utility class:
public static class PropertyPath<TSource>
{
public static IReadOnlyList<MemberInfo> Get<TResult>(Expression<Func<TSource, TResult>> expression)
{
var visitor = new PropertyVisitor();
visitor.Visit(expression.Body);
visitor._path.Reverse();
return visitor._path;
}
public static string GetAsStr<TResult>(Expression<Func<TSource, TResult>> expression)
{
return string.Join(".", Get(expression).Select(p => p.Name));
}
private sealed class PropertyVisitor : ExpressionVisitor
{
internal readonly List<MemberInfo> _path = new();
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member is not PropertyInfo)
{
throw new ArgumentException("The path can only contain properties", nameof(node));
}
_path.Add(node.Member);
return base.VisitMember(node);
}
}
}