web-dev-qa-db-fra.com

Comment actualiser un DBContext Entity Framework Core?

Lorsque ma table est mise à jour par une autre partie, le contexte de base de données dans CoreNet renvoie toujours l'ancienne valeur. Comment puis-je forcer l'actualisation du contexte de base de données?

J'ai effectué des recherches, mais je n'ai constaté que l'utilisation de la méthode Reload, qui n'est pas disponible dans EF core, pour forcer le contexte à s'actualiser.

Une autre solution suggère de supprimer le contexte après l’utilisation, mais j’obtiens une erreur en disant que le contexte de la base de données est créé par une injection de dépendance et que je ne devrais pas le gâcher.

5
MiDaa

Dépendance Injection et DbContext

Vous indiquez que lorsque vous essayez de recréer votre DbContext, vous obtenez une erreur concernant le contexte géré par votre système d'injection de dépendance. Il existe deux styles d'utilisation d'un système d'injection de dépendances pour la création d'objets. L’ID peut soit créer une instance singleton globale partagée en tant que service entre tous les consommateurs, soit créer une instance par étendue/unité de travail (par exemple, par demande sur un serveur Web).

Si votre système DI est configuré pour créer une seule instance partagée globale de DbContext, vous rencontrerez divers problèmes associés à DbContext de longue durée. DbContext, de par sa conception, ne supprime jamais automatiquement les objets de son cache car il n'est pas conçu pour durer. Ainsi, une DbContext de longue durée conservera la mémoire inutilement. En outre, votre code ne verra jamais les modifications apportées aux éléments chargés dans son cache sans recharger manuellement chaque entité qu'il charge. Ainsi, la meilleure pratique consiste à utiliser une seule DbContext par unité de travail . Votre système DI peut vous aider à cet égard en étant configuré pour fournir une nouvelle instance pour chaque demande traitée par votre application dans une étendue. Par exemple, le système Dependency Injection d’ASP.NET Core prend en charge les instances de cadrage sur demande.

Rafraîchir une seule entité

Le moyen le plus simple d'obtenir de nouvelles données consiste à créer une nouvelle variable DbContext. Toutefois, au sein de votre unité de travail, ou dans les limites de la granularité de la portée fournie par votre système d’ID, vous pouvez déclencher un processus externe censé modifier votre entité directement dans la base de données. Vous devrez peut-être prendre connaissance de cette modification avant de quitter le domaine de votre ID ou de terminer votre unité de travail. Dans ce cas, vous pouvez forcer un rechargement en détachant votre instance de l'objet de données.

Pour ce faire, commencez par obtenir le EntityEntry<> de votre objet. C’est un objet qui vous permet de manipuler le cache interne de DbContext pour cet objet. Vous pouvez ensuite marquer cette entrée comme détachée en définissant EntitytState.Detached dans sa propriété State. Je pense que cela laisse l’entrée dans le cache, mais provoque le retrait de la DbContext et le remplace lorsque vous chargez l’entrée ultérieurement. Ce qui compte, c’est qu’un chargement ultérieur renvoie une instance d’entité fraîchement chargée à votre code. Par exemple:

var thing = context.Things.Find(id);
if (thing.ShouldBeSentToService) {
    TriggerExternalServiceAndWait(id);

    // Detach the object to remove it from context’s cache.
    context.Entities(thing).State = EntityState.Detached;

    // Then load it. We will get a new object with data
    // freshly loaded from the database.
    thing = context.Things.Find(id);
}
UseSomeOtherData(thing.DataWhichWasUpdated);
7
binki

Oh, cette question m'a noué pendant des jours. 

J'utilise Visual Studio 2017 avec .Net Core 2.1, et mon code EF Core ressemblait à ceci:

//  1.  Load a [User] record from our database 
int chosenUserID = 12345;
User usr = dbContext.Users.FirstOrDefault(s => s.UserID == chosenUserID);

//  2. Call a web service, which updates that [User] record
HttpClient client = new HttpClient()
await client.PostAsync("http://someUrl", someContent);

//  3. Attempt to load an updated copy of the [User] record
User updatedUser = dbContext.Users.FirstOrDefault(s => s.UserID == chosenUserID);

À l'étape 3, il suffirait de définir "updatedUser" sur original version de l'enregistrement [User], plutôt que d'essayer de charger une nouvelle copie. Ainsi, si, après l’étape 3, je modifié cet enregistrement [Utilisateur], les paramètres que le service Web lui avait appliqués seraient perdus.

