Execute Linq to SQL queries on Navigation properties in Model - c#

Is it possible to make Statistics.Sum(s => s.Conversions) linq query as Linq to SQL and not Linq to Object like in this code below. Every time when I access TotalConversions property, the whole Statistics table downloaded from database and then SUM linq executed locally. I want to do that in database server as SQL.
public class User : Entity
{
public int Id { get; set; }
public string Email { get; set; }
public virtual ICollection<Statistic> Statistics { get; set; }
[NotMapped]
public int TotalConversions
{
get
{
return Statistics.Sum(s => s.Conversions);
}
}
}

Yes, but you need a reference to the DbContext. This is one of the costs of having the Entities be persistence-ignorant.
Then the property would look something like:
return db.Users.Single(s => s.Id = this.Id).Statistics.Sum(s => s.Conversions);

Related

How do I speed up this EF query?

How do I speed up this EntityFramework query? The profiler tells me that most of the time is spent in od.Order with ~5000 calls.
var orderDetails = context.OrderDetails.ToList();
foreach (OrderDetail od in orderDetails)
{
var date = od.Order.date;
if (!trPerDay.ContainsKey(date))
{
trPerDay.Add(date, od.quantity);
}
else
{
trPerDay[date] += od.quantity;
}
}
Order property is defined like this:
[MetadataType(typeof(OrderDetailMetaData))]
public partial class OrderDetail
{
public int orderID { get; set; }
public string productID { get; set; }
public int quantity { get; set; }
public bool upgraded { get; set; }
public virtual Order Order { get; set; }
public virtual Product Product { get; set; }
}
What you posted loads the entire OrderDetails table in a single query, from a single thread. Then it tries to lazily load each order which results in a separate call to the database.
It's far faster to let the database do the calculations and only load the final results.
In this case it seems the loop is trying to calculate the total order quantity per order date. The SQL query that produces this would be something like :
SELECT Date,SUM(details.Quantity)
FROM Orders inner join OrderDetails details
on Orders.ID=details.OrderID
GROUP BY Orders.Date
The equivalent in LINQ can be :
var query=context.OrderDetails
.GroupBy(d=>d.Order.Date)
.Select(g=>new {
Date=g.Key,
Total=g.Sum(dt=>dt.Quantity)
});
var totals=await query.ToListAsync();
or
var totals=await query.ToDictionaryAsync(t=>t.Date,t=>t.Quantity)
In both cases, a GROUP BY query will be generated that calculates the totals by date.
This assumes that Date is what it says - a date. Either a date-typed field in the database, or a datetime without a time component. If it's actually a Date+Time, the query will have to be adjusted to use only the date part. Luckily, EF Core maps DateTime.Date to the equivalent SQL function call:
var query=context.OrderDetails
.GroupBy(d=>d.Order.Date)
.Select(g=>new {
Date=g.Key,
Total=g.Sum(dt=>dt.Quantity)
});

Querying a Single Item in a List

