Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EntityCollection - ICollection<InterfaceTEntity> - Runtime Issue/Quirk

Background Info

I'm using the Database-First approach.

I created a TextTemplate (.tt) file that generates interfaces from the EDMX file, also, I modified the original TextTemplate file included/produced by the EDMX file (project item) to have the generated classes implement those interfaces.

[Person] (Partial Public Class)

namespace TestSolution.Domain.Entities
{
  using System;
  using System.Collections.Generic;
  using TestSolution.Domain.Entities;

  public partial class Person : IPerson
  {
    public Person()
    {
        //this.CrewMembers = new HastSet<CrewMember>();
        this.CrewMembers = new HashSet<ICrewMember>();
    }

    public Person(IPerson iPerson)
        {
        this.PersonID = iPerson.PersonID;
        this.First = iPerson.First;
        this.Last = iPerson.Last;
        //this.CrewMembers = new HastSet<CrewMember>();
        this.CrewMembers = new HashSet<ICrewMember>();
    }
    public int PersonID { get; set; }
    public string First { get; set; }
    public string Last { get; set; }

    //public virtual ICollection<CrewMember> CrewMembers { get; set; }
    public virtual ICollection<ICrewMember> CrewMembers { get; set; }
  }
}

As you can see in the code sample above, I also added a constructor that takes the interface to initialize the Person class...

[IPerson] (Interface)

namespace TestSolution.Domain.Entities
{
    using System;
    using System.Collections.Generic;       

    public interface IPerson
    {       
         int PersonID { get; set; }
         string First { get; set; }
         string Last { get; set; }

         //ICollection<CrewMember> CrewMembers { get; set; }
         ICollection<ICrewMember> CrewMembers { get; set; }
    }
}

The CrewMember Class and ICrewMember Inteface contain:

  • boolean properties (IsCaptain and IsAssigned)
  • Int PersonID as a property, Int CrewMemberID as a property,
  • and Person Person as Person of course. (I have tried IPerson Person as well).

My Intention; The Issue

I intended to use ICollection<ICrewMember> for the Navigation. Then use DTO/Model/ModelView Objects/Classes that Implement those interfaces.

When I generated ICollection<InterfaceTEntity> types in the navigation property, I wasn't seeing any issues.. with EntityFramework.. until I attempted to update two Entities using the same context: context.CrewMembers and context.People in the same function.

public void UpdateCrewMemberModel(CrewMemberModel CrewMember)
    {
        var query = (from crewMember in UnitOfWork.DataContext.CrewMembers
                     where crewMember.CrewMemberID == CrewMember.CrewMemberID
                     select crewMember).First<CrewMember>();
        query.IsAssigned = CrewMember.IsAssigned;
        query.IsCaptain = CrewMember.IsCaptain;


        // Exception is thrown here 
        var queryPerson = (from person in UnitOfWork.DataContext.People
                           where person.PersonID == query.PersonID
                           select person).First<Person>();
        queryPerson.First = CrewMember.Person.First;
        queryPerson.Last = CrewMember.Person.Last;

        //Note that UnitOfWork uses a Factory Repository Pattern;
        //Commit just calls on UnitOfWork.DataContext.SaveAll() Method
        UnitOfWork.Commit();
    }

The issue I am having/seeing seemed to be with EntityFramework's Change-Tracking Feature. However, it is actually rooted with asynchronous operations within EntityFramework itself.

If I debug and step through the above method, there is no exception, and the database/tables updates properly..

If I run it without catching a debug point before the statement declaring queryPerson.. it will throw an exception ( a misleading exception regarding Person.CrewMembers having to be ICollection<T>)..

This certainly seems to be a timing issue


Attempted Workaround/ Points of Interest

I attempted to remove the virtual attribute on the ICollection<> properties; didn't affect the issue.

I attempted to remove Lazy Loading; didn't affect the issue.

I attempted to commit the changes between the queries; did not affect the issue.

I attempted to perform to use FirstAsync<> on query; but I'm not sure if I'm even using it properly.. I'm still fiddling with this approach.


