EF Core - What is the correct configuration? - c#

I have the following model:
public class Book
{
public Guid Id { get; set; }
public List<IndexPage> IndexPages { get; set; }
}
public class IndexPage
{
public Guid Id { get; set; }
public List<IndexWord> Words { get; set; }
public int IndexType { get; set; }
public Guid BookId { get; set; }
}
public class IndexWord {
public Guid Id { get; set; }
public String Value { get; set; }
public IndexPageId { get; set; }
}
It is configured with the following configuration:
public class BookConfiguration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("Books");
builder.HasKey(b => b.Id);
builder.HasMany(b => b.IndexPages)
.WithOne()
.HasForeignKey(b => b.BookId);
}
}
public class IndexPageConfiguration : IEntityTypeConfiguration<IndexPage>
{
public void Configure(EntityTypeBuilder<IndexPage> builder)
{
builder.ToTable("IndexPages");
builder.HasKey(ip => new { ip.BookId, ip.IndexType });
builder.HasMany(ip => ip.IndexWords)
.WithOne()
.HasForeignKey(iw => iw.IndexPageId)
.HasPrincipalKey(ip => ip.Id);
builder.HasIndex(ip => ip.Id).IsUnique();
}
}
public class IndexWordConfiguration : IEntityTypeConfiguration<IndexWord>
{
public void Configure(EntityTypeBuilder<IndexWord> builder)
{
builder.ToTable("IndexWords");
builder.HasKey(iw => iw.Id);
builder.Property(iw => iw.Value).IsRequired();
}
}
Some context; The queries are performed with AsNoTracking() and the update is called as following:
DbContext.Set<Book>.Update(book);
If the book is updated, the indexpages are in total replaced with a new set of indexpages.
If calling update the first time it all seems to work correctly and inserts the rows. However when called the second time it raises an Primary Key constraint exception; Which makes sense, however I expected the old indexpages to be removed and the new indexpages to be inserted. Due to the fact that the primary key exists and update is called, not Add.
The reason behind the composite key is that a book can only have a fixed subset of indexpages. this is also the reason why the HasPrincipalKey has been used.
When only using the IndexPage Id as a key in the configuration. The relation between the book exists, but it just keeps inserting new indexpages on top of the old indexpages (I guess due to the AsNoTracking?).
Furthermore, in the code behind I add the indexpages on the book, but these initially have empty id's.

As per docs Update begins tracking the entity. Which means that if you modify it, it's enough to call DbContext.SaveChanges() for it to be saved. If you call Update on a tracked entity it tries to insert it.

If I understand correctly, the old IndexPage entities are not being replaced by the new ones when you attempt to Update ?
You mentioned that you're using No Tracking for your queries. Without tracking the original entity, returned from your query, EF Core has no way to tell that the entity has been modified. If you want the new IndexPage objects to replace the old ones, then you need to track the queries and configure the delete behavior for your relationships.
You can look at the EF Core docs for a reference regarding required relationships and delete behaviors. Be careful to configure these things on the proper entities, so you don't end up deleting everything by removing an IndexPage.
Allow for change tracking and configure if the relationships are required and their delete behavior. That should accomplish what you're seeking to achieve.
https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#manual-configuration

Related

Foreign key in Owned entity: "There is no corresponding CLR property or field"

