Entity Framework How mapping with condition? - c#

I have three class Student,Teacher and Document. Student and Teacher may have many Documents
public class BaseEntity
{
public int ItemId { get; set; }
public bool IsDeleted { get; set; }
}
//ParentType Student = 1
public class Student : BaseEntity
{
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Document> Documents { get; set; }
}
//ParentType Teacher = 2
public class Teacher : BaseEntity
{
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Document> Documents { get; set; }
}
public class Document
{
public int ParentId { get; set; } //Foreign Key
public int ParentTypeId { get; set; }
}
I use Entity Framework(Fluent API). For example, I create map for Student and I don't know how to configure Document in student with two Condition (where parentId = itemId and ParentType = 1)?
public class StudentMap : EntityTypeConfiguration<Student>
{
public StudentMap()
{
ToTable("Student", "dbo");
// Primary Key
HasKey(t => new {t.ItemId});
// Properties
Property(t => t.ItemId).HasColumnName("ItemId");
Property(t => t.IsDeleted)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("IsDeleted");
Property(t => t.Name)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("Name");
Property(t => t.Surname)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("Surname");
}

You can't have a conditional foreign key in EF just like I am pretty sure you can't have one in most DBMSs (like sql server). You have 2 options:
Omit that relationship from the schema and model and create your join(s) when you need them. You also have to keep this in mind when creating the entities that you have to manually set the key values.
You could create a nullable column per relationship in the document entity which is supported. You could add a check constraint in the database to ensure exactly one relationship key has a value (per record).

Related

Cascade delete in Entity Framework 6 fails to delete dependents. Any suggestions

I am new to Eternity Framework 6 and I read that by default Cascade Delete is enabled. I currently have the following models
public class Student
{
public int Id { get; set; }
public List<Sport> Sports { get; set; }
public string StudentName { get; set; }
}
public class Sport
{
public int Id { get; set; }
public string SportName { get; set; }
public List<Coach> {get; set;}
}
public class Coach
{
public int Id { get; set; }
public string CoachName { get; set; }
}
This is what my context looks like
public class MyContext: DbContext
{
public MyContext(): base("name=MContext")
{
}
public DbSet<Student> Students{ get; set; }
}
Now this is how I am removing Sports played by a student. If cascade delete is turned on then all the coaches who are asscociated with a Sport should also be deleted but they are not being deleted.
This is how I am removing items
//Currently there is only 1 student in the DB
Student st = context.Students.SingleOrDefault();
// Get all the Sports of this student
this.context.Entry(st).Collection(x => x.Sports).Load();
// Clear the actions
dm.Sports.Clear();
The above basically assigns the Foreign Key of Student in Sports Table to NULL and it does not delete any of the asscociated coaches with a sport in Coach Table. Any suggestions on why the Coach name is not being deleted and how I can fix this ?
You need in your DBContext to represent relationships
entity.HasOne(d => d.IdStudentNavigation)
.WithMany(p => p.Sports)
.HasForeignKey(d => d.IdStudent)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_Sports_Students");
this article helped me get the idea
You will have to use the fluent API to do this.
Try adding the following to your DbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOptional(a => a.UserDetail)
.WithOptionalDependent()
.WillCascadeOnDelete(true);
}

How to allow cascade delete for complex object that has two lists of other complex objects?

