EF Code First Declaring Entity relationships 2 fields to the same collection - c#

I am using Entity Framework with Code First. I have a table in my database that stores relationships between users. The table structure looks a lot like this:
RequestID
UserFromID
UserToID
Both UserFromID and UserToID are foreign keys to my User table.
In my User entity I have a virtual property called Relationships setup. I want this property to be a collection of of all RelationshipRequests from the table listed above where the current users UserID is either the UserFromID OR the UserToID.
These are my bindings in the context:
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.Relationships).WithRequired(e => e.UserFrom)
.HasForeignKey(e => e.UserFromID).WillCascadeOnDelete(false);
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.Relationships).WithRequired(e => e.UserTo)
.HasForeignKey(e => e.UserToID).WillCascadeOnDelete(true);
But once the relationships are retrieved for a user, the only relationships in the collection are the one's where the users ID is the UserToID. I have tried swapping them around in which case the collection only contains the relationships where the users ID is the UserFromID. It seems as though the second binding is overriding the first instead of appending to it like I expected. I'm obviously doing this wrong. My question, is there another way to do this binding so that both keys are bound the way I want, or is this something I will have to implement another way?
Thanks!

the problem is that you use twice the same property, and of course your second binding overrides the first. You have to create 2 navigation properties on your user model, one for each foreign key
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.FromRelationships).WithRequired(e => e.UserFrom)
.HasForeignKey(e => e.UserFromID).WillCascadeOnDelete(false);
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.ToRelationships).WithRequired(e => e.UserTo)
.HasForeignKey(e => e.UserToID).WillCascadeOnDelete(true);
And on the model you will have something like this
public virtual List<Relationships> FromRelationships { get; set; }
public virtual List<Relationships> ToRelationships { get; set; }

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.

EF Code First created two properties for one field

In database, I have two tables - Show and Language. Table Show besides other things has Foreign Key to Language. Its one-to-many relationship (Show has one language).
When I run Code First from ADO.NET Entity Data Model creates two properties for language field:
public class ShowModel
{
...
public LanguageModel Language1 {get; set;}
public string Language {get; set;}
...
}
public class LanguageModel
{
...
public string Language {get; set;}
...
}
When I debug on sample data, value of Language field from DB is inserted into string property.
Question is - Why it generates those properties? Is it because I can add Language object into the LanguageModel one, but from DB, it always write to string one?
EDIT
modelBuilder.Entity<LanguageModel>()
.HasMany(e => e.Shows)
.WithOptional(e => e.Language1)
.HasForeignKey(e => e.Language);
modelBuilder.Entity<ShowModel>()
.Property(e => e.Language)
.IsUnicode(false);
If you are hinting at the fact that the field 'Language' is the foreign key to the Language table for the 'Laguage1' navigation property, then I can give you a couple of pointers:
The key in the Language table is your Language property, but might not be marked as the key. EF doesn't figure this out by itself unless you name the field Id or something, or mark it with a [Key] attribute.
EF doesn't see the 'Language' field as the Foreign key, but as another scalar property, this will result in having 2 Language fields on the Show property. This can be solved by telling the ModelBuilder to use the proper navigation property.
The syntax to solve no. 2 is similar to this:
modelBuilder.Entity<ShowModel>()
.HasRequired(t => t.Language1)
.WithMany(t => t.Shows)
.HasForeignKey(d => d.Language);

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

Entity Framework many-to-many relationship error

I have two classes:
public class Cluster
{
public int Id { get; set; }
public virtual ICollection<Blob> Blobs { get; set; }
}
public class Blob
{
public int Id { get; set; }
public virtual ICollection<Cluster> Clusters { get; set; }
}
public ClusterConfiguration ()
{
this.HasKey(p => p.Id)
.HasRequired(p => p.Frame)
.WithMany(p => p.Clusters)
.HasForeignKey(p => p.FrameId)
.WillCascadeOnDelete(true)
;
this.HasMany(p => p.Blobs)
.WithMany(p => p.Clusters)
;
}
public BlobConfiguration ()
{
this.HasKey(p => p.Id)
.HasRequired(p => p.Frame)
.WithMany(p => p.Blobs)
.HasForeignKey(p => p.FrameId)
.WillCascadeOnDelete(true)
;
this.HasMany(p => p.Clusters)
.WithMany(p => p.Blobs)
;
}
There are references to other tables in these classes but I do not think that is the problem. The error is:
[{"Introducing FOREIGN KEY constraint 'FK_dbo.ClusterBlobs_dbo.Blob_Blob_Id' on table 'ClusterBlobs' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.\r\nCould not create constraint. See previous errors."}].
I'm not quite sure how to tell EF to cascade delete Blobs if Clusters are deleted but not to delete Clusters if Blobs are deleted. Please advise.
UPDATE: Using EF5 by the way.
The multiple cascading delete path is actually in effect if you delete a Frame, not a Cluster or Blob:
Frame is deleted -> Cascades to Clusters -> Cascades to link table
Frame is deleted -> Cascades to Blobs -> Cascades to link table
So, these are the two paths from Frame to the link table.
I would suggest to disable cascading delete for one of the two (or both) relationships from Frame to Cluster or Blob. (Use WillCascadeOnDelete(false) there.) Disabling cascading delete for the link table is not possible on an individual relationship basis. The only way is to disable the convention globally:
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
But this will affect all many-to-many relationships in your model.
I'm not quite sure how to tell EF to cascade delete Blobs if Clusters
are deleted but not to delete Clusters if Blobs are deleted.
This is by the way not possible. There is no cascading delete between Cluster and Blob because from database viewpoint the many-to-many relationship is actually modeled with two one-to-many relationships with the link table in between. Cascading delete only acts on the link table which is the dependent in the relationships. Cluster and Blob are both principals.
I believe that turning off ManyToManyCascadeDeleteConvention globally is not a wise option. Instead, it's better to turn it off only for the concerned table.
This can be achieved through editing the generated migration file, for property cascadeDelete. For example:
AddForeignKey("dbo.ClusterBlobs", "Blob_Id", "dbo.Blob", "Blob_Id", cascadeDelete: false);

Categories

Resources