WillCascadeOnDelete without base entity knowing of foreign entity - c#

I have an entity in my core application:
public class Contact : BaseEntity
{
//Some properties
}
I then have an entity in a plugin (not part of the main DLL) like so:
public class AdditionalContactData
{
public string SomePropertyThatIsntOnTheMainContact { get; set; }
public Contact Contact { get; set;}
}
After binding the models etc via EF, this creates the tables with the foreign keys as I would expect. The issue I have is that I want to be able to delete a Contact entity and it cascade down to remove AdditionalContactData. This wouldn't be an issue if Contact could know about AdditionalContactData i.e:
HasRequired(m => m.Contact)
.WithOptional(m => m.AdditionalContactData)
.WillCascadeOnDelete();
This would work (and would be what I'd do in normal circumstances)
How can I achieve the same thing BUT without Contact knowing about AdditionalContactData. Is this possible?
I'm hoping I've been clear enough but please let me know if more information is required.

Assuming, that this is code from AdditionalContactData configuration, just remove optional property expression from WithOptional:
HasRequired(m => m.Contact)
.WithOptional()
.WillCascadeOnDelete();

You can create an unidirectional relationship:
HasRequired(m => m.Contact)
.WithOptional()
.WillCascadeOnDelete();
Check this link for more info about this.

Related

Problem with master/detail tables and Entity Framework

I have a typical master/detail (User / Settings table) table schema (SQL Server) and setup Entity Framework using Fluent API to work with those tables.
I define this as an independent association, so the UserProfileSetting class doesn't include the UserId property, but I understand is correctly mapped in the configuration.
Well, my problem is that when one item of Settings is updated for a profile, at the database level that settings is updated for all users. Basically USER_ID is not considered.
The SQL query produced is this:
UPDATE [dbo].[T_USERPROFILE_SETTING]
SET [VALUE] = #0
WHERE ([KEY] = #1)
Any idea what could be wrong? I guess that if I finally add the UserId property to UserProfileSettings, that will fix the problem, but I wanted to try to fix this without it.
Current code below...
Code updating the data
var entry = profile.Settings.Where(s => s.Key == key).SingleOrDefault();
if (entry != null)
{
entry.Value = value;
} else {
var setting = /* Here create a new setting */
profile.Settings.Add(setting);
}
DataContext.SaveChanges();
Entities:
public partial class UserProfile
{
[Key]
public string UserId { get; set; }
public DateTimeOffset LastLogin { get; set; }
public ICollection<UserProfileSetting> Settings { get; set; }
}
public class UserProfileSetting
{
public UserProfileSetting() { }
public string Key { get; set; }
public string Value { get; set; }
}
Entity configuration:
public class UserProfileConfiguration : EntityTypeConfiguration<UserProfile>
{
public UserProfileConfiguration()
{
ToTable("T_USERPROFILE");
HasKey<string>(p => p.UserId);
Property(p => p.UserId)
.HasColumnName("USER_ID")
.HasMaxLength(50)
.IsUnicode()
.IsRequired();
Property(p => p.LastLogin)
.HasColumnName("LAST_LOGIN_AT")
.IsRequired();
HasMany<UserProfileSetting>(p => p.Settings)
.WithOptional()
.Map(m => m.MapKey("USER_ID"));
}
}
public class UserProfileSettingConfiguration : EntityTypeConfiguration<UserProfileSetting>
{
public UserProfileSettingConfiguration()
{
ToTable("T_USERPROFILE_SETTING");
HasKey(p => p.Key );
Property(p => p.Key)
.HasColumnName("KEY")
.HasMaxLength(50)
.IsUnicode()
.IsRequired();
Property(p => p.Value)
.HasColumnName("VALUE")
.IsUnicode()
.IsRequired();
}
}
From EF documentation...
When foreign key columns are not included in the model, the association information is managed as an independent object. Relationships are tracked through object references instead of foreign key properties. This type of association is called an independent association. The most common way to modify an independent association is to modify the navigation properties that are generated for each entity that participates in the association.
So, I was wrong. In my code, UserProfile should include UserProfileSetting either as a FK (Just the ID) or as an independent Object.
In the 1st case a UserId should be mapped into UserProfileSetting and the navigation property in UserProfile should be changed to...
HasMany<UserProfileSetting>(p => p.Settings)
.WithOptional()
.HasForeignKey(s => s.UserId);
In the 2nd case, (this is what is called an Independent Association) a new navigation property should be added into UserProfileSetting for UserProfile.
Entity framework maps to relational database and so it must stick with some of it concepts. The main thing here is, that each entity is mapped to a table containing all the records of that entity and it needs some data to distinguish the relation.
Therefore you need to add USER_ID to tell which record is for which user (to define the relation). In other words you need to have it in table and also in C# entity.
I don’t think it is possible in code first to not have the relation property on entity. On the other hand, you can create some extra DTO layer to hide it.

Entity Framework Core fluent api One-To-Many and One-To-One produces duplicate foreign key

I'm changing my ASP.NET MVC project to ASP.NET Core MVC with Entity Framework Core and Fluent API. When I try to configure a one-to-one and one-to-many relationship, it generates duplicate foreign key columns in the dependent table.
For example: I have this in my context's OnModelCreating method:
builder.Entity<Session>()
.HasKey(s=>s.Id);
builder.Entity<Session>()
.Property(s=>s.CourseId)
.IsRequired();
builder.Entity<Session>()
.HasOne<Course>()
.WithMany(c => c.Sessions)
.HasForeignKey(s=>s.CourseId)
.IsRequired();
Session model is like this:
public class Session
{
public int Id { get; set; }
// foreign key
public int CourseId { get; set; }
// navigation properties
public virtual Course Course { get; set; }
}
Course model is like this:
public class Course
{
public int Id { get; set; }
// Navigation properties
public ICollection<Session> Sessions { get; set; }
}
Instead of getting this back in the migration:
modelBuilder.Entity("Blackboard.Models.DomainModels.Session", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CourseId");
b.HasKey("Id");
b.HasIndex("CourseId");
b.ToTable("Sessions");
});
I get this:
modelBuilder.Entity("Blackboard.Models.DomainModels.Session", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CourseId");
b.Property<int?>("CourseId1");
b.HasKey("Id");
b.HasIndex("CourseId");
b.HasIndex("CourseId1");
b.ToTable("Sessions");
});
So even though I put .IsRequired(); for the relationship, the relationship seems to be made optional and an optional CourseId1 is added to the table.
The application is developed on Mac OSX with Visual Studio for Mac.
I've been configuring this for so long, I only found something for Entity Framework instead of Entity Framework Core. They two don't configure the same way. Can someone help me please?
Thank you.
I found a fix. I changed the context to:
builder.Entity<Session>()
.HasOne(s=>s.Course)
.WithMany(c => c.Sessions)
.HasForeignKey(s=>s.CourseId)
.IsRequired();
I remember I saw somewhere saying that if there is only one navigation property, then .HasOne<Type>()(no parameters passed) can be used. Obviously, it's not working. So always use lambda parameter in .HasOne.