I'm struggling with trying to make it work.
I'd want to have an Dealer who has Two Lists of the same type of Items that are in relation, but I'd want also to be able to cascadly delete them.
public class Dealer
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public List<Car> OldCars { get; set; } = new List<Car>();
public List<Car> NewCars { get; set; } = new List<Car>();
}
public class Car
{
[Key]
public Guid Id { get; set; }
public string Model { get; set; }
public Dealer Dealer { get; set; }
}
What I've been trying:
Attempt #1
modelBuilder.Entity<Car>()
.HasOne(s => s.Dealer)
.WithMany(s => s.OldCars)
.HasForeignKey(x => x.Dealer)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Car>()
.HasOne(s => s.Dealer)
.WithMany(s => s.NewCars)
.HasForeignKey(x => x.Dealer)
.OnDelete(DeleteBehavior.Cascade);
System.InvalidOperationException: ''Dealer' cannot be used as a property on entity type 'Car' because it is configured as a navigation.'
Attempt #2
Meanwhile with:
modelBuilder.Entity<Car>()
.HasOne(s => s.Dealer)
.WithMany(s => s.NewCars)
.HasPrincipalKey(x => x.Id) <--- Principal!
.OnDelete(DeleteBehavior.Cascade);
It works properly, but shows warning that
warn: Microsoft.EntityFrameworkCore.Model[10605]
There are multiple relationships between 'Car' and 'Dealer' without configured foreign key properties causing EF to create shadow properties on 'Car' with names dependent on the discovery order.
Unfortunely while attempting to perform Delete (cascade) it throws an exception:
SqlException: The DELETE statement conflicted with the REFERENCE constraint "FK_Cars_Dealers_DealerId1". The conflict occurred in database "testDB", table "dbo.Cars", column 'DealerId1'.
The statement has been terminated.
Attempt #3
When I add Dealer's Id to Car
public class Car
{
[Key]
public Guid Id { get; set; }
public string Model { get; set; }
public Dealer Dealer { get; set; }
public Guid DealerID { get; set; }
}
It yells about
System.Data.SqlClient.SqlException: 'Cannot create, drop, enable, or disable more than one constraint, column, index, or trigger named 'FK_Cars_Dealers_DealerId' in this context. Duplicate names are not allowed.
Column names in each table must be unique. Column name 'DealerId' in table 'Cars' is specified more than once.'
#Attempt #3.1
So, after renaming Guid DealerID to e.g Guid DealerID123
it yells
SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Cars_Dealers_DealerID123". The conflict occurred in database "testDB", table "dbo.Dealers", column 'Id'.
The statement has been terminated.
Anybody has an idea how to make it work?
Thanks to Ivan Stoev recommendation, I managed to resolve it as follow:
public class Dealer
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[NotMapped]
public List<Car> OldCars { get; set; } => NewCars.Where(c => c.IsOld == true);
public List<Car> NewCars { get; set; } = new List<Car>();
}
public class Car
{
[Key]
public Guid Id { get; set; }
public string Model { get; set; }
public Dealer Dealer { get; set; }
public bool IsOld { get; set; }
}
In EF6 ModelBuilder I did as follow:
modelBuilder.Entity<Dealer>()
.HasMany(d => d.NewCars)
.WithRequired()
.WillCascadeOnDelete(true);

EF Code First - mapping two foreign key columns in child table to the same primary key

Say you have a table of road mile-marker points (mile-markers are signs placed every mile on US highways). Then you have a second table of spans between these mile markers. The Span table has two int columns StartMileMarkerId and EndMileMarkerId which are foreign keys referencing MileMarker Id column; These are the tables;
tblMileMarkers
[Table("tblMileMarkers")]
public class MileMarker
{
public MileMarker()
{
Spans = new HashSet<Span>();
}
[Key]
public int Id { get; set; }
public string Name { get; set; }
public DbGeography Location { get; set; }
public virtual ICollection<Span> Spans { get; set; }
}
tblSpans
[Table("tblSpans")]
public class Span
{
[Key]
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
public int StartMileMarkerId { get; set; }
public int EndMileMarkerId { get; set; }
public virtual MileMarker MileMarker { get; set; }
}
If it was only one foreign key (StartMileMarkerId), I could configure the one-many relationship in the DbContext with Fluent Api as below
modelBuilder.Entity<Span>().HasRequired(s => s.MileMarker)
.WithMany(m => m.Spans)
.HasForeignKey(s => s.StartMileMarkerId);
How can I map these 2 columns (StartMileMarkerId and EndMileMarkerId) to the same primary key?
Since you have 2 foreign keys, you need 2 collection navigation properties in MileMarker and 2 reference navigation properties in Span. Something like this:
MileMarker class:
public virtual ICollection<Span> StartSpans { get; set; }
public virtual ICollection<Span> EndSpans { get; set; }
Span class:
public virtual MileMarker StartMileMarker { get; set; }
public virtual MileMarker EndMileMarker { get; set; }
Configuration:
modelBuilder.Entity<Span>()
.HasRequired(s => s.StartMileMarker)
.WithMany(m => m.StartSpans)
.HasForeignKey(s => s.StartMileMarkerId);
modelBuilder.Entity<Span>()
.HasRequired(s => s.EndMileMarker)
.WithMany(m => m.EndSpans)
.HasForeignKey(s => s.EndMileMarkerId);
P.S. If your idea is to have ModelMarker.Spans collection mapped to spans with either start or end marker being this marker, it's just not possible.

Entity Framework Core code first - 0..1 to many relationship and cascade delete

