How to not assign an id when id is a fk with a custom id generator in ef - entity-framework

I have a project where I'm using EF5, I made a custom Guid Generator and I have an override of the SaveChanges method to assign the ids of my entities.
Everything is working fine except in one case: when the ID of one entity is a FK to another ID of another entity.
A little bit of code to explain the problem:
I have two entities I cannot change:
public class FixedEntityA
{
public Guid Id { get; set;}
public string SomeText { get; set; }
}
public class FixedEntityB
{
public Guid Id { get; set;}
public int OneInt { get; set; }
}
In my project I have an entity defined like this:
public class ComposedEntity
{
public Guid Id { get; set;}
public FixedEntityA FixedA { get; set; }
public FixedEntityB FixedB { get; set; }
public double OneDouble { get; set; }
}
The relationships are:
ComposedEntity may have 0 or 1 FixedEntityA
ComposedEntity may have 0 or 1 FixedEntityB
The constraints on the id are:
The Id of FixedEntityA is a FK pointing to the Id of ComposedEntity
The Id of FixedEntityB is a FK pointing to the Id of ComposedEntity
The mapping class are:
public ComposedEntity(): EntityTypeConfiguration<ComposedEntity>
{
HasOptional(fea => fea.FixedA).WithRequired();
HasOptional(feb => feb.FixedB).WithRequired();
}
Here is my SaveChanges override:
foreach (var entry in ChangeTracker.Entries<IEntity>().Where(e => e.State == EntityState.Added))
{
Type t = entry.Entity.GetType();
List<DatabaseGeneratedAttribute> info = t.GetProperty("Id")
.GetCustomAttributes(typeof (DatabaseGeneratedAttribute), true)
.Cast<DatabaseGeneratedAttribute>().ToList();
if (!info.Any() || info.Single().DatabaseGeneratedOption != DatabaseGeneratedOption.Identity)
{
if (entry.Entity.Id == Guid.Empty)
entry.Entity.Id = (Guid) _idGenerator.Generate();
}
}
return base.SaveChanges();
This code works fine everywhere for all kind of relationships except in this case, I am missing a test to make sure I'am not setting an id on id that are foreign keys, and I have no clue on how to check if an Id is a FK...
Here is a sample object where this code fails:
var fea = new FixedEntityA();
var feb = new FixedEntityB();
var composedEntity = new ComposedEntity();
composedEntity.FixedA = fea;
composedEntity.FixedB = feb;
If you insert the whole graph, all three objects are marked as Added and all Ids are default.
The problem is, with the current SaveChanges method, I will go through all object with the Added state in the change tracker and I will assign an Id to all entity with a default Guid and break my FK constraints.
Thanks in advance guys!

