User has roles for systems - how to (object-)model ternary relation? - c#

I've got the following Entities:
Privilege ( Id, Name )
Role ( Id, Name, ICollection<Privilege> )
System ( Id, Name )
User ( Id, Name, Pass, ? )
Now I want to model "A user may have for each of zero or more systems zero or more roles", e.g.:
IDictionary<System, ICollection<Role>> SystemRoles { get; set; }
Is this possible with ASP.NET EntityFramework? If yes, how? What attributes do I have to set?
Been looking around for quite some time now, however I don't find anything useful on the net for "entity framework code first ternary relation"
Can you point me to some nice link where this is covered in detail? Or maybe give me a hint how to model it / which attributes I can put on the dictionary?
Additional question: If the IDictionary solution works somehow, is there any change to get change tracking proxy performance? IDictionary is not an ICollection...

A user may have for each of zero or more systems zero or more roles
You will need an entity that describes the relationship between the three:
public class UserSystemRole
{
public int UserId { get; set; }
public int SystemId { get; set; }
public int RoleId { get; set; }
public User User { get; set; }
public System System { get; set; }
public Role Role { get; set; }
}
I would create a composite primary key from all three properties because each combination may only occur once and must the unique. Each part of the key is a foreign key for the respective navigation property User, System and Role.
Then the other entities would have collections refering to this "link entity":
public class User
{
public int UserId { get; set; }
//...
public ICollection<UserSystemRole> UserSystemRoles { get; set; }
}
public class System
{
public int SystemId { get; set; }
//...
public ICollection<UserSystemRole> UserSystemRoles { get; set; }
}
public class Role
{
public int RoleId { get; set; }
//...
public ICollection<UserSystemRole> UserSystemRoles { get; set; }
}
And then the mapping with Fluent API would look like this:
modelBuilder.Entity<UserSystemRole>()
.HasKey(usr => new { usr.UserId, usr.SystemId, usr.RoleId });
modelBuilder.Entity<UserSystemRole>()
.HasRequired(usr => usr.User)
.WithMany(u => u.UserSystemRoles)
.HasForeignKey(usr => usr.UserId);
modelBuilder.Entity<UserSystemRole>()
.HasRequired(usr => usr.System)
.WithMany(s => s.UserSystemRoles)
.HasForeignKey(usr => usr.SystemId);
modelBuilder.Entity<UserSystemRole>()
.HasRequired(usr => usr.Role)
.WithMany(r => r.UserSystemRoles)
.HasForeignKey(usr => usr.RoleId);
You can remove one of the collection properties if you don't need them (use WithMany() without parameter then).
Edit
In order to get a dictionary for a user you could introduce a helper property (readonly and not mapped to the database) like so:
public class User
{
public int UserId { get; set; }
//...
public ICollection<UserSystemRole> UserSystemRoles { get; set; }
public IDictionary<System, IEnumerable<Role>> SystemRoles
{
get
{
return UserSystemRoles
.GroupBy(usr => usr.System)
.ToDictionary(g => g.Key, g => g.Select(usr => usr.Role));
}
}
}
Note that you need to load the UserSystemRoles property with eager loading first before you can access this dictionary. Or alternatively mark the UserSystemRoles property as virtual to enable lazy loading.

Related

Having trouble with entity framework core many to many relation

