L'objet entité ne peut pas être référencé par plusieurs instances de IEntityChangeTracker. lors de l'ajout d'objets liés à l'entité dans Entity Framework 4.1

165

J'essaie de sauvegarder les détails de l'employé, qui ont des références avec City. Mais chaque fois que j'essaie d'enregistrer mon contact, qui est validé, j'obtiens l'exception "ADO.Net Entity Framework Un objet entité ne peut pas être référencé par plusieurs instances de IEntityChangeTracker"

J'avais lu tellement de messages mais je ne savais toujours pas exactement quoi faire ... mon code de clic sur le bouton Enregistrer est donné ci-dessous

protected void Button1_Click(object sender, EventArgs e)
    {
        EmployeeService es = new EmployeeService();
        CityService cs = new CityService();

        DateTime dt = new DateTime(2008, 12, 12);
        Payroll.Entities.Employee e1 = new Payroll.Entities.Employee();

        Payroll.Entities.City city1 = cs.SelectCity(Convert.ToInt64(cmbCity.SelectedItem.Value));

        e1.Name = "Archana";
        e1.Title = "aaaa";
        e1.BirthDate = dt;
        e1.Gender = "F";
        e1.HireDate = dt;
        e1.MaritalStatus = "M";
        e1.City = city1;        

        es.AddEmpoyee(e1,city1);
    }

et code de service aux employés

public string AddEmpoyee(Payroll.Entities.Employee e1, Payroll.Entities.City c1)
        {
            Payroll_DAO1 payrollDAO = new Payroll_DAO1();
            payrollDAO.AddToEmployee(e1);  //Here I am getting Error..
            payrollDAO.SaveChanges();
            return "SUCCESS";
        }
Sourire
la source

Réponses:

241

Parce que ces deux lignes ...

EmployeeService es = new EmployeeService();
CityService cs = new CityService();

... ne prenez pas de paramètre dans le constructeur, je suppose que vous créez un contexte dans les classes. Lorsque vous chargez le city1...

Payroll.Entities.City city1 = cs.SelectCity(...);

... vous attachez le city1au contexte dans CityService. Plus tard, vous ajoutez une city1référence au nouveau Employee e1et ajoutez e1 cette référence aucity1 contexte dans EmployeeService. En conséquence, vous avezcity1 attaché à deux contextes différents, ce dont l'exception se plaint.

Vous pouvez résoudre ce problème en créant un contexte en dehors des classes de service, en l'injectant et en l'utilisant dans les deux services:

EmployeeService es = new EmployeeService(context);
CityService cs = new CityService(context); // same context instance

Vos classes de service ressemblent un peu à des référentiels qui ne sont responsables que d'un seul type d'entité. Dans un tel cas, vous aurez toujours des problèmes dès que les relations entre les entités sont impliquées lorsque vous utilisez des contextes séparés pour les services.

Vous pouvez également créer un service unique qui est responsable d'un ensemble d'entités étroitement liées, comme un EmployeeCityService (qui a un seul contexte) et déléguer toute l'opération de votre Button1_Clickméthode à une méthode de ce service.

