Ändringsidentifiering och meddelanden

Varje DbContext instans spårar ändringar som gjorts i entiteter. Dessa spårade entiteter driver i sin tur ändringarna i databasen när SaveChanges anropas. Detta beskrivs i Ändringsspårning i EF Core, och det här dokumentet förutsätter att entitetstillstånd och grunderna i Ef Core-ändringsspårning (Entity Framework Core) förstås.

Spårning av egenskaps- och relationsändringar kräver att DbContext kan identifiera dessa ändringar. Det här dokumentet beskriver hur den här identifieringen sker, samt hur du använder egenskapsmeddelanden eller proxyservrar för ändringsspårning för att tvinga fram omedelbar identifiering av ändringar.

Tips/Råd

Du kan köra och felsöka all kod i det här dokumentet genom att ladda ned exempelkoden från GitHub.

Spårning av ändringar i ögonblicksbilder

Som standard skapar EF Core en ögonblicksbild av varje entitets egenskapsvärden när den först spåras av en DbContext-instans. Värdena som lagras i den här ögonblicksbilden jämförs sedan med de aktuella värdena för entiteten för att avgöra vilka egenskapsvärden som har ändrats.

Den här identifieringen av ändringar sker när SaveChanges anropas för att säkerställa att alla ändrade värden identifieras innan uppdateringar skickas till databasen. Men identifieringen av ändringar sker också vid andra tillfällen för att säkerställa att applikationen fungerar med up-to-datumspårningsinformation. Identifiering av ändringar kan när som helst framtvingas genom att anropa ChangeTracker.DetectChanges().

När ändringsidentifiering behövs

Det krävs identifiering av ändringar när en egenskap eller navigering har ändrats utan att använda EF Core för att göra den här ändringen. Överväg till exempel att läsa in bloggar och inlägg och sedan göra ändringar i dessa entiteter:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Om du tittar på felsökningsvyn för ändringsspåraren innan du anropar ChangeTracker.DetectChanges() visas att de ändringar som gjorts inte har identifierats och därför inte återspeglas i entitetstillstånden och ändrade egenskapsdata:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, <not found>]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Mer specifikt är tillståndet för blogginlägget fortfarande Unchanged, och det nya inlägget visas inte som en spårad entitet. (Astute ser att egenskaperna rapporterar sina nya värden, även om dessa ändringar ännu inte har identifierats av EF Core. Det beror på att felsökningsvyn läser aktuella värden direkt från entitetsinstansen.)

Jämför detta med felsökningsvyn när du har anropat DetectChanges:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Nu är bloggen korrekt markerad som Modified och det nya inlägget har identifierats och spåras som Added.

I början av det här avsnittet uppgav vi att det krävs identifiering av ändringar när du inte använder EF Core för att göra ändringen. Det här är vad som händer i koden ovan. Det vill: ändringarna i egenskapen och navigeringen görs direkt på entitetsinstanserna och inte med hjälp av några EF Core-metoder.

Jämför detta med följande kod som ändrar entiteterna på samma sätt, men den här gången med hjälp av EF Core-metoder:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");

// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";

