Partager via


Détection et notifications des modifications

Chaque instance DbContext suit les modifications apportées aux entités. Ces entités suivies à son tour entraînent les modifications apportées à la base de données lorsque SaveChanges est appelé. Ce sujet est abordé par Change Tracking dans EF Core. Ce document suppose que les états d’entité et les principes de base du suivi des modifications d’Entity Framework Core (EF Core) sont bien compris.

Le suivi des modifications de propriété et de relation nécessite que DbContext puisse détecter ces modifications. Ce document explique comment cette détection se produit et comment utiliser des notifications de propriétés ou des proxies de suivi des modifications pour forcer la détection immédiate de modifications.

Conseil

Vous pouvez exécuter et déboguer dans tout le code de ce document en téléchargeant l’exemple de code à partir de GitHub.

Suivi des modifications par instantané

Par défaut, EF Core crée une capture instantanée des valeurs de propriété de chaque entité lorsqu’elle est suivie pour la première fois par une instance DbContext. Les valeurs stockées dans cette capture instantanée sont ensuite comparées aux valeurs actuelles de l’entité afin de déterminer les valeurs de propriété qui ont changé.

Cette détection des modifications se produit lorsque SaveChanges est appelé pour assurer que toutes les valeurs modifiées sont détectées avant d’envoyer des mises à jour à la base de données. Toutefois, la détection des modifications se produit également à d’autres moments pour assurer que l’application fonctionne avec des informations de suivi à jour. La détection des modifications peut être forcée à tout moment en appelant ChangeTracker.DetectChanges().

Moments où la détection des modifications est nécessaire

La détection des modifications est nécessaire lorsqu’une propriété ou une navigation a été modifiée sans utiliser EF Core pour effectuer cette modification. Prenez par exemple le chargement de blogs et de billets, puis les modifications effectuées à ces entités :

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(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);

L’analyse de la vue de débogage du suivi des modifications avant d’appeler ChangeTracker.DetectChanges() indique que les modifications apportées n’ont pas été détectées et ne sont donc pas reflétées dans les états de l’entité et les données de propriété modifiées :

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}

Plus précisément, l’état de l’entrée de blog est toujours Unchanged et le nouveau billet n’apparaît pas comme entité suivie. (Une personne avisée remarquera que les propriétés signalent leurs nouvelles valeurs, même si ces modifications n’ont pas encore été détectées par EF Core. Ceci s’explique par le fait que la vue de débogage lit les valeurs actuelles directement à partir de l’instance d’entité.)

Contrastez cela avec la vue de débogage après avoir appelé 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}

Le blog est désormais correctement marqué comme Modified et le nouveau billet a été détecté et est suivi en tant que Added.

Au début de cette section, nous avons indiqué que la détection des modifications est nécessaire lorsque vous n’utilisez pas EF Core pour apporter la modification. C’est ce qui se passe dans le code ci-dessus. Autrement dit, les modifications à la propriété et à la navigation sont apportées directement sur les instances d’entité et non à l’aide de quelconque méthode EF Core.

Comparez cela au code suivant qui modifie les entités de la même façon, mais cette fois à l’aide de méthodes EF Core :

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(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);

Dans ce cas, la vue de débogage du suivi des modifications indique que tous les états et les modifications de propriété de l’entité sont connus, bien que la détection des modifications n’a pas eu lieu. Cela est dû au fait que PropertyEntry.CurrentValue est une méthode EF Core, ce qui signifie qu’EF Core est immédiatement au courant de la modification apportée par cette méthode. De même, l’appel de DbContext.Add permet à EF Core de connaître immédiatement la nouvelle entité et de la suivre convenablement.

Conseil

N’essayez pas d’éviter la détection de modifications en utilisant toujours des méthodes EF Core pour effectuer des modifications d’entité. Cela est souvent plus fastidieux et fonctionne moins bien que d’apporter des modifications aux entités de manière normale. Le but de ce document est d’indiquer le moment auquel la détection des modifications est nécessaire et quand ce n’est pas le cas. L’objectif n’est pas d’inciter à éviter la détection des modifications.

Méthodes qui détectent automatiquement les modifications

DetectChanges() est appelée automatiquement par des méthodes où cela est susceptible d’avoir un impact sur les résultats. Ces méthodes sont les suivantes :

