EF Sum nested list values after group by - entity-framework

So I have a list of tickets, I want to group them by customer and then sum all the prices of the ticket operations of these tickets.
query2 = query.GroupBy(x => new {x.Customer.Id, x.Customer.BusinessName}).Select(x => new TicketStatisticsRow {
Field = x.Key.BusinessName,
Tickets = x.Count(),
TotalPrice = x.Sum(t => t.TicketOperations.Sum(to => to.Price))
});
Other than trying the sum of the sum for TotalPrice i tried x.SelectMany(e => e.TicketOperations).Sum(x=> x.Price) both these solutions cannot be evaluated locally.. What could be the best approach in this situation?
For reference the ticket class:
public class Ticket {
public Customer Customer { get; set; }
public ICollection<TicketOperation> TicketOperations { get; set; }
public class TicketOperation {
public decimal Price { get; set; }
}
}

You'll want something like this
query2 = query.GroupBy(x=>x.Customer.Id).ToDictionary(x=>x.Key, x=> new
TicketStatisticsRow {
Field = x.Key.BusinessName,
Tickets = x.Count(),
TotalPrice = x.Sum(t => t.TicketOperations.Sum(to => to.Price))
});

Related

How to properly replace an owned collection of items?

I receive an exception when trying to replace the owned collection with the one I receive through API (EF Core 6).
System.InvalidOperationException: 'The instance of entity type 'Product' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Here is my model configuration:
public class Order
{
public int Id { get; set; }
public ICollection<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
modelBuilder.Entity<Product>(x =>
{
x.HasKey(e => e.Id);
x.Property(e => e.Name);
});
modelBuilder.Entity<Order>(x =>
{
x.HasKey(e => e.Id);
x.OwnsMany(e => e.OrderItems, p =>
{
p.ToTable("OrderItems")
.WithOwner();
p.HasOne(e => e.Product).WithMany();
p.Property(e => e.Quantity);
});
});
Act:
//these are updated items received through API
var newItems = new List<OrderItem>
{
new OrderItem{ Product = new Product { Id = 2}, Quantity = 2},
new OrderItem{ Product = new Product { Id = 3}, Quantity = 3}
};
var existingOrder = _dbContext.Orders
.Include(o => o.OrderItems).ThenInclude(i => i.Product)
.Single(o => o.Id == 1);
// Existing OrderItems are already referencing Product with Id 2
existingOrder.OrderItems = newItems;
// Error occurs because Product with Id = 2 was already tracked
_dbContext.SaveChanges();
I know that the issue might be resolved by replacing Product with ProductId within my OrderItem class but I would rather prefer to keep my domain model as it is.
What is the best practice to perform such update?
it's all about understanding of the modeling. You can create a model response to update also, but just have to update manually by calling it.
you have existingOrder from the query include, then include like this I guess
var existingOrder = ...
existingOrder.OrderItems.Update(yourentity_OrderItem);
await _dbContext.SaveChangesAsync();
So this mean you have to retrieve each Product.id by a for, and update each new items, then savechanges.

The right way to apply GroupBy extension method with aggregate function

I have this simple model.
public class Room
{
public Guid Id { get; set; }
public Guid? postSubjectId { get; set; }
[ForeignKey("postSubjectId")]
public PostSubject postSubject { get; set; }
public string MemberId { get; set; }
[ForeignKey("MemberId")]
public AppUser Member { get; set; }
}
Basically I need to get Grouped postSubjectId along with MemberId.Count() , I know it's easy .. but it never comes with the expected result.
I made this simple GroupBy query
var mmbrs = _context.Rooms
.Select(g => new { id = g.postSubjectId, mmbrscount = g.MemberId })
.AsEnumerable()
.GroupBy(g => new { id = g.id , mmbrscount = g.mmbrscount.Count() }).ToList();
but it gives me unexpected result
However I did the same using ordinary sql query
select [postSubjectId] as postId, count([MemberId]) as mmbrsCount from [dbo].[Rooms] group by [postSubjectId]
and It gives me result as expected
I need to apply that expected result using LINQ GruoupBy extention method
The grouping key new { id = g.postSubjectId, mmbrscount = g.MemberId }) is like typing group by [postSubjectId], count([MemberId]) in SQL.
The correct statement is:
_context.Rooms
.GroupBy(r => r.postSubjectId)
.Select(g => new
{
id = g.Key,
mmbrscount = g.Count()
})
So every Room has exactly one property PostSubjectId, and one string property MemberId.
I need to get Grouped postSubjectId along with MemberId.Count()
Apparently you want to make groups of Rooms that have the same value for property PostSubjectId AND have the same value for MemberId.Count().
var result = dbContext.Rooms.GroupBy(room => new
{
PostSubjectId = room.PostSucjectId,
MemberIdLength = room.MemberId.Count(),
});
The result is a sequence of groups of Rooms. Every group has a key, which is a combination of [PostSubjectId, MemberIdLength]. The group is a sequence of Rooms. All rooms in one group have the same combination of [PostSubjectId, MemberIdLength].
If you don't want a sequence of groups of Rooms, you can use the overload of GroupBy that has a parameter resultSelector
var result = dbContext.Rooms.GroupBy(
// parameter keySelector
room => new
{ PostSubjectId = room.PostSucjectId,
MemberIdLength = room.MemberId.Count(),
},
// parameter resultSelector:
// from every combination of [PostSubjectId, MemberIdLength] (= the key) and
// all rooms that have this combination, make one new object:
(key, roomsWithThisKey) => new
{
// select the properties that you actually plan to use, for example
PostSubjectId = key.PostSubjectId,
MemberIdLength = key.MemberIdLength,
RoomInformations = roomsWithThisKey.Select(roomWithThisKey => new
{
Id = roomWithThisKey.Id,
Member = roomWithThisKey.Member,
...
})
.ToList(),
});

