Code-first Entity Framework - many-to-many relationships - c#

I have the following
class Human
int id
string gender
List<PhysicalAttribute> attributes
class PhysicalAttributes
int id
string description
string type
List<Human> humans
When I add the first human with attributes, the tables are created and the data is populated properly.
The problem is when I add the next humans with similar attributes. Let's say I have attributes
type:"body"
description:"slim"
for both the first and second human. When I create new and add the second human, another entry with the same type and description is added to the PhysicalAttributes table.
Is there some way of doing this so that the existing entry will be used?
Do I have to do a lookup on the PhysicalAttributes table first to see that the entry has been created?

Is there some way of doing this so that the existing entry will be used?
Yes. Make (type,description) the Entity Key for PhysicalAttributes. You seem to have introduced a meaningless ID key property that allows multiple PhysicalAttributes to have the same type and description.
It is also possible to not fix the model, and fetch the existing PhysicalAttributes from the database to discover if any of the ones you need already exist. But that's just a tedious workaround for having the wrong entity key structure.
If you are loading from JSON it's going to be inconventient to attach existing entities, and instead you can do the following.
Override SaveChanges and fetch all the PhyscialAttribute values from the database. Then for the PhysicalAttribute entities that already exist in the database, Detach them from the DbContext, and EF won't attempt to insert them.
After you call PhysicalAttributes.Load(), you will have two entries in the ChangeTracker for every PhysicalAttribute on a new Entity that already exists in the database.
EG
public override int SaveChanges()
{
PhysicalAttributes.Load();
var entryGroup = from entry in ChangeTracker.Entries<PhysicalAttribute>()
group entry by new { entry.Entity.Description, entry.Entity.Type } into byKey
where byKey.Any( e => e.State == EntityState.Unchanged)
select byKey;
foreach (var eg in entryGroup)
{
foreach (var e in eg )
{
if (e.State == EntityState.Added)
{
e.State = EntityState.Detached;
}
}
}
return base.SaveChanges();
}

Related

EFCore update: The instance of entity type '' cannot be tracked because another instance with the key value '{ID: 0}' is already being tracked

This relates to my previous question from a few days ago: EF Core duplicate keys: The instance of entity type '' cannot be tracked because another instance with the key value '' is already being tracked
I am using context.ChangeTracker.TrackGraph to give me more control when inserting data. The insert process works great as show by my own answer on the question above. This problem results when I update items and attempt to save them.
My primary model is as follows (type names have been changed for simplicity):
public class Model
{
public List<Data1> List1 { get; set; }
...
}
Data1 looks like this:
public class Data1
{
public string Name{ get; set; }
...
}
To update to the database, I use this:
using var context = Factory.CreateDbContext();
context.ChangeTracker.TrackGraph(model, node =>
{
if (!(node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)) && node.Entry.IsKeySet)) //this is a workaround but it solves the pk problems I have been having
{
node.Entry.State = EntityState.Modified;
}
else if (!node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Added;
}
});
return await context.SaveChangesAsync();
This works great as it prevents my existing entities (inheriting fromEFChangeIgnore) from being inserted and otherwise handles updates and inserts of other entities well. However, if I have more than one Data item in List1, I get the error:
The instance of entity type 'Data1' cannot be tracked because another instance with the key value '{ID: 0}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
I understand this error but I am confused as to why it is appearing in this scenario. Yes, both entities have an ID of zero, but only because they have yet to be inserted to the database. What am I doing wrong here?
It turns out my bug was due to a misplaced parenthesis. Go figure.
In my SaveChanges I had accidently negated the entire conditional at the top, such that entities that should have been Modified became Added, and vice versa. This led to multiple entities with an ID of 0 that I intended to add being marked as Modified. EF then complained that two existing entities had the same ID.
That is, this line:
if (!(node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)) && node.Entry.IsKeySet))
now becomes (conditionals swapped for clarity):
if (node.Entry.IsKeySet && (!node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore))))
And my entire SaveChanges method now is:
using var context = Factory.CreateDbContext();
context.ChangeTracker.TrackGraph(model, node => //all entities inheriting from EFChangeIgnore will not be touched by the change tracker. Other entities with keys will be modified, and items without keys will be added
{
if (node.Entry.IsKeySet && (!node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)))) //this is a workaround but it solves key duplication problems
{
node.Entry.State = EntityState.Modified;
}
else if (!node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Added;
}
return await context.SaveChangesAsync();

