Problem Saving with Entity Framework (Need conceptual help) - entity-framework

Problem Summary: I have a Master and Detail entities. When I initialize a Master (myMaster), it creates an instance of Details (myMaster.Detail) and both appear to persist in the database when myMaster is added. However, when I reload the context and access myMasterReloaded.detail its properties are not initialized. However, if I pull the detail from the context directly, then this magically seems to initialize myMasterReloaded.detail. I've distilled this down with a minimal unit test example below. Is this a "feature" or am I missing some important conceptual detail?
//DECLARE CLASSES
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
public Detail Detail { get; set; }
public Master() { Detail = new Detail(); }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
public Master MyMaster{ get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
//PERFORM UNIT TEST
[TestMethod]
public void UnitTestMethod()
{
//Start with fresh DB
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This should NOT Pass but it does..
Assert.AreNotEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//Let's say 'hi' to the instance of details in the db without using it.
contextReloaded.Details.First();
//By simply referencing the instance above, THIS now passes, contracting the first Assert....WTF??
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
(This is the sticking point for a more sophisticated entity set. I've simply distilled this down to its simplest case I can't simply replace details with a complex type).
Cheers,
Rob

I think it's because when you first reload the Master, you have not eager-loaded the Detail, so the Detail entity will not be in the Entity Framework "graph" (internal memory). The only thing in the graph will be a single Master entity.
But when you query the Detail ("Let's say hi"), it is loaded into the graph and the reference was resolved based on the FK association, therefore your final test passes as the Master is now related to the Detail.
I could be wrong though - but that's what it sounds like.
Do you have lazy-loading enabled? If not, you need to eager-load the relationships you need.
Instead of this:
var masterReloaded = contextReloaded.Masters.First();
Try this:
var masterReloaded = contextReloaded.Masters.Include(x => x.Detail).First();

Matt Hamilton was right (See above). The problem was:
The Detail property should not be instantiated within the constructor, nor through the getters/setters via a backing member. If it's convenient to instantiate a Entity containing Properties in your new instance, then it may be helpful to have a separate initialize method which will not be automatically executed by the Entity Framework as it reconstructs objects from the database.
The Detail property needs to be declared virtual in the Master class for this to work properly.
The following WILL PASS (As expected/hope)
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
//Key new step: Detail MUST be declared VIRTUAL
public virtual Detail Detail { get; set; }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
//Set this to be VIRTUAL as well
public virtual Master MyMaster { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//This sets up a BI-DIRECTIONAL relationship
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
[TestMethod]
public void UnitTestMethod()
{
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
//Key new step: Detail must be instantiated and set OUTSIDE of the constructor
master.Detail = new Detail();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This NOW Passes, as it should
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//This line is NO LONGER necessary
contextReloaded.Details.First();
//This shows that there is no change from accessing the Detail from the context
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
Finally, it is also not necessary to have a bidirectional relationship. The reference to "MyMaster" can be safely removed from the Detail class and the following mapping can be used instead:
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithMany()
.IsIndependent();
With the above, performing context.Details.Remove(master.Detail), resulted in master.Detail == null being true (as you would expect/hope).
I think some of the confusion emerged from the X-to-many mapping where you can initialize a virtual list of entities in the constructor (For instance, calling myDetails = new List(); ), because you are not instantiating the entities themselves.
Incidentally, in case anyone is having some difficulties with a one-to-many unidirectional map from Master to a LIST of Details, the following worked for me:
modelBuilder.Entity<Master>()
.HasMany(x => x.Details)
.WithMany()
.Map((x) =>
{
x.MapLeftKey(m => m.MasterId, "MasterId");
x.MapRightKey(d => d.DetailId, "DetailId");
});
Cheers, Rob

Related

Entity Framework CORE Seeding Joining table

I am working on .NET CORE 6 along with EF CORE 7. I need to seed data in joining table but unable to do so and get error.
I am seed FileTypeId but not sure why EF core migration throwing error...
error
The seed entity for entity type 'JobFileType' cannot be added because it has the navigation 'FileType' set. To seed relationships, add the entity seed to 'JobFileType' and specify the foreign key values {'FileTypeId'}. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the involved property values.
ClassA
public class JobProfile
{
public JobProfile()
{
this.JobFileTypes = new HashSet<JobFileType>();
}
public Guid JobProfileId { get; set; }
public string Name { get; set; }
public ICollection<JobFileType>? JobFileTypes { get; set; }
}
ClassB
public class FileType
{
public FileType()
{
this.JobFileTypes = new HashSet<JobFileType>();
}
public Guid FileTypeId { get; set; }
public string Extension { get; set; } = string.Empty;
public ICollection<JobFileType>? JobFileTypes { get; set; }
}
Joing Table
public class JobFileType
{
public Guid JobFileTypeId { get; set; }
public Guid JobProfileId { get; set; }
public JobProfile JobProfile { get; set; } = new JobProfile();
public Guid FileTypeId { get; set; }
public FileType FileType { get; set; } = new FileType();
}
Seed Extension
public static class JobFileTypeSeed
{
public static void Seed(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<JobFileType>()
.HasData(
new JobFileType {JobFileTypeId = Guid.Parse("aaa"), JobProfileId = Guid.Parse("ccc"), FileTypeId = Guid.Parse("yyy") },
new JobFileType { JobFileTypeId = Guid.Parse("bbb"), JobProfileId = Guid.Parse("ccc"), FileTypeId = Guid.Parse("zzz") }
);
}
}
config
internal class JobFileTypeConfiguration : IEntityTypeConfiguration<JobFileType>
{
public void Configure(EntityTypeBuilder<JobFileType> builder)
{
builder.ToTable("JobFileType", "dbo");
builder.HasKey(column => column.JobFileTypeId);
builder
.HasOne(jobFileType => jobFileType.JobProfile)
.WithMany(jobProfile => jobProfile.JobFileTypes)
.HasForeignKey(jobFileType => jobFileType.JobProfileId);
builder
.HasOne(jobFileType => jobFileType.FileType)
.WithMany(fileType => fileType.JobFileTypes)
.HasForeignKey(jobFileType => jobFileType.FileTypeId);
}
}
There is not much to say about the concrete issue (which btw is not specific to joining entity, but any entity model seeding):
I am seed FileTypeId but not sure why EF core migration throwing error...
as the cause of the issue is included at the beginning of the error message:
because it has the navigation 'FileType' set.
And your entity has
public FileType FileType { get; set; } = new FileType();
// ^ ^ ^
// the problem
and the same for
public JobProfile JobProfile { get; set; } = new JobProfile();
which will be the next error if you resolve the original.
Remove both navigation property initializers (= new ...) and the problem will be gone.
As a general rule, you should never initialize reference navigation properties because it causes many side effects and/or improper behaviors (not only for seeding, but also eager/lazy/explicit data loading). Initializing collection navigation properties is arbitrary, but ok. Only reference navigation property initialization must be avoided. For more info, see EF codefirst : Should I initialize navigation properties? - quite old EF topic, but still applies.
If you are trying to resolve NRT warnings (as I guess), initializing with new is definitely not a proper way. One reason I don't like NRT is because it is forcing people to use "workarounds" for preventing compiler warnings, which in fact break the primary functionality. Specifically in EF Core, enabling NRT also changes the optional/required attribute of some properties, hence database column types (most noticeable for string properties/columns and reference navigations). You could read more about this in the Working with Nullable Reference Types topic in the official EF Core documentation, but in general I would just disable NRT for EF entity model classes.
The proper order is to set the "master data" first and then try to set the join table, as you would expect.
The defaulting
{get;set;} = new Something();
Could be the offending declaration, since any instance upon creation will have the relation JobFileType already set

Deleting association between one or zero to one entities with EntityFramework

I have entities set up something like this:
public class MyThing
{
public int Id { get; set; }
public virtual MyOtherThing { get;set; }
}
public class MyOtherThing
{
public int Id { get; set; }
public virtual MyThing MyThing { get; set; }
}
My intention is that 'MyThing' can have one or none of MyOtherThing, and I also want a navigation link from MyOtherThing to it's parent.
I have configured the following EntityBaseConfiguration for the 'MyOtherThing' entity:
this.HasOptional(x => x.MyThing)
.WithOptionalPrincipal(x => x.MyOtherThing);
I can assign and modify MyOtherThing to MyThing no problem, but when I want to unassign 'MyOtherThing' from 'MyThing', how do I do this?
I tried the following:
myThing.MyOtherThing = null;
and then editing the entity by setting the EntityState.Modified state, but this didn't remove the association between the entities.
I tried adding the following to my MyThing entity, but this resulted in an EF 'Multiplicity is not valid' error when updating my database model:
public int? MyOtherThingId{ get; set; }
Thanks in advance!
I tried the following:
myThing.MyOtherThing = null;
If you want to remove an optional dependent entity (here: MyOtherThing) from a principal entity (here MyThing) by setting it to null, you have to pull the entity from the database with the dependent entity included, for example:
var mything = context.MyThings.Include(m => m.MyOtherThing)
.Single(t => t.Id == idValue);
(It's also OK when the belonging MyOtherThing is loaded into the context later, for example by lazy loading).
Without Include, myThing.MyOtherThing already is null and EF doesn't detect any change. Note that the statement myThing.MyOtherThing = null; doesn't execute lazy loading, which is a bit confusing because with collections the behavior is different.
By the way, the dependent entity can also be removed from the database directly, which is more efficient.
var ot = context.Set<MyOtherThing>().Find(idValue);
context.Set<MyOtherThing>().Remove(ot);
context.SaveChanges();

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();
}
}

EF Model with collection and navigation to a particular element

How to configure a EF6 migration with a model class having?
A collection o items
A navigation property to one particular item
public class MyModel
{
[Key]
public int Id { get; set; }
// My collection of elements
public virtual ICollection<MyCollectionElement> MyCollection { get; set; }
// Optional navigation to a particular element from the collection
[ForeignKey("CurrentElement")]
public int? CurrentElementId { get; set; }
public virtual MyCollectionElement CurrentElement { get; set; }
}
public class MyCollectionElement
{
[Key]
public int Id { get; set; }
// Required navigation to MyClass
[ForeignKey("MyModel")]
public int MyModelID { get; set; }
public virtual MyModel Model { get; set; }
}
Configuration
modelBuilder.Entity<MyModel>()
.HasMany(x => x.MyCollection)
.WithRequired(x => x.Model)
.HasForeignKey(x => x.MyModelID)
.WillCascadadeOnDelete(false);
Throws several errors on Update-Database, like
Unable to determine a valid ordering for dependent operations.
I would like a solution which doesn't involve a boolean IsCurrent in MyCollectionElement to make another query later and find which element is the current; instead, I would like to store the current element's id with my model, like exposed.
Also, I don't mind making int CurrentElementId non nullable (required) if it's easier.
Thanks.
This chicken-and-egg problem always looms when there are circular relationships. The error...
Unable to determine a valid ordering for dependent operations.
...is not thrown when the database is created. The database can be created just fine. It occurs when you try to insert a MyModel record and a MyCollectionElement referring to one another in the same unit of work. In the Seed method you probably have something like
var element = new MyCollectionElement();
var model = new MyModel();
model.MyCollection.Add(element);
model.CurrentElement = element;
The statement model.MyCollection.Add(element); requires model to be inserted first, so element can refer to it in its foreign key. But model.CurrentElement = element; requires element to be inserted first.
You can only avoid this situation by calling SaveChanges twice, and wrapping everything in a TransactionScope if you want the assignments to be transactional:
using(var ts = new TransactionScope())
{
using(var db = new MyContext()
{
var element = new MyCollectionElement();
var model = new MyModel();
model.MyCollection.Add(element);
db.MyModels.Add(model);
db.SaveChanges();
model.CurrentElement = element;
db.SaveChanges();
}
ts.Complete();
}
This also means that int CurrentElementId should remain nullable.

EntityFramework is naming my mapping table wrong

I have the following Entity class definition:
[Table("Users")]
public class WebUser
{
public virtual int Id { get; set; }
public virtual ICollection<Client> Clients { get; set; }
// more properties...
}
Notice that table name is different than the class name. I also have a ClientUsers table which is a many-to-many mapping for clients and users. Problem is, when I try to access the webUser.Clients property I get the following exception:
"Invalid object name 'dbo.ClientWebUsers'."
Looks like Entity Framework is trying to guess the name of the third table, but it apparently was not smart enough to take into account the table attribute that I have there. How can I tell EF that it is ClientUsers and not ClientWebUsers? Also what rule does it follow to know which table name comes first and which one comes second in the new table name? I think it's not alphabetical order.
I'm using EF 5.0. Thanks!
From the looks of things you're using Code First, so I'll answer accordingly. If this is incorrect, please let me know.
I believe the convention being used to determine the name of the many-to-many table is determined by the order in which they occur as DbSet properties in your SomeContext : DbContext class.
As for forcing EntityFramework to name your table whatever you like, you can use the Fluent API in the OnModelCreating method of your SomeContext : DbContext class as follows:
public class DatabaseContext : DbContext
{
public DatabaseContext()
: base("SomeDB")
{
}
public DbSet<WebUser> Users { get; set; }
public DbSet<Client> Clients { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<WebUser>().HasMany(c => c.Clients)
.WithMany(p => p.WebUsers).Map(
m =>
{
m.MapLeftKey("ClientId");
m.MapRightKey("UserId");
m.ToTable("ClientUsers");
});
}
}
This assumes your classes are something like the following:
[Table("Users")]
public class WebUser
{
public virtual int Id { get; set; }
public virtual ICollection<Client> Clients { get; set; }
// more properties...
}
public class Client
{
public int Id { get; set; }
public ICollection<WebUser> WebUsers { get; set; }
// more properties
}
Finally, here's an integration test (NUnit) demonstrating the functionality working. You may need to drop your database before running it as Code First should want to update/migrate/recreate it.
[TestFixture]
public class Test
{
[Test]
public void UseDB()
{
var db = new DatabaseContext();
db.Users.Add(new WebUser { Clients = new List<Client> { new Client() } });
db.SaveChanges();
var webUser = db.Users.First();
var client = webUser.Clients.FirstOrDefault();
Assert.NotNull(client);
}
}
Edit: Link to relevant documentation for the Fluent API
Rowan's answer (adding here for reference):
Here is the information on how to configure a many-to-many table (including specifying the table name). The code you are after is something like:
modelBuilder.Entity<WebUser>()
.HasMany(u => u.Clients)
.WithMany(c => c.WebUsers)
.Map(m => m.ToTable("ClientUsers");
~Rowan