Linq To Entities Exclude Related Data - asp.net-mvc-3

I am building an ASP.Net MVC 3 Web application which uses Entity Framework 4.1. I also use the Unit of Work pattern with a Generic Repository. I have two Entities in my Model, Shift and ShiftDate. Please see the diagram below for their structure. The idea is, a Shift can have 1 or many ShiftDates, and a ShiftDate can belong to only one Shift.
I wish to create a query which will return a List of all Shifts like so
public IList<Shift> GetShifts(int id)
{
return _UoW.Shifts.Get(s => s.organisationID == id).OrderBy(s => s.startDate).ToList();
}
Then in my Razor View, I wish to use a foreach loop and list the details of each Shift. One of these details is the total number of ShiftDates each Shift has. To do this I will do something like
foreach(var shiftDate in Model.Shifts)
{
#Html.DisplayFor(modelItem => item.ShiftDates.Count)
}
This works fine, however, I want the count to exclude ShiftDates whose shiftDateStatusID is set to 442. To do this I tried amending my Linq Query to this
return _UoW.Shifts.Get(s => s.organisationID == id && s.ShiftDates.Any(sd => sd.shiftDateStatusID != 442))
.OrderBy(s => s.startDate).ToList();
However, this still pulls back ShiftDates who have a shiftDateStatusID of 442.
Hopefully this makes sense.
Any feedback would be greatly appreciated.
Thanks.