This is a tale of optional owned entities and foreign keys.
I'm working with EF 5 (code first) and I do this :
public class Parent {
public Guid Id { get; private set; }
public OwnedType1? Owned1 { get; private set; }
public OwnedType2? Owned2 { get; private set; }
public Parent(Guid id, OwnedType1? owned1, OwnedType2? owned2) {
Id = id; Owned1 = owned1; Owned2 = owned2;
}
}
public class OwnedType1 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType1 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class OwnedType2 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType2 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class Shared {
public Guid Id { get; private set; }
public Shared (Guid id) {
Id = id;
}
}
Now, the configuration :
//-------- for Parent ------------
public void Configure(EntityTypeBuilder<Parent> builder) {
builder
.ToTable("Parents")
.HasKey(p => p.Id);
builder
.OwnsOne(p => p.Owned1)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
builder
.OwnsOne(p => p.Owned2)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
}
//-------- for OwnedType1 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for OwnedType2 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for Shared ---------------
public void Configure(EntityTypeBuilder<Shared> builder) {
builder
.ToTable("Shareds")
.HasKey(p => p.Id);
}
Side note : If you're wondering why OwnedType1 and OwnedType2 don't each have a property called 'ParentId', it's because it's created implicitly by the "OwnsOne".
My problem is this :
When I create a new Migration, then OwnedType1 works like a charm, but for OwnedType2 (which is quasi-identical), I get his error :
The property 'OptionalExternalId' cannot be added to the type
'MyNameSpace.OwnedType2' because no property type was specified and
there is no corresponding CLR property or field. To add a shadow state
property, the property type must be specified.
I don't understand what it's complaining about. And why it's complaining only for one of them.
I know that you probably can't work it out with this simplified version of my schema, but what I'm asking is what you think it might be (follow your guts of EF guru) :
Some missing constructor?
Incorrect visibility on one of the fields?
Bad navigation definition?
A typo?
Something tricky (like : If you're going to have TWO different entity classes having a one-to-many relation with Shared, then they can't use the same name for external key. Or I need to use a composite key. Or whatnot).
It was a configuration issue that had nothing to do with Owned entities. Another case of "EF error message is obscure but issue is somewhere there in plain sight".
Unfortunately I don't remember how I fixed it. But it was along the lines of "Need an extra constructor with all the paramaters" or "one of the fields had a different name in the constructor parameters" or one of those classic EF mishaps.

How to use Fluent API for creating a simple one-to-many relationship

Let me first give some background: I'm creating an application, which should handle a DB. That DB might evolve (extra tables/columns/constraints might be added, but nothing gets removed, in fact the DB gets more and more elaborated).
I started with a "Database First" approach and as a result, I have created an Entity Framework diagram, with according classes in *.cs files. Two of those files are (only some interesting fields):
Area.cs:
public partial class Area
{
public Area() { }
public string Name { get; set; }
public int Id { get; set; }
}
Location.cs:
public partial class Location
{
public Location() { }
public string Name { get; set; }
public int Id { get; set; }
...
public Nullable<int> AreaId { get; set; }
}
This is generated from a version of the DB, which does not cover constraints, and now I would like to add a ForeignKeyConstraint to the corresponding Entity Framework model:
Location.AreaId is a foreign key towards Area.Id
There are many Location objects for one Area object
It's the idea to prevent deletion of Area objects, being referred to by Location objects).
I believe this should be done as follows:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Area>().HasKey(t => t.Id); // Creation of primary key
modelBuilder.Entity<Location>().HasKey(t => t.Id); // Creation of primary key
modelBuilder.Entity<Location>().HasRequired(n => n.AreaId)
.WithMany(...)
.HasForeignKey(n => n.AreaId);
...
This, obviously, does not work. I'm missing following information:
My "Area.cs" file does not contain a reference to the Location object (as this version of the DB does not contain constraints, this has not been added by the "database first" wizard), should I add this or can I solve my issue without?
What do I need to fill in instead of the ellipsis .WithMany(...)?
Extra question: I'm aware of the ForeignKey directive. Should I replace public Nullable<int> AreaId { get; set; } in "Location.cs" by [ForeignKey("AreaId")], followed by public virtual Area Area { get; set; }?
Edit
Important remark: as "Location.cs" and "Area.cs" are auto-generated, I like to minimise changes in those files.
Next edit
Meanwhile I've updated my "Location.cs" file as follows:
...
// public Nullable<int> AreaId { get; set; }
[ForeignKey("AreaId")]
public Area Area { get; set;}
....
My OnModelCreating() has been changed into:
modelBuilder.Entity<Location>().HasRequired(n => n.Area)
.WithMany(...)
.HasForeignKey(n => n.Area);
That leaves only the ellipsis problem to be solved.
Another edit
Since it takes such a long time for an answer (even for a comment), I've decided to add following line of source code to my "Area.cs" file:
public virtual ICollection<Location> Locations { get; set; }
I've then filled in the ellipsis as follows:
modelBuilder.Entity<Location>().HasRequired(l => l.Area)
.WithMany(a => a.Locations)
.HasForeignKey(l => l.Area);
Now just one question: how can I mention that the link between the Area and the Location should be handled by Location.AreaId and Area.Id (I know that Location.AreaId is the foreign key, but how can I know that it refers to Area.Id)?
Thanks in advance
The simple answer to your last question. EF is recognizing that Area.Id is a primary key so connects Location.AreaId to Area.Id
Also, here is a simple guide on how to do it.

