how can one seed a many-to-many relation in EF Core, couldn't find anything in this area?
So this is the entities
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<StudentGrade> StudentGrades { get; set; }
}
public class Grade
{
public int Id { get; set; }
public int Grade { get; set; }
public virtual List<StudentGrade> StudentGrades { get; set; }
}
public class StudentGrade
{
public int GradeId { get; set; }
public Grade Grade { get; set; }
public int StudentId { get; set; }
public Student Student { get; set; }
}
so the official documentation says you have to have a joining entity defined (in my case StudentGrade) and this should be referenced within the entities that are in a many-to-many relation.
Ef core documentation for many-to-many.
Now in EF, you wouldn't have to do this, it would figure out those things, and so instead of having the join-entity, you would simply reference each entity into the other.
So how can you seed this type of relation in EF Core?
Thanks
So what worked for me was overriding DbContext::OnModelCreating(ModelBuilder modelBuilder) with something similar to this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentGrade>()
.HasKey(s => new { s.GradeId , s.StudentId });
var students= new[]
{
new Student{Id=1, Name="John"},
new Student{Id=2, Name="Alex"},
new Student{Id=3, Name="Tom"}
}
var grades = new[]
{
new Grade{Id=1, Grade=5},
new Grade{Id=2, Grade=6}
}
var studentGrades = new[]
{
new StudentGrade{GradeId=1, StudentId=1},
new StudentGrade{GradeId=2, StudentId=2},
// Student 3 relates to grade 1
new StudentGrade{GradeId=1, StudentId=3}
}
modelBuilder.Entity<Student>().HasData(stdudents[0],students[1],students[2]);
modelBuilder.Entity<Grade>().HasData(grades[0],grades[1]);
modelBuilder.Entity<StudentGrade>().HasData(studentGrades[0],studentGrades[1],studentGrades[2]);
base.OnModelCreating( modelBuilder );
}
I've followed the custom initialization logic, as explained here, since my commitment is just data for testing and developing.
I like to do the seeding in a synchronous way, as you'll see in the code.
Important: Previous to this step, I do a 'commit' (context.SaveChanges();) with the entities data that I have to join, so EF will pick them from the DB with the ID inserted. That's nice if the DB backend has an autoincremental ID.
Following with your example (I've pluralized the dbSets), and assuming you have inserted the proper data
var student= context.Students.FirstOrDefault(a => a.Name == "vic");
var grade= context.Grades.FirstOrDefault(b => b.Grade == 2);
var studentGrade = context.StudentsGrades.Include(a => a.Students).Include(b => b.Grades)
.FirstOrDefault(c => c.Students.Name == "vic" && c.Grades.Grade = 2);
if (studentGrade == null)
{
context.StudentsGrades.Add(new StudentGrade
{
StudentId = student.Id,
GradeId = grade.Id
});
}
context.SaveChanges();
Related
I am new at C# entity framework. I am trying to build an API, but stuck in retrieving data from relational table.
I have a pei_crops table in MS SQL database, where c_id is the primary key. I have another table called pei_pests, where p_id is the primary key. Another table is pei_cropspests where I have built relation for which pest attack which crop. Multiple pests can attack one crop and one pest can attack multiple crops. In this pei_cropspests table I have put p_id as primary and foreign key and c_id as primary and foreign key as well.
pei_crops table:
c_id
c_name
c_description
1
Corn
NULL
pei_pests table:
p_id
p_name
p_URL
1
pest1
NULL
2
pest2
NULL
pei_cropspests table:
p_id
c_id
1
1
2
1
Now In my API I want to show something like that
[
{
"cId":1,
"pests":[
{
"pId":1,
"pName": pest1,
"pURL": null
},
{
"pId":2,
"pName": pest2,
"pURL": null
}
]
}
]
My get request looks like this so far in C# web API project:
[Route("Getspecific/{cropId}")]
[HttpGet]
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops.Where(c=>c.CId == cropId).Include(i=>i.PeiCropspests).ToListAsync();
return Ok(cropDetails);
}
This code returns me only the pID and URL of the pest that effects cID number 1. But I also want the pest name and URL along with their id.
Could someone please show me how to do it. Maybe there is some way to join two table and show the data? I just do not know how to do it in C#. Any help appreciated. Thank you.
Entities class:
PeiCrop:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCrop
{
public PeiCrop()
{
PeiCropimages = new HashSet<PeiCropimage>();
PeiCropsdiseases = new HashSet<PeiCropsdisease>();
PeiCropspests = new HashSet<PeiCropspest>();
}
public int CId { get; set; }
public string CName { get; set; }
public string CPhotoUrl { get; set; }
public string CDescription { get; set; }
public virtual ICollection<PeiCropimage> PeiCropimages { get; set; }
public virtual ICollection<PeiCropsdisease> PeiCropsdiseases { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
}
}
PeiPest:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiPest
{
public PeiPest()
{
PeiCropspests = new HashSet<PeiCropspest>();
PeiPestimages = new HashSet<PeiPestimage>();
}
public int PId { get; set; }
public string PName { get; set; }
public string PPhotoUrl { get; set; }
public string PDescription { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
public virtual ICollection<PeiPestimage> PeiPestimages { get; set; }
}
}
PeiCropspest:
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCropspest
{
public int PId { get; set; }
public int CId { get; set; }
public virtual PeiCrop CIdNavigation { get; set; }
public virtual PeiPest PIdNavigation { get; set; }
}
}
You're pretty close, but you're also not entirely using EF like you could, I mean you do not actually have to make the relationship table yourself but could refer directly to a list of the entity pei_pests from the entity pei_crop and let EF create the other.
//Example just getting one property from each,
//but you can new a composite return type up if you wish, using select
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Include(i=>i.PeiCropspests)
.ThenInclucde(t => t.Pests)
.Select(s => new { CropId = s.p_id, PestName = s.PeiCropsPests.Pest.p_name })
.ToListAsync();
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.select?view=net-5.0
First, you need to configure the relationships :
class MyContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PeiCropspest>()
.HasKey(cp => new { cp.PId, cp.CId });
//Configure one PeiPest to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation property to one PeiPest
.HasOne(cp => cp.PIdNavigation)
// Specify PeiPest's navigaton property to many PeiCropspest
.WithMany(p => p.PeiCropspests)
// Specify PeiCropspest's navigation property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.PId);
//Configure one PeiCrop to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation shadow property to one PeiCrop
.HasOne<PeiCrop>()
// Specify PeiCrop's navigaton property to many PeiCropspest
.WithMany(c => c.PeiCropspests)
// Specify PeiCropspest's navigation shadow property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.CId);
}
public DbSet<PeiCrop> PeiCrops { get; set; }
}
Then you can do a projection in the LINQ query :
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Select(c => new {
cId = c.CId,
pests = c.PeiCropspests.Select(p => new {
pId = p.PIdNavigation.PId,
pName = p.PIdNavigation.PName,
pUrl = p.PIdNavigation.PPhotoUrl
})
})
.ToListAsync();
return Ok(cropDetails);
}
Do you know? From EF Core 5, it's possible to do many to many relationship without intermediary entity. This can simplify your entity model. cf. the documentation
This is the simplified version of the table structure I have:
[Table("PolicyMapping")]
public class PolicyMapping
{
[Key]
public Guid Id { get; set; }
public Policy PolicyA { get; set; }
public Policy PolicyB { get; set; }
public Lookup_Bank Bank { get; set; }
}
[Table("Policy")]
public class Policy
{
[Key]
public Guid Id { get; set; }
public string PolicyNm { get; set; }
public string Description { get; set; }
}
[Table("Lookup_Bank")]
public class Lookup_Bank
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
I am working on the edit screen for policy mapping where you can have the same values for PolicyA and PolicyB attributes.
After using automapper for DTO to the entity, here is my entity object looks like:
var policyMapping = new PolicyMapping
{
Id = "b27fb632-330b-46be-a649-2e2463d58626",
PolicyA = new Policy
{
Id = "a4f1cf6f-034d-4727-ab8f-49e95b2c9d23",
PolicyNm = null,
Description = null
},
PolicyB = new Policy
{
Id = "a4f1cf6f-034d-4727-ab8f-49e95b2c9d23",
PolicyNm = null,
Description = null
},
Bank = new Lookup_Bank()
{
Id = "98ed2bae-631b-490c-8ddf-3e02232d4231",
Name = null,
Code = null
}
}
I am mapping only selected id value of dropdown to entity id using automapper. Values are present for Code, Description and other attributes in the database. It's just not getting populated after automapper.
dbContext.PolicyMapping.Attach(policyMapping);
dbContext.SaveChanges();
This is the error, I am getting
The instance of entity type Policy cannot be tracked because another instance with the same key value for {'a4f1cf6f-034d-4727-ab8f-49e95b2c9d23'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
The reason for the error maybe because I am attaching two different entities with the same Id. I am still not sure how can I make it work in the most efficient way?
Solution 1: (not efficient)
var fromdatabase = dbContext.PolicyMapping.Include(x => x.PolicyA)
.Include(x => x.Bank)
.Include(x => x.PolicyB)
.FirstOrDefault(x => x.Id == policyMapping.Id);
fromdatabase.PolicyA = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyA.Id);
fromdatabase.PolicyB = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyB.Id);
dbContext.PolicyMapping.Attach(fromdatabase);
dbContext.SaveChanges();
This is working. But I would like to avoid a trip to the database just to fetch the entire entity.
Edit: Based on Xing's answer
#Xing, pointed out to change the Model structure by adding navigational properties and some changes in OnModelCreating method. (This method is currently blank in my code)
However, I went through a couple of articles (This & This) related to EF Core Code First approach, none of them are saying about navigational properties and all.
I am wondering how they are updating the column in this scenario?
If you just would like to update the Id of the navigation properties, you could add foreign key for them and update it.
1.Model:
public class PolicyMapping
{
[Key]
public Guid Id { get; set; }
public Guid PolicyAId { get; set; }
public Policy PolicyA { get; set; }
public Guid PolicyBId { get; set; }
public Policy PolicyB { get; set; }
public Guid BankId { get; set; }
public Lookup_Bank Bank { get; set; }
}
2.DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.PolicyA)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.PolicyAId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.PolicyB)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.PolicyBId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.Bank)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.BankId)
.OnDelete(DeleteBehavior.Restrict);
}
3.Add migrations.
4.Controller:
var policyMapping = new PolicyMapping
{
Id = new Guid("b27fb632-330b-46be-a649-2e2463d58626"),
PolicyAId = new Guid("a4f1cf6f-034d-4727-ab8f-49e95b2c9d23"),
PolicyBId = new Guid("a4f1cf6f-034d-4727-ab8f-49e95b2c9d23"),
BankId = new Guid("98ed2bae-631b-490c-8ddf-3e02232d4231")
};
dbContext.PolicyMapping.Attach(policyMapping);
dbContext.Entry(policyMapping).Property("PolicyAId").IsModified = true;
dbContext.Entry(policyMapping).Property("PolicyBId").IsModified = true;
dbContext.Entry(policyMapping).Property("BankId").IsModified = true;
dbContext.SaveChanges();
Then you could retrieve PolicyA or PolicyB from their foreign key PolicyAId or PolicyBId
var policyA = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyAId);
I'm currently using MVC with EF to have a small server with API querying a SQL database. But in the API reply I'm not able to hide some parameters.
The main object
public class AssetItem
{
[Key]
public Int32 AssetId { get; set; }
public String AssetName { get; set; }
public int OdForeignKey { get; set; }
[ForeignKey("OdForeignKey")]
public OperationalDataItem OperationalDataItem { get; set; }
}
The other one:
public class OperationalDataItem
{
[Key]
public Int32 OperationalDataId { get; set; }
public String Comunity { get; set; }
public List<AssetItem> AssetItems { get; set; }
}
From what I have read, this should be ok, I have also set the context:
public AssetContext(DbContextOptions<AssetContext> options) : base(options)
{}
public DbSet<AssetItem> AssetItems { get; set; }
public DbSet<OperationalDataItem> OperationalDataItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AssetItem>().HasOne(p =>
p.OperationalDataItem).WithMany(b => b.AssetItems).HasForeignKey(p =>
p.OdForeignKey);
}
And the seeding in program.cs
context.AssetItems.Add(
new AssetItem { AssetName = "Test test", OdForeignKey = 1,
OperationalDataItem =
new OperationalDataItem {Comunity = "Comunity1" }});
So calling the API this results in:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":null }
From what I read this is because of the lazy loading, how can I hide the result operationalDataItem?
In case is not possible i have of course try to query for it and give it back and it give something like:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":
{ "operationalDataId":1,
"comunity":"Comunity1",
"assetItems":[
But in this case I would like to hide "assetsItems" in the reply to the FE.
How can I hide those parameters?
The API is quite simple, just an example code:
var todoItem = await _context.AssetItems.FindAsync((Int32)id);
var item = _context.OperationalDataItems.Find((Int32)todoItem.OdForeignKey);
todoItem.OperationalDataItem = item;
return todoItem
If you want to fetch data from the database, but you only want to fetch some properties, use Select. Usually this is more efficient than using Find, because you'll only transfer the data that you actually plan to use.
To fetch some properties of the assetItem that has primary key assetItemId:
var result = dbContext.AssetItems
.Where(assetItem => assetItem.AssetItmId = assetItemId)
.Select(assetItem => new
{
// Select only the properties that you plan to use
Id = assetItem.AssertItemId,
Name = assetItem.Name,
OperationalData = new
{
// again, select only the properties that you plan to use
Id = assetItem.OperationalData.OperationalDataId,
Community = assetItem.OperationalData.Community,
},
})
.FirstOrDefault();
Or the other way round:
Fetch several properties of all (or some) OperationalDataItems, each with some properties of all (or some) of its AssetItems:
var result = dbContext.OperqationalDataItems
.Where(operationalDataItem => ...) // only if you don't want all
.Select(operationalDataItem => new
{
Id = operationalDataItem.Id,
Community = operationalDataItem.Community
AssetItems = operationalDataItem.AssetItems
.Where(assetItem => ...) // only if you don't want all its assetItems
.Select(assetItem => new
{
// Select only the properties you plan to use:
Id = assetItem.Id,
...
// not useful: you know the value of the foreign key:
// OperationalDataId = assetItem.OperationalDataId,
})
.ToList();
})
.ToList(); // or: FirstOrDefault if you expect only one element
Entity framework knows your one-to-many relation and is smart enough to know which (group-)join is needed for your query.
Some side remarks
You've declare your many-relation a List<AssetItem>. Are you sure that operationalDataItem.AssetItems[4] has a defined meaning? Wouldn't it be better to stick to the entity framework code first conventions? This would also eliminate the need for most attributes and / or fluent API
public class OperationalDataItem
{
public int Id { get; set; }
public String Comunity { get; set; }
...
// Every OperationalDataItem has zero or more AssetItems (one-to-many)
public virtual ICollection<AssetItem> AssetItems { get; set; }
}
public class AssetItem
{
public int Id { get; set; }
public String Name { get; set; }
...
// every AssetItem belongs to exactly one OperationalDataItem, using foreign key
public int OperationDataItemId { get; set; }
public virtual OperationalDataItem OperationalDataItem { get; set; }
}
In entity framework the columns of a table are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many)
Because I stuck to the conventions, no attributes nor fluent API is needed. Entity framework is able to detect the one-to-many relation and the primary and foreign keys. Only if I am not satisfied with the names or the types of the columns I would need fluent API.
I'm using EF code first with automatic migrations enabled.
I have below entities.
public class Order
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int OrderNo { get; set; }
[Required]
public int BranchId { get; set; }
[Required]
[ForeignKey("BranchId")]
public virtual Branch Branch { get; set; }
}
public class Branch
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int Id{ get; set; }
public virtual ICollection<Order> Order { get; set; }
}
Below is my seed on Configuration.cs and it works fine when I run Update-Database command via package manager console.
protected override void Seed(DataAccessLayer.ApplicationDbContext db)
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
}
db.Order.Add(order);
db.SaveChanges();
}
But when I use above code in a normal class it throws an Entity Framework validation error
private void AddOrder()
{
using (var db = new ApplicationDbContext())
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
}
db.Order.Add(order);
db.SaveChanges();
}
}
Above gives below error
ErrorMessage = "The Branch field is required."
So when I change above to below code with additional two lines it works fine
private void AddOrder()
{
using (var db = new ApplicationDbContext())
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
Branch = branch
}
db.Entry(branch).State = EntityState.Unchanged; // needed to avoid duplicating branch information
db.Order.Add(order);
db.SaveChanges();
}
}
It makes sense why the validation error due to obvious reasons, but my question is why is this not enforced in the seed method?
I'd prefer to use the code same as from seed method in my other classes, is there a way to achieve this ?
I want to insert 2 new records in their tables and also to create the relation many to many.
There are a lot of questions with this topic, I tried many of the answers and now I have no idea why my code is not working.
Please help me!
This is the code:
class Program
{
static void Main(string[] args)
{
MyContext db = new MyContext();
var st1 = new Student() { Name = "Joe" };
var c1 = new Course() { Name = "Math" };
db.Courses.Attach(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
db.SaveChanges();
}
}
public class Student
{
public Student()
{
Courses = new HashSet<Course>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(p => p.Courses)
.WithMany(d => d.Students)
.Map(t =>
{
t.MapLeftKey("studentId");
t.MapRightKey("courseid");
t.ToTable("StudentCourse");
});
base.OnModelCreating(modelBuilder);
}
}
Edit: Like sugested, I initialized the Courses:
public Student()
{
Courses = new HashSet<Course>();
}
and now I get this error on db.SaveChanges();
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. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
You're apparently trying to add a new Student to the database and associate it to an existing Course. The problem is that you attach a new Course entity to the context without a proper primary key.
It certainly is a good idea to use a so-called stub entity here, because it saves a roundtrip to the database to fetch an existing Course, but the primary key is required for EF to create a correct association record. It's even the only property you need to set in this Course stub:
var st1 = new Student() { Name = "Joe" };
var c1 = new Course() { CourseId = 123 };
db.Courses.Attach(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
If you want to add a new course and a new student, you should Add both of them:
db.Courses.Add(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
Your code isn't initialising the ICollection object in either class.
public class Student
{
private Student()
{
Courses = new List<Course>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Edit
Try changing your modelBuilder code to the following
modelBuilder.Entity<Student>()
.HasMany<Course>(s => s.Courses)
.WithMany(c => c.Students)
.Map(cs =>
{
cs.MapLeftKey("StudentRefId");
cs.MapRightKey("CourseRefId");
cs.ToTable("StudentCourse");
});
Code sample taken directly from:
http://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx