Entity Framework Core multiple relations to a table - c#

Using Entity Framework Core 7.0.2 in ASP.NET Core Web API written in C#.
I have multiple tables and they are all connected with one-to-many relation.
See the following tree how all tables are connected.
I just added 2 extra tables which are connected to Trades.
This is how my modal looks like:
public class Trade
{
[Required][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][Column(TypeName = "varchar(50)")] public string TradingJournalId { get; set; }
public ICollection<Attachment> Attachments { get; set; }
public ICollection<Improvement> Improvements { get; set; }
public ICollection<Tag> Tags { get; set; }
}
Before I added the other 2 tables called Improvements and Tags my query for collecting the user with the relation is the following:
var user = _context.Users
.Include(u => u.TradingJournal)
.ThenInclude(t => t.Trades)
.ThenInclude(f => f.Attachments)
.SingleOrDefault(u => u.Id == Id);
If I try the following it will result in a error because in will look into the Attachment table.
var user = _context.Users
.Include(u => u.TradingJournal)
.ThenInclude(t => t.Trades)
.ThenInclude(f => f.Attachments)
.ThenInclude(f => f.Tags)
.ThenInclude(f => f.Improvements)
.SingleOrDefault(u => u.Id == Id);
This is my preferable output.
{
"Id": "114af6db-0124-4573-9ab9-d676f8c05a48",
"TradingJournal": [
{
"Id": "fa0eef8c-16fd-416e-97ec-df6f43c448f2",
"UserId": "114af6db-0124-4573-9ab9-d676f8c05a48",
"Trades": [
{
"Id": "4e7ebfd4-54dc-48c7-9e23-01312295f431",
"TradingJournalId": "fa0eef8c-16fd-416e-97ec-df6f43c448f2",
"Attachments": [
{
"Id": "362be948-8bc3-4172-ad19-9625bb5aebf4",
"TradeId": "4e7ebfd4-54dc-48c7-9e23-01312295f431",
}
],
"Tags": [
{
"Id": "362be948-8bc3-4172-ad19-96252b54ebf2",
"TradeId": "4e7ebfd4-54dc-48c7-9e23-01312295f431",
}
],
"Improvements": [
{
"Id": "362be948-8bc3-4172-ad19-9625bb5ae3f8",
"TradeId": "4e7ebfd4-54dc-48c7-9e23-01312295f431",
}
],
}
]
}
]
}
At the moment the Tags and Improvements are both null.
I got no clue how I need to solve this by adding a reference to the other 2 tables. I did some research how to do it but my case is much more complicated due the multiple relations hence the help I need.
Edit:
Here are all the entities modals.
public class User
{
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string Id { get; set; }
public ICollection<TradingJournal> TradingJournal { get; set; }
}
public class TradingJournal
{
[Required][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][Column(TypeName = "varchar(50)")] public string UserId { get; set; }
public ICollection<Trade> Trades { get; set; }
}
public class Trade
{
[Required][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][Column(TypeName = "varchar(50)")] public string TradingJournalId { get; set; }
public ICollection<Attachment> Attachments { get; set; }
public ICollection<Improvement> Improvements { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Attachment
{
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string TradeId { get; set; }
}
public class Improvement
{
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string TradeId { get; set; }
}
public class Tags
{
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string Id { get; set; }
[Required][StringLength(50)][Column(TypeName = "varchar(50)")] public string TradeId { get; set; }
}
I have removed all the irrelevant properties in the modals which aren't needed for the reference tables so that this code is much more readable.

ThenInclude allows to include properties of "previous" entity, so in this case it would be Attachments which have not Tags or Improvements. AFAIK EF Core still does not allow "walk backs" so try the following:
var user = _context.Users
.Include(u => u.TradingJournal)
.ThenInclude(t => t.Trades)
.ThenInclude(f => f.Attachments)
.Include(u => u.TradingJournal)
.ThenInclude(t => t.Trades)
.ThenInclude(f => f.Tags)
.Include(u => u.TradingJournal)
.ThenInclude(t => t.Trades)
.ThenInclude(f => f.Improvements)
.SingleOrDefault(u => u.Id == Id);
Check out the Including multiple levels section of eager loading docs.

Related

How to get objects stored inside collection that is stored inside object?

I am trying to retrieve posts from database. Post has collection of Connections and Connection has Peer object. I want to include Peer objects where PeerId is equal to passed Id but instead it returns all the peers inside Connection collection. How should query look like?
var posts = await context.Post.AsQueryable()
.Include(u => u.Connections).ThenInclude(u => u.Peer)
.Include(u => u.Connections).ThenInclude(u => u.Issuer).Include(u => u.Location)
.Include(u => u.Images)
.Where(c=>c.Creator.UserSettings.ShowMe == true
&&
c.Connections.Any(connection => connection.PeerId==id)) //this line
.OrderBy(item => item.Status)
.ThenByDescending(d => d.CreatedDate)
.ToListAsync();
public class Post
{
public long Id { get; set; }
public long CreatorId { get; set; }
public User Creator { get; set; }
public long LocationId { get; set; }
public virtual Location Location { get; set; }
public ICollection<Connection> Connections { get; set; }
public ICollection<Image> Images { get; set; }
public string Status { get; set; }
}
public class Connection
{
public long Id { get; set; }
public long IssuerId { get; set; }
public virtual User Issuer { get; set; }
public long PeerId { get; set; }
public virtual User Peer { get; set; }
public long PostId { get; set; }
public virtual Post Post { get; set; }
}
public class User
{
public long Id { get; set; }
}
You don't need AsQuerable if you get result as ListAsync. This query will be working with ef core 5:
var posts = await context.Post
.Include(u => u.Location)
.Include(u => u.Images)
.Include(u => u.Connections.Where(c=> c.PeerId==id))
.ThenInclude(u => u.Peer)
.ThenInclude(u => u.Issuer)
.Where(c=>c.Creator.UserSettings.ShowMe == true)
.OrderBy(item => item.Status)
.ThenByDescending(d => d.CreatedDate)
.ToListAsync();

Entity Framework get SUM from child property

I have the following model where I'd like to get the sum of all OrderTotalItems for all Orders of a Customer where the OrderTotalType (Enumeration) is "total" or 99:
public class Customer
{
...
public ICollection<Order> Orders { get; set; } = new Collection<Order>();
}
public class Order
{
...
public ICollection<OrderTotalItem> OrderTotalItems { get; set; } = new Collection<OrderTotalItem>();
}
public class OrderTotalItem
{
[Required]
public int Id { get; set; }
[Required]
[Column(TypeName = "decimal(10, 4)")]
public decimal Value { get; set; }
[Required]
public OrderTotalType Type { get; set; }
}
I am building a CustomerAdminDTO to include all relevant data of a customer for the admin client:
public class CustomerAdminDto
{
public int Id { get; set; }
public string UserId { get; set; }
public Gender Gender { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string VATId { get; set; } = "";
public bool VATIdValid { get; set; } = false;
public DateTime Added { get; set; }
public DateTime LastModified { get; set; }
public decimal OrdersTotal { get; set; }
public CustomerStatusShortDto CustomerStatus { get; set; }
public CustomerAddressDto CustomerAddress { get; set; }
public CustomerAddressDto BillingAddress { get; set; }
public ICollection<OrderListShortDto> Orders { get; set; }
}
In my data service I fill the DTO like that
var customerAdmin = await _context.Customers
.Include(x => x.Addresses)
.Include(x => x.CustomerStatus)
.Include(x => x.Orders)
.ThenInclude(x => x.OrderTotalItems)
.Where(x => x.UserId == userid)
.Select(customer => new CustomerAdminDto
{
Id = customer.Id,
UserId = customer.UserId,
Gender = customer.Gender,
FirstName = customer.FirstName,
LastName = customer.LastName,
VATId = customer.VATId,
VATIdValid = customer.VATIdValid,
Added = customer.Added,
LastModified = customer.LastModified,
OrdersTotal = customer.Orders.Sum(x => x.OrderTotalItems
.Where(x => x.Type == Enums.OrderTotalType.Total)
.Sum(x => x.Value)),
CustomerStatus = new CustomerStatusShortDto
{
Id = customer.CustomerStatus.Id,
Name = customer.CustomerStatus.Name,
},
...
}
.FirstOrDefaultAsync();
where everything works, except the OrdersTotal.
API compiles fine but throws the following error at runtime:
Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
Thanks for your hints!
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
This error in SQL server means that you tried to call aggregation function (customer.Orders.Sum() in your case) on other expression that contains aggregation function (.Sum(x => x.Value) in your case). In order to avoid this you can simplify your LINQ expression for OrdersTotal:
OrdersTotal = customer.Orders.SelectMany(o => o.OrderTotalItems).Where(x => x.Type == Enums.OrderTotalType.Total).Sum(x => x.Value)
There is only one aggregation here so it should work fine

How to use an ICollection navigation property with include()

I am trying to get each user with its projects using entity framework core in a web api project in the controller with linq
I tried with this query but it gave me an empty object
var users = _context.Users.Include(x => x.userProjects.Select(up => up.UserId == x.Id)).ToListAsync();
I also tried this one and got the same result
var users = _context.Users.Include(x => x.userProjects.Where(up => up.UserId == x.Id)).ToListAsync();
This is the User class
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<UserProject> userProjects { get; set; }
}
This is the Project class
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<User> Users { get; set; }
public ICollection<UserProject> UserProjects { get; set; }
}
and this is the UserProject class
public class UserProject
{
[ForeignKey("User")]
public int UserId { get; set; }
public User User { get; set; }
[ForeignKey("Project")]
public int ProjectId { get; set; }
public Project Project { get; set; }
}
I want to get a json with each user and an array of its projects
For multiple level of includes, you need ThenInclude.
var users = _context.Users
.Include(x => x.userProjects)
.ThenInclude(y => y.Project)
.ToListAsync();
var users = _context.Users.Include(u => u.userProjects)
.ThenInclude(up => up.Projects))
.Where(u => u.UserId == Id)
.ToListAsync();
What happens here:
You will retrieve all users which have UserId = yourId and also all the UserProjects and Projects of those users.
Example of code which shows you you can access all the projects of the first returned user:
var projectsForFirstUser = users.First().Select(x => x.UserProjects).Select(x => x.Project).ToList();
EDIT: Modified to ThenInclude because of EF Core.

Asp.Net Core Include values of FK of another FK

I have 3 Models, 3 tables in DB (created with EF migrations):
public class Announce {
public int Id { get; set; }
public Location Location { get; set;}
public int LocationId { get; set; }
}
public class Location {
public int Id { get; set; }
public string Name { get; set; }
public District District { get; set; }
public string DistrictId { get; set; }
}
public class District {
public int Id { get; set; }
public string Name { get; set; }
}
and a Dto class :
public class AnnounceForListDto {
public int Id { get; set; }
public string LocationName { get; set; }
public string DistrictName{ get; set; }
}
And in AutoMapperProfile :
CreateMap<District, AnnounceForListDto>()
.ForMember(dest => dest.DistrictName, opt =>
{
opt.MapFrom(dis => dis.Name);
});
And I want to getAnnounces as :
public async Task<IEnumerable<Announce>> GetAnnounces()
{
var announces = await _context.Announce
.Include(prop => prop.Photos)
.Include(prop => prop.Location)
.ToListAsync();
return announces;
}
I need to include the District Name (or District Id, because I use AutoMapper to equalise DistrictName from AnnounceForListDto with District.Name) in my list.
I tried something as .Include(prop => prop.Location.District.Name), but get an error, "Include can use only one "dot").
Maybe .ThenInclude(Location => Location.District) this help and I wrong in my Dto declaration?
My Dto is used in Controller :
[HttpGet]
public async Task<IActionResult> GetAnnounces()
{
var announces = await _repo.GetAnnounces();
var announcesToReturn = _mapper.Map<IEnumerable<AnnounceForListDto>>(announces);
return Ok(announcesToReturn);
}
Solved :
Create relation between Location and District
then, my Repo method :
var announces = await _context.Announce
.Include(prop => prop.Photos)
.Include(prop => prop.Location)
.ThenInclude(prop => prop.District)
.ToListAsync();
return announces;

Entity Framework Core does not save related data

In continuation of yesterday's post
Two Entities
public class Realtor
{
public Realtor()
{
Guid = Guid.NewGuid();
Registration = DateTime.Now;
}
public int Id { get; set; }
public Guid Guid { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Registration { get; set; }
public int SubdivId { get; set; }
public Subdiv Subdiv { get; set; }
}
public class Subdiv
{
public Subdiv()
{
Created = DateTime.Now;
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public List<Realtor> Realtors { get; set; }
}
I spend test
I added one Subdiv (TOSTER TM) and received his ID
Next, I add a Realtor and I push Subdiv property found on the ID, the newly created TOSTER TM
Realtor.Subdiv is an object of type Subdiv. OK.
Then I try to select from the base the newly added Realtor.
Realtor.Subdiv = null OMG!!
We get Subdiv object, which is lacking in Realtor above and see his List<Realtor> = null
Please help in solving this problem.
Try this:
Relator rl = Context.Relators.Include(r => r.Subdiv).First(s => s.Id == id);
Now you can access to Subdiv property
For more related date you can call Include Methods more times:
Relator rl = Context.Relators
.Include(r => r.Subdiv)
.Include(r => r.AnotherRel)
.First(s => s.Id == id);
For Entities with multiple levels in depth:
If Subdir is a collection
Relator rl = Context.Relators
.Include(r => r.Subdiv)
.ThenInclude(sub => sub.SecondLevelDepth)
.First(s => s.Id == id);
if Subdir is an Entity
Relator rl = Context.Relators
.Include(r => r.Subdiv.Select(s => s.SecondLevelDepth)
.First(s => s.Id == id);
The problem is not with saving the related data (it should be saved correctly, you could check that inside the database), but loading it.
EF Core currently does not support lazy loading, so in order to get the related data you need to explicitly request it (the so called eager loading):
Realtor rl = context.Realtors.Include(r => r.Subdiv).First(r => r.Id == id);
For more info, see EF Core: Loading Related Data.
Entity Framework core allows to save related entities, you need to define Fluent API settings for both Master and detail table
public class Order
{
public int Id { get; set; }
public int AddressId { get; set; }
public string DeliveryNotes { get; set; }
public int PurchaseOrderNo { get; set; }
public virtual ICollection<OrderItem> Items { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UserPrice { get; set; }
public string Comment { get; set; }
[ForeignKey("OrderId ")]
public int OrderId { get; set; }
public virtual Order Order { get; set; }
}
Inside the DBConext OnModelCreating method , define the relation like, it will make sure when you have order object, its related or child objects i.e Enrolments will also be saved
modelBuilder.Entity<Order>()
.HasMany(c => c.Items)
.WithOne(e => e.Order);
modelBuilder.Entity<OrderItem>().Ignore(x => x.Order);
Now your code should look like this
Order _order = new Order{ AddressId = 1, DeliveryNotes ="some notes", PurchaseOrderNo =1};
_order.Items = new List< OrderItem>();
_ordert.Items.add(new OrderItem{ ProductName =”Laptop”, Quantity =1, UserPrice =1500.00, Comment =”some testing comments”});
repository.order.insert(_order);
repository.save();

Categories

Resources