Il existe également certains endroits où la détection des modifications se produit sur une seule instance d’entité, plutôt que sur l’ensemble du graphique des entités suivies. Ces endroits sont les suivants :

  • Lors de l’utilisation de DbContext.Entry, pour assurer que l’état et les propriétés modifiées de l’entité sont à jour.
  • Lors de l’utilisation de méthodes EntityEntry telles que Property, Collection, Reference ou Member pour assurer que les modifications de propriété, les valeurs actuelles, etc. sont à jour.
  • Lorsqu’une entité dépendante/enfant va être supprimée, car une relation requise a été rompue. Cela détecte lorsqu’une entité ne doit pas être supprimée, car elle a été re-parentée.

La détection locale de modifications pour une seule entité peut être explicitement déclenchée en appelant EntityEntry.DetectChanges().

Remarque

Les modifications de détection locale peuvent passer à côté de certaines modifications qui seraient trouvées par une détection complète. Cela se produit lorsque des actions en cascade résultant de modifications non détectées apportées à d’autres entités ont un impact sur l’entité en question. Dans de telles situations, l’application peut avoir besoin de forcer une analyse complète de toutes les entités en appelant explicitement ChangeTracker.DetectChanges().

Désactiver la détection automatique des modifications

Les performances de détection des modifications ne sont pas un goulot d’étranglement pour la plupart des applications. Toutefois, la détection des modifications peut devenir un problème de performance pour certaines applications qui suivent plusieurs milliers d’entités. (Le nombre exact dépend de plusieurs éléments, tel que le nombre de propriétés dans l’entité.) Pour cette raison, la détection automatique des modifications peut être désactivée à l’aide de ChangeTracker.AutoDetectChangesEnabled. Prenez par exemple le traitement des entités de jointure dans une relation plusieurs-à-plusieurs avec des charges utiles :

public override int SaveChanges()
{
    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 base.SaveChanges(); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

Comme nous le savons grâce à la section précédente, ChangeTracker.Entries<TEntity>() et DbContext.SaveChanges détectent automatiquement les modifications. Cependant, une fois Entries appelé, le code n’apporte ensuite aucune modification d’état d’entité ou de propriété. (La définition des valeurs de propriété normales sur les entités ajoutées n’entraîne aucune modification d’état.) Par conséquent, le code désactive la détection automatique inutile des modifications lors de l’appel dans la méthode SaveChanges de base. Le code utilise également un bloc try/finally pour assurer que le paramètre par défaut est restauré, même si SaveChanges échoue.

Conseil

Ne présumez pas que votre code doit désactiver la détection automatique des modifications pour bien fonctionner. Cela n’est nécessaire que lorsque le profilage d’une application suivant de nombreuses entités indique que les performances de détection des modifications posent problème.

Détection des modifications et conversions de valeurs

Pour utiliser le suivi des modifications de captures instantanées avec un type d’entité, EF Core doit pouvoir :

  • Créer une capture instantanée de chaque valeur de propriété lorsque l’entité est suivie
  • Comparer cette valeur à la valeur actuelle de la propriété
  • Générer un code de hachage pour la valeur

Cela est géré automatiquement par EF Core pour les types qui peuvent être directement mappés à la base de données. Toutefois, lorsqu’un convertisseur de valeur est utilisé pour mapper une propriété, ce convertisseur doit spécifier comment effectuer ces actions. Cela se fait à l’aide d’un comparateur de valeurs et le processus est décrit en détail dans la documentation Comparateurs de valeurs.

Entités de notification

Le suivi des modifications de captures instantanées est recommandé pour la plupart des applications. Toutefois, les applications qui suivent de nombreuses entités et/ou apportent de nombreuses modifications à ces entités peuvent tirer parti de l’implémentation d’entités qui notifient automatiquement EF Core de changements à leurs valeurs de propriété et de navigation. Celles-ci s’appellent « entités de notification ».

Implémentation d’entités de notification

Les entités de notification utilisent les interfaces INotifyPropertyChanging et INotifyPropertyChanged qui font partie de la bibliothèque de classes de base .NET. Ces interfaces définissent les événements qui doivent être lancés avant et après la modification d’une valeur de propriété. Par exemple :

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>();
}

En outre, toutes les navigations de collection doivent implémenter INotifyCollectionChanged. Dans l’exemple ci-dessus, cette condition est satisfaite par l’utilisation d’une ObservableCollection<T> de billets. EF Core est également fourni avec une implémentation ObservableHashSet<T> qui a des recherches plus efficaces au détriment d’un ordre stable.

La plupart de ce code de notification est généralement déplacé dans une classe de base non mappée. Par exemple :

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));
}

Configuration des entités de notification