Entity Framework duplicate entries in many to many relationship

I have already read many posts about the entity framework problem with many to many and its beeing a pain in the neck again.
Colaborador has an ICollection of Time and
Time has an Icollection of Colaborador
Some people say that it´s necessary to attach the child entity before Add in the context(didn´t work for me, Pk error).
I am using simple injector and my context is per request.
My associative table is mapped like this:
HasMany<Time>(c => c.Times)
.WithMany(t => t.Colaboradores)
.Map(ct =>
{
ct.MapLeftKey("ColaboradorId");
ct.MapRightKey("TimeId");
ct.ToTable("Receptor");
});
It creates the associative table in the database.
When i try to insert a Colaborador(entity), i add in its list some Times(Teams), add to DbContext and then SaveChanges().
When i do this, it creates a new Colaborador, insert correctly in the associative table(the ids) but also duplicate the Time.
var colaborador = Mapper.Map<ColaboradorViewModel, Colaborador>(colaboradorVm);
List<TimeViewModel> timesVm = new List<TimeViewModel>();
colaboradorVm.TimesSelecionados.ForEach(t => timesVm.Add(_serviceTime.BuscarPorId(t)));
colaborador.Times = Mapper.Map<ICollection<TimeViewModel>, ICollection<Time>>(timesVm);
The function BuscarPorId does the Find method and returns a Time.
I have figured out that if i call the Add command, the entity will mark the child´s state as Added as well, but if i attempt to attach the Time or change it´s state to Unchanged, i get a primary key error...
foreach (var item in colaborador.Times)
{
lpcContext.Set<Time>().Attach(item);
//lpcContext.Entry(item).State = EntityState.Unchanged;
}
Is there any way of tell to entity framework to not insert a specific child? So only the main and associative table are populated?
Mapper.Map creates new Time objects which are not attached to your context, so you must attach them as Unmodified, but attaching them causes another error due to duplicate PK because your context is already tracking the original copies of the Time entities. Using the Find method will retrieve these tracked and locally cached entities.
Find and use the entity already attached to your context:
Instead of:
colaborador.Times = Mapper.Map<ICollection<TimeViewModel>, ICollection<Time>>(timesVm);
Use:
var times = new List<Time>();
var dbSet = lpcContext.Set<Time>();
foreach( var t in timesVm )
{
var time = dbSet.Find( t.Id );
if( null == time )
{
time = Mapper.Map<TimeViewModel, Time>( t );
}
times.Add( time );
}
collaborador.Times = times;

Inserting different instances of same Disconnected Entities into Entity Framework

