EF7 Foreign Keys , cascade and restrict - entity-framework

Trying to learn how to model relationships in EF7 and how to use Foreign Keys,Those are the models that I made up simplified
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
//..
public virtual List<BlogPost> BlogPosts { get; set; }
public virtual List<Comment> Comments { get; set; }
}
public class BlogPost
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int BlogPostId { get; set; }
//...
public int UserId { get; set; }
public virtual User User { get; set; }
public virtual List<Comment> Comments { get; set; }
}
public class Comment
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CommentId { get; set; }
//..
public int BlogPostId { get; set; }
public virtual BlogPost BlogPost { get; set; }
public int UserId { get; set; }
public virtual User User { get; set; }
}
User has One to Many Relationship with Comments
User has One to Many Relationship with BlogPosts
BlogPost has One to One Relationship with User
BlogPost has One to Many Relationship with Comment
Comment has One to One Relationship with User
Comment has One to One Relationship with BlogPost
When I delete a User all of his Comments and BlogPosts should be
deleted using the releveant FK
When I delete a BlogPost all of its Comments should be deleted using the relevant FK
So I translated the above into this
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Comment>().
HasOne(p => p.User).
WithMany(p => p.Comments).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Restrict).
HasForeignKey(p => p.UserId);
modelBuilder.Entity<Comment>().
HasOne(p => p.BlogPost).
WithMany(p => p.Comments).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Restrict).
HasForeignKey(p => p.BlogPostId);
modelBuilder.Entity<User>().
HasMany(p => p.Comments).
WithOne(p => p.User).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Cascade).
HasForeignKey(p => p.CommentId);
modelBuilder.Entity<User>().
HasMany(p => p.BlogPosts).
WithOne(p => p.User).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Cascade).
HasForeignKey(p => p.BlogPostId);
modelBuilder.Entity<BlogPost>().
HasOne(p => p.User).
WithMany(p => p.BlogPosts).
HasForeignKey(p => p.UserId).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Restrict);
modelBuilder.Entity<BlogPost>().
HasMany(p => p.Comments).
WithOne(p => p.BlogPost).
OnDelete(Microsoft.Data.Entity.Metadata.DeleteBehavior.Cascade).
HasForeignKey(p => p.CommentId);
}
But this looks like too much typing, and I can't find good docs yet on Entity Framework 7. My question is, am I doing anything unnecessary given the points I mentioned above, can I type less and still get what I mentioned?

Related

Correct way to use Many2Many in EF Core6?