Here is some code that will get the FK properties for a given type (it's horrible I know). Should be simple enough to plug this into your code.
var typeName = "Category";
var fkProperties = ((IObjectContextAdapter)db)
.ObjectContext
.MetadataWorkspace
.GetItems<AssociationType>(DataSpace.CSpace)
.Where(a => a.IsForeignKey)
.Select(a => a.ReferentialConstraints.Single())
.Where(c => c.FromRole.GetEntityType().Name == typeName)
.SelectMany(c => c.FromProperties)
.Select(p => p.Name);

Related

What is the proper way of updating navigation properties in EF Core?

In my EF Core solution I have the following model:
public class Deal
{
public string Id { get; set; }
public ResponsiblePerson ResponsiblePerson1 { get; set; }
public ResponsiblePerson ResponsiblePerson2 { get; set; }
public ResponsiblePerson ResponsiblePerson3 { get; set; }
}
public class ResponsiblePerson
{
public string Id { get; set; }
public string Name { get; set; }
}
When I am trying to update Deal navigations properties:
private void UpdateResponsiblePersons(string dealId, string person1Id, string person2Id, string person3Id)
{
var existingdeal = _dbContext.Deals
.Include(d => d.ResponsiblePerson1)
.Include(d => d.ResponsiblePerson2)
.Include(d => d.ResponsiblePerson3)
.Single(d => d.Id == dealId);
existingDeal.ResponsiblePerson1 = new ResponsiblePerson { Id = person1Id };
existingDeal.ResponsiblePerson2 = new ResponsiblePerson { Id = person2Id };
existingDeal.ResponsiblePerson3 = new ResponsiblePerson { Id = person3Id };
_dbContext.Entry(deal.ResponsiblePerson1).State = EntityState.Unchanged;
_dbContext.Entry(deal.ResponsiblePerson3).State = EntityState.Unchanged;
_dbContext.Entry(deal.ResponsiblePerson3).State = EntityState.Unchanged;
_dbContext.SaveChanges();
}
EF often fails with
System.InvalidOperationException: The instance of entity type 'ResponsiblePerson' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
That is because sometimes existingdeal already contains the link to ResponsiblePerson with one of provided IDs in either ResponsiblePerson1 of ResponsiblePerson2 or ResponsiblePerson3 Navigation properties.
I know that one of possible solutions will be first to get ResponsiblePersons used for update from dbContext like
existingDeal.ResponsiblePerson1 = _dbContext.ResponsiblePersons.Find(person1Id)
But that means extra DB roundtrips.
Another solution is to expose foreign keys instead of navigation properties but it would make Deal model quite ugly.
Please advice me what is the best way of updating such references?

How to deep clone/copy in EF Core

What I would like to do is duplicate/copy my School object and all of its children/associations in EF Core
I have something like the following:
var item = await _db.School
.AsNoTracking()
.Include(x => x.Students)
.Include(x => x.Teachers)
.Include(x => x.StudentClasses)
.ThenInclude(x => x.Class)
.FirstOrDefaultAsync(x => x.Id == schoolId);
I have been reading up on deep cloning and it seems that I should be able to do just add the entity...so pretty much the next line.
await _db.AddAsync(item);
Then EF should be smart enough to add that entity as a NEW entity. However, right off the bat I get a conflict that says "the id {schoolId} already exists" and will not insert. Even if I reset the Id of the new item I am trying to add, I still get conflicts with the Ids of the associations/children of the school iteam.
Is anyone familiar with this and what I might be doing wrong?
I had the same problem too, but in my case EF core was throwing exception "the id already exists".
Following the answer of #Irikos so I have created method which clones my objects.
Here's example
public class Parent
{
public int Id { get; set; }
public string SomeProperty { get; set; }
public virtual List<Child> Templates { get; set; }
public Parent Clone()
{
var output = new Parent() { SomeProperty = SomeProperty };
CloneTemplates(output);
return output;
}
private void CloneTemplates(Parent parentTo, Child oldTemplate = null, Child newTemplate = null)
{
//find old related Child elements
var templates = Templates.Where(c => c.Template == oldTemplate);
foreach (var template in templates)
{
var newEntity = new Child()
{
SomeChildProperty = template.SomeChildProperty,
Template = newTemplate,
Parent = parentTo
};
//find recursivly all related Child elements
CloneTemplates(parentTo, template, newEntity);
parentTo.Templates.Add(newEntity);
}
}
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
public int? TemplateId { get; set; }
public virtual Child Template { get; set; }
public string SomeChildProperty { get; set; }
}
Then I just call DbContext.Parents.Add(newEntity) and DbContext.SaveChanges()
That worked for me. Maybe this will be useful for someone.
I had the same problem, but in my case, ef core was smart enough save them as new entities even with existing id. However, before realising that, I just made a copy constructor for all the items, created a local task variable containing only the desired properties and returned the copy.
Remove certain properties from object upon query EF Core 2.1

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

"A dependent property in a ReferentialConstraint is mapped to a store-generated column" with Id change

Situation
I have searched for the answer to this extensively (on SO and elsewhere) and I am aware that there are many questions on SO by this same title.
I had a table mapping and model that were working. Then the schema was changed (I do not have direct control of the DB) such that a new Primary Key was introduced and the old Primary Key became the Foreign Key to another table. I believe this is the heart of the problem as no other entities seem to have issues
Mapping
Here is the method that maps my entity (called from OnModelCreating)
private static void MapThing(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Thing>().ToTable("ThingTable");
modelBuilder.Entity<Thing>().HasKey(p => p.Id);
modelBuilder.Entity<Thing>().Property(p => p.Id).HasColumnName("NewId");
modelBuilder.Entity<Thing>().Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Thing>().Property(p => p.FileName).HasColumnName("ColumnWhosNameChanged");
modelBuilder.Entity<Thing>().HasRequired(p => p.MetaDataOnThing);
}
The old PK of the table is now defined as a property on the model and it is the same name as the column (the reason it is not defined in the mapping above).
Model
Here is the Model (I have applied names that I hope will make it more clear what has changed):
public class Thing
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
//This used to be the PK, its names (Property AND Column) have not changed
public int OldId { get; set; }
//The column name for FileName changed to something else
public string FileName { get; set; }
//Unchanged
public byte[] Document { get; set; }
public string ContentType { get; set; }
//Navigation Property
public ThingMetaData MetaDataOnThing { get; set; }
}
Integration test
I removed a lot of structure to, hopefully, make it clear..the test is pretty straight forward
[TestMethod]
public void ThenThingWillBePersisted()
{
var thing = new Thing()
{
OldId = metaDataObject.Id,
Document = new byte[] { 42 },
FileName = "foo.jpg",
ContentType = "image/jpeg"
};
context.Things.Add(thing);
context.SaveChanges();
}
This test produces the error "A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column:'NewId'" and the inner exception points to the NewId as being the issue. It does so on the SaveChanges() call.
Admittedly, I have a lot more experience with nHibernate than I do with Entity Framework but I am pretty sure my mappings and model are setup properly.
Has anyone seen this issue and how did you solve it?

entity framework 5 take and order by in include

I want to retrieve an object plus its filtered/ordered collection property using EF 5. However, my current code throws an exception:
The Include path expression must refer to a navigation property
defined on the type. Use dotted paths for reference navigation
properties and the Select operator for collection navigation
properties
Here is the class of the object I want to retrieve:
public class EntryCollection
{
[Key]
public int Id { get; set; }
public ICollection<Entry> Entries { get; set; }
...
}
And here is the definition of Entry:
public class Entry
{
[Key]
public int Id { get; set; }
public DateTime Added { get; set; }
...
}
I wanted to retrieve the EntryCollection which contains only the most recent entries, so here is the code I tried:
using (var db = new MyContext())
{
return db.EntryCollections
.Include(ec => ec.Entries.OrderByDescending(e => e.Added).Take(5))
.SingleOrDefault(ec => ec.Foo == "bar');
}
Any ideas?
You cant use OrderBy inside an include.
what about the following
using (var db = new MyContext())
{
return db.EntryCollections
.Where(ec => ec.Foo == "bar")
.Select(ec=> new Something{Entries = ec.Entries.OrderByDescending(e => e.Added).Take(5) }, /*some other properties*/)
.SingleOrDefault();
}
or do it in two seperate queries