EF Core 2.1 evaluates locally when Sum complex and Grouping

I'm using EF Core 2.1 and query don't evaluates on SQL server side.
Model using in this query is:
public class V_TurnoverByDivision
{
public long Id { get; set; }
public decimal LineAmount { get; set; }
public DateTime? PostingDate { get; set; }
public decimal Quantity { get; set; }
public decimal UnitCostLcy { get; set; }
public string DivisionCode { get; set; }
public string DivisionDescription { get; set; }
public string TopDivisionCode { get; set; }
public string TopDivisionDescription { get; set; }
public decimal RUCAmount { get; set; }
}
This LINQ statement is run completely in SQL Server:
return query
.GroupBy(g => new { g.DivisionCode, g.DivisionDescription, g.TopDivisionCode, g.TopDivisionDescription, g.PostingDate })
.Select(s =>
new V_TurnoverByDivision
{
DivisionCode = s.Key.DivisionCode,
DivisionDescription = s.Key.DivisionDescription,
TopDivisionCode = s.Key.TopDivisionCode,
TopDivisionDescription = s.Key.TopDivisionDescription,
PostingDate = s.Key.PostingDate,
LineAmount = s.Sum(ss => ss.LineAmount),
RUCAmount = s.Sum(ss => ss.LineAmount - (ss.Quantity * ss.UnitCostLcy))
});
and generates the following SQL
SELECT
[v].[BIInvCNLinesID]
,[v].[DivisionCode]
,[v].[DivisionDescription]
,[v].[LineAmount]
,[v].[PostingDate]
,[v].[Quantity]
,[v].[TopDivisionCode]
,[v].[TopDivisionDescription]
,[v].[UnitCostLcy]
FROM [V_TurnoverByDivision] AS [v]
WHERE [v].[PostingDate] >= #__firstDayOfcurrentMonth_0
ORDER BY [v].[DivisionCode], [v].[DivisionDescription], [v].[TopDivisionCode], [v].[TopDivisionDescription], [v].[PostingDate]
This LINQ statement works but performs the GroupBy in memory
and I get warrnings in the Output windows
Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'Sum()' could not be translated and will be evaluated locally.
BUT WHEN I use this query
return query
.GroupBy(g => new { g.DivisionCode, g.DivisionDescription, g.TopDivisionCode, g.TopDivisionDescription, g.PostingDate })
.Select(s =>
new V_TurnoverByDivision
{
DivisionCode = s.Key.DivisionCode,
DivisionDescription = s.Key.DivisionDescription,
TopDivisionCode = s.Key.TopDivisionCode,
TopDivisionDescription = s.Key.TopDivisionDescription,
PostingDate = s.Key.PostingDate,
LineAmount = s.Sum(ss => ss.LineAmount)
});
};
and SQL generates query that should be:
SELECT
[v].[DivisionCode]
,[v].[DivisionDescription]
,[v].[TopDivisionCode]
,[v].[TopDivisionDescription]
,[v].[PostingDate]
,SUM([v].[LineAmount]) AS [LineAmount]
FROM [V_TurnoverByDivision] AS [v]
WHERE [v].[PostingDate] >= #__firstDayOfcurrentMonth_0
GROUP BY [v].[DivisionCode]
,[v].[DivisionDescription]
,[v].[TopDivisionCode]
,[v].[TopDivisionDescription]
,[v].[PostingDate]
How to solve problem with:
RUCAmount = s.Sum(ss => ss.LineAmount - (ss.Quantity * ss.UnitCostLcy))
This is EF Core GroupBy translation limitation (probably will be resolved in some future version). In order to be translatable to SQL, the aggregate method expression should be simple property accessor.
That's why
s.Sum(ss => ss.LineAmount)
translates, but
s.Sum(ss => ss.LineAmount - (ss.Quantity * ss.UnitCostLcy))
doesn't.
Hence the solution is to pre-select the expression(s) needed for aggregates. One way to do that is to use the GroupBy overload with element selector:
return query
.GroupBy(e => new // Key
{
e.DivisionCode,
e.DivisionDescription,
e.TopDivisionCode,
e.TopDivisionDescription,
e.PostingDate
},
e => new // Element
{
e.LineAmount,
RUCAmount = e.LineAmount - (e.Quantity * e.UnitCostLcy) // <--
})
.Select(g => new V_TurnoverByDivision
{
DivisionCode = g.Key.DivisionCode,
DivisionDescription = g.Key.DivisionDescription,
TopDivisionCode = g.Key.TopDivisionCode,
TopDivisionDescription = g.Key.TopDivisionDescription,
PostingDate = g.Key.PostingDate,
LineAmount = g.Sum(e => e.LineAmount),
RUCAmount = g.Sum(e => e.RUCAmount) // <--
});