In my MVC4 project, I am trying to insert some data, that I populated from an excel worksheet, into database (Oracle 10g DB using 12c ODAC) by using EF5.
Story:
First I read data from excel and then convert them to entities. However excel data has no Primary Key (ID) column (I DON'T HAVE CONTROL OVER EXCEL DATA). Therefore for every row in excel first I am searching DB for a match. If there is a match I am getting the matched entity by using the DbContext. In this case my instantiated class/entity has ID property filled.
On the other hand, if there is no match in the DB I am instantiating a new class of entity type without an ID information. If entity exists in more than one row of excel worksheet then my final List would have more than one instance of the same entity each have ID = 0.
After populating the data I add my entities to my DbContext one by one and call SaveChanges() method of the context. Then when I check the Database I see that different instances of the same class getting different IDs, but this is not what I want.
I understand why this is happening since DbContext cannot relate different instances of a class if the PK is not set. All entities's EntityState becomes Added :(
My question is:
"Is there a way to tell Entity Framework that if "all the properties of an entity are equal to each other && the PK/ID is not set" treat these entities as same entities when inserting into database?".
Simplified:
After executing MyMethod() in below code, I want only one row of data created in my database Table:
// I implemented IEquatable to see if it works for EF (I heard it works in NHibernate) but unfortunately not working.
public class MyEntity(): IEquatable<MyEntity>
{
// This is an autoincrementing PK in DB
public int ID { get; set; }
public string Name { get; set; }
public bool Equals(MyEntity other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
return Equals(this.ID, other.ID) && Equals(this.Name, other.Name);
}
public override bool Equals(object obj)
{
return Equals(obj as MyEntity);
}
public override int GetHashCode()
{
unchecked
{
var result = this.ID.GetHashCode();
result = (result * 397) ^ (this.Name!= null ? this.Name.GetHashCode() : 0);
return result;
}
}
}
public void MyMethod()
{
DbContext db = new DbContext();
db.MyEntity.Add(new MyEntity { Name = "Foo" }) // ID=0 initially.
db.MyEntity.Add(new MyEntity { Name = "Foo" }) // ID=0 initially.
// When I check, the Entities in ChangeTracker are Equal!
// This is equal to 1.
int distinctNewEntryCount = db.ChangeTracker.Entries<MyEntity>().Select(e => e.Entity).Distinct().Count();
// This is equal to 2.
// But I want it to be 1 (1 Added, 1 Unchanged or only 1 Added).
int newEntryCount = db.ChangeTracker.Entries<MyEntity>().Count(e => e.State == System.Data.EntityState.Added);
db.SaveChanges();
}
By the way I am using an Oracle DB and in my EDMX file I set "StoreGeneratedPattern" attribute of autoincrement PK as "Identity" and assigning ID values to inserted entities by using before insert triggers which calls sequence.nextval() in them.
DbSet.Add returns the entity that was added to the DbSet, maybe it's returning a different instance than the one passed in. Also, what does SaveChanges return? It's supposed to return the # of objects added, updated and deleted. Does it match the # of objects you're trying to insert or update?
"If entity exists in more than one row of excel worksheet then my final List would have more than one instance of the same entity each have ID = 0."
That is also probably a problem. You're saying duplicate/shared rows will get their own unique entity instance with an ID of 0. They should be using the same instance.

Entity Framework - retrieve ID before 'SaveChanges' inside a transaction

In Entity Framework - Is there any way to retrieve a newly created ID (identity) inside a transaction before calling 'SaveChanges'?
I need the ID for a second insert, however it is always returned as 0...
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
objectContext.Connection.Open();
using (var transaction = objectContext.Connection.BeginTransaction())
{
foreach (tblTest entity in saveItems)
{
this.context.Entry(entity).State = System.Data.EntityState.Added;
this.context.Set<tblTest>().Add(entity);
int testId = entity.TestID;
.... Add another item using testId
}
try
{
context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
objectContext.Connection.Close();
throw ex;
}
}
objectContext.Connection.Close();
The ID is generated by the database after the row is inserted to the table. You can't ask the database what that value is going to be before the row is inserted.
You have two ways around this - the easiest would be to call SaveChanges. Since you are inside a transaction, you can roll back in case there's a problem after you get the ID.
The second way would be not to use the database's built in IDENTITY fields, but rather implement them yourself. This can be very useful when you have a lot of bulk insert operations, but it comes with a price - it's not trivial to implement.
EDIT: SQL Server 2012 has a built-in SEQUENCE type that can be used instead of an IDENTITY column, no need to implement it yourself.
As others have already pointed out, you have no access to the increment value generated by the database before saveChanges() was called – however, if you are only interested in the id as a means to make a connection to another entity (e.g. in the same transaction) then you can also rely on temporary ids assigned by EF Core:
Depending on the database provider being used, values may be generated client side by EF or in the database. If the value is generated by the database, then EF may assign a temporary value when you add the entity to the context. This temporary value will then be replaced by the database generated value during SaveChanges().
Here is an example to demonstrate how this works. Say MyEntity is referenced by MyOtherEntity via property MyEntityId which needs to be assigned before saveChanges is called.
var x = new MyEntity(); // x.Id = 0
dbContext.Add(x); // x.Id = -2147482624 <-- EF Core generated id
var y = new MyOtherEntity(); // y.Id = 0
dbContext.Add(y); // y.Id = -2147482623 <-- EF Core generated id
y.MyEntityId = x.Id; // y.MyEntityId = -2147482624
dbContext.SaveChangesAsync();
Debug.WriteLine(x.Id); // 1261 <- EF Core replaced temp id with "real" id
Debug.WriteLine(y.MyEntityId); // 1261 <- reference also adjusted by EF Core
The above also works when assigning references via navigational properties, i.e. y.MyEntity = x instead of y.MyEntityId = x.Id
If your tblTest entity is connected to other entities that you want to attach, you don't need to have the Id to create the relation. Lets say tblTest is attached to anotherTest object, it the way that in anotherTest object you have tblTest object and tblTestId properties, in that case you can have this code:
using (var transaction = objectContext.Connection.BeginTransaction())
{
foreach (tblTest entity in saveItems)
{
this.context.Entry(entity).State = System.Data.EntityState.Added;
this.context.Set<tblTest>().Add(entity);
anotherTest.tblTest = entity;
....
}
}
After submitting the relation would be created and you don't need to be worry about Ids and etc.
You can retreive an ID before calling .SaveChanges() by using the Hi/Lo alhorithm. The id will be assigned to the object once it is added to dbcontext.
Example configuration with fluent api:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Entity>(e =>
{
e.Property(x => x.Id).UseHiLo();
});
}
An excerpt from the relevant Microsoft article:
The Hi/Lo algorithm is useful when you need unique keys before committing changes. As a summary, the Hi-Lo algorithm assigns unique identifiers to table rows while not depending on storing the row in the database immediately. This lets you start using the identifiers right away, as happens with regular sequential database IDs.
#zmbq is right, you can only get the id after calling save changes.
My suggestion is that you should NOT rely on the generated ID's of the database.
The database should only a detail of your application, not an integral and unchangeable part.
If you can't get around that issue use a GUID as an identifier due it's uniqueness.
MSSQL supports GUID as a native column type and it's fast (though not faster than INT.).
Cheers
A simple work around for this would be
var ParentRecord = new ParentTable () {
SomeProperty = "Some Value",
AnotherProperty = "Another Property Value"
};
ParentRecord.ChildTable.Add(new ChildTable () {
ChildTableProperty = "Some Value",
ChildTableAnotherProperty = "Some Another Value"
});
db.ParentTable.Add(ParentRecord);
db.SaveChanges();
Where ParentTable and ChildTable are two tables connected with Foregin key.
You can look up the value in the ChangeTracker like this:
var newEntity = new MyEntity();
var newEntity.Property = 123;
context.Add(newEntity);
//specify a logic to identity the right entity here:
var entity = context.ChangeTracker.Entries()
.FirstOrDefault(e => e.Entity is MyEntity myEntity &&
myEntity.Property == newEntity.Property);
//In this case we look up the value for an autogenerated id of type int/long
//it will be a negative value like -21445363467
var value = entity.Properties?
.FirstOrDefault(pe => pe.Metadata.GetColumnName() == nameof(MyEntity.Id))?.CurrentValue;
//if you set it on another entity, it will be replaced on SaveChanges()
My setup was mysql 5.7, but should work in other environments also.

EF Many To Many Exisiting object

I have the following database tables:
Document
Country (never added to)
DocumentLanguage
DocumentLanguage is a link table between Country and Document only containing the ID's. Entity designer (correctly) adds this a a navigation property.
My code is as follows:
public void IdentifyLanguages(List<Country> languages)
{
foreach (Country c in languages)
{
foreach (var languageIdentifier in c.LanguageIdentifiers)
{
if (document.DocumentText.ToLower().Contains(languageIdentifier.LanguageIdentifier1.ToLower()))
{
document.Countries.Add(c);
break;
}
}
}
}
and later save the object:
context.Document.AddObject(_document);
context.SaveChanges();
And i get the following error:
The EntityKey property can only be set when the current value of the property is null.
There is probably a very obvious solutution, i have tried using attach, using stub objects, but there must be a simple solution as am sure i am not the first person who has had this requirment!
When you save the object using AddObject, does your document already exist in your database? AddObject is only for adding new objects, not updating existing ones

Categories