Entity Framework recursive Relations and Attaching errors - c#

I am creating an Application to manage Projects, Employees and assign Employees to Projects using WPF and EF 6.2.0.
So I've got an Employee entity with different properties (some of which are references to other entities), and I got a Project entity with different propeties (again, some of which are other entities).
This is my database scheme:
Now I want to create a Job entity which is assigned to an Employee and a Project.
So to add one, I have to attach the whole Employee and Project entities (including all their referenced entities) to my context before I can add the Job right?
If I don't do that or forget one of the entity's relations/properties, it's trying to create that whole entity on the database, and throws some error like the primary key for that employee already exists (obviously, because it shouldn't create it but rather attach the existing local one)
On the other side if I do try to attach it, it fails upon attaching the Project because (now comes the crazy part) the Project.CostCentre.Location.CostCentres[0].Projects[0].Contact (Employee) is already attached. It is trying to attach every single thing recursively including all Lists?!
That's the stack trace that causes this:
private void Attach(Job job, bool attachEntity = true)
{
Attach(job.Employee);
Attach(job.Project); // Calls next func
if (attachEntity && !Attached(job))
Context.Jobs.Attach(job);
}
private void Attach(Project project, bool attachEntity = true)
{
Attach(project.Contact);
Attach(project.CostCentre); // Calls next func
Attach(project.CostType);
Attach(project.Status);
if (attachEntity && !Attached(project))
Context.Projects.Attach(project);
}
private void Attach(CostCentre costCentre, bool attachEntity = true)
{
Attach(costCentre.Location); // Calls next func
if (attachEntity && !Attached(costCentre))
Context.CostCentres.Attach(costCentre);
}
private void Attach(Location location, bool attachEntity = true)
{
if (attachEntity && !Attached(location))
Context.Locations.Attach(location); // The error (that code tries to attach location.CostCentres[0].Projects[0].Contact aswell, which is an already attached Employee!!)
}
The full error message:
System.InvalidOperationException: 'Attaching an entity of type 'ForecastLibrary.Employee' failed because another entity of the same type already has the same primary key value.
This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values.
This may be because some entities are new and have not yet received database-generated key values.
In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.'
The full source code is here: https://pastebin.com/8YU0xQUC
I am already trying to solve this obscure error for some weeks, I hope someone here can help me out successfully adding this Job.
EDIT: Here's the code calling all of this:
// DataService::Add Function
public void Add(Job job)
{
Attach(job, false);
Context.Jobs.Add(job);
}
// UI Add job snippet
using (var service = new DataService(Session.ConnectionString))
{
service.Add(JobToAdd);
await service.SaveAsync();
}

I'd recommend using Eager loading if you know exactly what you need and have bigger data models.
Call Context.Configuration.LazyLoadingEnabled = false; in the constructor of you DataService and use .Include wherever you need a navigation property.
Then you need to use Find instead of Attach.
So your Job Add function could look this:
public void Add(Job job)
{
var employee = Context.Employees.Find(job.Employee.Id);
var project = Context.Projects.Find(job.Project.Id);
Context.Jobs.Add(new Job { Employee = employee, Project = project, Time = job.Time });
}

Related

C# - NHibernate: evicting an entity and then getting it