Problem
I want to show a single pet on my details page. I'm having trouble understanding how to write LINQ(Method Syntax) to pull a single pet/item from the database.
My understanding so far
When working with LINQ, it's shining purpose is to be able to loop through an IEnumerable array or list in the database. As you look in my HomeController, I feel fairly confident that I have the right pieces of code to build what I want MINUS the query.
Request
Using method syntax, what query should I be using to pull a single pet?
HomeController.cs
[HttpGet("pet/{PetId}")]
public IActionResult Detail(Pet singlePet)
{
List<Pet> Animal = _context.Pets
//***MISSING QUERY CODE HERE***
return View("Pet", singlePet);
}
Details.cshtml
#model PetViewModel
<div>
<h1>Pet Shelter</h1>
#foreach (Pet creature in Model.Pets)
{
<h3>Details about: #creature.Name</h3>
<div>
<p>Pet Type: #creature.Type</p>
<p>Description: #creature.Description</p>
<p>Skill One:#creature.Skill1</p>
<p>Skill Two:#creature.Skill2</p>
<p>Skill Three:#creature.Skill3</p>
</div>
}
</div>
PetViewModel.cs
using System.Collections.Generic;
namespace petShelter.Models
{
public class PetViewModel
{
public Pet Animal { get; set; }
public List<Pet> Pets { get; set; }
public List<Owner> Owners { get; set; }
public Owner Owner { get; set; }
}
}
Pet.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace petShelter.Models
{
public class Pet
{
[Key]
public int PetId { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Type { get; set; }
[Required]
public string Description { get; set; }
public string Skill1 { get; set; }
public string Skill2 { get; set; }
public string Skill3 { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public Owner Owner { get; set; }
public int? OwnerId { get; set; }
}
}
Short Version
Use
var singlePet=_context.Pets.Find(someId);
Or
var singlePet=_context.Pets.FirstOrDefault(x=>x.PetId=someId);
Explanation
LINQ queries don't query lists. They're translated by a LINQ provider to whatever the target data storage understands. When querying a database, they're translated to SQL.
List<Pet> Animal = _context.Pets won't compile, which is a very good thing - if it did, it would load the entire table in memory instead of querying it. Filtering will be performed on the client, without the benefit of indexing. Performance will be orders of magnitude worse than a simple SQL query due to wasted IO, wasted RAM and even worse, taking locks on every row in the table when only one was needed. Excessive locks will cause delays for every other request that tries to use the same table.
Pets is a DbSet<T>, not a List<T>. It's not a container, it represents an entity without holding any data itself. It can be used to write queries that translate to SQL.
Assuming PetId is the primary key for Pet, the easiest and fastest way to load a single record is calling DbSet.Find. This method retrieves an object using the supplied primary key values.
This query:
var singlePet=_context.Pets.Find(someId);
translates to
select ...
from Pets
where PetID=#id
This will load and cache a Pet instance inside DbContext. After that, every time Find is called with the same PK value during the same request, the cached object will be returned.
The query
var singlePet=_context.Pets.FirstOrDefault(x=>x.PetId=someId);
Will translate to
select top 1 ...
from Pets
where PetID=#id
Nothing is cached in this case.
If you don't care and just want a single result from the database you can do something like this
var singlePet = _context.Pets.First();
If you want the first result that matches an expression you can do something like this
var singlePet = _context.Pets.First( e => e.Id == '1');
If you are expecting ONLY a single result that matches an expression you can do something like this
var singlePet = _context.Pets.Single( e => e.PetId == '1' );
If you want just one, with no filter at all on which, you can use this, it will get you the first on the list:
var aSpecificPet = Pets.FirstOrDefault();
If you need to specify some kind of condition to determine which pet to bring, you can use something like:
var aMoreSpecificPet = Pets.FirstOrDefault(x=>x.Type == "Dog");
Take in mind that whatever you pass as a condition will filter the Pets list, and with FirstOrDefault you will get the first (if there are any) pets that match your criteria.
Also, default value is usually null (in case no pets match your criteria), so its a good idea to always add a line like
if(pet != null){
your code
}
If you want all pets that match that criteria, use:
var allDogsInPets = Pets.Where(x=>x.Type == "Dog").ToList();
The .ToList() returns a new list with the pets that match your criteria.

Entity.HasRequired returning items with NULL property

I have two related entities built and linked with Fluent API.
public class EDeal : IEntityBase
{
public int ID { get; set; }
public string Customer_id { get; set; }
public virtual ECustomer Customer { get; set; }
...etc
}
public class ECustomer : IEntityBase
{
public int ID { get; set; }
public string Customer_id { get; set; }
public string Customer_name { get; set; }
public virtual ICollection<EDeal> Deals { get; set; }
...etc
}
linked with
modelBuilder.Entity<ECustomer>().HasKey(c => c.Customer_id);
modelBuilder.Entity<EDeal>().HasRequired<ECustomer>(s => s.Customer)
.WithMany(r => r.Deals)
.HasForeignKey(s => s.Customer_id);
I recognize that this is inefficient linking but I had to link it in this way because I don't have control over the db structure.
The important thing to note is that the EDeal requires an ECustomer (.HasRequired). The database contains many rows in EDeal that have a null Customer_id field and I do not want to ever pull those lines when I query the entity.
I thought that the .HasRequired would make sure that I never got back any EDeals that do not have ECustomers associated with them but that doesn't seem to be the case. Instead, it only seems to ignore those lines with NULL Customer_id values when I try to order by a property in the Customer. And even then, returning the .Count() of the query behaves strangely.
var count1 = db.Set<EDeal>().Count(); //returns 1112
var count2 = db.Set<EDeal>().ToList().Count(); //returns 1112
var count3 = db.Set<EDeal>().OrderBy(c => c.Customer.Customer_name).Count(); //returns 1112
var count4 = db.Set<EDeal>().OrderBy(c => c.Customer.Customer_name).ToList().Count(); //returns 967
I know I can add a .Where(c => c.Customer.Customer_id != Null) to make sure I only get back what I'm looking for, but I'm hoping for a solution in the Entity's configuration because I have many generic functions acting on my IEntityBase class that build dynamic queries on generic Entities and I don't want to use a workaround for this case.
Questions:
1) Is there a way to limit the entity to only return those EDeals that have a corresponding ECustomer?
2) In my example above, why do count3 and count4 differ?
Thanks in advance.

Linq query to nest 3 tables and select certain records

