Prevent deletion if relationship exist - Fluent API - entity-framework

There's a One to Many relationship between Catergory and Product. One catergory will have many products. However, when I try to delete a catergory, and if there's a product with that catergory I shouldn't be allowed to do so.
In the code I wrote it allows deletion. When I delete a Catergory it also deletes the associated product.
What I want my code to do is, to do is to prevent me from deleting a Catergory if there are corresponding records.
Can someone help me sort this out.
Catergory
public class Catergory
{
public int CatergoryId { get; set; }
public string CatergoryName { get; set; }
public string CatergoryDescription { get; set; }
public ICollection<Product> Products { get; set; }
}
Product
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public int CatergoryID { get; set; }
public Catergory Catergory { get; set; }
}
Using Fluent API to map relationships
CatergoryConfiguration
class CatergoryConfiguration : IEntityTypeConfiguration<Catergory>
{
public void Configure(EntityTypeBuilder<Catergory> builder)
{
builder.ToTable("Catergory");
builder.HasKey(c => c.CatergoryId);
builder.Property(c => c.CatergoryName)
.IsRequired(true)
.HasMaxLength(400);
builder.Property(c => c.CatergoryDescription)
.IsRequired(true);
}
}
ProductConfig
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Product");
builder.HasKey(p => p.ProductID);
builder.Property(p => p.ProductName)
.HasMaxLength(400)
.IsRequired(true);
builder.Property(p => p.ProductDescription)
.HasMaxLength(2000)
.IsRequired(true);
builder.HasOne(f => f.Catergory)
.WithMany(r => r.Products)
.HasForeignKey(f => f.CatergoryID);
.OnDelete(DeleteBehavior.Restrict);
}
}

Unfortunately you cannot configure this in FluentAPI.
OnDelete only set the behaviour of how to handle related entities when a principal entity is deleted.
In your Delete Method you need to include logic to check if a Category has Products before deleting.

Related

Configuring the IdentityModels navigation property with Guid for Entity Framework

