Mike Chamberlain
Mike Chamberlain

Reputation: 42440

Efficient way of checking if many-to-many relationship exists in EF4.1

I have a many-to-many relationship between two entities - Media and MediaCollection. I want to check if a certain Media already exists in a collection. I can do this as follows:

mediaCollection.Media.Any(m => m.id == mediaId)

However, mediaCollection.Media is an ICollection, so to me this looks like it will have to retrieve every Media in the collection from the database just to make this check. As there could be many media in a collection, this seems very inefficient. I'n thinking that I should use a method of IQueryable, but I can't see how to do this for many-to-many relationships.

How can I check for the existence of the relationship without retrieving the whole collection?

EDIT

I am generating the EF data model from my database, then using the built in VS POCO T4 templates to generate my data context and entity classes. I think the problem is that the generated code does not return EntityCollection for the navigation properties, but instead ObjectSet. ObjectSet implements IQueryable, but does not expose a CreateSourceQuery() method.

Here is a stripped down version of the relevant lines from the context:

    public partial class Entities : ObjectContext
    {
        public const string ConnectionString = "name=Entities";
        public const string ContainerName = "Entities";

        #region Constructors

        public Entities()
            : base(ConnectionString, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }

        public Entities(string connectionString)
            : base(connectionString, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }

        public Entities(EntityConnection connection)
            : base(connection, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }

        #endregion

        #region ObjectSet Properties

        public ObjectSet<MediaCollection> MediaCollections
        {
            get { return _mediaCollections  ?? (_mediaCollections = CreateObjectSet<MediaCollection>("MediaCollections")); }
        }
        private ObjectSet<MediaCollection> _mediaCollections;

        // snipped many more

        #endregion
    }

And here is a stripped down version of the class for the MediaCollection entity:

    public partial class MediaCollection
    {
        #region Primitive Properties

        // snipped

        #endregion

        #region Navigation Properties    

        public virtual ICollection<Medium> Media
        {
            get
            {
                if (_media == null)
                {
                    var newCollection = new FixupCollection<Medium>();
                    newCollection.CollectionChanged += FixupMedia;
                    _media = newCollection;
                }
                return _media;
            }
            set
            {
                if (!ReferenceEquals(_media, value))
                {
                    var previousValue = _media as FixupCollection<Medium>;
                    if (previousValue != null)
                    {
                        previousValue.CollectionChanged -= FixupMedia;
                    }
                    _media = value;
                    var newValue = value as FixupCollection<Medium>;
                    if (newValue != null)
                    {
                        newValue.CollectionChanged += FixupMedia;
                    }
                }
            }
        }
        private ICollection<Medium> _media;

        private void FixupMedia(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Medium item in e.NewItems)
                {
                    if (!item.MediaCollections.Contains(this))
                    {
                        item.MediaCollections.Add(this);
                    }
                }
            }

            if (e.OldItems != null)
            {
                foreach (Medium item in e.OldItems)
                {
                    if (item.MediaCollections.Contains(this))
                    {
                        item.MediaCollections.Remove(this);
                    }
                }
            }
        }

        // snip

        #endregion
    }

And finally, here is the FixupCollection that the template also generates:

    public class FixupCollection<T> : ObservableCollection<T>
    {
        protected override void ClearItems()
        {
            new List<T>(this).ForEach(t => Remove(t));
        }

        protected override void InsertItem(int index, T item)
        {
            if (!this.Contains(item))
            {
                base.InsertItem(index, item);
            }
        }
    }

Upvotes: 2

Views: 3611

Answers (3)

Ladislav Mrnka
Ladislav Mrnka

Reputation: 364259

You can do that but you need a context for that:

bool exists = context.Entry(mediaCollection)
                     .Collection(m => m.Media)
                     .Query()
                     .Any(x => x.Id == mediaId);

Edit:

If you are using ObjectContext API with proxied POCOs instead of DbContext API the former sample will not work. You can try this:

context.ContextOptions.LazyLoadingEnabled = false;
bool exists = ((EntityCollection<Media>)mediaCollection.Media).CreateSourceQuery()
                                                              .Any(x => x.Id == mediaId);
context.ContextOptions.LazyLoadingEnabled = true;

Upvotes: 4

Mike Chamberlain
Mike Chamberlain

Reputation: 42440

So it seems that the built in VS POCO T4 template does not generate anything equivalent to CreateSourceQuery(). No matter! We can code it ourselves. If you add the following code at to the context's .tt file and regenerate:

public ObjectQuery<T> CreateNavigationSourceQuery<T>(object entity, string navigationProperty)
{
    var ose = ObjectStateManager.GetObjectStateEntry(entity);
    var rm = ObjectStateManager.GetRelationshipManager(entity);

    var entityType = (System.Data.Metadata.Edm.EntityType)ose.EntitySet.ElementType;
    var navigation = entityType.NavigationProperties[navigationProperty];

    var relatedEnd = rm.GetRelatedEnd(navigation.RelationshipType.FullName, navigation.ToEndMember.Name);

    return ((dynamic)relatedEnd).CreateSourceQuery();
}

then we can check for the existence of a many-to-many as follows:

var exists = _context.CreateNavigationSourceQuery<Medium>(mediaCollection, "Media")
    .Any(m => m.Id == medium.Id);

Props to Rowan's answer on Using CreateSourceQuery in CTP4 Code First for this one.

Upvotes: 2

Akash Kava
Akash Kava

Reputation: 39916

Try,

 mediaCollection.CreateSourceQuery()
     .Any(....

CreateSourceQuery will create IQueryable for the association.

Upvotes: 1

Related Questions