I am designing a database i have three models(user, movie, review).The idea is each user can submit a movie each user can leave a review for a movie.The user model has ICollection and ICollecyion, the movie model has foreign key UserId and ICollection, and the review model has foreign keys for UserId and MovieId. When i try update-database i get this error:
Introducing FOREIGN KEY constraint 'FK_Review_User_UserId' on table 'Review' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Here are my models :
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public int FavouriteGenre { get; set; }
public ICollection<Movie> SubmittedMovies { get; set; }
public ICollection<Review> SubmittedReviews { get; set; }
}
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string PosterUrl { get; set; }
public int Year { get; set; }
public int Genre { get; set; }
public DateTime Posted { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public ICollection<Review> Reviews { get; set; }
}
public class Review
{
public int Id { get; set; }
public int Rating { get; set; }
public string UserReview { get; set; }
public int MovieId { get; set; }
public Movie Movie { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public DateTime Posted { get; set; }
}
And my on creating method in the dbContext :
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Review>()
.HasOne(r => r.User)
.WithMany(u => u.SubmittedReviews)
.HasForeignKey(r => r.UserId);
builder.Entity<Review>()
.HasOne(r => r.Movie)
.WithMany(m => m.Reviews)
.HasForeignKey(r => r.MovieId);
}
I have tried this :
builder.Entity<Review>()
.HasOne(r => r.User)
.WithMany(u => u.SubmittedReviews)
.HasForeignKey(r => r.UserId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<Review>()
.HasOne(r => r.Movie)
.WithMany(m => m.Reviews)
.HasForeignKey(r => r.MovieId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
I have also tried making the foreign keys in the Review model nullable(i have tried separately and both at the same time) with DeleteBehaviour.Restrict in the context , but nothing seems to work , when i deleted a movie it worked well , but when i tried to delete a user i got this error in SSMS :
The DELETE statement conflicted with the REFERENCE constraint "FK_Review_User_UserId". The conflict occurred in database "MovieDb", table "dbo.Review", column 'UserId'.
I think i understand the problem but i cant find a solution.
From this article
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
Your schema contains a circular reference because a user can have a review on their own movie.
Here's what's (potentially) happening when you try to delete a user:
Reviews related to that user are cascade-deleted
Movies related to that user are cascade-deleted
Reviews related to that movie are cascade-deleted
The last two points there are what is causing the issue. Since both User and Movie have (I'm assuming) required relationships, you can have a scenario where the DB tries to delete the same record twice because it has a foreign key reference to a User and that same review is on a movie that the User created.
I would make the UserId field nullable/optional on the Reviews table.
Based on your description, I'm not sure if you set the property to be nullable as well, but your code should look like this:
public class Review
{
// Other fields hidden
public int? UserId { get; set; }
// ...
}
In your FluentAPI model builder definitions, you can set Movie Reviews to cascade, and SubmittedReviews on the users table to restrict on delete. You can also remove the .IsRequired(false) line from the model builder definition for Movie Reviews
Personally, this is why I just use "Soft Delete" columns (like DeletedBy, DeletedOn) in models with lots of dependent relationships. It's a bit more involved in your API code, as you have to manually go in and delete/mark
as deleted the related records, but it prevents headaches like this.

How to Map Twitter follower/following type of relation in EF Core 5

How do you configure something similar to Twitter Following and Follower type of relationship using EF Core 5 with the Fluent API? I tried various different ways of configuring it and the only few ways I was able to get it to work is if I ignored the navigation properties on the User entity. I am currently migrating my code from EF Core 2.1 to 5. The following configuration worked earlier. (Not sure if it is misconfigured)
public class User
{
public long Id { get; set; }
public string Name { get; set; }
public ICollection<UserFollower> Followers { get; set; }
public ICollection<UserFollower> Following { get; set; }
}
public class UserFollower
{
public long UserId { get; set; }
public User User { get; set; }
public long FollowedById { get; set; }
public User FollowedBy { get; set; }
}
public class UserFollowerConfiguration : IEntityTypeConfiguration<UserFollower>
{
public void Configure(EntityTypeBuilder<UserFollower> builder)
{
builder.HasKey(p => new { p.UserId, p.FollowedById });
builder.HasOne(p => p.User)
.WithMany(i => i.Followers)
.HasForeignKey(i => i.UserId);
builder.HasOne(p => p.FollowedBy)
.WithMany(i => i.Following)
.HasForeignKey(i => i.FollowedById);
}
}
This configuration throws an error when saving to the database.
SqlException: Violation of PRIMARY KEY constraint 'PK_UserFollower'.
Cannot insert duplicate key in object 'dbo.UserFollower'. The duplicate key value is (111, 111).
Even when trying to directly add to the DbContext and calling SaveChanges() on it.
Context.Add(new UserFollower() {UserId = 222, FollowedById = 111});
What is the recommended way of mapping such a relationship with EF Core 5? Note that I do need to access the UserFollowers table without going through the Navigation properties of the User.
Edit #1
The following is the OnModelCreating() for the DbContext
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurations(typeof(DbContext).Assembly);
/*few configurations unrelated to UserFollower entity*/
}
User entity has the following configuration,
builder.HasKey(i => i.Id);
builder.Property(i => i.Id).ValueGeneratedOnAdd();
Try configuring it like this.
builder.Entity<User>().HasMany(s => s.Followers)
.WithOne(f => f.FollowedBy);
builder.Entity<User>().HasMany(s => s.Following)
.WithOne(f => f.);
Also, The PK is missing for the UserFollower table, I don't know if an Id is being generated somehow somewhere. If not, maybe this is why it's trying to wrongly use FollowedById as key, but define an Id for the UserFollower table and see.
public class UserFollower
{
public long Id {get;set;}
public long UserId { get; set; }
public User User { get; set; }
public long FollowedById { get; set; }
public User FollowedBy { get; set; }
}
Even if this works, I would recommend you change the structure of your model, it looks ambigous for the twitter requirements you described. If I query Userfollowers
var userFollowers = _context.UserFollowers.ToList();
For each result in the list, there is no way for me to tell if the user is following or being followed. You could change your models to these ones;
public class User
{
public long Id { get; set; }
public string Name { get; set; }
public ICollection<UserFollower> Followers { get; set; }
public ICollection<UserFollowing> Following { get; set; }
}
public class UserFollower
{
public long UserId { get; set; }
public User User { get; set; }
public long UserFollowingMeId { get; set; }
public User UserFollowingMe { get; set; }
}
public class UserFollowing
{
public long UserId { get; set; }
public User User { get; set; }
public long UserIAmFollowingId { get; set; }
public User UserIAmFollowing { get; set; }
}
This way, everybody knows when they check the UserFollowings table, the UserId is the Id of the person that is following and vice versa for the UserFollowers table. If I had an Id of 8 in the system, I can query my followers and people I follow like this;
var myFollowers = _context.UserFollowers.Where(UserId = 8);
var peopleIFollow = _context.UserFollowing.Where(UserId = 8);

Entity Framework core multiple foreign keys same table error

i have looked at a ton of previous posts on how to have multiple foreign keys from the same table but I cant seem to get it working, even though my code seems identical to these posts. As far as I can tell, the below should work.
My User model
public string Name{get;set;}
public ICollection<UserOwnership> Slaves { get; set; } //Users this user owns
public ICollection<UserOwnership> Masters { get; set; } //Users that own this user
My user ownership model
public class UserOwnership
{
public int Id { get; set; }
public int MasterUserId { get; set; }
public int SlaveUserId { get; set; }
public virtual User Master { get; set; }
public virtual User Slave { get; set; }
}
UserOwnership mapping
public class UserOwnershipMapping : IEntityTypeConfiguration<UserOwnership>
{
public void Configure(EntityTypeBuilder<UserOwnership> builder)
{
builder.ToTable("UserOwnership");
builder.HasKey(e => new { e.Id });
builder.HasOne(e => e.Master).WithMany(e => e.Slaves).HasForeignKey(e => e.MasterUserId).OnDelete(DeleteBehavior.Restrict); // one master many slaves
builder.HasOne(e => e.Slave).WithMany(e => e.Masters).HasForeignKey(e => e.SlaveUserId).OnDelete(DeleteBehavior.Restrict); // one slave many masters
}
}
my error:
Unable to determine the relationship represented by navigation property 'User.Slaves' of type 'ICollection<UserOwnership>'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

Object is null when configuring one to one relationship with fluent api

I have three classes: Role, Permission and RolePermission(role permission is the third table in a many to many relationship)
public class Role : Entity
{
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<RolePermission> RolePermissions { get; set; }
}
public class Permission : Entity
{
public string Name { get; set; }
public virtual ICollection<RolePermission> RolePermissions { get; set; }
}
public class RolePermission : Entity
{
public int RoleId { get; set; }
public int PermissionId { get; set; }
public Permission Permission { get; set; }
public Role Role { get; set; }
}
Then I used fluentAPI in order to configure relationship:
For Role:
HasMany(role => role.RolePermissions)
.WithRequired(rolePermission => rolePermission.Role)
.HasForeignKey(rolePermission => rolePermission.RoleId);
For Permission:
HasMany(permission => permission.RolePermissions)
.WithRequired(rolePermission => rolePermission.Permission)
.HasForeignKey(rolePermission => rolePermission.PermissionId);
For RolePermission:
HasRequired(rolePermission => rolePermission.Permission)
.WithMany(permission => permission.RolePermissions)
.HasForeignKey(rolePermission => rolePermission.PermissionId);
HasRequired(rolePermission => rolePermission.Role)
.WithMany(role => role.RolePermissions)
.HasForeignKey(rolePermission => rolePermission.RoleId);
The problem is that only Role object is populated.
The code in this question pertains to setting up a relationship. The reported issue in this question pertains to related data not being loaded automatically. These are two different things that have little to do with one another.
Did you miss an Include somewhere? Have you accessed (and therefore lazily loaded) the Role nav prop, but not the Permission nav prop? I would like to see the code starting from where you launch the query up to where you inspect this object (as per your screenshot)
You responded with the requested code:
var user = _userRepository
.FirstOrDefaultAsync(us => us.Email == email);
var userPermissions =
user.UserRoles
.First()
.Role
.RolePermissions
.Select(rp => rp.Permission)
.ToList();
If you insert an Include() statement in your query, you will see that the Permission will actually be fetched correctly.
I am not quite sure which object you're inspecting. The screenshot tells me you're looking at a RolePermission, but the posted code suggests that you fetch a list of Permission objects.
Regardless, you seem to already have fixed it using an Include:
Mihai Alexandru-Ionut #Flater, yes, I have to use include and the problem is solved. This is the solution, so please post it as an answer in order to accept it.
Id Property is missing for both Role and Permission tables. When you say RoleId property in RolePermission table, EF looks for Id Property in Role table.
Update your Role and Permission tables like this and give a try:
public class Role : Entity
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<RolePermission> RolePermissions { get; set; }
}
public class Permission : Entity
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<RolePermission> RolePermissions { get; set; }
}