Slauma
la source
4
J'aime la façon dont vous l'avez compris même si la réponse n'incluait pas d'informations de base.
Daniel Kmak
On dirait que cela résoudra mon problème, je ne sais tout simplement pas comment écrire la nouvelle instance de contexte :(
Ortund
12
Abstraire ORM, c'est comme mettre du rouge à lèvres jaune sur un étron.
Ronnie Overby
Il me manque peut-être quelque chose ici, mais dans certains ORM (en particulier EntityFramework), le contexte des données doit toujours être de courte durée. L'introduction d'un contexte statique ou réutilisé introduira un tout autre ensemble de défis et de problèmes.
Maritim
@Maritim cela dépend de l'utilisation. Dans les applications Web, il s'agit généralement d'un aller-retour. Dans les applications de bureau, vous pouvez également en utiliser un par Form(quoi qu'il arrive , cela ne représente qu'une unité de travail) par Thread(car il DbContextn'est pas garanti d'être threadsafe).
LuckyLikey
30

Les étapes de reproduction peuvent être simplifiées à ceci:

var contextOne = new EntityContext();
var contextTwo = new EntityContext();

var user = contextOne.Users.FirstOrDefault();

var group = new Group();
group.User = user;

contextTwo.Groups.Add(group);
contextTwo.SaveChanges();

Code sans erreur:

var context = new EntityContext();

var user = context.Users.FirstOrDefault();

var group = new Group();
group.User = user; // Be careful when you set entity properties. 
// Be sure that all objects came from the same context

context.Groups.Add(group);
context.SaveChanges();

Un seul EntityContextpeut résoudre ce problème. Référez-vous à d'autres réponses pour d'autres solutions.

Pavel Shkleinik
la source
2
disons que vous vouliez utiliser contextTwo? (peut-être en raison de problèmes de portée ou autre) comment vous détachez-vous de contextOne et attachez-vous à contextTwo?
NullVoxPopuli
Si vous avez besoin de faire quelque chose comme ça, il est fort probable que vous le fassiez de la mauvaise manière ... Je suggère d'utiliser un contexte.
Pavel Shkleinik
3
Il existe des cas où vous souhaitez utiliser une instance différente, par exemple lorsque vous pointez vers une autre base de données.
Jay
1
C'est une simplification utile du problème; mais cela ne fournit pas de vraie réponse.
BrainSlugs83
9

C'est un ancien thread, mais une autre solution, que je préfère, consiste simplement à mettre à jour le cityId et à ne pas attribuer le modèle de trou City à Employee ... pour ce faire, l'employé devrait ressembler à:

public class Employee{
    ...
    public int? CityId; //The ? is for allow City nullable
    public virtual City City;
}

Ensuite, il suffit d'assigner:

e1.CityId=city1.ID;
user3484623
la source
5

Alternativement à l'injection et encore pire à Singleton, vous pouvez appeler Detach méthode avant Add.

EntityFramework 6: ((IObjectContextAdapter)cs).ObjectContext.Detach(city1);

EntityFramework 4: cs.Detach(city1);

Il existe encore un autre moyen, au cas où vous n'auriez pas besoin du premier objet DBContext. Il suffit de l' envelopper avec l' aide de mots - clés:

Payroll.Entities.City city1;
using (CityService cs = new CityService())
{
  city1 = cs.SelectCity(Convert.ToInt64(cmbCity.SelectedItem.Value));
}
O romain
la source
1
J'ai utilisé ce qui suit: dbContext1.Entry(backgroundReport).State = System.Data.Entity.EntityState.Detached'pour détacher et puis j'ai pu utiliser dbContext2.Entry(backgroundReport).State = System.Data.Entity.EntityState.Modified;pour mettre à jour. A travaillé comme un rêve
Peter Smith
Oui, Peter. Je devrais mentionner de marquer l'état comme modifié.
Roman O
Dans la logique de démarrage de mon application (global.asax), je chargeais une liste de widgets .. une simple liste d'objets de référence que je stockais en mémoire. Puisque je faisais mon contexte EF dans les instructions Using, je pensais qu'il n'y aurait plus de problème plus tard lorsque mon contrôleur se serait mis à assigner ces objets dans un graphique métier (hé, cet ancien contexte a disparu, non?) - Cette réponse m'a sauvé .
bkwdesign
4

J'ai eu le même problème mais mon problème avec la solution de @ Slauma (bien que géniale dans certains cas) est qu'elle recommande que je passe le contexte dans le service, ce qui implique que le contexte est disponible depuis mon contrôleur. Cela force également un couplage étroit entre mon contrôleur et les couches de service.

J'utilise Dependency Injection pour injecter les couches de service / référentiel dans le contrôleur et, en tant que tel, je n'ai pas accès au contexte du contrôleur.

Ma solution consistait à faire en sorte que les couches service / référentiel utilisent la même instance du contexte - Singleton.

Classe de singleton de contexte:

Référence: http://msdn.microsoft.com/en-us/library/ff650316.aspx
et http://csharpindepth.com/Articles/General/Singleton.aspx

public sealed class MyModelDbContextSingleton
{
  private static readonly MyModelDbContext instance = new MyModelDbContext();

  static MyModelDbContextSingleton() { }

  private MyModelDbContextSingleton() { }

  public static MyModelDbContext Instance
  {
    get
    {
      return instance;
    }
  }
}  

Classe de référentiel:

public class ProjectRepository : IProjectRepository
{
  MyModelDbContext context = MyModelDbContextSingleton.Instance;
  [...]

D'autres solutions existent, comme instancier le contexte une fois et le transmettre aux constructeurs de vos couches de service / référentiel ou à une autre que j'ai lu sur l'implémentation du modèle d'unité de travail. Je suis sûr qu'il y en a plus ...

kmullings
la source
9
... cela ne fonctionne-t-il pas dès que vous essayez d'utiliser le multithreading?
CaffGeek
8
Un contexte ne doit pas rester ouvert plus longtemps que nécessaire, utiliser un Singleton pour le garder ouvert pour toujours est la dernière chose que vous voulez faire.
enzi
3
J'ai vu de bonnes implémentations de ceci par demande. L'utilisation du mot-clé Static est incorrect, mais si vous créez ce modèle pour instancier le contexte au début de la requête et le supprimer à la fin de la requête, ce serait une solution légitime.
Aidin
1
C'est vraiment un mauvais conseil. Si vous utilisez DI (je ne vois pas les preuves ici?), Vous devriez laisser votre conteneur DI gérer la durée de vie du contexte et cela devrait probablement être par demande.
Casey
3
C'est mauvais. MAUVAIS. MAUVAIS. MAUVAIS. MAUVAIS. Surtout s'il s'agit d'une application Web, car les objets statiques sont partagés entre tous les threads et utilisateurs. Cela signifie que plusieurs utilisateurs simultanés de votre site Web piétineront votre contexte de données, le corrompant potentiellement, enregistrant les modifications que vous n'aviez pas prévues, ou même créant simplement des plantages aléatoires. Les DbContexts ne doivent JAMAIS être partagés entre les threads. Ensuite, il y a le problème que la statique n'est jamais détruite, donc elle restera assise et continuera à utiliser de plus en plus de mémoire ...
Erik Funkenbusch
3

Dans mon cas, j'utilisais ASP.NET Identity Framework. J'avais utilisé la UserManager.FindByNameAsyncméthode intégrée pour récupérer une ApplicationUserentité. J'ai ensuite essayé de référencer cette entité sur une entité nouvellement créée sur un autre DbContext. Cela a abouti à l'exception que vous avez vue à l'origine.

J'ai résolu ce problème en créant une nouvelle ApplicationUserentité avec uniquement Idla UserManagerméthode from et en référençant cette nouvelle entité.

Justin Skiles
la source
1

J'ai eu le même problème et j'ai pu résoudre la création d'une nouvelle instance de l'objet que j'essayais de mettre à jour. Ensuite, j'ai passé cet objet à mon dépôt.

karolanet333
la source
Pouvez-vous s'il vous plaît aider avec un exemple de code. ? donc ce que vous essayez de dire sera clair
BJ Patel
1

Dans ce cas, il s'avère que l'erreur est très claire: Entity Framework ne peut pas suivre une entité en utilisant plusieurs instances de IEntityChangeTrackerou généralement plusieurs instances de DbContext. Les solutions sont: utiliser une instance de DbContext; accéder à toutes les entités nécessaires via un référentiel unique (en fonction d'une instance de DbContext); ou désactivation du suivi pour toutes les entités accessibles via un référentiel autre que celui lançant cette exception particulière.

Lorsque vous suivez une inversion de modèle de contrôle dans l'API Web .Net Core, je constate fréquemment que j'ai des contrôleurs avec des dépendances telles que:

private readonly IMyEntityRepository myEntityRepo; // depends on MyDbContext
private readonly IFooRepository fooRepo; // depends on MyDbContext
private readonly IBarRepository barRepo; // depends on MyDbContext
public MyController(
    IMyEntityRepository myEntityRepo, 
    IFooRepository fooRepo, 
    IBarRepository barRepo)
{
    this.fooRepo = fooRepo;
    this.barRepo = barRepo;
    this.myEntityRepo = myEntityRepo;
}

et utilisation comme

...
myEntity.Foo = await this.fooRepository.GetFoos().SingleOrDefaultAsync(f => f.Id == model.FooId);
if (model.BarId.HasValue)
{
    myEntity.Foo.Bar = await this.barRepository.GetBars().SingleOrDefaultAsync(b => b.Id == model.BarId.Value);
}

...
await this.myEntityRepo.UpdateAsync(myEntity); // this throws an error!

Étant donné que les trois référentiels dépendent de différentes DbContextinstances par requête, j'ai deux options pour éviter le problème et maintenir des référentiels séparés: modifiez l'injection du DbContext pour créer une nouvelle instance une seule fois par appel:

// services.AddTransient<DbContext, MyDbContext>(); <- one instance per ctor. bad
services.AddScoped<DbContext, MyDbContext>(); // <- one instance per call. good!

ou, si l'entité enfant est utilisée en lecture seule, désactivez le suivi sur cette instance:

myEntity.Foo.Bar = await this.barRepo.GetBars().AsNoTracking().SingleOrDefault(b => b.Id == model.BarId);
Kjata30
la source
0

Utilisez le même objet DBContext tout au long de la transaction.

Nalan Madheswaran
la source
0

J'ai rencontré ce même problème après avoir implémenté IoC pour un projet (ASP.Net MVC EF6.2).

Habituellement, je initialiserais un contexte de données dans le constructeur d'un contrôleur et utiliserais le même contexte pour initialiser tous mes référentiels.

Cependant, l'utilisation d'IoC pour instancier les référentiels les a tous amenés à avoir des contextes séparés et j'ai commencé à recevoir cette erreur.

Pour l'instant, je suis revenu à la simple création de nouveaux référentiels avec un contexte commun tout en pensant à une meilleure façon.

Richard Moore
la source
0

C'est ainsi que j'ai rencontré ce problème. Je dois d'abord enregistrer mon Orderqui a besoin d'une référence à ma ApplicationUsertable:

  ApplicationUser user = new ApplicationUser();
  user = UserManager.FindById(User.Identity.GetUserId());

  Order entOrder = new Order();
  entOrder.ApplicationUser = user; //I need this user before saving to my database using EF

Le problème est que j'initialise un nouveau ApplicationDbContext pour enregistrer ma nouvelle Orderentité:

 ApplicationDbContext db = new ApplicationDbContext();
 db.Entry(entOrder).State = EntityState.Added;
 db.SaveChanges();

Donc, pour résoudre le problème, j'ai utilisé le même ApplicationDbContext au lieu d'utiliser le UserManager intégré d'ASP.NET MVC.

Au lieu de cela:

user = UserManager.FindById(User.Identity.GetUserId());

J'ai utilisé mon instance ApplicationDbContext existante:

//db instance here is the same instance as my db on my code above.
user = db.Users.Find(User.Identity.GetUserId()); 
Willy David Jr
la source
-2

Source d'erreur:

ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.Name);
ApplicationDbContext db = new ApplicationDbContent();
db.Users.Uploads.Add(new MyUpload{FileName="newfile.png"});
await db.SavechangesAsync();/ZZZZZZZ

J'espère que quelqu'un gagnera un temps précieux

Bourne Kolo
la source
Je ne suis pas sûr que cela réponde à la question. Peut-être qu'un contexte pourrait aider.
Stuart Siegler