EntityFramework CodeFirst: CASCADE DELETE for same table many-to-many relationship

I have an entry removal problem with the EntityFramework and a many-to-many relationship for the same entity. Consider this simple example:
Entity:
public class UserEntity {
// ...
public virtual Collection<UserEntity> Friends { get; set; }
}
Fluent API Configuration:
modelBuilder.Entity<UserEntity>()
.HasMany(u => u.Friends)
.WithMany()
.Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("FriendId");
m.ToTable("FriendshipRelation");
});
Am I correct, that it is not possible to define the Cascade Delete in Fluent API?
What is the best way to delete a UserEntity, for instance Foo?
It looks for me now, I have to Clear the Foo's Friends Collection, then I have to load all other UserEntities, which contain Foo in Friends, and then remove Foo from each list, before I remove Foo from Users. But it sounds too complicateda.
Is it possible to access the relational table directly, so that I can remove entries like this
// Dummy code
var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id);
dbCtx.Set("FriendshipRelation").RemoveRange(query);
Thank you!
Update01:
My best solution for this problem for know is just to execute the raw sql statement before I call SaveChanges:
dbCtx.Database.ExecuteSqlCommand(
"delete from dbo.FriendshipRelation where UserId = #id or FriendId = #id",
new SqlParameter("id", Foo.Id));
But the disadvantage of this, is that, if SaveChanges failes for some reason, the FriendshipRelation are already removed and could not be rolled back. Or am I wrong?
Problem 1
The answer is quite simple:
Entity Framework cannot define cascade delete when it doesn't know which properties belong to the relationship.
In addition, in a many:many relationship there is a third table, that is responsible for managing the relationship. This table must have at least 2 FKs. You should configure the cascade delete for each FK, not for the "entire table".
The solution is create the FriendshipRelation entity. Like this:
public class UserFriendship
{
public int UserEntityId { get; set; } // the "maker" of the friendship
public int FriendEntityId { get; set; }´ // the "target" of the friendship
public UserEntity User { get; set; } // the "maker" of the friendship
public UserEntity Friend { get; set; } // the "target" of the friendship
}
Now, you have to change the UserEntity. Instead of a collection of UserEntity, it has a collection of UserFriendship. Like this:
public class UserEntity
{
...
public virtual ICollection<UserFriendship> Friends { get; set; }
}
Let's see the mapping:
modelBuilder.Entity<UserFriendship>()
.HasKey(i => new { i.UserEntityId, i.FriendEntityId });
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.Friends)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(true); //the one
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany()
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(true); //the one
Generated Migration:
CreateTable(
"dbo.UserFriendships",
c => new
{
UserEntityId = c.Int(nullable: false),
FriendEntityId = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.UserEntityId, t.FriendEntityId })
.ForeignKey("dbo.UserEntities", t => t.FriendEntityId, true)
.ForeignKey("dbo.UserEntities", t => t.UserEntityId, true)
.Index(t => t.UserEntityId)
.Index(t => t.FriendEntityId);
To retrieve all user's friends:
var someUser = ctx.UserEntity
.Include(i => i.Friends.Select(x=> x.Friend))
.SingleOrDefault(i => i.UserEntityId == 1);
All of this works fine. However, there is a problem in that mapping (which also happens in your current mapping). Suppose that "I" am a UserEntity:
I made a friend request to John -- John accepted
I made a friend request to Ann -- Ann accepeted
Richard made a friend request to me -- I accepted
When I retrieve my Friends property, it returns "John", "Ann", but not "Richard". Why? because Richard is the "maker" of the relationship not me. The Friends property is bound to only one side of the relationship.
Ok. How can I solve this? Easy! Change your UserEntity class:
public class UserEntity
{
//...
//friend request that I made
public virtual ICollection<UserFriendship> FriendRequestsMade { get; set; }
//friend request that I accepted
public virtual ICollection<UserFriendship> FriendRequestsAccepted { get; set; }
}
Update the Mapping:
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.FriendRequestsMade)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany(i => i.FriendRequestsAccepted)
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(false);
There are no migrations necessary.
To retrieve all user's friends:
var someUser = ctx.UserEntity
.Include(i => i.FriendRequestsMade.Select(x=> x.Friend))
.Include(i => i.FriendRequestsAccepted.Select(x => x.User))
.SingleOrDefault(i => i.UserEntityId == 1);
Problem 2
Yes, you have to iterate the collection and remove all children objects. See my answer in this thread Cleanly updating a hierarchy in Entity Framework
Following my answer, just create a UserFriendship dbset:
public DbSet<UserFriendship> UserFriendships { get; set; }
Now you can retrieve all friends of a specific user id, just delete all of them in one shot, and then remove the user.
Problem 3
Yes, it is possible. You have a UserFriendship dbset now.
Hope it helps!
1) I don't see any straightforward way to control the cascade on the many-to-many relationships using FluentApi.
2) The only available way I can think of to control that is by using the ManyToManyCascadeDeleteConvention, which I guess is enabled by default, at least it is for me. I just checked one of my migrations including a many-to-many relationship and indeed the cascadeDelete: true is there for both keys.
EDIT: Sorry, I just found that the ManyToManyCascadeDeleteConvention does not cover the self-referencing case. This related question's answer says that
You receive this error message because in SQL Server, a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement. For example, the tree of cascading referential actions must only have one path to a particular table on the cascading referential actions tree.
So you end up having to have a custom delete code (like the sql command that you already have) and execute it in a transaction scope.
3) You should not be able to access that table from the context. Usually the table created by a many-to-many relationship is a by-product of the implementation in a relational DBMS and is considered a weak table respective to the related tables, which means that its rows should be cascade-deleted if one of the related entities is removed.
My advice is that, first, check if your migration is setting your table foreign keys to cascade delete. Then, if for some reason you need to restrict the deletion of a record which has related records in the many-to-many relationship, then you just check for it in your transactions.
4) In order to do that, if you really want to (FluentApi enables by default ManyToManyCascadeDeleteConvention), is to enclose the sql command and your SaveChanges in a transaction scope.

