question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ChangeTracker.DetectChanges() issue with DDD-style enumerations

See original GitHub issue

First off, many thanks for the amazing tools you are building!

We are implementing enumeration classes as per this link: https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types

The enumerations are seeded at application startup, while ChangeTracker_Tracked and ChangeTracker_StateChanged events ensure enumeration objects are always in a detached state.

You can download a working solution here: https://github.com/vardalis/ChangeTrackerIssue

TL;DR

When I run ChangeTracker.DetectChanges() after I have made a change to a single entity I get the expected outcome. When I make an additional change to a different (unrelated) entity and then run ChangeTracker.DetectChanges() the change to the first entity is reset.

var mainEntities = actContext.MainEntities.Include(me => me.TimeUnit)
    .ToDictionary(me => me.Name);

// Normal case (problematic)
mainEntities["MainEntity1"].TimeUnit = TimeUnit.min;
mainEntities["MainEntity2"].TimeUnit = TimeUnit.s;
actContext.ChangeTracker.DetectChanges(); // After DetectChanges mainEntities["MainEntity2"].TimeUnit is set to TimeUnit.min

// Alternative case (calling DetectChanges after each assignment solves the problem)

mainEntities["MainEntity1"].TimeUnit = TimeUnit.min;
actContext.ChangeTracker.DetectChanges(); // Works correctly

mainEntities["MainEntity2"].TimeUnit = TimeUnit.s;
actContext.ChangeTracker.DetectChanges(); // Works correctly

More code

The MainEntity (entity) and TimeUnit (enumeration) look like this:

public class MainEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public TimeUnit TimeUnit { get; set; }
    protected MainEntity() { }
    public MainEntity(string name, TimeUnit timeUnit)
    {
        Name = name;
        TimeUnit = timeUnit;
    }
}

public class TimeUnit
{
    public static TimeUnit s = new(1, "s");
    public static TimeUnit min = new(2, "min");

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    protected TimeUnit() { }
    public TimeUnit(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

Here is the AppDbContext:

    public class AppDbContext : DbContext
    {
        public DbSet<MainEntity> MainEntities { get; set; }
        public DbSet<TimeUnit> TimeUnits { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=.\SQLEXPRESS;Database=ChangeTrackerDb;Trusted_Connection=True;");
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

        }

        public void AddTrackingEvents()
        {
            ChangeTracker.Tracked += ChangeTracker_Tracked;
            ChangeTracker.StateChanged += ChangeTracker_StateChanged;
        }
        public void ClearTrackingEvents()
        {
            ChangeTracker.Tracked -= ChangeTracker_Tracked;
            ChangeTracker.StateChanged -= ChangeTracker_StateChanged;
        }
        private void ChangeTracker_Tracked(object sender, Microsoft.EntityFrameworkCore.ChangeTracking.EntityTrackedEventArgs entityEvent)
        {
            if (typeof(TimeUnit).IsAssignableFrom(entityEvent.Entry.Entity.GetType()))
                entityEvent.Entry.State = EntityState.Detached;
        }
        private void ChangeTracker_StateChanged(object sender, Microsoft.EntityFrameworkCore.ChangeTracking.EntityStateChangedEventArgs entityEvent)
        {
            if (typeof(TimeUnit).IsAssignableFrom(entityEvent.Entry.Entity.GetType()))
                entityEvent.Entry.State = EntityState.Detached;
        }
    }

And there is the main (.NET 6.0 scaffolding style):

// See https://aka.ms/new-console-template for more information
using ChangeTrackerIssue;
using ChangeTrackerIssue.Entities;
using Microsoft.EntityFrameworkCore;
using System.Reflection;

Console.WriteLine("Hello, World!");

var arrangeContext = new AppDbContext();

arrangeContext.Database.EnsureDeleted();
arrangeContext.Database.EnsureCreated();

// Seed TimeUnits
arrangeContext.Add(TimeUnit.s);
arrangeContext.Add(TimeUnit.min);
arrangeContext.SaveChanges();

// Disable TimeUnit tracking
arrangeContext.AddTrackingEvents();

// Populate Db
var mainEntity1 = new MainEntity("MainEntity1", TimeUnit.s);
var mainEntity2 = new MainEntity("MainEntity2", TimeUnit.min);

arrangeContext.AddRange(mainEntity1, mainEntity2);
arrangeContext.SaveChanges();

var actContext = new AppDbContext();

// Disable TimeUnit tracking
actContext.AddTrackingEvents();

var mainEntities = actContext.MainEntities.Include(me => me.TimeUnit)
    .ToDictionary(me => me.Name);

// Normal case (problematic)
mainEntities["MainEntity1"].TimeUnit = TimeUnit.min;
mainEntities["MainEntity2"].TimeUnit = TimeUnit.s;
actContext.ChangeTracker.DetectChanges(); // After DetectChanges mainEntities["MainEntity2"].TimeUnit is set to TimeUnit.min

// Alternative case (calling DetectChanges after each assignment solves the problem)

mainEntities["MainEntity1"].TimeUnit = TimeUnit.min;
actContext.ChangeTracker.DetectChanges(); // Works correctly

mainEntities["MainEntity2"].TimeUnit = TimeUnit.s;
actContext.ChangeTracker.DetectChanges(); // Works correctly

Provider and version information

EF Core version: 6.0.8 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0 Operating system: Windows 11 IDE: Visual Studio 2022, 17.3.3

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
vardaliscommented, Sep 7, 2022

For the record, I took @ajcvickers suggestion and experimented with a value converter and I now have a solution that goes a long way towards solving my problem.

I created a TimeUnitConverted:

public class TimeUnitConverter : ValueConverter<TimeUnit, int>
{
    public TimeUnitConverter()
    : base(
            timeUnit => timeUnit.Id,
            timeUnitId => TimeUnit.FromValue<TimeUnit>(timeUnitId)
    )
    {
    }
}

And configured the relevant convention:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<TimeUnit>()
        .HaveConversion<TimeUnitConverter>();
}

Here is FromValue:

public static T FromValue<T>(int value) where T : TimeUnit
{
    var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
    return matchingItem;
}

Since the name for each unit is also unique we could store that instead of the id so we can see the time unit when looking in the database.

It would still be nice to have all TimeUnit information in the database and also enforce foreign key constraints, but compared to the three options I mentioned above this one seems to have the fewer drawbacks.

1reaction
ajcvickerscommented, Sep 7, 2022

We are implementing enumeration classes as per this link: docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types

Personally, I would steer clear of all that guidance. It does not promote EF Core best practices.

The enumerations are seeded at application startup, while ChangeTracker_Tracked and ChangeTracker_StateChanged events ensure enumeration objects are always in a detached state.

This is not supported. If you must use these patterns, then consider not mapping the enum properties at all.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Change Detection and Notifications - EF Core
Detecting property and relationship changes using DetectChanges or notifications.
Read more >
Handling Entity Framework Core database migrations in ...
This uses EF Core' ChangeTracker to find changes to entity classes ... Count(), Any() etc. of large list of entries in Cosmos DB...
Read more >
Page 6 – I am a freelance .NET Core back-end developer
This uses EF Core' ChangeTracker to find changes to entity classes that will affect ... Migrate() on startup – easy, but has some...
Read more >
Entity Framework Core in Action [2 ed.] 1617298360, ...
NOTE Using the tracking snapshot is the normal way that DetectChanges finds the changed properties. But chapter 11 describes an alternative to the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found