I'm trying to understand how NHibernate works. To do so I've created a small test, given below. But the test is failing on the marked line and I don't understand why.
What am I misunderstanding?
To briefly explain the code chunk... I create an entity in the DB. Then I call Evict to remove the entity from session cache so that the next call for it would force a DB read. Then I do a DB read, but instead of getting back an entity instance read from DB, I get NULL, on the marked line.
using NHibernate;
using MyCorp.MyProject.Resources.MyEntity;
using MyCorp.MyProjectTests.Common.Fixture;
using Xunit;
namespace MyCorp.MyProjectTests.Common.DB
{
[Collection("Component")]
public class NHibernateTest
{
private readonly ISessionFactory dbSessionFactory;
public NHibernateTest(ComponentFixture componentFixture)
{
this.dbSessionFactory = componentFixture.DatabaseFixture.DBSessionFactory;
}
[Fact]
[Trait("Category", "Component")]
public void TestSessionCache()
{
const string QUERY = #"DELETE MyEntityModel mg WHERE mg.Id = :id";
const string TITLE = "NHibernate session test object";
using (ISession dbSession = this.dbSessionFactory.OpenSession())
{
// Create new entity and then remove it from session cache.
long id = (long) dbSession.Save(new MyEntityModel
{
Title = TITLE
});
dbSession.Evict(dbSession.Get<MyEntityModel>(id));
// Entity loaded from DB and stored into session cache.
Assert.Equal(TITLE, dbSession.Get<MyEntityModel>(id).Title); // ===== FAILS HERE =====
// Delete entity from DB, but don't evict from session cache yet.
dbSession.CreateQuery(QUERY).SetParameter("id", id).ExecuteUpdate();
// Entity still reachable through session cache.
Assert.Equal(TITLE, dbSession.Get<MyEntityModel>(id).Title);
// Evict deleted entity from session cache.
dbSession.Evict(dbSession.Get<MyEntityModel>(id));
// Entity not available in neither DB nor session cache.
Assert.Null(dbSession.Get<MyEntityModel>(id));
}
}
}
}
Save() is not equal to SQL INSERT.
Save() means: make the session aware of this object and have the session send it to the database at a suitable time. Depending on mappings and configuration, this can be before Save() returns, or not.
So you evict the object from the session before it gets persisted.
If you omit the call to Evict(), your test works because none of the other code actually depends on the item being in the database (the DELETE statement may indicate it found 0 rows to delete, but this is not a problem for the test).
To use automatic flush behaviour, you should always be inside a transaction, not just a session. In fact, for best reliability, you really should always be inside a transaction whenever you are inside a session (other patterns are possible, but tend to be more complicated to get right).
Here is the documentation on when flushing of changes to the database happens:
http://nhibernate.info/doc/nhibernate-reference/manipulatingdata.html#manipulatingdata-flushing

How to obtain other Entities in C# WebCore Entity Framework

I have a C# WebCore REST service that talks to a front end and uses Entity Framework to CRUD into a database. This uses Dependency Injection to add the contexts upon startup:
services.AddDbContext<FileCacheContext>(options => options.UseSqlServer(Settings.ConnectionSetting));
services.AddDbContext<FileImportContext>(options => options.UseSqlServer(Settings.ConnectionSetting));
The functionality that I have allows a user to upload a file, which is manipulated by the server and some properties of that file are returned to the front end. The uploaded file is cached in the database (FileCacheContext).
After some time has passed, the user now wishes to confirm their action and "promote" the file from the Cache (FileCacheContext) to the Import (FileImportContext); this is done by an action in the front end that contains the id of the cached file.
This parameter is passed to a different REST Controller, which is being invoked using the FileImport context, rather than the FileCache context:
public PromoteController(FileImportContext context)
{
_context = context;
}
[HttpPost]
public void Post([FromBody] int fileCacheId)
{
...
What I now need to do is to "move" this cached file from one "context" to another, something along the lines of:
var cachedFile = _context.FileCache.Where(f => f.FileCacheId == cachedFileId).FirstOrSingle();
var importedFile = new FileImport() { FileData = cachedFile.FileData };
_context.FileImport.Add(importedFile);
_context.SaveChanges();
My issue is that I can only see the one context, so I cannot get hold of the other context to either read from or write into.
The two underlying tables have no relationship, so I cannot link them in any way to load one based upon the other.
How can I get access to another (unrelated) context in EF?
So to answer my own question, I found that I could of course include the additional contexts required when instantiating the controller:
public PromoteController(FileImportContext contextImport, FileCacheContext contextCache)
{
_contextImport = contextImport;
_contextCache = contextCache;
}
And then use these as required.
This doesn't feel like the right way to do it, but if it works, it works...

Create: An entity object cannot be referenced by multiple instances of IEntityChangeTracker

I have created a page asp.net MVC for inserting records to database where i am giving a preview button to see how the data will looks on client side before saving it. I am using session to pass model to the preview page. On preview page i have created a button which will save the model in session to database but it is throwing exception "An entity object cannot be referenced by multiple instances of IEntityChangeTracker".
I am using the same dbContext. I had tried many solutions given by users but they are not working for me. I have attached the part of the code that's throwing exception. Please see where I am doing wrong.
Here is the code where I am saving record
var model = Session[Constants.SessionVariables.ProjectModelForPreview] as Project;
if (create != null)
{
if (model.Id == 0)
{
if (model.IsFeatured)
{
foreach (var item in dbContext.Projects.Where(p => p.IsFeatured == true))
{
item.IsFeatured = false;
}
dbContext.SaveChanges();
}
dbContext.Entry(model).State = EntityState.Unchanged;
dbContext.SaveChanges();
TempData["SuccessMessage"] = "Project created successfully.";
return RedirectToAction("Index");
}
}
Your controller, and therefore your DbContext, is instantiated per request. So your application follows this flow:
Request 1 instantiates DbContext 1.
You load an entity through DbContext 1. This entity is being tracked, and the entity itself holds a reference to DbContext 1.
You store this entity in the session, keeping the old DbContext alive. This works, because the default session state works InProc, and not by serialization.
Request 2 comes in, DbContext 2 gets instantiated.
The entity is retrieved from the session.
You try to save the entity, still being tracked by DbContext 1, through DbContext 2. This throws.
Now for the solution there are various approaches:
Don't save entities in the session at all. Persist them, and look them up again in successive requests.
Save entities in the session by manually serializing them.
Load the entity with .AsNoTracking().
Let's ignore the original problem for now,it will be solved once you refactor the code
1)if for some reason you are using the same context across request ,stop.
(I don't think you do though).
2)Don't save tracked entities in the Session*
Search on google to see how EF tracks changes.
3)Read 1 and 2 again
*Use .AsNoTracking() or project your entity in a new model and save that in the session