J'ai finalement trouvé deux solutions.

Je pourrais changer les paramètres ChangeTracker. Cela a fonctionné, mais je m'inquiétais des effets secondaires de cette opération:

dbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;

Ou, je pourrais glisser la commande suivante avant de tenter de recharger l'enregistrement [Utilisateur] ...

await dbContext.Entry(usr).ReloadAsync();

Cela semble forcer .Net Core à recharger cet enregistrement [User], et la vie est redevenue belle.

J'espère que c'est utile ... 

Repérer et corriger ce bogue m'a pris des jours ...

Il existe également un excellent article décrivant les différentes manières de contourner ce problème de mise en cache ici .

3
Mike Gledhill

Vous devrez detach l'entité du contexte ou implémenter votre propre extension pour .Reload()

Voici l'implémentation .Reload(). Source: https://weblogs.asp.net/ricardoperes/implementing-missing-features-in-entity-framework-core

public static TEntity Reload<TEntity>(this DbContext context, TEntity entity) where TEntity : class
{
    return context.Entry(entity).Reload();
}

public static TEntity Reload<TEntity>(this EntityEntry<TEntity> entry) where TEntity : class
{
    if (entry.State == EntityState.Detached)
    {
        return entry.Entity;
    }

    var context = entry.Context;
    var entity = entry.Entity;
    var keyValues = context.GetEntityKey(entity);

    entry.State = EntityState.Detached;

    var newEntity = context.Set<TEntity>().Find(keyValues);
    var newEntry = context.Entry(newEntity);

    foreach (var prop in newEntry.Metadata.GetProperties())
    {
        prop.GetSetter().SetClrValue(entity, 
        prop.GetGetter().GetClrValue(newEntity));
    }

    newEntry.State = EntityState.Detached;
    entry.State = EntityState.Unchanged;

    return entry.Entity;
}

GetEntityKey():

public static object[] GetEntityKey<T>(this DbContext context, T entity) where T : class
{
    var state = context.Entry(entity);
    var metadata = state.Metadata;
    var key = metadata.FindPrimaryKey();
    var props = key.Properties.ToArray();

    return props.Select(x => x.GetGetter().GetClrValue(entity)).ToArray();
}
1
penleychan

Voici ma solution, j'espère que cela vous aidera. 

  • La fonction detach détachera toutes les entités imbriquées et le tableau d'entités.
  • FindByIdAsync aura un paramètre optionnel pour détacher l'entité et la rechargera.

Repository

    public void Detach(TEntity entity)
    {
        foreach (var entry in _ctx.Entry(entity).Navigations)
        {
            if (entry.CurrentValue is IEnumerable<IEntity> children)
            {
                foreach (var child in children)
                {
                    _ctx.Entry(child).State = EntityState.Detached;
                }
            }
            else if (entry.CurrentValue is IEntity child)
            {
                _ctx.Entry(child).State = EntityState.Detached;
            }
        }
        _ctx.Entry(entity).State = EntityState.Detached;
    }

Donc par exemple avec:

Des classes:

public interface IEntity<TPrimaryKey>
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    TPrimaryKey Id { get; set; }
}

public class Sample : IEntity
{
    public Guid Id { get; set; }
    public string Text { get; private set; }   
    public Guid? CreatedByUserId { get; set; }
    public virtual User CreatedByUser { get; set; }     
    public List<SampleItem> SampleItems { get; set; } = new List<SampleItem>();
}

public class SampleItem : IEntity
{
    public Guid Id { get; set; }
    public string Text { get; private set; }   
    public Guid? CreatedByUserId { get; set; }
    public virtual User CreatedByUser { get; set; }     
}

Manager

    public async Task<Sample> FindByIdAsync(Guid id, bool includeDeleted = false, bool forceRefresh = false)
    {
        var result = await GetAll()
            .Include(s => s.SampleItems)
            .IgnoreQueryFilters(includeDeleted)
            .FirstOrDefaultAsync(s => s.Id == id);

        if (forceRefresh)
        {
            _sampleRepository.Detach(result);
            return await FindByIdAsync(id, includeDeleted);
        }

        return result;
    }

Manette

    SampleManager.FindByIdAsync(id, forceRefresh: true);
0
Cedric Arnould