// Add a new entity to the DbContext
context.Add(
    new Post
    {
        Blog = blog,
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

I det här fallet visar felsökningsvyn för ändringsspåraren att alla entitetstillstånd och egenskapsändringar är kända, även om identifiering av ändringar inte har gjorts. Det beror på att PropertyEntry.CurrentValue det är en EF Core-metod, vilket innebär att EF Core omedelbart känner till den ändring som görs med den här metoden. DbContext.Add På samma sätt kan EF Core omedelbart känna till den nya entiteten och spåra den på rätt sätt.

Tips/Råd

Försök inte att undvika att identifiera ändringar genom att alltid använda EF Core-metoder för att göra entitetsändringar. Att göra det är ofta mer besvärligt och presterar mindre bra än att göra ändringar i entiteter på normalt sätt. Avsikten med det här dokumentet är att informera om när det behövs för att identifiera ändringar och när det inte är det. Avsikten är inte att uppmuntra till undvikande av ändringsidentifiering.

Metoder som automatiskt identifierar ändringar

DetectChanges() anropas automatiskt av metoder där det sannolikt kommer att påverka resultatet. Dessa metoder är:

Det finns också vissa platser där identifiering av ändringar endast sker på en enda entitetsinstans, i stället för i hela diagrammet över spårade entiteter. Dessa platser är:

  • När du använder DbContext.Entry kan du säkerställa att entitetens tillstånd och ändrade egenskaper är uppdaterade i enlighet med up-to-datum.
  • När du använder EntityEntry metoder som Property, Collectioneller ReferenceMember för att säkerställa att egenskapsändringar, aktuella värden osv. är up-to-date.
  • När en beroende/underordnad entitet kommer att tas bort eftersom en nödvändig relation har avbrutits. Detta identifierar när en entitet inte ska tas bort eftersom den har fått en ny överordnad.

Lokal identifiering av ändringar för en enskild entitet kan utlösas explicit genom att anropa EntityEntry.DetectChanges().

Anmärkning

Lokala ändringsdetekteringar kan missa vissa ändringar som en fullständig detektering skulle upptäcka. Detta inträffar när sammanhängande åtgärder till följd av oidentifierade ändringar i andra entiteter påverkar den aktuella entiteten. I sådana situationer kan programmet behöva framtvinga en fullständig genomsökning av alla entiteter genom att uttryckligen anropa ChangeTracker.DetectChanges().

Inaktivera automatisk ändringsdetektering

Prestanda för att identifiera ändringar är inte en flaskhals för de flesta program. Att identifiera ändringar kan dock bli ett prestandaproblem för vissa program som spårar tusentals entiteter. (Det exakta antalet beror på många saker, till exempel antalet egenskaper i entiteten.) Därför kan automatisk identifiering av ändringar inaktiveras med hjälp av ChangeTracker.AutoDetectChangesEnabled. Överväg till exempel att bearbeta kopplingsentiteter i en många-till-många-relation med laster:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
            entityEntry.Entity.TaggedOn = DateTime.Now;
        }
    }

    try
    {
        ChangeTracker.AutoDetectChangesEnabled = false;
        return await base.SaveChangesAsync(cancellationToken); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

Som vi vet från föregående avsnitt identifierar både ChangeTracker.Entries<TEntity>() och DbContext.SaveChanges automatiskt ändringar. Men efter att ha anropat Entries, gör koden inga ändringar i entitets- eller egenskapstillståndet. (Om du ställer in normala egenskapsvärden för tillagda entiteter leder det inte till några tillståndsändringar.) Koden inaktiverar därför onödig automatisk ändringsidentifiering när du anropar ned till basmetoden SaveChanges. Koden använder också ett try/finally-block för att säkerställa att standardinställningen återställs även om SaveChanges misslyckas.

Tips/Råd

Anta inte att koden måste inaktivera automatisk ändringsidentifiering för att fungera bra. Detta behövs bara när profilering av ett program som spårar många entiteter indikerar att prestanda för ändringsidentifiering är ett problem.

Identifiera ändringar och värdekonverteringar

Om du vill använda spårning av ändring av ögonblicksbilder med en entitetstyp måste EF Core kunna:

  • Skapa en ögonblicksbild av varje egenskapsvärde när entiteten spåras
  • Jämför det här värdet med det aktuella värdet för egenskapen
  • Generera en hash-kod för värdet

Detta hanteras automatiskt av EF Core för typer som kan mappas direkt till databasen. Men när en värdekonverterare används för att mappa en egenskap måste konverteraren ange hur dessa åtgärder ska utföras. Detta uppnås med en värdejämförare och beskrivs i detalj i dokumentationen om värdejämförare.

Meddelandeentiteter

Spårning av ändring av ögonblicksbilder rekommenderas för de flesta program. Program som spårar många entiteter och/eller gör många ändringar i dessa entiteter kan dock dra nytta av att implementera entiteter som automatiskt meddelar EF Core när deras egenskaps- och navigeringsvärden ändras. Dessa kallas "meddelandeentiteter".

Implementera meddelandeentiteter

Meddelandeentiteter använder gränssnitten INotifyPropertyChanging och INotifyPropertyChanged som ingår i .NET-basklassbiblioteket (BCL). Dessa gränssnitt definierar händelser som måste utlöses före och efter ändring av ett egenskapsvärde. Till exempel:

public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;

    public int Id
    {
        get => _id;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
            _id = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
        }
    }

    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

Dessutom måste alla samlingsnavigeringar implementera INotifyCollectionChanged. I exemplet ovan uppfylls detta med hjälp av ett ObservableCollection<T> inlägg. EF Core levereras också med en ObservableHashSet<T> implementering som har effektivare sökningar på bekostnad av stabil beställning.

Merparten av den här meddelandekoden flyttas vanligtvis till en ommappad basklass. Till exempel:

public class Blog : NotifyingEntity
{
    private int _id;

    public int Id
    {
        get => _id;
        set => SetWithNotify(value, out _id);
    }

    private string _name;