Entity framework object not returning over WCF

I am using Entity Framework code first to create an object graph of linked objects and manipulate them in a web service, which is all hooked up fine. The problem I have is when I try and return the parent object across WCF. It is a User object with a number of child related entities.
The odd thing is if I deep clone the user object (my own implementation which creates a new User object with identical properties at all levels) and return the CLONED object, it works fine. It only doesnt work when I try to return the Object Entity returned by the EF.
The code which returns the object is
var user = _userRepository.GetUser(userId);
return user;
The IUserDataRepository is created via the Unity DI factory by a bootstrapped config that attaches to the factory in the Service Startup. The Context is also injected as a constructor dependency to the repository, wrapped in an IUnitOfWork object I have created. In the repository you have
public class UserDataRepository : IUserDataRepository
{
public UserDataRepository(IUnitOfWork work)
{
this.Work = work;
}
private IUnitOfWork Work { get; set; }
public DbUser GetUser(int id)
{
return UnitOfWork.Context.Users.FirstOrDefault(u => u.UserId == id);
}
}
When I try and return the loaded DbUser object, I get a 400 error, System.ServiceModel.ProtocolException (Bad Request)
However if I clone the object to a new DbUser object I create myself, that returns fine.
The only thing I can think of is EF still has some hooks into the DbUser object and won't let you pass that object back, whereas an identical object created manually is fine as it doesn't have the EF hooks in.
I have tried turning lazy loading off at a context level for all entities but still no joy.
Any thoughts?

Entity Framework: An entity object cannot be referenced by multiple instances of IEntityChangeTracker

I have a User table, with a many-to-many relationship to an Alerts table. After creating a Membership user, I am adding some extra info into the database.
MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);
if (createStatus == MembershipCreateStatus.Success) {
User user = new MidTier.Models.User();
user.FullName = model.FullName;
if (Alerts.Count() > 0)
{
var userAlerts = SetAlert(Alerts); // creates an IEnumerable of Alerts (from a list of int )
foreach (var alert in userAlerts)
{
user.Alerts.Add(alert); //add each alert to the user
}
}
userRepository.Add(user); //throwing error
userRepository.Save();
}
I get an error (' An entity object cannot be referenced by multiple instances of IEntityChangeTracker.') on calling the Add method. there are lots of references about this error on the net even here on SO, but after reading all those comments and suggestions, I a havent found a solution or the reason I am getting this error.
there are lots of references about
this error on the net even here on SO,
but after reading all those comments
and suggestions, I a havent found a
solution or the reason I am getting
this error.
If you really searched you should already know that error says you that some entity in object graph is already attached to other context. Because of that your code sample is mostly not related. The real important code is wrapped in your methods - probably SetAlerts and userRepository.Add. If these two methods use internally context and they don't use the same instance it is the reason for your exception.

Categories