EF 6.0 Cannot retrieve navigation property (Collection) using a Bounded (focused) context

I have started breaking up my "uber" context into smaller focused ones. In a simple scenario, I have Student and Lectures POCOS and my EntityTypeConfiguration defines a many to many relationship between the two in a new table called StudentsAndLectures.
These tables are part of a relationship network of tables defined in my uber context. However, I'd like to manage students and their lectures in a more targeted fashion with a focused context.
My POCO classes below.
public class Student
{
public Student()
{
Lecture = new List<Lecture>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Lecture> Lectures { get; set; }
}
public class Lecture
{
public Lecture()
{
Students = new List<Student>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
Finally, My entity type mappers.
public class StudentMapper : EntityTypeConfiguration<Student>
{
public StudentMapper()
{
HasKey(x => x.Id);
HasMany(x => x.Lectures)
.WithMany(x => x.Students)
.Map(m =>
{
m.MapLeftKey("LectureId");
m.MapRightKey("StudentId");
m.ToTable("StudentsAndLectures");
});
Property(x => x.Name);
}
}
public class LectureMapper : EntityTypeConfiguration<Lecture>
{
public LectureMapper()
{
HasKey(x => x.Id);
HasMany(x => x.Students)
.WithMany(x => x.Lectures)
.Map(m =>
{
m.MapLeftKey("LectureId");
m.MapRightKey("StudentId");
m.ToTable("StudentsAndLectures");
});
Property(x => x.Name);
}
}
Also, My Focused context contains DbSets for only the Students and Lectures.
My problem, If I query for a specific student like below, using my focused context, my Navigation property for .Lectures returns empty. However if I use the full(uber) context that created the db my navigation property gets lazy loaded or eager loaded as i wish. Anyone know why this could be?
using(FocusedStudentContext db = new FocusedStudentContext())
{
var student = db.Students.Include(s => s.Lectures)
.FirstOrDefault(s => s.StudentID == 1234);
// Inspecting student here for the Lectures navigation property
// collection has 0 elements.
}
After further testing and experimenting I found that if I included One particular (none others) additional DbSet that exists in my model and it's related ModelBuilder configurations then all works fine. The DbSet is for an entity, Registration, and it's one that has a navigation property to Student with a HasRequired (x => x.Student). Another twist is, if i leave the ModelBuilder configurations for the Registration entity, but remove the DbSet<Registration> from my focused context, then my navigation property for Lectures stops getting added again. (The collection has 0 elements).
My confusion, how can adding a DbSet to my Focused context affect the way my navigation properties get resolved for tables/entities described above? And how can I resolve this issue. Any help will be appreciated.
You only need one many-to-many mapping, not two. But even though you could have two mappings, they should be identical. In your case, they aren't. Both mappings have the same columns in MapLeftKey and MapRightKey, but they start at different ends. Only the LectureMapper is correct.
Apparenty, the StudentMapper takes precedence, which I think is determined by the order in which mappings are added to the configuration. The effect is that EF is looking for Lectures by the StudentId value in the junction table: very wrong. I can't really explain the effect of including the other mappings and entities that you describe. I just assume that under different circumstances makes EF takes the other mapping first.
But it's just too easy to get MapLeftKey and MapRightKey wrong. I try to keep them apart by picturing it:
Lecture HasMany Student
Left: LectureId Right: StudentId
The MSDN description isn't too helpful, e.g. MapLeftKey:
Configures the name of the column(s) for the left foreign key. The left foreign key points to the parent entity of the navigation property specified in the HasMany call
The navigation property specified in the HasMany call is Students, the parent (or owner) of the property is Lecture, which is identitfied by LectureId... I go for the visualization.
UPDATE I guess I resolved this but not really. I found that if I remove the explicit mapping on the Student and Lectures many to many table and let EF do it that things work fine now.
HasMany(x => x.Students).WithMany(x => x.Lectures);

How to model look-up tables with EF DbContext

I am using Entity Framwork DbContexts with a legacy database. I have 2 different properties of an entity that both need to reference the same lookup table, like so:
public class Address
{
public virtual AddressType AnAddressType {get; set;}
public virtual AddressType AnotherAddressType {get; set;}
}
// now here's a LINQ query that just flat won't work:
from a in Addresses select a;
The exception indicates that we tried to include a completely fictional field in the select list -- the field doesn't appear in my POCO or the table -- it looks like it was expected by convention, it's named AnAddressType_AddressType or something close to that
The AddressType entity does not have a corresponding navigation property on it. I cannot seem to get this to work. When I attempt to select data with my LINQ query, I get runtime errors.
Edit
I have other relations that are working (this code is generated from the "stock" DbContext generator). The thing about this one relation that is different is that the lookup table does not have a navigation property back to the main table (the lookup table is used all over the place, so I don't really want to add nav properties from it to everything that uses it). EF seems to be having a problem with that. It's probably a configuration vs. convention thing, and I have inadvertently tripped on some kind of convention problem.
You have a foreign key column name in your legacy database which doesn't follow the EF conventions, for example: The foreign key column in your Addresses table to the AddressTypes table for the AnAddressType relationship has the name MyCrazyAnAddressTypeNumberCodeKeyId.
But EF will assume by convention that the FK column name is: [Nav.property]_[KeyColumn]. For example: If AddressType has a PK with name AddressTypeId EF will assume the FK column has the name AnAddressType_AddressTypeId. Because this doesn't match you get the exception you describe. You must specify the FK column name explicitely to fix this problem:
modelBuilder.Entity<Address>()
.HasRequired(a => a.AnAddressType)
.WithMany()
.Map(c => c.MapKey("MyCrazyAnAddressTypeNumberCodeKeyId"))
.WillCascadeOnDelete(false);
(code snippet partially stolen from Ladislav's answer for convenience)
That's my hypothesis.
Edit
Alternatively you can introduce a foreign key property into your model and tell EF by data annotations that this property is a FK to the corresponding navigation property:
public class Address
{
[ForeignKey("AnAddressType")]
public int MyCrazyAnAddressTypeNumberCodeKeyId {get; set;}
public virtual AddressType AnAddressType {get; set;}
}
If you don't want navigation properties on both sides of the relation you should help EF with fluent mapping so that model is represented correctly:
public class YourContext : DbContext
{
public DbSet<Address> Addresses { get; set; }
public DbSet<AddressType> AddresTypes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Address>()
.HasRequired(a => a.AnAddressType)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<Address>()
.HasRequired(a => a.AnotherAddressType)
.WithMany()
.WillCascadeOnDelete(false);
}
}

Categories

Resources