Entity Framework Core shared table with cascade delete - entity-framework

I try to create the following database design with EF Core (code-first)
Entity "Recipe" can have a list of type "Resource"
Entity "Shop" can have a single "Resource"
Entity "InstructionStep" can have a list of type "Resource"
If I delete a resource from the "Recipe", "InstructionStep" (collections) or from the "Shop" (single-property) then the corresponding "Resource" entity should be also deleted. (Cascade Delete)
I already tried several things with and without mapping tables but none of my approach was successful.
Another idea was to have a property "ItemRefId" in the "Resource" entity to save the "RecipeId/ShopId/InstructionStepId" but I don't get it to work...
Example Classes:
public class Recipe
{
public int RecipeId { get; set; }
public string Title { get; set; }
public ICollection<RecipeResource> Resources { get; set; } = new List<RecipeResource>();
}
public class Shop
{
public int ShopId { get; set; }
public string Title { get; set; }
public Resource Logo { get; set; }
}
public class Resource
{
public int ResourceId { get; set; }
public string Path { get; set; }
public int ItemRefId { get; set; }
}
public class InstructionStep
{
public string InstructionStepId { get; set; }
public string Title { get; set; }
public ICollection<RecipeResource> Resources { get; set; } = new List<RecipeResource>();
}
Any suggestions? Many thanks in advance.

That's not cascade delete. Cascade delete would be when a Recipe is deleted, all of the related Resources are deleted as well.
In EF Core 3, you can use Owned Entity Types for this. The generated relational model is different from what you are proposing, in that Recipe_Resource and InstructionStep_Resource will be seperate tables, and Shop.Logo will be stored in columns on the Shop table. But that's the correct relational model. Having one Resource table with some rows referencing a Recipe and some rows referencing an InstructionStep is a bad idea.
This scenario is sometimes called a "Strong Relationship" where the identity of the related entity is dependent on the main entity, and should be implemented in the relational model by having the the Foreign Key columns be Primary Key columns on the dependent entity. That way there's no way remove a Recipe_Resource without deleting it.
eg
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
namespace EfCore3Test
{
public class Recipe
{
public int RecipeId { get; set; }
public string Title { get; set; }
public ICollection<Resource> Resources { get; } = new List<Resource>();
}
public class Shop
{
public int ShopId { get; set; }
public string Title { get; set; }
public Resource Logo { get; set; }
}
public class Resource
{
public int ResourceId { get; set; }
public string Path { get; set; }
public int ItemRefId { get; set; }
}
public class InstructionStep
{
public string InstructionStepId { get; set; }
public string Title { get; set; }
public ICollection<Resource> Resources { get; } = new List<Resource>();
}
public class Db : DbContext
{
public DbSet<Recipe> Recipes { get; set; }
public virtual DbSet<Shop> Shops { get; set; }
public virtual DbSet<InstructionStep> InstructionSteps { get; set; }
private static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddFilter((category, level) =>
category == DbLoggerCategory.Database.Command.Name
&& level == LogLevel.Information).AddConsole();
});
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(loggerFactory)
.UseSqlServer("Server=.;database=EfCore3Test;Integrated Security=true",
o => o.UseRelationalNulls());
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Shop>().OwnsOne(p => p.Logo);
modelBuilder.Entity<InstructionStep>().OwnsMany(p => p.Resources);
modelBuilder.Entity<Recipe>().OwnsMany(p => p.Resources);
}
}
class Program
{
static void Main(string[] args)
{
using var db = new Db();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var r = new Recipe();
r.Resources.Add(new Resource() { ItemRefId = 2, Path = "/" });
db.Recipes.Add(r);
db.SaveChanges();
r.Resources.Remove(r.Resources.First());
db.SaveChanges();
var s = new Shop();
s.Logo = new Resource { ItemRefId = 2, Path = "/" };
db.Shops.Add(s);
db.SaveChanges();
s.Logo = null;
db.SaveChanges();
}
}
}

Related

Entity Framework Core multiple relationships to same table