Il n’existe aucun moyen pour EF Core de valider que INotifyPropertyChanging ou INotifyPropertyChanged est entièrement implémenté pour une utilisation avec EF Core. En particulier, certaines utilisations de ces interfaces le font avec des notifications uniquement sur certaines propriétés, plutôt que sur toutes les propriétés (y compris les navigations), tel qu’exigé par EF Core. Pour cette raison, EF Core ne s’accroche pas automatiquement à ces événements.

EF Core doit plutôt être configuré pour utiliser ces entités de notification. Cela se fait généralement pour tous les types d’entités en appelant ModelBuilder.HasChangeTrackingStrategy. Par exemple :

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

(La stratégie peut également être définie différemment pour différents types d’entités à l’aide de EntityTypeBuilder.HasChangeTrackingStrategy. Toutefois, cela est généralement contre-productif, car DetectChanges reste nécessaire pour les types qui ne sont pas des entités de notification.)

Le suivi complet des modifications de notification nécessite que INotifyPropertyChanging et INotifyPropertyChanged soient implémentés. Cela permet aux valeurs d’origine d’être enregistrées juste avant la modification de la valeur de propriété, ce qui évite d’avoir besoin qu’EF Core crée une capture instantanée lors du suivi de l’entité. Les types d’entités qui implémentent uniquement INotifyPropertyChanged peuvent également être utilisés avec EF Core. Dans ce cas, EF continue de créer une capture instantanée lors du suivi d’une entité pour surveiller les valeurs d’origine. Cependant, il utilise ensuite les notifications pour détecter immédiatement les modifications, plutôt que d’avoir besoin d’appeler DetectChanges.

Les différentes valeurs de ChangeTrackingStrategy sont résumées dans le tableau suivant.

ChangeTrackingStrategy Interfaces requises Exige DetectChanges Prend une capture instantanée des valeurs d’origine
Instantané Aucun Oui Oui
ChangedNotifications INotifyPropertyChanged Non Oui
ChangingAndChangedNotifications INotifyPropertyChanged et INotifyPropertyChanging Non Non
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged et INotifyPropertyChanging Non Oui

Utilisation des entités de notification

Les entités de notification se comportent comme toute autre entité, sauf que les modifications apportées aux instances d’entité ne nécessitent pas d’appel à ChangeTracker.DetectChanges() pour détecter ces modifications. Par exemple :

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(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);

Avec les entités normales, la vue de débogage du suivi des modifications montre que ces modifications ne sont pas détectées tant que DetectChanges n’a pas été appelé. L’examen de la vue de débogage lorsque des entités de notification sont utilisées montre que ces modifications sont immédiatement détectées :

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}

Proxies de suivi des modifications

EF Core peut générer dynamiquement des types de proxies qui implémentent INotifyPropertyChanging et INotifyPropertyChanged. Cela nécessite l’installation du package NuGet Microsoft.EntityFrameworkCore.Proxies et l’activation des proxies de suivi des modifications avec UseChangeTrackingProxies. Par exemple :

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

La création d’un proxy dynamique implique la création d’un nouveau type .NET dynamique (à l’aide de l’implémentation des proxies Castle.Core), qui hérite du type d’entité, puis remplace tous les setters de propriétés. Les types d’entités pour les proxies doivent donc être des types qui peuvent être hérités et doivent avoir des propriétés qui peuvent être remplacées. En outre, les navigations de collection explicitement créées doivent implémenter INotifyCollectionChanged. Par exemple :

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; }
}

Un inconvénient important pour les proxies de suivi des modifications est qu’EF Core doit toujours suivre les instances du proxy et jamais les instances du type d’entité sous-jacent. Cela est dû au fait que les instances du type d’entité sous-jacent ne génèrent pas de notifications, ce qui signifie que les modifications apportées à ces entités sont manquées.

EF Core crée automatiquement des instances de proxy lors de l’interrogation de la base de données. Cet inconvénient se limite donc généralement au suivi de nouvelles instances d’entité. Ces instances doivent être créées à l’aide des méthodes d’extension CreateProxy et non pas de manière normale à l’aide de new. Cela signifie que le code des exemples précédents doit désormais utiliser CreateProxy :

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(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);

Modification des événements de suivi

EF Core lance l’événement ChangeTracker.Tracked lorsqu’une entité est suivie pour la première fois. Les modifications futures de l’état d’entité entraînent des événements ChangeTracker.StateChanged. Pour plus d’informations, consultez Événements .NET dans EF Core.

Remarque

L’événement StateChanged n’est pas lancé lorsqu’une entité est suivie pour la première fois, bien que l’état ait changé de Detached à l’un des autres états. Soyez à l’écoute des événements StateChanged et Tracked pour obtenir toutes les notifications pertinentes.