I was wondering why there are separate methods for populating navigation properties.
If I work on an entire set, i can call Include
on either a property or a collection.
However, if I work on a single entity, there are two separate methods to call depending on whether the item is a collection (Collection
) or single reference (Reference
).
Is there any way around this - this is making things more complicated than I think is necessary. And could anyone explain why this was decided during designing EF?
EDIT
Looking into it further, the problem goes deeper. What I was trying to do is create a generic way to load collection/navigation properties on a single entity. This can be done easy enough on the whole set using Include. But the method signatures for Reference
and Collection
are slightly different.
Never mind, will have to scatter these calls around my app.
e.g.
dbSet<T>().Include(e => e.Property).Include(e => e.Collection).Include(e => e.Collection.Property)
all seem to work.
However the calls for the single entity are different:
context.Entry(entity).Reference(e => e.Property).Load();
context.Entry(entity).Reference(e => e.Property.Select(e => e.SubProperty)).Load();
context.Entry(entity).Collection(e => e.Collection).Load();
If you want to continue to support property expressions so that you can support "Include" syntax FindAsync, the following solution retrieves the PropertyInfo for the referred property then uses Expression.Convert
so that you can now support the context.Entry(entity).Member(e => e.Property).Load()
syntax you wished.
Add the following two classes to some namespace and using
it in your class:
public class MemberEntry
{
/// <summary>
/// If this MemberEntry refers to a CollectionEntry, this will be not null
/// </summary>
public CollectionEntry? CollectionEntry { get; init; }
/// <summary>
/// If this MemberEntry refers to a ReferenceEntry, this will be not null
/// </summary>
public ReferenceEntry? ReferenceEntry { get; init; }
public MemberEntry(CollectionEntry collectionEntry)
{
this.CollectionEntry = collectionEntry;
}
public MemberEntry(ReferenceEntry referenceEntry)
{
this.ReferenceEntry = referenceEntry;
}
//
// Summary:
// Loads the entity or entities referenced by this navigation property, unless Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.IsLoaded
// is already set to true.
// Note that entities that are already being tracked are not overwritten with new
// data from the database.
public void Load()
{
if (this.CollectionEntry != null)
{
this.CollectionEntry.Load();
}
else
{
this.ReferenceEntry!.Load();
}
}
//
// Summary:
// Loads the entity or entities referenced by this navigation property, unless Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.IsLoaded
// is already set to true.
// Note that entities that are already being tracked are not overwritten with new
// data from the database.
// Multiple active operations on the same context instance are not supported. Use
// 'await' to ensure that any asynchronous operations have completed before calling
// another method on this context.
//
// Parameters:
// cancellationToken:
// A System.Threading.CancellationToken to observe while waiting for the task to
// complete.
//
// Returns:
// A task that represents the asynchronous operation.
public Task LoadAsync(CancellationToken cancellationToken = default)
{
if (this.CollectionEntry != null)
{
return this.CollectionEntry.LoadAsync(cancellationToken);
}
else
{
return this.ReferenceEntry!.LoadAsync(cancellationToken);
}
}
}
public static class EntityEntryExtensions
{
public static MemberEntry Member<TEntity>(this EntityEntry<TEntity> entityEntry, Expression<Func<TEntity,object?>> prop)
where TEntity : class
{
var propInfo = GetPropertyInfo(prop);
MemberEntry memberEntry;
if (propInfo.PropertyType.IsAssignableTo(typeof(IEnumerable)))
{
Expression converted = Expression.Convert(prop.Body, typeof(IEnumerable<object>));
Expression<Func<TEntity, IEnumerable<object>>> collProp = Expression.Lambda<Func<TEntity, IEnumerable<object>>>(converted, prop.Parameters);
memberEntry = new(entityEntry.Collection(collProp));
}
else
{
memberEntry = new(entityEntry.Reference(prop));
}
return memberEntry;
}
private static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
if (propertyLambda.Body == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a method, not a property.",
propertyLambda.ToString()));
if (propertyLambda.Body is MemberExpression member)
{
if (member.Member == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a field, not a property.",
propertyLambda.ToString()));
if (member.Member is PropertyInfo propInfo)
{
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType!))
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a property that is not from type {1}.",
propertyLambda.ToString(),
type));
else
return propInfo;
}
}
throw new ArgumentException(string.Format(
"Expression '{0}' doesn't refer to a class property of {1}.",
propertyLambda.ToString(),
type));
}
}
Synchronous:
this.Entry(myObject).Member(_ => _.MyProperty).Load();
Asynchronous:
await this.Entry(myObject).Member(_ => _.MyProperty).LoadAsync();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With