ASP.NET Core cannot sort grouped items

I have the following model in my ASP.NET Core application:
public class LocationTypeGroup {
public string Name { get; set; }
public IEnumerable<LocationType> LocationTypes { get; set; }
}
public class LocationType
{
[Key]
public int LocationTypeID { get; set; }
public string Name { get; set; }
public string IntExt { get; set; }
}
I am trying to run a query that groups them by IntExt, and sorts by Name within each group.
The following works, but doesn't sort:
public async Task<List<LocationTypeGroup>> GetGroupedLocationTypes()
{
return await _context.LocationTypes
.GroupBy(p => p.IntExt)
.Select(g => new LocationTypeGroup
{
Name = g.Key,
LocationTypes = g.Select(x => x)
})
.OrderBy(x=>x.Name)
.ToListAsync();
}
If I change to this:
LocationTypes = g.Select(x => x).OrderBy(x => x)
Then I still do not get a sorted result.
What am I doing wrong?
It's possible that EF can't build SQL query.
So you need simplify it manually. and split to 2 queries:
var groups = await context.LocationTypes
.GroupBy(p => p.IntExt)
.ToListAsync();
return groups.Select(g => new LocationTypeGroup
{
Name = g.Key,
LocationTypes = g.Select(x => x)
})
.OrderBy(x=>x.Name);
The first query loads simply groups, and the second sorts them and converts to LocationTypeGroup.
May be it caused by too old version of Entity Framework Core. Try this approach, moreover it will be less expensive:
//data is loaded into memory
var data = await _context.LocationTypes.ToListAsync();
//data's transform
var answer = data.GroupBy(x => x.IntExt)
.Select(x => new LocationTypeGroup
{
Name = x.Key,
LocationTypes = x.AsEnumerable()
}).OrderBy(x => x.Name).ToList();

IQueryable.Select to a List type sub POCO

I have an Entity Framework model in which there is a "Customers" and a "CustomerPhones" table. A customer can have multiple phone numbers so the "Customer" entity has a collection of "Phone". I can query the model with no problem :
using (CustomerEntities context = new CustomerEntities())
{
Customer customer = context.Customers.FirstOrDefault();
CustomerPhone phone = customer.Phones.FirstOrDefault();
MessageBox.Show(customer.Name + " " + phone.Number);
}
The model is too complex for what I need to do (even though my example is basic) so I'm trying to boil it down to simpler POCOs. Here are the 2 simple classes :
public class SimplePhone
{
public int Id { get; set; }
public string Number { get; set; }
}
public class SimpleCustomer
{
public int Id { get; set; }
public string Name { get; set; }
//Phones is a list because a single Customer can have multiple phone numbers
public List<SimplePhone> Phones { get; set; }
}
I can populate the simple properties of the object using the "Select" method of "IQueryable" :
using (CustomerEntities context = new CustomerEntities())
{
IQueryable<SimpleCustomer> customers = context.Customers.Select(
c => new SimpleCustomer
{
Id = c.Id,
Name = c.Name
}
);
SimpleCustomer customer = customers.FirstOrDefault();
MessageBox.Show(customer.Name);
}
So my question is pretty simple : how can I populate the "Phones" property which is a list?
using (CustomerEntities context = new CustomerEntities())
{
IQueryable<SimpleCustomer> customers = context.Customers.Select(
c => new SimpleCustomer
{
Id = c.Id,
Name = c.Name
Phones = ///????
}
);
SimpleCustomer customer = customers.FirstOrDefault();
SimplePhone phone = customer.Phones.FirstOrDefault();
MessageBox.Show(customer.Name + " " + phone.Number);
}
Let me know if I'm unclear and/or you need more details.
Thanks!
I'm not sure if there isn't something more to your question, but as far as I understand, you can just call ToList and it will be materialized as a list:
IQueryable<SimpleCustomer> customers =
context.Customers.Select(c => new SimpleCustomer
{
Id = c.Id,
Name = c.Name,
Phones = c.Phones.Select(p => new SimplePhone
{
Id = p.Id, // Unless you want the custom Id, i.e. c.Id
Number = p.Number
}).ToList();
});