If you do
_UoW.Shifts.Get(s => s.organisationID == id
&& s.ShiftDates.Any(sd => sd.shiftDateStatusID != 442)
you get shifts that have at least one ShiftDate having a shiftDateStatusID other than 442. Well, there are probably a lot of shifts meeting this condition. The ShiftDates collections of these shifts will be loaded completely, including those with 442 (if any).
Either you have to exclude the ShiftDates with status 442 when you populate your view model, or when it's only for the count, exclude them when you do the count:
item.ShiftDates.Count(sd => sd.shiftDateStatusID != 442)

Related

Adding a custom sorting to listing with an aggregate in shopware 6

I am trying to build a custom sorting for the product listings in shopware 6.
I want to include a foreign table (entity is: leasingPlanEntity), get the min of one of the fields of that table (period_price) and then order the search result by that value.
I have already built a Subscriber, and try it like that, what seems to work.
public static function getSubscribedEvents(): array
{
return [
//ProductListingCollectFilterEvent::class => 'addFilter'
ProductListingCriteriaEvent::class => ['addCriteria', 5000]
];
}
public function addCriteria(ProductListingCriteriaEvent $event): void
{
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->addAggregation(new MinAggregation('min_period_price', 'leasingPlan.periodPrice'));
// Sortierung hinzufügen.
$availableSortings = $event->getCriteria()->getExtension('sortings') ?? new ProductSortingCollection();
$myCustomSorting = new ProductSortingEntity();
$myCustomSorting->setId(Uuid::randomHex());
$myCustomSorting->setActive(true);
$myCustomSorting->setTranslated(['label' => 'My Custom Sorting at runtime']);
$myCustomSorting->setKey('my-custom-runtime-sort');
$myCustomSorting->setPriority(5);
$myCustomSorting->setFields([
[
'field' => 'leasingPlan.periodPrice',
'order' => 'asc',
'priority' => 1,
'naturalSorting' => 0,
],
]);
$availableSortings->add($myCustomSorting);
$event->getCriteria()->addExtension('sortings', $availableSortings);
}
Is this already the right way to get the min(periodPrice)? Or is it taking just a random value out of the leasingPlan table to define the sort-order?
I didn't find a way, to define the min_period_price aggregate value in the $myCustomSorting->setFields Methods.
Update 1
Some days later, I asked a less complex question in the shopware community on slack:
Is it possible to use the DAL to define a subquery for an association in the product-listing?
It should generate something like:
FROM
JOIN (
SELECT ... FROM ... WHERE ... GROUP BY ... ORDER BY ...
) AS ...
The answer there was:
Don't think so
Update 2
I also did an in-deep anlysis of the DAL-Query-Builder, and it really seems to be not possible, to perform a subquery with the current version.
Update 3 - Different approach
A different approach might be, to define custom fields in the main entity. Every time a change is made on the main entity, the values of this custom fields should be recalculated.
It is a lot of overhead work, to realize this. Especially when the fields you are adding, are dependend on other data like the availability of a product in the store, for example.
So check, if it is worth the extra work. Would be better, to have a solution for building subqueries.
Unfortunately it seems that in your case there is no easy way to achieve this, if I understand the issue correctly.
Consider the following: for each product you can have multiple leasingPlan entities, and I assume that for a given context (like a specific sales channel or listing) that still holds. This means that you would have to sort the leasingPlan entities by price, then take the one with the lowest price, and then sort the products by their lowest-price leasingPlan's price.
There seems to be no other way to achieve that, and unfortunately for you, sorting is applied at the end, even if it is sort of a subquery.
So, for example, if you have the following snippet
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->getAssociation('leasingPlan')
->addSorting(new FieldSorting('price', FieldSorting::ASCENDING))
->setLimit(1)
;
The actual price-sorting would be applied AFTER the leasingPlan entities are fetched - essentially the results would be sorted, meaning that you would not get the cheapest leasing plan per product, instead getting the first one.
You can only do something like that with filters, but in this case there is nothing to filter by - I assume you don't have one leasingPlan per SalesChannel or per language, so that you could limit that list to just one entry that could be used for sorting
That is not to mention that this could not be included in a ProductSortingEntity, but you could always work around that by plugging into the appropriate events and modifying the criteria during runtime
I see two ways to resolve your issue
Making another table which would store the cheapest leasingPlan per product and just using that as your association
Storing the information about the cheapest leasingPlans in e.g. cache and using that for filtering (caution: a mistake here would probably break the sorting, for example if you end up with too few or too many leasingPlans per product)
public function applyCustomSorting(ProductListingCriteriaEvent $event): void
{
// One leasingPlan per one product
$cheapestLeasingPlans = $this->myCustomService->getCheapestLeasingPlanIds();
$criteria = $event->getCriteria();
$criteria->addAssociation('leasingPlan');
$criteria->getAssociation('leasingPlan')
->addSorting(new FieldSorting('price', FieldSorting::ASCENDING))
->addFilter(new EqualsAnyFilter('id', $cheapestLeasingPlans))
;
}
And then you could sort by
$criteria->addSorting(new FieldSorting('leasingPlan.periodPrice', FieldSorting::ASCENDING));
There should be no need to add the association manually and to add the aggregation to the criteria, that should happen automatically behind the scenes if your custom sorting is selected in the storefront.
For more information refer to the official docs.

complex projection from many-to-many tables, DbContext and entity framework and Linq

I have a DbContext with these four many-to-many related entities:
Classes <-> Students
Classes <-> Assignments
Classes <-> Contents
Classes <-> Announcements
Now I need a Linq code (or sth. better!) which will give us last 3 Assignments, last 3 Contents, last 3 Announcements of each Class where the student with StudentId = X is in those classes.
In another hand, the student logged in to website and we wanna show him/her last Assignments, Contents, Announcements of each Classes which he/she is study in it.
this code is not correct but may help u understand my need. Also this code takes many times to run (50ms isn't many?):
Edit: Code was almost correct so moved to answer , look at accepted answer. Any other (better, faster) solutions appreciated.
Thanks in advance.
You better start the query with student:
from s in db.Students
where s.StudentId == CurrentUser
from c in s.Classes
from as in c.Assignments.OrderByDescending(As => As.Id).Take(3)
from co in c.Contents.OrderByDescending(Co => Co.Id).Take(3)
from an in c.Announcements.OrderByDescending(An => An.Id).Take(3)
select new { <selected properties> }
The last part (selected properties) is important. By selecting only a subset of properties you narrow down the result set from the database. Without this, you create a very wide and long result set because of the large number of joins in the (SQL) query.
Based on #Gert Arnold recommendation I had changed a little my code:
start the query with student
narrow down the result by changing selected properties
db.Students.Where(st => st.StudentId.Equals(CurrentUser)).SelectMany(S => S.Classes, (S, C) => new
{
Name = C.Name,
Assignments = C.Assignments.Select(AS => new { AS.Id, AS.Name }).OrderByDescending(As => As.Id).Take(3),
Contents = C.Contents.Select(Co => new { Co.Id, Co.Title }).OrderByDescending(Co => Co.Id).Take(3),
Announcements = C.Announcements.Select(An => new { An.Id, An.Title }).OrderByDescending(An => An.Id).Take(3)
});

Is it normal to fetch the entire DB with the entity framework?

In the application we are developping, we have the following code
EntityContext entity = TransactionManager.GetEntityContext();
int idType = entity.InfoSectnGess.Where(e => e.NoSeqInfoSecGes == noSeqInfoSecGes).Single().NoSeqTypeInfo;
return entity.ListValues.Where(e => e.NoSeqTypeInfo == idType && e.NoSeqChoixValeur == noSeq).Single().DescChoixValeur;
EntityContext is based on System.Data.Objects.ObjectContext
When I run the debugger, put a break point at the first line and look at what entity contains, I see that it contains all the "tables" (entity fields?) in the DB, but they are all empty (null).
If I go to the next step, then my "tables" are filled with... well... the whole DB.
Of course, my concern here is performances.
Is that solution viable or will it become too slow with time?
I asked a the guy behind this idea and he told me there would be no such troubles. Yet, when I see that entity.InfoSectnGess contains 380 elements (temporary data for dev) and that it will contains a few thousands, if not millions of records quickly, it makes me wonder... Where's and what's the magic?

Query using EF "Include" and including the related tables on Where

So far, I thought I could do:
var num = db.MyTable.Include(x => x.RelatedTable)
.Count( x.idTenant == CurrentTenantID && x.Active &&
x.RelatedTable.SomeProperty.Value == true);
This always return zero records.
Am I assuming wrongly that Including the RelatedTable I can use it in the where part?
By the way... the "SomeProperty" is Nullable, that is why the ".Value".
I'm using Entity Framework 4.1. (Database first)
Are you trying to get the number of records? If so, why do you even need the Include? Entity Framework will lazy-load the RelatedTable entity set for you when it evaluates your Count condition. Also, if SomeProperty is a bool?, you should check if it has a value before you check the value itself.
var num = db.MyTable.Count(x =>
x.idTenant == CurrentTenantID &&
x.Active &&
(x.RelatedTable.SomeProperty.HasValue &&
x.RelatedTable.SomeProperty.Value));
You don't need to use Include if you only want to access navigation property in Where part. Include is only used to fetch (eager load) related records together with the main record from database to your application but it doesn't make sense if you only want to count records.

Filtering Aggregate root entity and child entity by a property on the child entity

Hope that someone out there can help with this!
I'll give an example based on the standard Order-->OrderLine-->Product rather than the actual situation to make it easier to explain!
Basically, I want to run a query that returns all orders for which there is an order line containing a TV. Simple enough:
IEnumerable<Order> orders;
using (var context = new DataContext())
{
var source =
context.Orders.Include("OrderLines").Include(
"OrderLines.Product");
orders= source.Where(o => o.OrderLines.Where(ol => ol.Product.Name == "TV")).ToList();
}
return orders;
This works in the sense that I get the correct collection of Order entities, but when I use look at each Order's collection of OrderLines it contains all OrderLines not just those containing at TV.
Hope that makes sense.
Thanks in advance for any help.
I does make sense in that the query is fulfilling your original criteria "to return all orders for which there is an order line containing a TV", each order will of course have all the orderlines. The filter is only being used to select the Orders, not the OrderLines.
To retrieve just the OrderLines containing TV from an Order you'd use the filter again, thus:
var OrderLinesWithTV = order.OrderLines.Where(ol => ol.Product.Name == "TV");
The main point is to know if you need to keep (or not) a reference to the order header in the filtered lines.
I.e. do you want the list of all the orders with a TV, and more precisely only their TV lines ? or do you want all the TV lines nevermind their order header ?
You seem to prefer the first option.
Then the best solution would certainly be
var relevantOrders = orders.Where(order => order.OrderLines.Any(ol => ol.Product.Name == "TV"))
to get the relevant orders, and then, for each order in relevantOrders :
order.OrderLines.Where(ol => ol.Product.Name == "TV")
to consider only the TV lines.
Other techniques would result in a loss of information or force you to build a new orders collection similar to the initial one but double-filtered on the headers and on the lines, which seems fairly bad as far as elegance and performance is concerned.

Resources