Cross model update in ASP.NET Core - c#

I'm really new to ASP.NET Core so I apologize if this is a silly question, but I've spent so many hours trying to figure this out.
I have 2 models, Teacher and Student. The relationship is one teacher to many students. This is a backend API project and Im working on a PUT method which can update fields in both models simultaneously in one request.
Here's my Teacher class:
public partial class Teacher
{
public Teacher()
{
Students = new HashSet<Student>();
}
public int TeacherId { get; set; }
public string Name { get; set; }
... tons of other properties ...
}
Here's my Student class:
public partial class Student
{
public int Id { get; set; }
public int TeacherId { get; set; }
public string Name { get; set; }
public virtual Teacher Teacher { get; set; }
}
Here's the controller:
[HttpPut("{id}")]
public async Task<IActionResult> PutTeachers(int id, TeacherViewModel model)
{
var result = await _service.UpdateAsync(model);
return Ok(result.Message);
}
(The code above is simplified)
- It takes in a TeacherViewModel which restricts the number of fields to be returned
- I used another class as a service to do the update
Here's the service class:
public class TeacherService
{
private readonly Repository<Teacher> _repository;
public TeacherService(DatabaseContextWrapper context)
{
_repository = new Repository<Teacher>(context);
}
public async Task<ITransactionResult> UpdateAsync(TeacherViewModel model)
{
var teacher = _repository.FindAsync(model.TeacherId).Result;
teacher.TeacherId = model.TeacherId;
teacher.Name = model.Name;
teacher.Students.Clear();
foreach(var student in model.Students)
{
teacher.Students
.Add(new Student
{
Id = Student.Id,
TeacherId = Student.TeacherId
Name = Student.Name
});```
}
}
}
My reasoning is to add the Student model to the to students under the Teacher model but it doesn't iterate through. If I comment out the clear code, the update will work but it won't cross update. It just simply wont iterate through. I guess I'm pretty lost at this point. Any help would be appreciated!
Edit 1 (Entity relationship configuration)
modelBuilder.Entity<Student>(entity => {
entity.HasOne(d => d.Teacher)
.WithMany(p => p.Students)
.HasForeignKey(d => d.TeacherId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Students_Teachers");
});
This is my configuration

Try adding the reverse config to your parents entity:
modelBuilder.Entity<Teacher>(entity =>
{
entity.HasMany(d => d.Students)
.WithOne(p => p.Teacher)
.HasForeignKey(d => d.TeacherId);
});

Related

Many-to-Many Entity Framework Core 6 (junction table not generated)

I've just started to work with Entity Framework Core 6.
I am working with a sample database where I have a many to many relationship.
I created my database on SQL server. I created three tables: Service, Document, ServiceDocs (used as a Junction Table).
Then I did :
scaffolf-dbcontext
both classes have been generated except the junction table ServiceDocs.
My question is: How can I add elements to the junction table and get data from it without the class of the junction table?
Thank you for your help.
Class document:
public partial class Document
{
public Document()
{
Services = new HashSet<Service>();
}
public Guid DocumentId { get; set; }
public string? DocTitre { get; set; }
public virtual ICollection<Service> Services { get; set; }
}
public partial class Service
{
public Service()
{
Docs = new HashSet<Document>();
}
public Guid ServiceId { get; set; }
public string? Libelle { get; set; }
public virtual ICollection<Document> Docs { get; set; }
}
Here some screenshots :
Database diagram
Document
Service
var result = await _dbContext.BillingGroupFakes
.Where(b => b.Customers.FirstOrDefault().ExternalCustomerId.Equals($"{id}"))
.Include(b => b.Customers)
.Select(m => new
{
m.Customers.FirstOrDefault().CustomerId,
CustomerName = $"{m.Customers.FirstOrDefault().CustomerLastName}, {m.Customers.FirstOrDefault().CustomerName}",
m.BillingGroupId,
m.BillingGroupCode,
m.BillingGroupDescription
})
.AsNoTracking()
.ToListAsync();
I found the answer how to get the data:
var services = await _context.Services
.Where(s => s.ServiceId == Id)
.Include(s =>s.Docs)
.ToListAsync();
return services;
Thank you.

Foreign key not populating in Entity Framework

I cannot get a table to update correctly that should be linking two of my entities. To explain in more detail...I have two entities, Class and Teacher, with a relationship in the form of:
Teacher can be assigned to many classes
Class can only have one teacher.
Below are these two entities.
public class Teacher
{
[Required, Key]
public Guid Id { get; private set; }
[StringLength(50)]
public string Name { get; set; }
public string Email { get; set; }
public List<Class> Classes = new List<Class>();
public Teacher()
{
Id = new Guid();
}
public Teacher(Guid id)
{
Id = id;
}
public void AssignClass(Class newClass)
{
Classes.Add(newClass);
}
}
public class Class
{
[Required, Key]
public Guid Id { get; private set; }
[Required, StringLength(20)]
public string Name { get; set; }
[Required, Range(5, 30)]
public int Capacity { get; set; }
public Teacher Teacher { get; set; }
public IEnumerable<StudentClass> StudentClasses { get; set; }
public Class()
{
Id = new Guid();
}
public Class(Guid id)
{
Id = id;
}
}
When I generate my migrations I get a foreign key of TeacherId in the Classes table as expected. Here is the SQL:
CREATE TABLE [dbo].[Classes] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[Name] NVARCHAR (20) NOT NULL,
[Capacity] INT NOT NULL,
[TeacherId] UNIQUEIDENTIFIER NULL,
CONSTRAINT [PK_Classes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Classes_Teachers_TeacherId] FOREIGN KEY ([TeacherId]) REFERENCES [dbo].[Teachers] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_Classes_TeacherId]
ON [dbo].[Classes]([TeacherId] ASC);
My class derived of DBContext looks like:
public class SchoolDatabaseContext : DbContext
{
public DbSet<Student> Students { get; private set; }
public DbSet<Class> Classes { get; private set; }
public DbSet<Teacher> Teachers { get; private set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
public SchoolDatabaseContext(DbContextOptions<SchoolDatabaseContext> options) : base(options)
{
}
}
No configuration for those entities yet. I use DI to serve the DbContext to the controller and that all seems fine.
I have aimed for a DDD type structure, but to make this issue easier to debug I have stripped everything all the way back to the controller so it is basically... controller => DbContext.
Here is my code in the controller:
[HttpPost]
[Route("assign-teacher-to-class")]
public async Task<IActionResult> AssignClass([FromBody] AssignTeacherToClass assignTeacherToClass)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var teacher = await schoolDatabaseContext.Teachers.FindAsync(assignTeacherToClass.TeacherId);
var classToAssign = await schoolDatabaseContext.Classes.FindAsync(assignTeacherToClass.ClassId);
teacher.AssignClass(classToAssign);
schoolDatabaseContext.Entry(teacher).State = EntityState.Modified;
await schoolDatabaseContext.SaveChangesAsync();
return Ok(teacher);
}
When I debug through the ids are fine from the post body, they are assigned correctly to the DTO AssignClass and the calls to the DbContext to find the data for each type (teacher and class) are fine. I then call a method in my teacher type to add the class to the List Classes property (see teachers entity code at beginning for reference), I then Save the changes with the DbContext method and Problem Defined Here: at no stage does the TeacherId in the database update whilst debugging/completing. I have tried all I can think of like instantiating collections in different ways, changing collection types, looking for config that might help map these entities in this way, stripping out all extra layers, changing accessibility of properties and classes and few more.
Any help would really be appreciated as I am getting a bit defeated on this one and I feel like this relationship should be fairly straight forward. I actually was able to get my many to many working with a bridge class so I was surprised to get stuck on this one :(
Thanks
try this:
var teacher = await schoolDatabaseContext.Teachers.Include(x => x.Classes).SingleOrDefaultAsync(x => x.Id == assignTeacherToClass.TeacherId);
I don't think teacher.Classes gets tracked by DbContext otherwise.
There are multiple ways to accomplish this with EF Core. It is easiest to find if you call it what the docs call it "Related Data".
Here is the parent doc: Related Data
Specifically as #Y Stroli has illustrated the Eager Loading method.
The below example is shown on the eager loading reference to load multiple levels of related data:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ThenInclude(author => author.Photo)
.ToList();
}
As of EF Core 5.0 you can also do filtered includes:
using (var context = new BloggingContext())
{
var filteredBlogs = context.Blogs
.Include(blog => blog.Posts
.Where(post => post.BlogId == 1)
.OrderByDescending(post => post.Title)
.Take(5))
.ToList();
}
As the suggestion from lvan, you should change public List<Class> Classes = new List<Class>(); to public List<Class> Classes { get; set; } = new List<Class>();.
For your current code, it seems you want to add Class and return the teacher, if so, you need to include the exsiting classes to teacher like below, otherwise, it will only return the new adding class.
public async Task<IActionResult> AssignClass()
{
var assignTeacherToClass = new AssignTeacherToClass {
TeacherId = new Guid("52abe5e0-bcd4-4827-893a-26b24ca7b1c4"),
ClassId =new Guid("50354c76-c9e8-4fc3-a7c9-7644d47a6854")
};
var teacher = await _context.Teachers.Include(t => t.Classes).FirstOrDefaultAsync(t => t.Id == assignTeacherToClass.TeacherId);
var classToAssign = await _context.Classes.FindAsync(assignTeacherToClass.ClassId);
teacher.AssignClass(classToAssign);
_context.Entry(teacher).State = EntityState.Modified;
await _context.SaveChangesAsync();
return Ok(teacher);
}
One more note, you need to configure SerializerSettings.ReferenceLoopHandling like
services.AddMvc()
.AddJsonOptions(opt => {
opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
You need to define the connection between Teacher and Class.
protected override void OnModelCreating(Modelbuilder modelBuilder)
{
modelBuilder.Entity<Class>()
.HasOne<Teacher>(p => p.Teacher)
.WithMany(q => q.Classes)
.HasForeignKey(r => r.TeacherId);
}
Also add TeacherId prop to Class.

How to write this C# statement in linq

I'm trying to get related tables Adress and PlzOrt into an object User.
The relationships are as follows:
User 1:1 Adress
Adress n:1 PlzOrt
Entities Scaffolded from the DB
public partial class User
{
//No hash sets for Adress Scaffolded (probably unnecessary since the is
//only ever one address per user
public int IdUser { get; set; }
public int? FidAdresse { get; set; }
public string Email { get; set; }
public string Vorname { get; set; }
public string Nachmname { get; set; }
[ForeignKey("FidAdresse")]
[InverseProperty("User")]
public virtual Adresse FidAdresseNavigation { get; set; }
}
public partial class Adresse
{
public Adresse()
{
User = new HashSet<User>();
}
public int IdAdresse { get; set; }
public int FidPlzOrt { get; set; }
public string Strasse { get; set; }
public string Hausnr { get; set; }
[ForeignKey("FidPlzOrt")]
[InverseProperty("Adresse")]
public virtual PlzOrt FidPlzOrtNavigation { get; set; }
[InverseProperty("FidAdresseNavigation")]
public virtual ICollection<User> User { get; set; }
}
public partial class PlzOrt
{
public PlzOrt()
{
Adresse = new HashSet<Adresse>();
}
public int IdPlzOrt { get; set; }
public string Plz { get; set; }
public string Ort { get; set; }
[InverseProperty("FidPlzOrtNavigation")]
public virtual ICollection<Adresse> Adresse { get; set; }
}
Here´s the linq that does not work.
return _context.User
.Include(u => u.FidPermissionNavigation)
.Include(c => c.FidAdresseNavigation)
.ThenInclude(c => c.FidPlzOrtNavigation)
.FirstOrDefault(c => c.IdUser == id);
The linq work when I don´t include the "ThenInclude(c => c.FidPlzOrtNavigation)" statement, but I want this information in my object.
Here´s the C# that gives me the expected results:
public User GetUser(int id)
{
foreach (User user in _context.User)
{
if (user.IdUser == id)
{
foreach (Adresse adresse in _context.Adresse)
{
if (adresse.IdAdresse == user.FidAdresse)
{
user.FidAdresseNavigation = adresse;
foreach (PlzOrt plzOrt in _context.PlzOrt)
{
if (plzOrt.IdPlzOrt == adresse.FidPlzOrt)
{
user.FidAdresseNavigation.FidPlzOrtNavigation = plzOrt;
break;
}
}
break;
}
}
return user;
}
}
return null;
}
Translating this linq statement would be of great help. Thanks in advance.
Generated db_context code in case you are interested or this helps
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.IdUser)
.HasName("PRIMARY");
entity.HasIndex(e => e.FidAdresse)
.HasName("fk_User_Adresse1_idx");
entity.HasOne(d => d.FidAdresseNavigation)
.WithMany(p => p.User)
.HasForeignKey(d => d.FidAdresse)
.HasConstraintName("fk_User_Adresse1");
});
modelBuilder.Entity<Adresse>(entity =>
{
entity.HasKey(e => e.IdAdresse)
.HasName("PRIMARY");
entity.HasIndex(e => e.FidPlzOrt)
.HasName("fk_Adresse_plz_ort1_idx");
entity.HasOne(d => d.FidPlzOrtNavigation)
.WithMany(p => p.Adresse)
.HasForeignKey(d => d.FidPlzOrt)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("fk_Adresse_plz_ort1");
});
modelBuilder.Entity<PlzOrt>(entity =>
{
entity.HasKey(e => e.IdPlzOrt)
.HasName("PRIMARY");
});
So you have an id, and you want the one and only user that has this id as primary key, together with his Address and his PlzOrt.
Whenever you query, use Select to fetch the data. Only use Include if you want to update the fetched data.
The reason for Select is that you have greater freedom of what you select. Besides you can limit the fetched data: if you query Schools with their Students, you know that every Student of School 10 will have a foreign key SchoolId equal to 10. So why fetch this foreign key for every of the Schools 1000 Students?
I'm not familiar with the possibilities of ef-core. Does it know that if you Select one of the virtual properties that a (group-)join is needed? In that case it is easier to use Select.
If you'll have to do your joins yourself:
var requestedUserWithAddresAndPlz = dbContext.Users.
// keep only the user with id
.Where(user => user.IdUser == id)
// every User has exactly one Address: use a normal join
.Join(dbContext.Addresses,
user => user.IdUser, // from every User take IdUser
address => addres.IdAddress, // from every Address take IdAddress
// result selector: take the user with its address to make one new
(user, address) => new
{
// Select only the User properties you plan to use
Id = user.IdUser,
Name = user.Name,
...
Address = new
{
// again: select only the properties you plan to use
// no need to select IdAddress, you know the value!
Street = address.Street,
HouseNumber = address.HouseNumber,
// to fetch the Plz: fetch the one-and-only PlzOrt with address.FidPlzOrt
Plz = dbContext.PlzOrts
.Where(plzOrt => plzOrt.PlzOrdIt == address.FidPlzOrt)
.FirstOrDefault(),
})
.FirstOrDefault();
Note: I used anonymous types to have greater freedom in selecting only the properties I actually plan to use. I can also give my properties the names that I want.
Disadvantage: you can't use anonymous types as return values. If you really need this outside your function, use create a class that contains your data, and use new SomeClass(). Advantage: if your database changes, SomeClass doesn't have to change, and thus your callers won't notice the change in your database.

Entity Relations for EF7 MVC6

My goal here is to use EF7 with MVC6 [BETA2] to list a number of bookshelves and the number of books on each shelf.
The database schema is created correctly with the correct table relationships. I can successfully add shelves and books to the database including the foreign key relationships (see code below).
When I test the index page that should show the book count on each shelf, I receive no book count data and no errors. In the Shelf entity the property Books remains unpopulated with Book entities thus the count is null (see code below).
In EF7 is there somewhere where I need to write code to populate Shelf.Books or should this happen automatically in EF7?
BookShelf.cs
namespace MyApp.Models
{
public class Shelf
{
public int ShelfId { get; set; }
public string Name { get; set; }
public virtual List<Books> Books { get; set; }
}
public class Book
{
public int BookId { get; set; }
public string Name { get; set; }
public int ShelfId { get; set; }
public Shelf Shelf{ get; set; }
}
}
ApplicationDbContext.cs
namespace MyApp
{
public class ApplicationDBContext
{
public DbSet<Shelf> Shelf { get; set; }
public DbSet<Book> Book { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Shelf>().Key(s => s.ShelfId);
builder.Entity<Book>().Key(b => b.BookId);
builder.Entity<Shelf>()
.OneToMany(s => s.Book)
.ForeignKey(k => k.ShelfId);
base.OnModelCreating(builder);
}
}
ShelfController.cs
namespace MyApp
{
private ApplicationDBContext db;
public BuildingsController(ApplicationDBContext context)
{
db = context;
}
// GET: Shelves
public async Task<IActionResult> Index()
{
return View(await db.Shelves.ToListAsync());
}
}
Index.cshtml
...
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Books.Count)
</td>
</tr>
}
....
Take a look at ICollection Vs List in Entity Framework. I have a feeling the minor examples of EF7 using List<> are just incorrect (hard to imagine that with EF7 the best practice is changed from ICollection<> to List<>, it's generally very poor practice to expose a concrete collection type as a property.)
Per your comment I would change:
Create View Models
public class IndexViewModel
{
public IEnumerable<ShelveModel> Shelves { get; set; }
}
public class ShelveModel
{
public string Name { get; set; }
public int BookCount { get ; set; }
}
Update the logic
// GET: Shelves
public async Task<IActionResult> Index()
{
var model = new IndexViewModel();
model.Shelves = db.Shelves
.Select(s => new
{
Name = s.Name,
BookCount = s.Books.Count()
})
.ToListAsync()
.Select(s => new ShelveModel()
{
Name = s.Name,
BookCount = s.Books.Count()
})
.ToList();
return View(model);
}
What I have discovered is that EF does not populate the parent object with related children objects out of the box. Example, myShelf.Books will be empty until populated in the controller action function.

One-Many via NHibernate Loquacious mapping by code or xml

I want to implement a one-to-many relationship between a person and car, and have CRUD operations on both person and car. Brief CRUD and relationships:
Update
A person has many cars
CRUD operations on both person and car via person object.
Deleting a person will delete all s/his cars
Ability to perform CRUD operation on someone's cars, either via person object or car object.
Is it possible via ORM, in particular NHibernate?
Classes like below:
public class PersonSet
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<CarSet> Cars { get; set; }
}
public class CarSet
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual PersonSet Person { get; set; }
}
Mapping is shown below:
public class PersonSetMap : ClassMapping<PersonSet>
{
public PersonSetMap()
{
Id(x => x.Id, m=>m.Generator(Generators.Identity));
Property(x=>x.Name);
Set(x => x.Cars, c =>
{
//c.Key(k =>
// {
// k.Column("PersonId");
// });
c.Cascade(Cascade.All);
c.Lazy(CollectionLazy.NoLazy);
// c.Inverse(true);
}
, r =>
{
r.OneToMany();
}
);
}
}
public class CarSetMap : ClassMapping<CarSet>
{
public CarSetMap()
{
Id(x => x.Id, m => m.Generator(Generators.Identity));
Property(x => x.Name);
ManyToOne(x => x.Person, m =>
{
m.Column("PersonId");
m.Cascade(Cascade.None);
m.NotNullable(true);
});
}
}
The problem I have is that if I update one car and try to save it on a person object, it doesn't change.
Update
I want to find out if it is possible, and where my mapping above is wrong. Any idea on either xml version or Loquacious would also be appreciated.
There should be a PersonId foreign key on table Car.
I don't know if this would solve your problem, but in a ManyToOne mapping the Unique and NotNullable methods should be applied at the column level.
ManyToOne(x => x.Person, m =>
{
m.Column(c =>
{
c.Name("PersonId");
c.NotNullable(true);
});
m.Cascade(Cascade.None);
});

Categories