Partager via


Autres fonctionnalités de suivi des modifications

Ce document couvre des fonctionnalités et scénarios divers impliquant le suivi des changements.

Conseil

Ce document suppose que les états d’entité et les principes de base du suivi des modifications EF Core sont compris. Pour plus d’informations sur ces rubriques, consultez Suivi des modifications dans EF Core .

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.

Add contre AddAsync

Entity Framework Core (EF Core) fournit des méthodes asynchrones chaque fois que l'utilisation de cette méthode peut entraîner une interaction avec la base de données. Des méthodes synchrones sont également fournies pour éviter les surcharges lors de l'utilisation de bases de données qui ne prennent pas en charge l'accès asynchrone à haute performance.

DbContext.Add et DbSet<TEntity>.Add n’accèdent pas normalement à la base de données, car ces méthodes commencent intrinsèquement simplement à suivre les entités. Toutefois, certaines formes de génération de valeur peuvent accéder à la base de données pour générer une valeur de clé. Le seul générateur de valeurs qui effectue cette opération et est fourni avec EF Core est HiLoValueGenerator<TValue>. L'utilisation de ce générateur est peu courante ; il n'est jamais configuré par défaut. Cela signifie que la grande majorité des applications doivent utiliser Add, et non AddAsync.

D’autres méthodes similaires telles que Update, Attach et Remove n’ont pas de surcharges asynchrones, car elles ne génèrent jamais de nouvelles valeurs de clé et n’ont donc jamais besoin d’accéder à la base de données.

AddRange, UpdateRange, AttachRange et RemoveRange

DbSet<TEntity> et DbContext fournissent d’autres versions de Add, Update, Attach et Remove qui acceptent plusieurs instances dans un seul appel. Ces méthodes sont AddRange, UpdateRange, AttachRange et RemoveRange respectivement.

Ces méthodes sont fournies de manière pratique. L'utilisation d'une méthode « portée » a la même fonctionnalité que des appels multiples à la méthode équivalente « sans portée ». Il n’y a aucune différence de performances significative entre les deux approches.

Remarque

Cela est différent d’EF6, où AddRange et Add sont automatiquement appelés DetectChanges, mais l’appel de Add plusieurs fois a provoqué l’appel de DetectChanges plusieurs fois au lieu d’une seule fois. Cela a rendu AddRange plus efficace dans EF6. Dans EF Core, aucune de ces méthodes n’appelle DetectChanges automatiquement.

DbContext et méthodes DbSet

De nombreuses méthodes, notamment Add, Update, Attach et Remove, ont des implémentations sur DbSet<TEntity> et DbContext. Ces méthodes ont exactement le même comportement pour les types d’entités normaux. En effet, le type CLR de l'entité est associé à un et un seul type d'entité dans le modèle EF Core. Par conséquent, le type CLR définit entièrement la place de l'entité dans le modèle, ce qui permet de déterminer implicitement le DbSet à utiliser.

L'exception à cette règle est l'utilisation de types d'entités de type partagé, qui sont principalement utilisés pour les entités de jonction plusieurs-à-plusieurs. Lors de l'utilisation d'un type d'entité de type partagé, un DbSet doit d'abord être créé pour le type de modèle EF Core utilisé. Les méthodes telles que Add, Update, Attach et Remove peuvent ensuite être utilisées sur DbSet sans ambiguïté quant au type de modèle EF Core utilisé.

Les types d'entités de type partagé sont utilisés par défaut pour les entités de jonction dans les relations de plusieurs à plusieurs. Un type d'entité de type partagé peut également être explicitement configuré pour être utilisé dans une relation de plusieurs à plusieurs. Par exemple, le code ci-dessous configure Dictionary<string, int> comme type d’entité de jointure :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

La modification des clés étrangères et des navigations montre comment associer deux entités en suivant une nouvelle instance d’entité de jointure. Le code ci-dessous effectue cette opération pour le type d'entité partagé Dictionary<string, int> utilisé pour l’entité de jointure :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

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

context.SaveChanges();

Notez que DbContext.Set<TEntity>(String) est utilisé pour créer un DbSet pour le type d’entité PostTag. Ce DbSet peut ensuite être utilisé pour appeler Add avec la nouvelle instance d’entité de jointure.

Important

Le type CLR utilisé pour les types d’entités de jointure par convention peut changer dans les futures versions afin d’améliorer les performances. Ne dépendez pas d’un type d’entité de jointure spécifique, sauf s’il a été configuré explicitement comme pour Dictionary<string, int> dans le code ci-dessus.

Accès aux propriétés et aux champs

