Entity Framework - Include / Reference / Collection
Asked Answered
F

4

18

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();
Ford answered 6/8, 2013 at 5:14 Comment(5)
Care to explain how I can make this question better?Ford
I didn't vote to close, but I did find it hard to understand the question first time around. Maybe it's worth adding some example code for Collection/Reference, then while referencing that an example of the call you would like to achieve?Omsk
I assume that by "work on a single entity" you mean using context.Entry(entity).Collection("collection").Load() method?Melanimelania
@Ford - you can improve this by showing us what you've tried, the errors you got and asking something specific.Repeated
The votes to close are because S/O is aimed more at questions that have specific answers eg "why is this code not working?/Because you forgot to use the X parameter". S/O specifically discourages questions that will provoke extended discussion (eg "what were they thinking when they designed feature X") - not because they aren't an interesting and useful question - but because S/O is not designed for that. Try asking on one of the computer-science related Stack Exchange sites instead :)Repeated
M
23

The only purpose of the Include() method is to explicitly eager load related data upon querying.

The Entry() method - on the other hand - is intended to give you specific control over the current state of an Entity attached to the context and not only Load() related data.

That is the reason why you have to explicitly choose between Collection, Reference and Property methods, each one exposes different set of functionality (hence returns different type).

For example:

  • Scalar (DbPropertyEntry) contains the IsModified property that denotes whether the value changed from 'x' to 'y' (for example).

  • Reference (DbReferenceEntry) contains the IsLoaded property that denotes whether the referenced data has been loaded from the database already.

  • Reference Collection (DbCollectionEntry) derives from ICollection (hence IEnumerable as well) which means that you can iterate over its data. Yet, it cannot contain an IsModified property because it may differ for each item in the collection.

Still, if you're only interested in Load(), you can leverage the polymorphic Member() method (that returns DbMemberEntry that is the base type to all of the above types) and check if the entry is "Loadable":

var memberEntry = this.Entry(entity).Member("NavigationProperty");

if (memberEntry is DbCollectionEntry collectionMember)
    collectionMember.Load();

if (memberEntry is DbReferenceEntry referenceMember)
    referenceMember.Load();
Melanimelania answered 6/8, 2013 at 8:1 Comment(2)
Thanks @Melanimelania - the member function may help!Ford
While partially helpful, the original question used expressions to reference the properties while the solution uses strings. There isn't a simple way to translate one to the other unfortunately. If "Member" also took in a property expression this would have been the definitive solution but I'm still looking for it.Pleurodynia
G
6

You can do it this way:

1.- Load the entity including the collections:

MyClass myObject = dbContext.MyClasses
                    .Include(cls => cls.ObjectCollection)
                    .Single(cls => cls.Pk == entityPk);

2.- Then you must retrieve that object Entry and tell EF to load the required properties in the collection objects:

dbContext.Entry(myObject).Collection("ObjectCollection").Query().Include("ReferenceClass").Load(); 

Further reading:

http://msdn.microsoft.com/en-us/data/jj574232#explicitFilter

Genous answered 30/10, 2014 at 10:3 Comment(0)
P
1

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();
Pleurodynia answered 18/2, 2022 at 5:2 Comment(0)
P
0

You can also load the referenced collection using Select.

db.MyObject.Include(x => x.RefObect.Select(y => y.RefRefObject));
Perfunctory answered 31/5, 2019 at 16:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.