*EDIT/UPDATE - (misleading) Exception, Stack Trace, and Target Site

System.Data.Entity.Core.EntityException
{"The navigation property 'CrewMembers' on entity of type 'System.Data.Entity.DynamicProxies.Person_1A1EF42B1FC8D2DD0084F803201DE1DE4CF6E704C5AE129D954BD5BEAB55826C' must implement ICollection<T> in order for Entity Framework to be able to track changes in collections."}

Source: EntityFramework

at System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1.CheckIfNavigationPropertyContainsEntity(IEntityWrapper wrapper)
at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedTarget, Boolean applyConstraints, Boolean addRelationshipAsUnchanged, Boolean relationshipAlreadyExists, Boolean allowModifyingOtherEndOfRelationship, Boolean forceForeignKeyChanges)
at System.Data.Entity.Core.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedEntity, Boolean applyConstraints)
at System.Data.Entity.Core.Objects.DataClasses.EntityReference`1.set_ReferenceValue(IEntityWrapper value)
at System.Data.Entity.Core.Objects.DataClasses.EntityReference.SetEntityKey(EntityKey value, Boolean forceFixup)
at System.Data.Entity.Core.Objects.EntityEntry.FixupEntityReferenceToPrincipal(EntityReference relatedEnd, EntityKey foreignKey, Boolean setIsLoaded, Boolean replaceExistingRef)
at System.Data.Entity.Core.Objects.EntityEntry.FixupReferencesByForeignKeys(Boolean replaceAddedRefs, EntitySetBase restrictTo)
at System.Data.Entity.Core.Objects.ObjectStateManager.FixupReferencesByForeignKeys(EntityEntry newEntry, Boolean replaceAddedRefs)
at System.Data.Entity.Core.Objects.ObjectStateManager.AddEntry(IEntityWrapper wrappedObject, EntityKey passedKey, EntitySet entitySet, String argumentName, Boolean isAdded)
at System.Data.Entity.Core.Common.Internal.Materialization.Shaper.HandleEntityAppendOnly[TEntity](Func`2 constructEntityDelegate, EntityKey entityKey, EntitySet entitySet)
at lambda_method(Closure , Shaper )
at System.Data.Entity.Core.Common.Internal.Materialization.Coordinator`1.ReadNextElement(Shaper shaper)
at System.Data.Entity.Core.Common.Internal.Materialization.Shaper`1.SimpleEnumerator.MoveNext()
at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at TestSolution.Infrastructure.Service.CrewMemberService.UpdateCrewMemberModel(CrewMemberModel CrewMember) in c:\Users\brett.caswell\Documents\Visual Studio 2012\Projects\TestSolution\TestSolution.Infrastructure.Service\Services\CrewMemberServices.cs:line 67

TargetSite: {Boolean CheckIfNavigationPropertyContainsEntity(System.Data.Entity.Core.Objects.Internal.IEntityWrapper)}

*EDIT/UPDATE - Question

Though to ask this question may be improperly scoping the issue I'm having...

When using Generic Type interfaces with the ICollection in Navigation Properties of the generated classes, How can/do I handle potentially unsafe threading instances(in my UpdateCrewmMemberModel method) that occur within/from EntityFramework?

like image 723
Brett Caswell Avatar asked Sep 29 '22 21:09

Brett Caswell


1 Answers

Typically you get this error if you are trying to use an Interface as your Entity (which is not supported by EF). Is CrewMember actually an Interface by any chance?

If it's not an interface, but it is a class that derives from an interface, it's possible EF may be getting confused at some point. Are you doing any Fluent configuration perhaps? Maybe you are trying to map to the base class or interface which would throw this error?

Your code is actually a bit confusing, since you're supposedly using a "unit of work", but you're accessing your dbcontext directly, so effectively bypassing the UoW, but then calling Commit() on the UoW... I would suggest that you probably need to rethink your data design here, and probably get rid of the UoW entirely, since EF is a Unit of work already.

like image 102
Erik Funkenbusch Avatar answered Oct 06 '22 20:10

Erik Funkenbusch