L'accès aux propriétés des entités utilise par défaut le champ de stockage de la propriété. Cette méthode est efficace et évite de déclencher des effets secondaires lors de l'appel aux getters et setters des propriétés. C'est ainsi, par exemple, que le lazy-loading permet d'éviter de déclencher des boucles infinies. Pour plus d’informations sur la configuration des champs de stockage dans le modèle, consultez Champs de stockage.

Il est parfois souhaitable que EF Core génère des effets de bord lorsqu'il modifie les valeurs de propriété. Par exemple, lors de la liaison de données avec des entités, la définition d'une propriété peut générer des notifications à l'utilisateur final, ce qui n'est pas le cas lorsque le champ est défini directement. Pour ce faire, modifiez PropertyAccessMode pour :

Les modes d’accès aux propriétés Field et PreferField entraînent EF Core à accéder à la valeur de la propriété via son champ de stockage. De même, Property et PreferProperty entraînent l’accès à EF Core à la valeur de la propriété par le biais de leur getter et setter.

Si vous utilisez Field ou Property si EF Core ne peut pas accéder à la valeur via le champ ou la propriété getter/setter respectivement, EF Core lève une exception. Cela permet de s'assurer que EF Core utilise toujours l'accès aux champs et propriétés lorsque vous pensez qu'il le fait.

En revanche, les modes PreferField et PreferProperty reviennent à utiliser la propriété ou le champ de stockage respectivement s’il n’est pas possible d’utiliser l’accès préféré. PreferField est la valeur par défaut. Cela signifie que EF Core utilisera les champs chaque fois qu'il le pourra, mais n'échouera pas si une propriété doit être accédée par son getter ou setter à la place.

FieldDuringConstruction et PreferFieldDuringConstruction configurent EF Core pour utiliser des champs de stockage uniquement lors de la création d’instances d’entité. Cela permet aux requêtes d'être exécutées sans effets secondaires de type getter et setter, alors que les changements de propriétés effectués ultérieurement par EF Core provoqueront ces effets secondaires.

Les différents modes d'accès à la propriété sont résumés dans le tableau suivant :

PropertyAccessMode Préférence Préférence lors de la création d’entités De base Création d’entités de secours
Field Champ Champ Exception Exception
Property Propriété Propriété Exception Exception
PreferField Champ Champ Propriété Propriété
PreferProperty Propriété Propriété Champ Champ
FieldDuringConstruction Propriété Champ Champ Exception
PreferFieldDuringConstruction Propriété Champ Champ Propriété

Valeurs temporaires

EF Core crée des valeurs clés temporaires lors du suivi de nouvelles entités qui auront des valeurs clés réelles générées par la base de données lors de l'appel de SaveChanges. Consultez Change Tracking dans EF Core pour obtenir une vue d’ensemble de la façon dont ces valeurs temporaires sont utilisées.

Accès aux valeurs temporaires

Les valeurs temporaires sont stockées dans l'outil de suivi des modifications et ne sont pas affectées directement aux instances des entités. Toutefois, ces valeurs temporaires sont exposées lors de l’utilisation des différents mécanismes permettant d’accéder aux entités suivies. Par exemple, le code suivant accède à une valeur temporaire à l’aide de EntityEntry.CurrentValues :

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

La sortie de ce code est la suivante :

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary peut être utilisé pour vérifier les valeurs temporaires.

Manipulation de valeurs temporaires

Il est parfois utile de travailler explicitement avec des valeurs temporaires. Par exemple, une collection de nouvelles entités peut être créée sur un client web, puis sérialisée vers le serveur. Les valeurs des clés étrangères sont un moyen d'établir des relations entre ces entités. Le code suivant utilise cette approche pour associer un graphique de nouvelles entités par clé étrangère, tout en permettant de générer des valeurs de clé réelles lors de l'appel de SaveChanges.

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -1,
        Title = "Announcing the Release of EF Core 5.0",
        Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
    },
    new Post
    {
        Id = -2,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

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

context.SaveChanges();

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

Notez que :

  • Les nombres négatifs sont utilisés comme valeurs de clé ; cela n'est pas obligatoire, mais il s'agit d'une convention courante pour éviter les conflits de clés.
  • La propriété FK Post.BlogId est affectée à la même valeur négative que le PK du blog associé.
  • Les valeurs PK sont marquées comme temporaires en définissant IsTemporary une fois que chaque entité est suivie. Cela est nécessaire car toute valeur de clé fournie par l'application est supposée être une valeur de clé réelle.

En examinant la vue de débogage du suivi des modifications avant d’appeler SaveChanges, les valeurs PK sont marquées comme temporaires et les publications sont associées aux blogs corrects, y compris la correction des navigations :

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  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}