I get on creating Migration some Warnings like this one:
The foreign key property 'AppUserClaim.UserId1' was created in shadow state because a conflicting property with the simple name 'UserId' exists in the entity type, but is either not mapped, is already used for another relationship, or is incompatible with the associated primary key type. See https://aka.ms/efcore-relationships for information on mapping relationships in EF Core.
It applies to all entities with AppUser navigation property. Other navigation properties has no warning.
public class AppUser : IdentityUser<Guid>, IChangeTrackerObject
{
public string FirstName { get; set; }
public string LastName { get; set; }
[Column(TypeName = "text")]
public string ProfilePictureDataUrl { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public string ChangedBy { get; set; }
public DateTime? ChangedOn { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedOn { get; set; }
public bool IsActive { get; set; }
public string RefreshToken { get; set; }
public DateTime RefreshTokenExpiryTime { get; set; }
public virtual ICollection<AppUserClaim> Claims { get; set; }
public virtual ICollection<AppUserLogin> Logins { get; set; }
public virtual ICollection<AppUserToken> Tokens { get; set; }
public virtual ICollection<AppUserRole> UserRoles { get; set; }
public AppUser()
{
}
}
public class AppUserClaim : IdentityUserClaim<Guid>, IChangeTrackerObject
{
public string ChangedBy { get; set; }
public DateTime? ChangedOn { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public virtual AppUser User { get; set; }
}
private static void BuildIdentity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AppUser>(entity =>
{
entity.ToTable(name: "Users", schema);
entity.Property(e => e.Id).ValueGeneratedOnAdd();
// Each User can have many UserClaims
entity.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
// Each User can have many UserLogins
entity.HasMany(e => e.Logins)
.WithOne()
.HasForeignKey(ul => ul.UserId)
.IsRequired();
// Each User can have many UserTokens
entity.HasMany(e => e.Tokens)
.WithOne()
.HasForeignKey(ut => ut.UserId)
.IsRequired();
// Each User can have many entries in the UserRole join table
entity.HasMany(e => e.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
modelBuilder.Entity<AppUserClaim>(entity =>
{
entity.ToTable("UserClaims", schema);
});
}
I ran into a similar issue. In my OnModelCreating method, I had flipped the order in which I was applying migrations.
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
Basically, it seemed like if the call to the base method came after the call to apply the configurations from my code, then the configurations in the base method would override my configurations, which gave me a similar error to what you had. So what I'm assuming is happening is you're calling BuildIdentity() after you call base.OnModelCreating(). You may need to reverse that order, otherwise the relationships defined in the default identity DB may take precedence.

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.

Entity Framework Core 3.0 Many-to-Many for the same table

I am using ef core 3.0 code-first database. I have a table, Status, and I need to create a relationship to itself to list the possible "next status" List<Status> SubsequentStatuses. This is of course to systematically control the workflow of the object.
Using this at face value, it creates a one-to-many relationship and a new StatusId column in the table; however, I need to be able to set a status to be a "SubsequentStatus" to more than one Status.
For example, if there are 4 statuses:
New
In Work
Complete
Cancelled
I want to have the following
New
Subsequent Statuses
In Work
Cancelled
In Work
Subsequent Statuses
Complete
Cancelled
Complete
None
Cancelled
None
Notice that "Cancelled" is related to both "New" and "In Work"
Here are the classes and config that I have at this point:
public class EstimateStatus
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<EstimateStatusRel> SubsequentStatuses { get; set; }
}
public class EstimateStatusRel
{
public int EstimateStatusId { get; set; }
public EstimateStatus EstimateStatus { get; set; }
public int SubsequentStatusId { get; set; }
public EstimateStatus SubsequentStatus { get; set; }
}
public class SapphireContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EstimateStatusRel>().HasKey(x => new { x.EstimateStatusId, x.SubsequentStatusId });
modelBuilder.Entity<StatusRel>()
.HasOne(pt => pt.Status)
.WithMany(p => p.SubsequentStatuses)
.HasForeignKey(pt => pt.StatusId);
}
}
The issue this is creating, is that when Entity Framework is building the migration, it errors out about the multiple cascading delete action, but when I add the NoAction modifier to the modelBuilder fluent API, it still does not clear the error
It ended up being because I didn't specify an OnDelete action
This is my final config:
modelBuilder.Entity<EstimateStatusRel>()
.HasOne(pt => pt.Status)
.WithMany(p => p.SubsequentStatus)
.HasForeignKey(pt => pt.EstimateStatusId)
.OnDelete(DeleteBehavior.NoAction);
For self-reference in one-to-many relationships, you could try the below code:
public class EstimateStatus
{
public int Id { get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public EstimateStatus ParentStatuses { get; set; }
public virtual ICollection<EstimateStatus> SubsequentStatuses { get; set; }
}
public class TestDbContext:DbContext
{
public TestDbContext (DbContextOptions<TestDbContext> options):base(options)
{ }
public DbSet<EstimateStatus> EstimateStatuse { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EstimateStatus>()
.HasMany(e => e.SubsequentStatuses)
.WithOne(s => s.ParentStatuses)
.HasForeignKey(e => e.ParentId);
}
}

Entity Framework Core: How to solve Introducing FOREIGN KEY constraint may cause cycles or multiple cascade paths

I'm using Entity Framework Core with Code First approach but recieve following error when updating the database:
Introducing FOREIGN KEY constraint 'FK_AnEventUsers_Users_UserId' on table 'AnEventUsers' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
My entities are these:
public class AnEvent
{
public int AnEventId { get; set; }
public DateTime Time { get; set; }
public Gender Gender { get; set; }
public int Duration { get; set; }
public Category Category { get; set; }
public int MinParticipants { get; set; }
public int MaxParticipants { get; set; }
public string Description { get; set; }
public Status EventStatus { get; set; }
public int MinAge { get; set; }
public int MaxAge { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public ICollection<AnEventUser> AnEventUsers { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
public class User
{
public int UserId { get; set; }
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Gender Gender { get; set; }
public double Rating { get; set; }
public ICollection<AnEventUser> AnEventUsers { get; set; }
}
public class AnEventUser
{
public int AnEventId { get; set; }
public AnEvent AnEvent { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
public class ApplicationDbContext:DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AnEventUser>()
.HasOne(u => u.User).WithMany(u => u.AnEventUsers).IsRequired().OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<AnEventUser>()
.HasKey(t => new { t.AnEventId, t.UserId });
modelBuilder.Entity<AnEventUser>()
.HasOne(pt => pt.AnEvent)
.WithMany(p => p.AnEventUsers)
.HasForeignKey(pt => pt.AnEventId);
modelBuilder.Entity<AnEventUser>()
.HasOne(eu => eu.User)
.WithMany(e => e.AnEventUsers)
.HasForeignKey(eu => eu.UserId);
}
public DbSet<AnEvent> Events { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<AnEventUser> AnEventUsers { get; set; }
}
The issue I thought was that if we delete a User the reference to the AnEvent will be deleted and also the reference to AnEventUser will also be deleted, since there is a reference to AnEventUser from AnEvent as well we get cascading paths. But I remove the delete cascade from User to AnEventUser with:
modelBuilder.Entity<AnEventUser>()
.HasOne(u => u.User).WithMany(u => u.AnEventUsers).IsRequired().OnDelete(DeleteBehavior.Restrict);
But the error doesn't get resolved, does anyone see what is wrong? Thanks!
In your sample code in OnModelCreating you have declared modelBuilder.Entity<AnEventUser>().HasOne(e => e.User)... twice: at start of method and at end.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AnEventUser>() // THIS IS FIRST
.HasOne(u => u.User).WithMany(u => u.AnEventUsers).IsRequired().OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<AnEventUser>()
.HasKey(t => new { t.AnEventId, t.UserId });
modelBuilder.Entity<AnEventUser>()
.HasOne(pt => pt.AnEvent)
.WithMany(p => p.AnEventUsers)
.HasForeignKey(pt => pt.AnEventId);
modelBuilder.Entity<AnEventUser>() // THIS IS SECOND.
.HasOne(eu => eu.User) // THIS LINES
.WithMany(e => e.AnEventUsers) // SHOULD BE
.HasForeignKey(eu => eu.UserId); // REMOVED
}
Second call overrides first. Remove it.
This is what I did from the answer of Dmitry,
and It worked for me.
Class:
public class EnviornmentControls
{
public int Id { get; set; }
...
public virtual Environment Environment { get; set; }
}
And it's Mapping
public EnviornmentControlsMap(EntityTypeBuilder<EnviornmentControls> entity)
{
entity.HasKey(m => m.Id);
entity.HasOne(m => m.Environment)
.WithMany(m => m.EnviornmentControls)
.HasForeignKey(m => m.EnvironmentID)
.OnDelete(DeleteBehavior.Restrict); // added OnDelete to avoid sercular reference
}
These solutions didn't work for my case, but I found a way. I am not quite sure yet if it is safe but there's just something that's happening with deleting. So I modified the generated Migration File instead of putting an override.
onDelete: ReferentialAction.Cascade
The reason I did this because all the overriding mentioned above is not working for me so I manually removed the code which relates to Cascading of Delete.
Just check which specific relation being mentioned at the error so you can go straightly.
Hope this will be able to help for some people who's having the same issue as mine.
public Guid? UsuarioId { get; set; }
builder.Entity<Orcamentacao>()
.HasOne(x => x.Usuario)
.WithMany(x => x.Orcamentacaos)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired(false)
.HasForeignKey(x => x.UsuarioId);

Entity Framework Navigation Properties Are Null

I am having a problem where my Entity Framework navigation properties are null. My two models are Order and OrderLine:
class Order
{
public string CustomerId { get; set; }
public string OrderNumber { get; set; }
public ICollection<OrderLine> Lines { get; set; }
}
class OrderLine
{
public int LineNumber { get; set; }
public string OrderNumber { get; set; }
public string ProductId { get; set; }
public int Quantity { get; set; }
public Order Order { get; set; }
}
My Context class looks like this
class MyContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<OrderLine> OrderLines { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasKey(p => p.OrderNumber);
modelBuilder.Entity<OrderLine>()
.HasKey(p => new { p.OrderNumber, p.LineNumber });
modelBuilder.Entity<OrderLine>()
.HasRequired(p => p.Order)
.WithMany(p => p.Lines)
.HasForeignKey(p => p.OrderNumber);
}
}
When I run the following code, my orders load (the message box shows the correct count), but the Order.Lines collection is null.
List<Order> orders = (from o in context.Orders select o).ToList();
// This message box shows the correct number of orders
MessageBox.Show(orders.Count.ToString());
// This line crashes because orders[0].Lines is null. There are lines in the database that should be joining to orders[0]
MessageBox.Show(orders[0].Lines.Count.ToString());
I've looked at a lot of examples, and I can't figure out what I'm doing incorrectly.
You need to declare the navigation properties as virtual in order to be lazy loaded:
public class Order
{
//...
public virtual ICollection<OrderLine> Lines { get; set; }
}
public class OrderLine
{
//...
public virtual Order Order { get; set; }
}
For more info check this link to see all the requirements you need to follow.