Fluent NHibernate Mapping / Parent/Child delete fail using composite key

I'm struggling to understand why when I remove a child Settings object from MyUser.Settings and SAVE MyUser I get SQL errors like below:
Cannot insert the value NULL into column 'MyUserId', table '###.Settings'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
What I would expect to happen is that removing the item from the collection, then saving MyUser causes NHibernate to issue a DELETE command for the given child. However, what it does is UPDATE the relevant row for the Settings object, setting MyUserId to NULL - which isn't allowed as I'm using a Composite Key.
I've tried so many combinations of Inverse() and the various Cascade options but nothing seems to work. I should point out that Adding to the collection works perfectly when I save MyUser.
I'm totally baffled!
Below is pseudo code to try and explain my entities and mappings.
public class SettingType
{
public virtual int SettingTypeId { get; set; }
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
}
public class Setting
{
public virtual MyUser MyUser { get; set; }
public virtual SettingType SettingType { get; set; }
public virtual DateTime Created { get; set; }
}
public class MyUser
{
public virtual int MyUserId { get; set; }
public virtual IList<Setting> Settings { get; set; }
public virtual string Email { get; set; }
public void AddSetting(SettingType settingType, DateTime now)
{
var existing = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existing != null)
{
existing.Updated = now;
}
else
{
var setting = new Setting
{
MyUser = this,
SettingType = settingType,
Created = now,
};
_settings.Add(setting);
}
}
public void RemoveSetting(SettingType settingType)
{
var existingPref = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existingPref != null)
{
_settings.Remove(existingPref);
}
}
private readonly IList<Setting> _settings = new List<Setting>();
}
And my mappings:
public class SettingTypeMap : IAutoMappingOverride<SettingType>
{
public void Override(AutoMapping<SettingType> mapping)
{
mapping.Table("SettingTypes");
mapping.Id(m => m.SettingTypeId).GeneratedBy.Identity();
mapping.Map(m => m.Name).Not.Nullable().Length(100);
mapping.Map(m => m.Active).Not.Nullable().Default("0");
}
}
public class SettingMap : IAutoMappingOverride<Setting>
{
public void Override(AutoMapping<Setting> mapping)
{
mapping.Table("Settings");
mapping.CompositeId()
.KeyReference(m => m.MyUser)
.KeyReference(m => m.SettingType);
mapping.Map(m => m.Created).Not.Nullable().Default("CURRENT_TIMESTAMP");
mapping.Map(m => m.Updated).Nullable();
}
}
public class MyUserMappingOverride : IAutoMappingOverride<MyUser>
{
public void Override(AutoMapping<MyUser> mapping)
{
mapping.Table("MyUsers");
mapping.Id(m => m.MyUserId).GeneratedBy.Identity();
mapping.Map(m => m.Email).Not.Nullable().Length(200);
mapping.HasMany(m => m.Settings).KeyColumn("MyUserId").Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
All using:
FluentNHibernate v1.3.0.733
NHibernate v3.3.1.4000
UPDATE: After a few suggestions I've tried to change the mapping for MyUser entity.
First to this:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This gives the error: Given key was not present in the dictionary
So tried to add second key column:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.KeyColumn("SettingTypeId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
But this then causes odd behaviour when loading the Settings collection from the DB for a given MyUserId. Looking at the nh profiler I see a second SELECT ... FROM Settings but setting the SettingTypeId same as value for MyUserId.
Still totally baffled. Has cost me too much time so going to revert to adding a primary key id field to the Settings entity. Maybe you just can't do what I'm trying using NHibernate. In pure SQL this is simple.
You should use the Inverse mapping
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This will allow NHibernate to ask the setting itself to be deleted. Otherwise, NHibernate firstly tries to delete the relation, and would try to delete the entity.
See: 6.4. One-To-Many Associations
Very Important Note: If the column of a
association is declared NOT NULL, NHibernate may cause constraint
violations when it creates or updates the association. To prevent this
problem, you must use a bidirectional association with the many valued
end (the set or bag) marked as inverse="true". See the discussion of
bidirectional associations later in this chapter.

Many to many relationship identity error

I'm working on a mvc4 app with ef5 codefirst and I cannot solve this error:
The member with identity 'xxxx' does not exist in the metadata collection.
Update:
I saw that I used two different contexts (the navigation object was called thorugh a repository that creates a different DbContext), probably this is a problem. I changed that, but now I get a new error:
Invalid column name 'Brewery_BreweryId'.
In the IntelliTrace I saw that ef tries to
select ..., Brewery_BreweryId from UserProfiles
This column is not present and shouldn't be present, I want a many to many, not a one-to-many.
I think that is something related to a many to many relation.
this is an example of my code
internal class BreweryConfiguration : EntityTypeConfiguration<Brewery>
{
public BreweryConfiguration()
{
// PK
HasKey(e => e.BreweryId);
// FK
HasMany(e => e.UserProfiles)
.WithMany()
.Map(m =>
{
m.MapLeftKey("BreweryId");
m.MapRightKey("UserId");
m.ToTable("BreweryUserProfiles");
});
namespace Project2.DAL.Entities
{
[Table("Breweries")]
public class Brewery : ABrewery
{
public int BreweryId { get; set; }
public ICollection<UserProfile> UserProfiles { get; set; }
}
}
namespace Project1.DAL.Entities
{
[Table("UserProfiles")]
public class UserProfile : IUserProfile
{
[Key]
public int UserId { get; set; }
...
}
}
c.MapLeftKey("ClassB_ID");
c.MapRightKey("ClassA_ID");
should be
c.MapLeftKey("ClassA_ID");
c.MapRightKey("ClassB_ID");
Edit:
You need to define the PK of the ClassB in the configuration as well. In the way you implemented, you may add another derived Configuration for ClassB.

How to delete a record with a foreign key constraint?

Started a new ASP.NET MVC 3 application and getting the following error:
The primary key value cannot be deleted because references to this key
still exist.
How to solve this?
Models (EF code-first)
public class Journal
{
public int JournalId { get; set; }
public string Name { get; set; }
public virtual List<JournalEntry> JournalEntries { get; set; }
}
public class JournalEntry
{
public int JournalEntryId { get; set; }
public int JournalId { get; set; }
public string Text { get; set; }
}
Controller
//
// POST: /Journal/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Journal journal = db.Journals.Find(id);
db.Journals.Remove(journal);
db.SaveChanges(); // **exception occurs here**
return RedirectToAction("Index");
}
DB Setup
public class FoodJournalEntities : DbContext
{
public DbSet<Journal> Journals { get; set; }
public DbSet<JournalEntry> JournalEntries { get; set; }
}
Found the solution:
public class FoodJournalEntities : DbContext
{
public DbSet<Journal> Journals { get; set; }
public DbSet<JournalEntry> JournalEntries { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Journal>()
.HasOptional(j => j.JournalEntries)
.WithMany()
.WillCascadeOnDelete(true);
base.OnModelCreating(modelBuilder);
}
}
Source
If you delete a record from a table(lets say "blah"), which has other relationships with other tables (xyz,abc). By default, the database will prevent you from deleting a row in "blah" if there are related rows in one of the other tables.
Solution #1:
You can manually delete the related rows first,this may require a lot of work.
Solution #2:
an easy solution is to configure the database to delete them automatically when you delete a "blah" row.
Follow this open your Database diagram,and click on the properties on the relationship
In the Properties window, expand INSERT and UPDATE Specification and set the DeleteRule property to Cascade.
Save and close the diagram. If you're asked whether you want to update the database, click Yes.
To make sure that the model keeps entities that are in memory in sync with what the database is doing, you must set corresponding rules in the data model. Open SchoolModel.edmx, right-click the association line between "blah" and "xyz", and then select Properties.
In the Properties window, expand INSERT and UPDATE Specification and set the DeleteRule property to Cascade.
Solution and images taken from http://www.asp.net/web-forms/tutorials/getting-started-with-ef/the-entity-framework-and-aspnet-getting-started-part-2
In EF Core (3.1.8), the syntax is a bit different than the accepted answer but the same general idea, what worked for me is below:
modelBuilder.Entity<Journal>()
.HasMany(b => b.JournalEntries)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
In your query to select the item to delete or remove from the database you want to make sure that you are explicitly including the items as well, otherwise it will continue to throw a FK error, something like below.
var item = _dbContext.Journal.Include(x => x.JournalEntries).SingleOrDefault(x => x.Id == id);
I found it...
Go TO SQL Server
Make his Database diagrammed
Right click on relation ship line between parent and child and open the property of it.
Set INSERT And Update Specification and simply set DELETE RULE TO CASCADE.
Remember No code is required in Project FOR this PURPOSE and simply debug and enjoy it.

Categories

Resources