I'm building a simple tour booking app.
It has 3 linked tables, setup as below.
Given a specific TourCategoryId and a date (dte), I'm trying to build an object which I can then go on to populate a viewmodel with.
However I'm not understanding how to check the TourDate field against dte.
Am I trying to do too much in one statement? Can anyone help me with what I thought would be a simple query?
Thanks for any advice:
My linq is:
var tours = db.Tours
.Include(x => x.TourDate)
.Include(x => x.TourDate.Select(b => b.Booking))
.Where(t => t.TourCategoryId == id &&
t.TourDate.Any(td => td.Date==dte));
(also pseudo code)
.Where(v => v.Booking.NumberBooked.Sum() < v.Tours.PlacesAvailable)
I've updated this with suggestions below, but it is returning all td.Date - and not filtering, as you can see from the screenshot below, showing all 3 records returned, and not just those where td.Date == dte:
Is it the .Any that is maybe not correct?
The end result should be
List of Tours => List of Dates => List of Bookings - where the dates are filtered to the dte eg:
Tour1- night tours
--TourDate - filtered to 02/03/2015
----Booking1
----Booking2
----Booking3
Tour2- day tours
--TourDate - filtered to 02/03/2015
----Booking1
----Booking2
Tour3- multi-day tours
--TourDate - filtered to 02/03/2015
----Booking1
----Booking2
----Booking3
----Booking4
Thanks again, Mark
public class Tour
{
public int TourId { get; set; }
public int TourCategoryId { get; set; }
public string TourName { get; set; }
public int PlacesAvailable { get; set; }
public virtual ICollection<TourDate> TourDate { get; set; }
}
public class TourDate
{
public int TourDateId { get; set; }
public int TourId { get; set; }
public DateTime Date { get; set; }
public virtual Tour Tour { get; set; }
public virtual ICollection<Booking> Booking { get; set; }
}
public class Booking
{
public int BookingId { get; set; }
public int TourDateId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int NumberBooked { get; set; }
public virtual TourDate TourDate { get; set; }
}
From what I understand you want any Tour in TourCategoryId which has a TourDate on dte.
This may work for you.
var tours =
db.Tours
.Include(x => x.TourDate)
.Include(x => x.TourDate.Select(b => b.Booking))
.Where(t => t.TourCategoryId == id &&
t.TourDate.Any(td => td.Date == dte));
To filter the TourDate.
foreach(var tour in tours)
{
tour.TourDate = tour.TourDate.Where(td => td.Date == dte).ToList()
}
The problem you are facing is that the business object you work with does not fit in the class hierarchy you use.
Try not to work strictly with the database mapped classes. Your business objects in this case are something different from just a list of instances of the Tour class that are matched to the db strucure. Just use the db classes to form a query, with the help of all the navigation properties you created, but dont try to fit all the queries you need to the same class hierarchy as you need for a view model.
You should build your query around bookings, not tours. select the bookings matching your criteria, including the one in pseudocode, then group by the dates and tours and select some kind of anonimous object, representing the groups. Then translate those to view model classes you need, not necessary all of which are database mapped.
In many cases, you do not even need ro hydrate an entity just to get some property loaded.
No need to load an elephant just to be able to take a picture.
Include is really just a workaround, like killing the elephant and loading by parts.

Entity Framework - set model property value dynamically from a query

I have the following models:
public class A_DTO
{
[Key]
public string Id { get; set; }
**public virtual B_DTO B { get; set; }**
public virtual List<B_DTO> Bs { get; set; }
}
public class B_DTO
{
[Key]
public string Id { get; set; }
public string AId { get; set; }
public string UserId {get; set; }
[ForeignKey("AId"]
public virtual A_DTO A { get; set; }
[ForeignKey("UserId"]
public virtual User User { get; set; }
}
I am trying to get a list of object A_DTO but also including property B:
using AutoMapper.QueryableExtensions;
public IQueryable<A_DTO> GetAllA_DTO()
{
string userId = "8b6e9332-7c40-432e-ae95-0ac052904752";
return context.A_DTO
.Include("Bs")
.Include("B")
.Project().To<A_DTO>()
.Where(a => a.Bs.Any(b => b.UserId == userId));
}
How do I dynamically set this property according to set UserId and A_DTO.Id?
Here is a bag of observations in which you may be lucky enough to find your solution:
The B property in a code first model will result in there being a foreign key in the database table for A_DTOs that contains a reference to the B_DTOs table. Entity Framework will expect to own the responsibility for filling the B navigation property with an object populated with the data from the referenced row in the B_DTOs table, hence you would not be able to change it dynamically.
There is no need to use the Automapper Project method if your source type and destination type are the same. In your example they would both appear to be A_DTO. Are you sure you don't actually intend to have an entity "A" that is included in the context and "A_DTO" that is mapped from "A" via Automapper? If that is what you really want then you could have code in a .Select call mapping A.Bs.FirstOrDefault(b => b.UserId == userId) to A_DTO.B. However, you would not be able to apply filtering on the basis of the userId in an Automapper map.
Without seeing any of the Automapper Map setup code, it is difficult to get an idea of intent here.
As an aside, when using .Include it is better, in my opinion, to use the overload that takes an expression. In your case the includes would be rewritten:
.Include(a => a.B)
.Include(a => a.Bs)
Using this overload ensures that you will get compile time errors if you rename a property but fail to update the string in the .Include statement.

Categories