fhevol
fhevol

Reputation: 996

Intermittent Issue With Entity Framework Creating Unwanted Rows

I have an MVC application that uses Entity Framework v6. We have a class

public class ChildObject
{
    public string Name { get; set; }
    ....
}

that maps to a table in the database. This table has 6 rows that are never changed. Neither will there ever be any additions. We have a second class defined along the lines of the following:

public class ParentClass
{
    public int ChildObjectId { get; set; }
    public ChildObject ChildObject { get; set; }
    ....
}

Whenever a ParentClass object is created or updated the logic only references the ChildObjectId property. The ChildObject property is only referenced when data is pulled back for viewing. However about once per month an extra row appears in the ChildObject table that is a duplicate of an existing row. This obviously causes issues. However I can't see how this could happen seeing as we only ever save using the Id value. Any thoughts on how this could be occurring would be very much appreciated.

Upvotes: 0

Views: 74

Answers (1)

Steve Py
Steve Py

Reputation: 34908

The typical culprit for behavior like you describe is when a new child entity is composed based on existing data and attached to the parent rather than the reference associated to the context. An example might be that you load child objects as a set to select from, and send the data to your view. The user wants to change an existing child reference to one of the 6 selections. The call back to the server passes a child object model where there is code something like:

parent.ChildObject = new ChildObject{ Name = model.Name, ... } 

rather than:

var child = context.Children.Single(x => x.Id = model.ChildObjectId);
parent.ChildObject = child;

Depending on how your domain is set up you may run into scenarios where the EF context creates a new child entity when a navigation property is set. Check with a FindUsages on the ChildObject property and look for any use of the setter.

In general you should avoid combining the use of FK properties (ChildObjectId) with navigation properties (ChildObject) because you can get confusing behavior between what is set in the navigation reference vs. the FK. Entities should be defined with one or the other. (Though at this time EF Core requires both if Navigation properties are used.)

A couple notables from your example: Mark the navigation property as virtual - This ensures that EF assigns a proxy and recognizes it.

Option A - Remove the FK child ID property. For the parent either use an EntityTypeConfiguration or initialize the DbContext to map the FK column:

EntityTypeConfiguration:

public class ParentClassConfiguration : EntityTypeConfiguration<ParentClass>
{
  public ParentClassConfiguration()
  {
    ToTable("ParentTable");
    HasKey(x => x.ParentObjectId)
      .Property(x => x.ParentObjectId)
      .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

    HasRequired(x => x.ChildObject)
      .WithMany()
      .Map(x => x.MapKey("ChildObjectId"));
      .WillCascadeOnDelete(false);
  }
}

or on context model generation: (Inside your DbContext)

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<ParentObject>().HasRequired(x => x.ChildObject).WithMany().Map(x => x.MapKey("ChildObjectId")).WillCascadeOnDelete(false);
}

or Option B - Ensure the FK is linked to the reference, and take measures to ensure that the two are always kept in sync:

public class ParentClassConfiguration : EntityTypeConfiguration<ParentClass>
{
  public ParentClassConfiguration()
  {
    ToTable("ParentTable");
    HasKey(x => x.ParentObjectId)
      .Property(x => x.ParentObjectId)
      .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

    HasRequired(x => x.ChildObject)
      .WithMany()
      .HasForeignKey(x => x.ChildObjectId));
      .WillCascadeOnDelete(false);
  }
}

or on context model generation: (Inside your DbContext)

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<ParentObject>().HasRequired(x => x.ChildObject).WithMany().HasForeignKey(x => x.ChildObjectId)).WillCascadeOnDelete(false);
}

Option B is the only one currently available with EF Core to my knowledge, and it may help mitigate your issue but you still have to take care to avoid discrepancies between the navigation property and the FK. I definitely recommend option A, though it will likely require a bit of change if your code is commonly accessing the FK column.

Upvotes: 1

Related Questions