I have a problem with two references to the same table with different columns:
public class MainApplicationContext : DbContext
{
public MainApplicationContext(MainSqlDbContext mainSqlDbContext)
{
MainSqlDbContext = mainSqlDbContext;
this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Organisation> Organisations { get; set; }
public DbSet<OrganisationContact> OrganisationContacts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Organisation>()
.HasKey(t => new { t.OrgId, t.OrgType, });
modelBuilder.Entity<OrganisationContact>().Property(p => p.OcsId).HasValueGenerator<SequenceNumberValueGenerator>().ValueGeneratedOnAdd();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(MainSqlDbContext.Database.GetDbConnection());
base.OnConfiguring(optionsBuilder);
}
private MainSqlDbContext MainSqlDbContext;
}
[SequenceNameAttribute("ORGANISATIONCONTACTS", "web")]
[Table("ORGANISATIONCONTACTS", Schema = "dbo")]
[Serializable]
public partial class OrganisationContact
{
[Column("OCS_ACTIVE")]
[MaxLength(1)]
public string OcsActive { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Key]
[Column("OCS_ID")]
public int OcsId { get; set; }
[Column("OCS_NAME")]
[MaxLength(255)]
public string OcsName { get; set; }
[Column("OCS_ORGANISATION_KEY")]
[RelationshipTableAttribue("ORGANISATIONS", "dbo")]
//Relationships
public int OcsOrganisationKey { get; set; }
[ForeignKey("OcsOrganisationKey")]
public Organisation Organisation { get; set; }
[Column("OCS_TYPE")]
[MaxLength(20)]
[RelationshipTableAttribue("ORGANISATIONS", "dbo")]
// Relationships
public string OcsType { get; set; }
[ForeignKey("OCS_TYPE")]
public Organisation Organisation1 { get; set; }
public OrganisationContact()
{
}
}
[SequenceNameAttribute("ORGANISATIONS", "web")]
[Table("ORGANISATIONS", Schema = "dbo")]
[Serializable]
public partial class Organisation
{
[Column("ORG_EMAIL")]
[MaxLength(255)]
public string OrgEmail { get; set; }
[Range(0, int.MaxValue)]
[Column("ORG_ID")]
public int OrgId { get; set; }
[Required]
[Column("ORG_NAME")]
[MaxLength(255)]
public string OrgName { get; set; }
[Required]
[Column("ORG_TYPE")]
[MaxLength(20)]
public string OrgType { get; set; }
[InverseProperty("Organisation")]
public ICollection<OrganisationContact> OrganisationContacts { get; set; }
[InverseProperty("Organisation1")]
public ICollection<OrganisationContact> ORGANISATIONCONTACTS1 { get; set; }
public Organisation()
{
this.OrganisationContacts = new HashSet<OrganisationContact>();
this.ORGANISATIONCONTACTS1 = new HashSet<OrganisationContact>();
}
}
I get this error:
System.InvalidOperationException: 'The property 'OCS_TYPE' cannot be added to the type 'OrganisationContact' because there was no property type specified and there is no corresponding CLR property or field. To add a shadow state property the property type must be specified.
The core issue here is that you define a composite primary key in table Organisation but you try to use single fields as foreign keys in table OrganisationContact.
If the primary key of the referenced table is composite, the foreign keys referencing it must be composite, as well, consisting of fields of the same number and type:
[Table("ORGANISATIONCONTACTS", Schema = "dbo")]
public partial class OrganisationContact
{
// irrelevant declarations omitted for brevity...
[Column("OCS_ORGANISATION_ORG_ID")]
public int Organisation_OrgId { get; set; }
[Column("OCS_ORGANISATION_ORG_TYPE")]
public string Organisation_OrgType { get; set; }
[ForeignKey(nameof(Organisation_OrgId) + "," + nameof(Organisation_OrgType))]
public Organisation Organisation { get; set; }
[Column("OCS_ORGANISATION1_ORG_ID")]
public int Organisation1_OrgId { get; set; }
[Column("OCS_ORGANISATION1_ORG_TYPE")]
public string Organisation1_OrgType { get; set; }
[ForeignKey(nameof(Organisation1_OrgId) + "," + nameof(Organisation1_OrgType))]
public Organisation Organisation1 { get; set; }
}
[Table("ORGANISATIONS", Schema = "dbo")]
public partial class Organisation
{
// irrelevant declarations omitted for brevity...
[InverseProperty(nameof(OrganisationContact.Organisation))]
public ICollection<OrganisationContact> OrganisationContacts { get; set; }
[InverseProperty(nameof(OrganisationContact.Organisation1))]
public ICollection<OrganisationContact> ORGANISATIONCONTACTS1 { get; set; }
}
Some suggestions:
Please post MCV code. There are some exotic attributes (like RelationshipTableAttribue) and unknown type references (MainSqlDbContext) which has nothing to do with the problem but makes more cumbersome to review the issue.
Try to avoid hardcoded strings as much as possible. The nameof operator has been available for quite a while (since C# 6.0).
The preferred way to configure your DB mappings is fluent API in EF Core. Data annotation attributes are pretty limited in functionality. (E.g. you cannot define a composite primary key using attributes in EF Core.)

Updating a relation between two Entity Framework entities?

I have two related Entity Framework 6 classes in my data layer.
public class Order
{
public int Id { get; set; }
public virtual SalesStatus SalesStatus { get; set; }
}
public class SalesStatus
{
public SalesStatus()
{
Orders = new List<Order>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Order> Orders { get; set; }
}
public class OrderVM
{
public int Id { get; set; }
public SalesStatus SalesStatus { get; set; }
}
I am using Automapper to map these to my view models and back again.
cfg.CreateMap<Order, OrderVM>()
.MaxDepth(4)
.ReverseMap();
The status entity is used to populate a drop down list.
In my method I am taking the selected value and trying to update the order record to the new selected status.
private bool SaveOrderToDb(OrderVM orderVM)
{
using (var db = new MyContext())
{
var order = AutomapperConfig.MapperConfiguration.CreateMapper().Map<OrderVM, Order>(orderVM);
order.SalesStatus = db.SalesStatuses.Find(Convert.ToInt16(orderVM.SalesStatusSelectedValue));
db.Set<Order>().AddOrUpdate(order);
db.SaveChanges();
}
return true;
}
This does not update the relationship in the database. Why? What am I missing?

how to configure cascade delete with EF Fluent API

I have the following classes and EF convention created the tables fine - all good. Now I want to configure cascade delete on the ToDoList (so that when I delete a ToDoList, all the ToDoItems and UserToDoLists also gets deleted automatically). Must I re-specify what EF convention is able to detect in order to set WillCascadeOnDelete? How do I specify this relationship in fluent API?
public class ToDoList
{
public int Id { get; set; }
public string Description { get; set; }
public virtual ICollection<ToDoItem> ToDoItems { get; set; }
public virtual ICollection<UserToDoList> UserToDoLists { get; set; }
}
public class ApplicationUser : IdentityUser
{
public ApplicationUser()
: base()
{
}
public virtual ICollection<UserToDoList> UserToDoLists { get; set; }
}
// a todolist can have many users and a user can have many todolist
// junction table will have addition fields
public class UserToDoList
{
public string UserId { get; set; }
public virtual ApplicationUser User { get; set; }
public virtual int ToDoListId { get; set; }
public virtual ToDoList ToDoList { get; set; }
// additional fields
public bool IsOwner { get; set; }
public int Ordinal { get; set; }
public bool AllowEdit { get; set; }
}
public class UserToDoListConfig : EntityTypeConfiguration<UserToDoList>
{
public UserToDoListConfig()
{
HasKey(ut => new { ut.UserId, ut.ToDoListId });
}
}
public class ToDoListConfig : EntityTypeConfiguration<ToDoList>
{
public ToDoListConfig()
{
Property(t => t.Description).IsRequired().HasMaxLength(50);
HasMany(t =>t.UserToDoLists).??
}
}
That's a simple one-to-many relationship from ToDoList to your junction table UserToDoList, so you just have to set up the mapping like this
HasMany(m => m.UserToDoLists)
.WithRequired(m => m.ToDoList)
.HasForeignKey(m => m.ToDoListId);
and EF will handle your cascading deletes automatically, because ToDoList is required on the junction table side.

Entity framework replaces delete+insert with an update. How to turn it off

I want to remove a row in database and insert it again with the same Id, It sounds ridiculous, but here is the scenario:
The domain classes are as follows:
public class SomeClass
{
public int SomeClassId { get; set; }
public string Name { get; set; }
public virtual Behavior Behavior { get; set; }
}
public abstract class Behavior
{
public int BehaviorId { get; set; }
}
public class BehaviorA : Behavior
{
public string BehaviorASpecific { get; set; }
}
public class BehaviorB : Behavior
{
public string BehaviorBSpecific { get; set; }
}
The entity context is
public class TestContext : DbContext
{
public DbSet<SomeClass> SomeClasses { get; set; }
public DbSet<Behavior> Behaviors { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Entity<SomeClass>()
.HasOptional(s => s.Behavior)
.WithRequired()
.WillCascadeOnDelete(true);
}
}
Now this code can be executed to demonstrate the point
(described with comments in the code below)
using(TestContext db = new TestContext())
{
var someClass = new SomeClass() { Name = "A" };
someClass.Behavior = new BehaviorA() { BehaviorASpecific = "Behavior A" };
db.SomeClasses.Add(someClass);
// Here I have two classes with the state of added which make sense
var modifiedEntities = db.ChangeTracker.Entries()
.Where(entity => entity.State != System.Data.Entity.EntityState.Unchanged).ToList();
// They save with no problem
db.SaveChanges();
// Now I want to change the behavior and it causes entity to try to remove the behavior and add it again
someClass.Behavior = new BehaviorB() { BehaviorBSpecific = "Behavior B" };
// Here it can be seen that we have a behavior A with the state of deleted and
// behavior B with the state of added
modifiedEntities = db.ChangeTracker.Entries()
.Where(entity => entity.State != System.Data.Entity.EntityState.Unchanged).ToList();
// But in reality when entity sends the query to the database it replaces the
// remove and insert with an update query (this can be seen in the SQL Profiler)
// which causes the discrimenator to remain the same where it should change.
db.SaveChanges();
}
How to change this entity behavior so that delete and insert happens instead of the update?
A possible solution is to make the changes in 2 different steps: before someClass.Behavior = new BehaviorB() { BehaviorBSpecific = "Behavior B" }; insert
someClass.Behaviour = null;
db.SaveChanges();
The behaviour is related to the database model. BehaviourA and B in EF are related to the same EntityRecordInfo and has the same EntitySet (Behaviors).
You have the same behaviour also if you create 2 different DbSets on the context because the DB model remains the same.
EDIT
Another way to achieve a similar result of 1-1 relationship is using ComplexType. They works also with inheritance.
Here an example
public class TestContext : DbContext
{
public TestContext(DbConnection connection) : base(connection, true) { }
public DbSet<Friend> Friends { get; set; }
public DbSet<LessThanFriend> LessThanFriends { get; set; }
}
public class Friend
{
public Friend()
{Address = new FullAddress();}
public int Id { get; set; }
public string Name { get; set; }
public FullAddress Address { get; set; }
}
public class LessThanFriend
{
public LessThanFriend()
{Address = new CityAddress();}
public int Id { get; set; }
public string Name { get; set; }
public CityAddress Address { get; set; }
}
[ComplexType]
public class CityAddress
{
public string Cap { get; set; }
public string City { get; set; }
}
[ComplexType]
public class FullAddress : CityAddress
{
public string Street { get; set; }
}

Entity Framework Code First Many to Many Setup For Existing Tables

I have the following tables Essence, EssenseSet, and Essense2EssenceSet
Essense2EssenceSet is the linking table that creates the M:M relationship.
I've been unable to get the M:M relationship working though in EF code first though.
Here's my code:
[Table("Essence", Schema = "Com")]
public class Essence
{
public int EssenceID { get; set; }
public string Name { get; set; }
public int EssenceTypeID { get; set; }
public string DescLong { get; set; }
public string DescShort { get; set; }
public virtual ICollection<EssenceSet> EssenceSets { get; set; }
public virtual EssenceType EssenceType { get; set; }
}
[Table("EssenceSet", Schema = "Com")]
public class EssenceSet
{
public int EssenceSetID { get; set; }
public int EssenceMakerID { get; set; }
public string Name { get; set; }
public string DescLong { get; set; }
public string DescShort { get; set; }
public virtual ICollection<Essence> Essences { get; set; }
}
[Table("Essence2EssenceSet", Schema = "Com")]
public class Essence2EssenceSet
{
//(PK / FK)
[Key] [Column(Order = 0)] [ForeignKey("Essence")] public int EssenceID { get; set; }
[Key] [Column(Order = 1)] [ForeignKey("EssenceSet")] public int EssenceSetID { get; set; }
//Navigation
public virtual Essence Essence { get; set; }
public virtual EssenceSet EssenceSet { get; set; }
}
public class EssenceContext : DbContext
{
public DbSet<Essence> Essences { get; set; }
public DbSet<EssenceSet> EssenceSets { get; set; }
public DbSet<Essence2EssenceSet> Essence2EssenceSets { get; set; }
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Entity<Essence>()
.HasMany(e => e.EssenceSets)
.WithMany(set => set.Essences)
.Map(mc =>
{
mc.ToTable("Essence2EssenceSet");
mc.MapLeftKey("EssenceID");
mc.MapRightKey("EssenceSetID");
});
}
}
This is the code I'm trying to run:
Essence e = new Essence();
e.EssenceTypeID = (int)(double)dr[1];
e.Name = dr[2].ToString();
e.DescLong = dr[3].ToString();
//Get Essence Set
int setID = (int)(double)dr[0];
var set = ctx.EssenceSets.Find(setID);
e.EssenceSets = new HashSet<EssenceSet>();
e.EssenceSets.Add(set);
ctx.Essences.Add(e);
ctx.SaveChanges();
And here's the error:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception.
I'm not able to find the problem. I'd greatly appreciate help setting this up right.
Thanks!
Remove your Essence2EssenceSet model class. If junction table contains only keys of related entities participating in many-to-many relations it is not needed to map it as entity. Also make sure that your fluent mapping of many-to-many relations specifies schema for table:
mb.Entity<Essence>()
.HasMany(e => e.EssenceSets)
.WithMany(set => set.Essences)
.Map(mc =>
{
mc.ToTable("Essence2EssenceSet", "Com");
mc.MapLeftKey("EssenceID");
mc.MapRightKey("EssenceSetID");
});