Après l’appel de SaveChanges, ces valeurs temporaires ont été remplacées par des valeurs réelles générées par la base de données :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{Id: 2}]
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}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

Utilisation des valeurs par défaut

EF Core permet à une propriété d’obtenir sa valeur par défaut à partir de la base de données quand SaveChanges est appelé. Comme pour les valeurs de clés générées, EF Core n'utilisera une valeur par défaut de la base de données que si aucune valeur n'a été explicitement définie. Par exemple, tenez compte du type d’entité suivant :

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

La propriété ValidFrom est configurée pour obtenir une valeur par défaut à partir de la base de données :

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

Lors de l'insertion d'une entité de ce type, EF Core laissera la base de données générer la valeur, à moins qu'une valeur explicite n'ait été définie à la place. Par exemple :

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

context.SaveChanges();

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

L’analyse de la vue de débogage du suivi des modifications montre que le premier jeton ValidFrom a été généré par la base de données, tandis que le deuxième jeton a utilisé la valeur définie explicitement :

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

Remarque

L'utilisation des valeurs par défaut de la base de données nécessite qu'une contrainte de valeur par défaut soit configurée pour la colonne de la base de données. Cette opération est effectuée automatiquement par les migrations EF Core lors de l’utilisation de HasDefaultValueSql ou HasDefaultValue. Veillez à créer la contrainte par défaut sur la colonne d'une autre manière si vous n'utilisez pas les migrations EF Core.

Utilisation de propriétés pouvant accepter la valeur Null

EF Core est capable de déterminer si une propriété a été définie ou non en comparant la valeur de propriété à la valeur par défaut du CLR pour ce type de propriété. Cela fonctionne bien dans la plupart des cas, mais signifie que la valeur par défaut du CLR ne peut pas être insérée explicitement dans la base de données. Par exemple, considérez une entité avec une propriété entière :

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

Lorsque cette propriété est configurée pour avoir une valeur par défaut de -1 dans la base de données :

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

L'intention est que la valeur par défaut de -1 soit utilisée chaque fois qu'une valeur explicite n'est pas définie. Cependant, si la valeur est fixée à 0 (valeur par défaut du CLR pour les entiers), EF Core ne peut pas la distinguer de l'absence de valeur, ce qui signifie qu'il n'est pas possible d'insérer 0 pour cette propriété. Par exemple :

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Notez que l’instance où Count a été explicitement défini sur 0 obtient toujours la valeur par défaut de la base de données, ce qui n’est pas ce que nous avons prévu. Un moyen simple de traiter cela consiste à rendre la propriété Count comme pouvant accepter la valeur Null :

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

Cela rend la valeur par défaut du CLR nulle, au lieu de 0, ce qui signifie que 0 sera désormais inséré lorsqu'il est explicitement défini :

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Utilisation de champs de stockage pouvant accepter la valeur Null

Le problème de la nullité de la propriété est qu'elle peut ne pas conceptuellement accepter la valeur Null dans le modèle de domaine. Forcer la propriété à accepter la valeur Null compromet donc le modèle.

La propriété peut être laissée non nulle, seul le champ d'appui pouvant accepter la valeur Null. Par exemple :

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

Cela permet d'insérer la valeur par défaut du CLR (0) si la propriété est explicitement définie à 0, sans qu'il soit nécessaire d'exposer la propriété comme pouvant accepter la valeur Null dans le modèle de domaine. Par exemple :

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Champs de stockage pouvant accepter la valeur Null pour les propriétés bool

Ce modèle est particulièrement utile lors de l'utilisation de propriétés bool avec des valeurs par défaut générées en magasin. Étant donné que la valeur par défaut du CLR pour bool est « fausse », cela signifie que « fausse » ne peut pas être inséré explicitement à l’aide du modèle normal. Par exemple, considérez un type d’entité User :

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

La propriété IsAuthorized est configurée avec une valeur par défaut de base de données « vraie » :

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

La propriété IsAuthorized peut être définie sur « vraie » ou « fausse » explicitement avant l’insertion, ou peut être laissée non définie, auquel cas la valeur par défaut de la base de données sera utilisée :

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

context.SaveChanges();

La sortie de SaveChanges lors de l'utilisation de SQLite montre que la base de données par défaut est utilisée pour Mac, tandis que des valeurs explicites sont définies pour Alice et Baxter :

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Valeurs par défaut du schéma uniquement

Il est parfois utile d'avoir des valeurs par défaut dans le schéma de la base de données créé par les migrations EF Core sans qu'EF Core n'utilise jamais ces valeurs pour les insertions. Pour ce faire, configurez la propriété comme PropertyBuilder.ValueGeneratedNever par exemple :

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();