I am quite new to EF Core 6.0. We currently have a projet to upgrade, we cannot change the actual tables (use by another program) so we use Database fisrt approch.
So I need to add some Permission on user (the database are in french) We curently have an UsagerEW table (user table) and we add an Permission Table and an joint table PermissionUsagerEW for the Many2Many. After doing Scaffold-dbContect here is the result:
UsagerEW (primary key is Code_Int)
public partial class UsagerEW
{
public UsagerEW()
{
PermissionUsagerEW = new HashSet<PermissionUsagerEW>();
RefreshToken = new HashSet<RefreshToken>();
}
public string Code { get; set; }
public string Nom { get; set; }
public string Email { get; set; }
public string ModeLogin { get; set; }
public string PasswordTemp { get; set; }
public DateTime? PasswordTempExp { get; set; }
public int code_int { get; set; }
public virtual ICollection<PermissionUsagerEW> PermissionUsagerEW { get; set; }
}
Pemrssion and PermissionUsagerEW
public partial class Permission
{
public Permission()
{
PermissionUsagerEW = new HashSet<PermissionUsagerEW>();
}
public int id { get; set; }
public string code { get; set; }
public string description { get; set; }
public int? moduleId { get; set; }
public virtual Module module { get; set; }
public virtual ICollection<PermissionUsagerEW> PermissionUsagerEW { get; set; }
}
public partial class PermissionUsagerEW
{
public int id { get; set; }
public int permissionId { get; set; }
public int usagerCodeInt { get; set; }
public virtual Permission permission { get; set; }
public virtual UsagerEW usagerCodeIntNavigation { get; set; }
}
That compile and I can "navigate with include" from UsagerEW and get an list of PermissionUsagerEW for a specific UsagerEW.
Now like I am in EF COre 6.0 that supposed to support Many2Many
I add this nav propertie in the Permnission class
public virtual ICollection<UsagerEW> UsagerEW { get; set; }
and this in the UsagerEW class:
public virtual ICollection<Permission> Permission { get; set; }
But I got execution error either I just try to load some user wintout any include:
UsagerEW user = _EWContext.UsagerEW.Where(u=>u.Code == usagerId).SingleOrDefault();
System.InvalidOperationException: 'Cannot use table
'PermissionUsagerEW' for entity type 'PermissionUsagerEW
(Dictionary<string, object>)' since it is being used for entity type
'PermissionUsagerEW' and potentially other entity types, but there is
no linking relationship. Add a foreign key to 'PermissionUsagerEW
(Dictionary<string, object>)' on the primary key properties and
pointing to the primary key on another entity type mapped to
'PermissionUsagerEW'.'
The FK are detect by the scaffold:
modelBuilder.Entity<PermissionUsagerEW>(entity =>
{
entity.HasOne(d => d.permission)
.WithMany(p => p.PermissionUsagerEW)
.HasForeignKey(d => d.permissionId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PermissionUsager_Permission");
entity.HasOne(d => d.usagerCodeIntNavigation)
.WithMany(p => p.PermissionUsagerEW)
.HasForeignKey(d => d.usagerCodeInt)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PermissionUsager_Usager");
});
Any idea?
---EDIT 1
I change your code to reflect the scaffolded PermissionUsagerEW table:
//--UsagewrEW
modelBuilder.Entity<UsagerEW>()
.HasKey(u => u.code_int);
modelBuilder.Entity<UsagerEW>()
.HasMany(u => u.Permissions)
.WithMany(p => p.Users)
.UsingEntity<PermissionUsagerEW>(
p => p.HasOne(e => e.permission)
.WithMany()
.HasForeignKey(e => e.permissionId),
p => p.HasOne(p => p.usagerCodeIntNavigation)
.WithMany()
.HasForeignKey(e => e.usagerCodeInt)
);
modelBuilder.Entity<PermissionUsagerEW>()
.HasOne(p => p.usagerCodeIntNavigation)
.WithMany()
.HasForeignKey(p => p.usagerCodeInt);
When testing with
UsagerEW user = _EWContext.UsagerEW.Where(u=>u.Code == usagerId).Include(u => u.Permissions).SingleOrDefault();
Now I got this error:
Microsoft.Data.SqlClient.SqlException: 'Invalid column name
'UsagerEWcode_int'.'
I think EF tries to link something automatically. I do not have any UsagerEWcode_int in my solution.
EDIT2:
There is the SQL generated. Wierd column name and some repetition...
SELECT [u].[code_int], [u].[Administrateur], [u].[Code], [u].[Email], [u].[EmpContact], [u].[Inactif], [u].[KelvinConfig], [u].[LectureSeule], [u].[ModeLogin], [u].[Nom], [u].[ParamRole], [u].[Password], [u].[PasswordTemp], [u].[PasswordTempExp], [u].[RestreintCommContrat], [u].[RestreintProjet], [u].[Role], [u].[UsagerAD], [u].[doitChangerPW], [u].[estSuperviseur], [u].[idSuperviseur], [u].[infoSession], [u].[paramRole2], [u].[permsGrps], [t].[id], [t].[Permissionid], [t].[UsagerEWcode_int], [t].[permissionId0], [t].[usagerCodeInt], [t].[id0], [t].[code], [t].[description], [t].[moduleId]
FROM [UsagerEW] AS [u]
LEFT JOIN (
SELECT [p].[id], [p].[Permissionid], [p].[UsagerEWcode_int], [p].[permissionId] AS [permissionId0], [p].[usagerCodeInt], [p0].[id] AS [id0], [p0].[code], [p0].[description], [p0].[moduleId]
FROM [PermissionUsagerEW] AS [p]
INNER JOIN [Permission] AS [p0] ON [p].[permissionId] = [p0].[id]
) AS [t] ON [u].[code_int] = [t].[usagerCodeInt]
WHERE [u].[Code] = #__usagerId_0
ORDER BY [u].[code_int], [t].[id]
You can configure direct Many-to-Many relationships with an existing database, and you can have the linking entity in the model or exclude it. There are several examples in the docs. And you can leave the foreign key properties in the model, or you can replace them with shadow properties. But the Scaffolding code doesn't do any of this for you. It creates the simplest correct model for the database schema.
Also you usually should rename the entities and properties to align with .NET coding conventions.
Anyway something like this should work:
public partial class UsagerEW
{
public string Code { get; set; }
public string Nom { get; set; }
public string Email { get; set; }
public string ModeLogin { get; set; }
public string PasswordTemp { get; set; }
public DateTime? PasswordTempExp { get; set; }
public int code_int { get; set; }
public virtual ICollection<Permission> Permissions { get; } = new HashSet<Permission>();
}
public partial class Permission
{
public int Id { get; set; }
public string Code { get; set; }
public string Description { get; set; }
public int? ModuleId { get; set; }
//public virtual Module module { get; set; }
public virtual ICollection<UsagerEW> Users { get; } = new HashSet<UsagerEW>();
}
public partial class PermissionUsagerEW
{
public int Id { get; set; }
public int PermissionId { get; set; }
public int UsagerCodeInt { get; set; }
public virtual Permission Permission { get; set; }
public virtual UsagerEW User { get; set; }
}
public class Db : DbContext
{
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<UsagerEW>()
.HasKey(u => u.code_int);
builder.Entity<UsagerEW>()
.HasMany(u => u.Permissions)
.WithMany(p => p.Users)
.UsingEntity<PermissionUsagerEW>(
p => p.HasOne(e => e.Permission)
.WithMany()
.HasForeignKey(e => e.PermissionId),
p => p.HasOne(p => p.User)
.WithMany()
.HasForeignKey( e => e.UsagerCodeInt)
);
builder.Entity<PermissionUsagerEW>()
.HasOne(p => p.User)
.WithMany()
.HasForeignKey(p => p.UsagerCodeInt);
foreach (var prop in builder.Model.GetEntityTypes().SelectMany(e => e.GetProperties()))
{
prop.SetColumnName(char.ToLower(prop.Name[0]) + prop.Name.Substring(1));
}
base.OnModelCreating(builder);
}
But when you're working in a database-first workflow, there's a downside to deeply customizing the EF model: you loose the ability to regenerate the EF model from the database.
So you can use a "nice" customized EF model, or a "plain" scaffolded model. If you customize the model, you can no longer regenerate it, and need to alter it to match future database changes by hand.
You can apply some customizations, though, like the convention-based property-to-column and entity-to-table mappings in the example. But changing the generated "indirect many-to-many" to "direct many-to-many" will prevent you from regenerating the EF model through scaffolding.

Use EF Core 5 many-to-many relation with the "Join table" that has extra data

I would like to ask if anyone can help me with EF Core 5.
I have two tables that are in "many-to-many" relationship: on the Join table, in addition to the columns that act as foreign keys I also have other columns that I would like to map in EF Core.
The only solution I can think of is to create the relationship as it was done in EF Core 3, that is to use a "one to many" relationship with the join table.
Any suggestions?
Thanks.
Ps. Sorry for my english.
The only solution I can think of is to create the relationship as it was done in EF Core 3, that is to use a "one to many" relationship with the join table.
You can have your cake and eat it too.
EF Core 5 supports custom linking entities and using skip-level navigation at the same time.
There is an example in the docs where the linking entity is in the model, has additional properties, but the main entities skip over the linking entity with Collection Navigation Properties.
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
PostTag is a regular entity, and you can access it with db.Set<PostTag>() or from a Post or a Tag. Note the (optional) navigation properties from Post and Tag to PostTag.

Self referencing loop detected for property with type

I have 3 instances, Author, EventInstance, and Comments. An author can write a comment. People (authors) can add comments to an EventInstance. Here's the model. I keep getting the following error.
public class Author
{
public Guid Id { get; set; }
//..
public List<EventInstance> Events { get; set; }
= new List<EventInstance>();
public ICollection<Comment> Comments { get; set; }
= new List<Comment>();
}
public class EventInstance
{
public Guid Id { get; set; }
public Guid AuthorId { get; set; } //Author FK
//..
public Author Author { get; set; }
public ICollection<Comment> Comments { get; set; }
= new List<Comment>();
}
public class Comment
{
public Guid Id { get; set; }
public Guid AuthorId { get; set; } //Author FK
public Guid EventId { get; set; }
public EventInstance Event { get; set; }
public Author Author { get; set; }
}
I've tried several ways to fix but nothing seems to be working. Even a simple query like:
var list = await db.Events
.Include(e => e.Author)
.ToListAsync();
Thank for helping.
EDIT
Here's my context class
public class EventContext : DbContext
{
public DbSet<EventInstance> Events { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<Author> Authors { get; set; }
public EventContext(DbContextOptions<EventContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<EventInstance>()
.HasOne(e => e.Author)
.WithMany(a => a.Events)
.HasForeignKey(e => e.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<Comment>()
.HasOne(c => c.Event)
.WithMany(e => e.Comments)
.HasForeignKey(c => c.EventId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<Comment>()
.HasOne(c => c.Author)
.WithMany(a => a.Comments)
.HasForeignKey(c => c.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
}
}
I'm using a Factory for add and run migrations
public class EventContextFactory : IDesignTimeDbContextFactory<EventContext>
{
public EventContext CreateDbContext(string[] args)
{
string connectionString =
Environment.GetEnvironmentVariable("SqlConnectionString");
var optionsBuilder = new DbContextOptionsBuilder<EventContext>();
optionsBuilder.UseSqlServer(connectionString);
return new EventContext(optionsBuilder.Options);
}
}
And this is the startup class
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
string connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
builder.Services.AddDbContext<EventContext>(
options => options.UseSqlServer(connectionString));
builder.Services.AddScoped<IEventService, EventService>();
}
}
I hope all the relevant pieces have been pasted here.
You should consider normalizing the structure to avoid the double referencing. For instance, if Authors have a collection of event instances and event instances have a list of comments then authors can get their comments through event instances.
public class Author
{
public Guid Id { get; set; }
//..
public virtual ICollection<EventInstance> Events { get; set; }
= new List<EventInstance>();
}
public class EventInstance
{
public Guid Id { get; set; }
[ForeignKey("Author")]
public Guid AuthorId { get; set; }
//..
public Author Author { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public Guid Id { get; set; }
[ForeignKey("Event")]
public Guid EventId { get; set; }
public EventInstance Event { get; set; }
}
Then to get the Comments for a particular Author:
var commentsForAuthor = context.Authors
.Where(x => x.Id == authorId)
.SelectMany(x => x.Events.SelectMany(e => e.Comments))
.ToList();
Alternatively, if Comments can be treated as a top-level entity (/w DbSet)
var commentsForAuthor = context.Comments
.Where(x => x.EventInstance.AuthorId == authorId)
.ToList();
Typically you would use Projection to get details about a comment, it's associated event, and author. (Using Select) This can navigate through the relational structure to get the appropriate details.
You likely can apply mapping expressions to remove the error with explicit mapping, but I'd recommend removing the denormalization. The trouble with the denormalization the references so that Author can have a collection of Comments is that there is no way to enforce that a Comment's Author reference matches the Author of the Comment's EventInstance. I.e. I can have an author ID 1, with EventInstance ID 1, and create a Comment with ID 101. Nothing stops me from changing that Comment's Author ID to 2. Now I have two sources of truth as to who the author is. Comment.Author (ID: 2) and Comment.EventInstance.Author. (ID: 1) Wherever possible, remove those duplicate reference paths.
EDIT: Mapping feedback
With mapping references and back, you will want to avoid double-mapping in the sense of mapping a relationship from both entities. Map relationships from one side. You could try:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EventInstance>()
.HasOne(x => x.Author)
.WithMany(x => x.Events)
.HasForeignKey(x => x.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<EventInstance>()
.HasMany(x => x.Comments)
.WithOne(x => x.Event)
.HasForeignKey(x => x.EventId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Author>()
.Ignore(a => a.AuthorName)
.HasMany(x => x.Comments)
.WithOne(x => x.Author)
.HasForeignKey(x => x.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
}
Edit #2: If Authors can create comments for another author's events, then I would suggest removing the Comments relationship from the Author side. Comments can be retrieved as a top-level entity or through the Events as a top level entity.
For instance:
public class Author
{
public Guid Id { get; set; }
//..
public virtual ICollection<EventInstance> Events { get; set; }
= new List<EventInstance>();
}
public class EventInstance
{
public Guid Id { get; set; }
public Guid AuthorId { get; set; }
//..
public Author Author { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public Guid Id { get; set; }
public Guid EventId { get; set; }
public virtual EventInstance Event { get; set; }
public Guid AuthorId { get; set; }
public virtual Author Author { get; set; }
}
Then in mapping:
modelBuilder.Entity<Comment>()
.HasOne(x => x.Author)
.WithMany()
.HasForeignKey(x => x.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Comment>()
.HasOne(x => x.Event)
.WithMany(x => x.Comments)
.HasForeignKey(x => x.EventId)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Author>()
.Ignore(a => a.AuthorName);
This switches around the mapping from the Event over to the Comment, but the Comment maintains a bi-directional reference to it's Event, but a single-direction reference to its Author. (HasMany()) This should satisfy EF's mapping. On a side note with the explicit relationship mapping using OnModelCreating or IEntityTypeConfiguration we don't need the [ForeignKey] attributes so I removed those.

Setting one to many relationship in EF fluent mapping

I've two classes like this,
public class Post
{
public int Id { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public virtual IList<Post> Posts { get; set; }
}
BlogDbContext.cs
OnModelCreating method:
modelBuilder.Entity<Post>()
.HasRequired(x => x.Category)
.WithMany(x => x.Posts)
.HasForeignKey(x => x.Id);
On running the application I'm getting the below error.
Post_Category_Source: : Multiplicity is not valid in Role 'Post_Category_Source' in relationship 'Post_Category'. Because the Dependent Role refers to the key properties, the upper bound of the multiplicity of the Dependent Role must be '1'.
Any help will be really appreciated.
If you look closely at the statement...
HasForeignKey(x => x.Id)
...you'll see that x is not a Category but a Post. So it's trying to use Post's primary key as foreign key pointing to Category. This is a valid configuration, but only in 1-1 associations, hence the somewhat cryptic exception message.
This is what you're after:
public class Post
{
public int Id { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
modelBuilder.Entity<Post>()
.HasRequired(x => x.Category)
.WithMany(x => x.Posts)
.HasForeignKey(x => x.CategoryId);

EF many-to-many with two properties

I have a sample code:
public class Tag
{
public int TagId { get; set; }
public virtual ICollection<User> Users { get; set; }
}
public class User
{
public int UserId { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
When I run EF over my model (i use code first approach), i get some tables automatically created in my db:
Users
Tags
UserTagUsers <-- junction table for many-to-many relationship
It is okay, till I decide to add one more property to User entity:
public class User
{
public int UserId { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public virtual ICollection<Tag> Tags2 { get; set; }
}
in this case EF generates completely different relations, it removes UserTagUsers junction table, but adds some additional properties to Tags table in order to make it one-to-one mapping.
How can I explicitly tell EF to make the property Tags and Tags2 to be many-to-many?
Use fluent API to configure the mappings
modelBuilder.Entity<User>()
.HasMany(u => u.Tags).WithMany(t => t.Users)
.Map(m =>
{
m.ToTable("UserTags");
m.MapLeftKey("UserId");
m.MapRightKey("TagId");
});
modelBuilder.Entity<User>()
.HasMany(u => u.Tags2).WithMany(t => t.Users2)
.Map(m =>
{
m.ToTable("UserTags2");
m.MapLeftKey("UserId");
m.MapRightKey("TagId");
});