Partager via


Fonctionnalités de suivi des modifications supplémentaires

Ce document traite de diverses fonctionnalités et scénarios impliquant le suivi des modifications.

Conseil / Astuce

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 Change Tracking in EF Core .

Conseil / Astuce

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 cette méthode peut entraîner une interaction de base de données. Les méthodes synchrones sont également fournies pour éviter la surcharge lors de l’utilisation de bases de données qui ne prennent pas en charge l’accès asynchrone hautes performances.

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 rare ; elle n’est jamais configurée 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, Attachet 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, AttachRangeet RemoveRange

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

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

Remarque

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

DbContext et méthodes DbSet

De nombreuses méthodes, notamment Add, Update, Attach et Remove, ont des implémentations sur les deux DbSet<TEntity> et DbContext. Ces méthodes ont exactement le même comportement pour les types d’entités normaux. Cela est dû au fait que le type CLR de l’entité est mappé sur un seul et un seul type d’entité dans le modèle EF Core. Par conséquent, le type CLR définit entièrement l’emplacement de l’entité dans le modèle, et par conséquent, dbSet à utiliser peut être déterminé implicitement.

L’exception à cette règle consiste à utiliser des types d’entités de type partagé, qui sont principalement utilisés pour les entités de jointure 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, Attachet 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 jointure dans des relations plusieurs-à-plusieurs. Un type d’entité de type partagé peut également être configuré explicitement pour une utilisation dans une relation 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 Dictionary<string, int> type d’entité de type partagé utilisé pour l’entité de jointure :

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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);

await context.SaveChangesAsync();

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 indiqué Dictionary<string, int> dans le code ci-dessus.

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

L’accès aux propriétés d’entité utilise le champ de stockage de la propriété par défaut. Cela est efficace et évite de déclencher des effets secondaires liés à l'appel des setters et des getters de propriétés. Par exemple, il s’agit de la façon dont le chargement différé est en mesure 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 .

Parfois, il peut être souhaitable qu’EF Core génère des effets secondaires lorsqu’il modifie les valeurs de propriété. Par exemple, lorsqu'on lie des données à des entités, la modification d’une propriété peut générer des notifications à l'interface utilisateur qui ne se produisent pas lorsqu'on modifie le champ directement. Pour ce faire, vous pouvez modifier le PropertyAccessMode comme suit :

Les modes Field d’accès aux propriétés 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îneront l'accès à la valeur de la propriété par EF Core via son accesseur (getter) et mutateur (setter).

Si Field ou Property sont utilisés et qu'EF Core ne peut pas accéder à la valeur via le champ ou, respectivement, les accesseurs (getter/setter) de la propriété, alors EF Core lèvera une exception. Cela garantit qu’EF Core utilise toujours l’accès champ/propriété lorsque vous pensez que c'est le cas.

En revanche, les modes PreferField et PreferProperty reviennent à utiliser respectivement la propriété ou le champ de stockage s’il n’est pas possible d’utiliser l’accès préféré. PreferField est la valeur par défaut. Cela signifie qu’EF Core utilisera des champs chaque fois qu’il peut, mais ne échouera pas si une propriété doit être accessible via 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 liés aux accesseurs, tandis que les modifications ultérieures des propriétés par EF Core entraîneront ces effets secondaires.

Les différents modes d’accès aux propriétés sont résumés dans le tableau suivant :

ModeAccèsPropriété Préférence Préférences pour la création d’entités Rubrique de base Création d’entités de secours
Field Terrain Terrain Exception Exception
Property Propriété Propriété Exception Exception
PreferField Terrain Terrain Propriété Propriété
PreferProperty Propriété Propriété Terrain Terrain
FieldDuringConstruction Propriété Terrain Terrain Exception
PreferFieldDuringConstruction Propriété Terrain Terrain Propriété

Valeurs temporaires

EF Core crée des valeurs de clé temporaires lors du suivi de nouvelles entités qui auront des valeurs de clés réelles générées par la base de données lorsque SaveChanges est appelé. Consultez Change Tracking in 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 le suivi des modifications et ne sont pas définies directement sur les instances d’entité. 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 d’entités peut être créée sur un client web, puis sérialisée sur le serveur. Les valeurs de clé étrangère sont un moyen de configurer 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 autorisant la génération de valeurs de clés réelles lorsque SaveChanges est appelé.

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

await context.SaveChangesAsync();

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

Notez que :

  • Les nombres négatifs sont utilisés comme valeurs de clé temporaires ; cela n’est pas obligatoire, mais il s’agit d’une convention commune pour empêcher les affrontements clés.
  • La Post.BlogId propriété FK 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 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 elle SaveChanges est appelée. Comme avec les valeurs de clé générées, EF Core utilise uniquement une valeur par défaut à partir de la base de données si aucune valeur n’a été définie explicitement. Par exemple, considérez le type d’entité suivant :

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

La ValidFrom propriété 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 permet à la base de données de générer la valeur, sauf si une valeur explicite a é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) });

await context.SaveChangesAsync();

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

L’analyse de la vue de débogage du suivi des modifications montre que le premier jeton a été ValidFrom 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 de valeurs par défaut de base de données nécessite que la colonne de base de données ait une contrainte de valeur par défaut configurée. Cette opération est effectuée automatiquement par les migrations EF Core lors de l’utilisation HasDefaultValueSql ou HasDefaultValue. Veillez à créer la contrainte par défaut sur la colonne d’une autre façon quand vous n’utilisez pas les migrations EF Core.

Utilisation de propriétés nullables

EF Core est en mesure de déterminer si une propriété a été définie ou non en comparant la valeur de propriété à la valeur CLR par défaut pour ce type. 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; }
}

Où cette propriété est configurée pour avoir une base de données par défaut de -1 :

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

L’intention est que la valeur par défaut de -1 sera utilisée chaque fois qu’une valeur explicite n’est pas définie. Toutefois, définir la valeur sur 0 (la valeur par défaut CLR pour les entiers) est indiscernable pour EF Core de ne pas définir de valeur. Cela signifie donc 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);
await context.SaveChangesAsync();

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

Notez que l’instance qui Count a été explicitement définie 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 Count propriété nullable :

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

Cela rend la valeur NULL par défaut du CLR, au lieu de 0, ce qui signifie que 0 sera désormais inséré lors de la définition explicite :

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);
await context.SaveChangesAsync();

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

Utilisation de champs de stockage nullables

Le problème avec la transformation de la propriété en nullable est qu'elle ne peut pas être conceptuellement nullable dans le modèle de domaine. Forcer la propriété à être nullable compromet donc le modèle.

La propriété peut être laissée non nulle, avec uniquement le champ de stockage nullable. Par exemple:

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

    private int? _count;

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

Cela permet au CLR par défaut (0) d’être inséré si la propriété est explicitement définie sur 0, alors qu’elle n’a pas besoin d’exposer la propriété comme nullable 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);
await context.SaveChangesAsync();

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

Champs de stockage nullables pour les propriétés booléennes

Ce modèle est particulièrement utile lors de l’utilisation de propriétés booléennes avec des valeurs par défaut générées automatiquement. Étant donné que la valeur par défaut bool du CLR est « false », cela signifie que « false » 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 IsAuthorized propriété est configurée avec une valeur par défaut de base de données « true » :

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

La IsAuthorized propriété peut être définie sur « true » ou « false » 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);

await context.SaveChangesAsync();

La sortie de SaveChanges lors de l’utilisation de SQLite indique que la base de données par défaut est utilisée pour Mac, tandis que les 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 base de données créé par les migrations EF Core sans EF Core n’utilisant 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();