Search code examples
c#unit-testingentity-framework-coreautofixture

Disabling tracking on property which represents an enumeration


I have a new system which contains some enumerations. Rather than store as int alone, I followed this post so that I could get them inserted automatically into my database for referential integrity.

However, now, when inserting many objects into my database during unit testing, I get the following error:

System.InvalidOperationException: The instance of entity type 'UnitOfMeasure' cannot be tracked because another instance with the key value '{Id: Box}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

Here are the following classes

public class Line : EntityBase
{
    public int BillId { get; set; }

    public int Quantity { get; set; }

    public UnitOfMeasureEnum UnitOfMeasureId { get; set; }

    public virtual UnitOfMeasure UnitOfMeasure { get; set; } = null!;

    public IList<LineItem> LineItems { get; set; } = new List<LineItem>();
}

public class LineConfiguration : IEntityTypeConfiguration<Line>
{
    public void Configure(EntityTypeBuilder<Line> builder)
    {
        // Primary Key
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Id)
            .HasColumnOrder(0);

        // ... snip ...
            
        builder.Property(x => x.UnitOfMeasureId)
            .HasColumnOrder(8);

        builder.HasOne(x => x.UnitOfMeasure)
            .WithMany()
            .HasForeignKey(x => x.UnitOfMeasureId)
            .OnDelete(DeleteBehavior.Restrict);

        builder.HasMany(x => x.LineItems);
    }
}

public void Configure(EntityTypeBuilder<UnitOfMeasure> builder)
{
    builder.ToTable(
        TableName,
        x => x.HasComment("Table representation of UnitOfMeasureEnum for referential integrity. Do not edit without matching change to Enum  in code."));

    builder.Property(x => x.Id)
        .ValueGeneratedNever();

    builder.HasData(
        new List<UnitOfMeasure>
        {
            new() { Id = UnitOfMeasureEnum.Bin, Name = "Bin" },
            new() { Id = UnitOfMeasureEnum.Box, Name = "Box" },
            new() { Id = UnitOfMeasureEnum.Case, Name = "Case" },
            new() { Id = UnitOfMeasureEnum.Carton, Name = "Carton" },
            new() { Id = UnitOfMeasureEnum.Drum, Name = "Drum" },
            new() { Id = UnitOfMeasureEnum.Each, Name = "Each" },
            new() { Id = UnitOfMeasureEnum.Lot, Name = "Lot" },
            new() { Id = UnitOfMeasureEnum.Pail, Name = "Pail" },
            new() { Id = UnitOfMeasureEnum.Piece, Name = "Piece" },
            new() { Id = UnitOfMeasureEnum.Pallet, Name = "Pallet" },
            new() { Id = UnitOfMeasureEnum.Roll, Name = "Roll" },
            new() { Id = UnitOfMeasureEnum.Skid, Name = "Skid" },
        });
}

public class UnitOfMeasure
{
    public UnitOfMeasureEnum Id { get; set; }

    public string Name { get; set; }
}

The UnitsOfMeasure table records should never change. They are fixed to the values of the enumeration. But I think EF is trying to track them in case they do change.

Is there a way to disable tracking or otherwise configure this object to not try and track the properties?


Solution

  • If you do want to use a UnitOfMeasure entity because there is more than just a Name to associate, or you want the Name to be potentially customized in the database rather than determined by code, then reference tracking still applies. This isn't to be confused with change tracking. EF works with references so it wants to ensure that when you are associating multiple items with a UnitOfMeasure of "UnitOfMeasure.Box" then all of those items are pointing to the same instance of the Box UnitOfMeasure entity. This can be problematic when dealing with things like deserialized entities.

    So for instance if you are working with two Line entities that were deserialized and wanted to treat those as detached entities to either add or update in the DbContext where both of those Lines referred to a UnitOfMeasure.Box... Deserializing or otherwise accepting a Line from a MVC View FormData or such will result in two separate instances of the line's UnitOfMeasure entity reference. Both will be "Box", but you are looking at two different instances. When handling the first, you might attach the Line.UnitOfMeasnre, but then when you get to the second line, attaching the other instance of the "Box" UnitOfMeasure would throw the exception that an instance is already tracked.

    Before attaching entities, you need to check the DbContext/DbSet's local cache to see if it happens to be tracking a reference, and if so, substitute the reference to that instance. For example:

    var existingUnitOfMeasure = _context.UnitOfMeasures.Local.FirstOrDefault(x => x.Id == line.UnitOfMeasureId);
    if (existingUnitOfMeasure != null)
        line.UnitOfMeasure = existingUnitOfMeasure;
    else
        _context.Attach(line.UnitOfMeasure);
    

    Otherwise if you are loading a Line from the database to update and want to update the data something like:

    var existingLine = _context.Lines
        .Include(x => x.UnitOfMeasure)
        .Single(x => x.Id == line.Id);
    // copy values across...
    
    existingLine.UnitOfMeasureId = line.UnitOfMeasureId;
    existingLine.UnitOfMeasure = line.UnitOfMeasure;
    
    _context.SaveChanges();
    

    You can run into the same problem. "line.UnitOfMeasure" is a different instance to a UnitOfMeasure that the DbContext may be tracking. Even if it isn't tracking it, you could encounter issues on save either with duplicate data being inserted, or PK constraint violations as EF treats the untracked instance as a new row. In these cases if you have the FK (Line.UnitOfMeasureId) exposed then you can update the data by just setting the FK, and not touching the navigation property:

    var existingLine = _context.Lines
        .Include(x => x.UnitOfMeasure)
        .Single(x => x.Id == line.Id);
    // copy values across...
    
    existingLine.UnitOfMeasureId = line.UnitOfMeasureId;
    // existingLine.UnitOfMeasure = line.UnitOfMeasure; // <- Don't set/change
    
    _context.SaveChanges();
    

    Don't set the navigation property to #null or any other change, just alter the FK and EF's change tracker will update the reference automatically. After SaveChanges() is called, you will see that the existingLine.UnitOfMeasure reference is updated automatically to reflect the new value. (if changed)

    An alternative I use where I want a table on the DB to maintain referential integrity, but where the values are deterministic in business logic, so I use an enumeration so the Keys are controlled in code. If the value in question is just a key and a name, I just use the FK (enum) in the association, then use a [Description] attribute on the enumeration to provide a descriptive name where the actual enum value isn't suitable. In this way I don't have a UnitOfMeasure entity/reference at all. In your case you could just use the enum value ToString() since the "name" is just the enum value. But if you had a value like "DoubleBox" and wanted a display name of "Double Box":

    public enum UnitOfMeasure
    {
       Box = 1,
       [Description("Double Box")]
       DoubleBox = 2,
       // ...
    }
    

    Then have a helper class for getting a display name for the Enumeration:

    public static class EnumExtensions
    {
    
        /// <summary>
        /// Helper method for resolving Description attributes.
        /// </summary>
        public static string Description(this Enum? value)
        {
            ArgumentNullException.ThrowIfNull(value, nameof(value));
    
            FieldInfo? fieldInfo = value.GetType().GetField(value.ToString());
            if (fieldInfo == null)
                return value.ToString();
    
            DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
            if (attributes.Length > 0)
                return attributes[0].Description;
            else
                return value.ToString();
        }
    }
    

    So when I want to display an enumeration value's descriptive name, I call line.UnitOfMeasure.Description(). This will return any [Description] attribute value if present, otherwise the enum value as string. (I.e. "Box")