Relationship troubles with Entity Framework

I need help creating the relationship in entity framework as everything I have tried gives me errors when trying to add the migration or if I get passed that then I try to update the database and get an error about indexes with the same name.
public class Profile
{
public Profile()
{
Environments = new HashSet<Environment>();
}
[Key]
public int Id { get; set; }
public string VersionCreated { get; set; }
public string DiskLocation { get; set; }
public string Name { get; set; }
public DateTime DateTime { get; set; }
public virtual Product Product { get; set; }
public virtual Instance OriginalInstance { get; set; }
public virtual ICollection<Environment> Environments { get; set; }
}
public class Instance
{
public Instance()
{
TestResults = new HashSet<TestResult>();
Environments = new HashSet<Environment>();
}
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public string UserFriendlyName { get; set; }
public virtual Product Product { get; set; }
public virtual Profile LastKnownProfile { get; set; }
public virtual Computer Computer { get; set; }
public virtual ICollection<TestResult> TestResults { get; set; }
public virtual ICollection<Environment> Environments { get; set; }
}
The problem with the above classes is that the OrginalInstance property on the Profile class and the LastKnownProfile in the Instance class are supposed to just be foreign keys to those specific tables and they probably won't be the same very often. They can also both possibly be null.
I have tried:
modelBuilder.Entity<Instance>().HasRequired(i => i.LastKnownProfile);
modelBuilder.Entity<Profile>().HasRequired(p => p.OriginalInstance);
This gave me an Unable to determine the principal end of an association between the types 'EcuWeb.Data.Entities.Instance' and 'EcuWeb.Data.Entities.Profile'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations. error.
and with:
modelBuilder.Entity<Instance>().HasRequired(i => i.LastKnownProfile).WithOptional();
modelBuilder.Entity<Profile>().HasRequired(p => p.OriginalInstance).WithOptional();
The database adds a foreign key reference back to itself.
...that the OrginalInstance property on the Profile class and the
LastKnownProfile in the Instance class are supposed to just be foreign
keys to those specific tables and they probably won't be the same very
often. They can also both possibly be null.
In this case you actually want two one-to-many relationships between Profile and Instance if I don't misunderstand your quote above. It would mean that many Profiles can have the same OriginalInstance and that many Instances can have the same LastKnownProfile. The correct mapping would look like this then:
modelBuilder.Entity<Profile>()
.HasOptional(p => p.OriginalInstance)
.WithMany()
.Map(m => m.MapKey("OriginalInstanceId"));
modelBuilder.Entity<Instance>()
.HasOptional(i => i.LastKnownProfile)
.WithMany()
.Map(m => m.MapKey("LastKnownProfileId"));
The lines with MapKey are optional. Without them EF will create a foreign key with a default name.
Also note that you must use HasOptional (instead of HasRequired) if "both can possibly be null".

Categories