I'm trying to create a commenting system backed by Entity Framework Core where multiple entities of different type can have comments attached to them.
These are my entities. (In the real application there are about 7 in total with varying relationships but this is how it generally looks)
public class Comment : IEntityBase
{
public int Id { get; set; }
public int? FreezerId{ get; set; }
public Freezer Freezer { get; set; }
public int? BoxId{ get; set; }
public Box Box{ get; set; }
public string Author { get; set; }
public DateTime CreatedAt { get; set; }
public string Content { get; set; }
}
public class Freezer: IEntityBase
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public ICollection<Box> Boxes{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Box: IEntityBase
{
public int Id { get; set; }
public Freezer Freezer{get; set;}
public int FreezerId{get; set;}
public string Data{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
I want the Comment entity to be attached to one Freezer or one Box, but not both at the same time.
I defined the relationship in the fluent API as the following:
builder.Entity<Box>(boxBuilder=>
{
boxBuilder.HasOne(box=> box.Freezer)
.WithMany(freezer => freezer.boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer=> freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Cascade);
});
When I try to update the database to this model I get the following error:
System.Data.SqlClient.SqlException: Introducing FOREIGN KEY constraint 'FK_Comment_Boxes_BoxId' on table 'Comment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
I think the error comes from the Box and the Freezer property in the Comment class not being optional which would make this a 1 to many relationship instead of a 0..1 to many relationship which is what I want.
With Entity Framework 6 I would just use the .HasOptional() method, but this doesn't exist in Entity Framework Core
I think one way to solve this would be to just subclass the Comment class and create a unique comment class for each entity that can be commented on and move the foreign key and reference property to that subclass instead.
But it feels like I shouldn't have to do it this way.
You have to disable the cascade delete(DeleteBehavior.Restrict) then it will works for you:
modelBuilder.Entity<Box>(boxBuilder =>
{
boxBuilder.HasOne(box => box.Freezer)
.WithMany(freezer => freezer.Boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer => freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Restrict);
});
base.OnModelCreating(modelBuilder);
}
Usage:
using (var myConext = new MyDbContext())
{
myConext.Database.EnsureCreated();
myConext.Boxes.Add(new Box() {Freezer = new Freezer()});
myConext.SaveChanges();
}

Creating one to many relationship in Entity Framework using Fluent API

I have the following POCO Classes:
public class Client
{
public string ClientId { get; set; }
public string CompanyId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
[ForeignKey("AccountId")]
public virtual ICollection<Note> Notes { get; set; }
}
public class Note
{
public decimal NoteId { get; set; }
public string AccountId { get; set; }
public string NoteValue { get; set; }
[ForeignKey("ClientId")]
public virtual Client Client { get; set; }
}
and the following mapping classes:
public ClientMap(string schema)
{
ToTable("CLIENTS", schema);
// Primary Key
HasKey(t => t.ClientId);
// Properties
Property(t => t.ClientId)
.HasColumnName("CLIENT_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.CompanyId)
.HasColumnName("COMPANY_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.LastName)
.HasColumnName("LAST_NAME")
.IsRequired()
.HasMaxLength(50);
Property(t => t.FirstName)
.HasColumnName("FIRST_NAME")
.IsRequired()
.HasMaxLength(50);
}
public NoteMap(string schema)
{
ToTable("NOTES", schema);
// Primary Key
HasKey(t => t.NoteId);
// Properties
Property(t => t.NoteId)
.HasColumnName("NOTE_ID")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.AccountId)
.HasColumnName("ACCOUNT_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.NoteValue)
.HasColumnName("NOTE")
.HasMaxLength(4000);
}
In this model (Using Fluent API), there is a one to many relationship between clients and notes. ClientID is the PK in clients and NoteId is the PK in Notes. There are no foreign keys in the DB. ClientId maps to the AccountId in Notes.
I can not get this to work. When I run it, I can get most of the client data back, but when trying to navigate to a note, I get a Funcation Evaluation Timed out error when trying to look at Notes. I can not get the relationship between clients and notes to work. Where have I gone wrong? (I would like to do this using Fluent API)
Thanks
You are trying to create a typical one to many relationship. First, remove the data annotations that you are using in your model, you don't need them if you are going to use Fluent Api. Second, add the ClientId FK property in your Note entity
public class Note
{
public decimal NoteId { get; set; }
public string AccountId { get; set; }
public string NoteValue { get; set; }
//FK property
public int ClientId{get;set;}
public virtual Client Client { get; set; }
}
Then, in the constructor of your NoteMap class, add this Fluent Api configuration:
HasRequired(n=>n.Client).WithMany(c=>c.Notes).HasForeignKey(n=>n.ClientId);

Categories