    public string Name
    {
        get => _name;
        set => SetWithNotify(value, out _name);
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
    protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
    {
        NotifyChanging(propertyName);
        field = value;
        NotifyChanged(propertyName);
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private void NotifyChanging(string propertyName)
        => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

Konfigurera meddelandeentiteter

Det finns inget sätt för EF Core att verifiera att INotifyPropertyChanging eller INotifyPropertyChanged är fullständigt implementerade för användning med EF Core. I synnerhet gör vissa användningar av dessa gränssnitt det med meddelanden endast på vissa egenskaper, snarare än på alla egenskaper (inklusive navigering) som krävs av EF Core. Därför ansluter EF Core inte automatiskt till dessa händelser.

I stället måste EF Core konfigureras för att använda dessa meddelandeentiteter. Detta görs vanligtvis för alla entitetstyper genom att anropa ModelBuilder.HasChangeTrackingStrategy. Till exempel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}

(Strategin kan också anges på olika sätt för olika entitetstyper med , EntityTypeBuilder.HasChangeTrackingStrategymen det är vanligtvis kontraproduktivt eftersom DetectChanges fortfarande krävs för de typer som inte är meddelandeentiteter.)

Fullständig spårning av meddelandeändringar kräver att båda INotifyPropertyChanging och INotifyPropertyChanged implementeras. Detta gör att ursprungliga värden kan sparas precis innan egenskapsvärdet ändras, vilket undviker behovet av att EF Core skapar en ögonblicksbild när entiteten spåras. Entitetstyper som endast INotifyPropertyChanged implementeras kan också användas med EF Core. I det här fallet skapar EF fortfarande en ögonblicksbild vid spårning av en entitet för att hålla koll på ursprungliga värden, men använder sedan meddelandena för att identifiera ändringar omedelbart, istället för att behöva anropa DetectChanges.

De olika ChangeTrackingStrategy värdena sammanfattas i följande tabell.

Strategi för ändringsspårning Gränssnitt som behövs Behöver använda DetectChanges Originalvärden för ögonblicksbilder
Ögonblicksbild Ingen Ja Ja
Ändrade Notifieringar INotifyPropertyChanged Nej Ja
Ändrings- och förändringsmeddelanden INotifyPropertyChanged och INotifyPropertyChanging Nej Nej
Ändrings- och ändrade notifikationer med ursprungliga värden INotifyPropertyChanged och INotifyPropertyChanging Nej Ja

Använda meddelandeentiteter

Meddelandeentiteter fungerar som andra entiteter, förutom att ändringar i entitetsinstanserna inte kräver ett anrop för att ChangeTracker.DetectChanges() identifiera dessa ändringar. Till exempel:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Med normala entiteter visade felsökningsvyn för ändringsspåraren att dessa ändringar inte identifierades förrän DetectChanges anropades. Om du tittar på felsökningsvyn när meddelandeentiteter används visas att dessa ändringar har identifierats omedelbart:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Proxyservrar för ändringsspårning

EF Core kan dynamiskt generera proxytyper som implementerar INotifyPropertyChanging och INotifyPropertyChanged. Detta kräver att du installerar NuGet-paketet Microsoft.EntityFrameworkCore.Proxies och aktiverar proxyservrar för ändringsspårning med UseChangeTrackingProxies till exempel:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseChangeTrackingProxies();

Att skapa en dynamisk proxy innebär att skapa en ny dynamisk .NET-typ (med implementeringen av Castle.Core-proxyservrar ), som ärver från entitetstypen och sedan åsidosätter alla egenskapsuppsättningar. Entitetstyper för proxyservrar måste därför vara typer som kan ärvas från och måste ha egenskaper som kan åsidosättas. Dessutom måste samlingsnavigeringar som skapas uttryckligen implementera INotifyCollectionChanged till exempel:

public class Blog
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }

    public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public class Post
{
    public virtual int Id { get; set; }
    public virtual string Title { get; set; }
    public virtual string Content { get; set; }

    public virtual int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

En viktig nackdel med proxyservrar för ändringsspårning är att EF Core alltid måste spåra instanser av proxyn, aldrig instanser av den underliggande entitetstypen. Det beror på att instanser av den underliggande entitetstypen inte genererar meddelanden, vilket innebär att ändringar som görs i dessa entiteter kommer att missas.

EF Core skapar proxyinstanser automatiskt när du kör frågor mot databasen, så den här nackdelen är vanligtvis begränsad till att spåra nya entitetsinstanser. Dessa instanser måste skapas med hjälp av tilläggsmetoderna CreateProxy och inte på det normala sättet med hjälp av new. Det innebär att koden från föregående exempel nu måste använda CreateProxy:

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    context.CreateProxy<Post>(
        p =>
        {
            p.Title = "What’s next for System.Text.Json?";
            p.Content = ".NET 5.0 was released recently and has come with many...";
        }));

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Ändringsspårningshändelser

EF Core utlöser ChangeTracker.Tracked händelsen när en entitet spåras för första gången. Framtida ändringar av entitetstillstånd resulterar i ChangeTracker.StateChanged händelser. Mer information finns i .NET-händelser i EF Core .

Anmärkning

Händelsen StateChanged utlöses inte när en entitet först spåras, även om tillståndet har ändrats från Detached till ett av de andra tillstånden. Se till att lyssna efter både StateChanged och Tracked händelser för att få alla relevanta meddelanden.