I have an odd issue with AutoMapper (I'm using .NET core 3.1 and AutoMapper 10.1.1)
I'm doing a simple project to list and a simple projected count for total records:
var data = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.ToList();
var count = Db.Customers
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.Count();
The first line creates the expected SQL:
exec sp_executesql N'SELECT [c].[Code], [c].[Id], [c].[Name], [c].[Website], [s].Name
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
ORDER BY (SELECT 1)
OFFSET #__p_0 ROWS FETCH NEXT #__p_1 ROWS ONLY',N'#__p_0 int,#__p_1 int',#__p_0=0,#__p_1=25
The second line, the Count(). Seems to ignore the projection entirely:
SELECT COUNT(*)
FROM [Customers] AS [c]
The result of this is that any customer with a null StatusId will be excluded from the first query but included in the count in the second. Which breaks paging.
I would have thought that project should create something like:
SELECT COUNT(*)
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
Anyone know why the Count() is ignoring the ProjectTo<>?
Edit
Execution plan:
value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Domain.Customer]).Select(dtoCustomer
=> new CustomerViewModel() { Code = dtoCustomer.Code, Id = dtoCustomer.Id, Name = dtoCustomer.Name, StatusName =
dtoCustomer.Status.Name, Website = dtoCustomer.Website})
Edit 2021/02/19
Mappings plan:
EF entities -
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Code { get; private set; }
public string Website { get; private set; }
public CustomerStatus Status { get; private set; }
public Customer() { }
}
public class CustomerStatus
{
public Guid Id { get; private set; }
public string Name { get; private set; }
}
ViewModel -
public class CustomerViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Website { get; set; }
public string StatusName { get; set; }
}
Mapping -
CreateMap<Customer, CustomerViewModel>();
Edit 2021/02/20 - Manually Excluding Status
As pointed out in #atiyar answer you can manually exclude the status. This crosses me as a work around. My reasoning is this:
If you execute this query, as the very root query:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider)
You get:
exec sp_executesql N'SELECT TOP(#__p_0) [c].[Id], [c].[Name], [c0].[Name]
AS [StatusName]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]',N'#__p_0
int',#__p_0=5
This shows automapper understands and can see that there is a needed relationship between Status and Customer. But when you apply the count mechanism:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Count()
Suddenly, the understood relationship between Status and Customer is lost.
SELECT COUNT(*)
FROM [Customers] AS [c]
In my experience with Linq each query step modifies the previous step in a predicable way. I would have expected the count to build on the first command and include the count as part of that.
Interestingly, if you execute this:
_context.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Take(int.MaxValue).Count()
Automapper applies the relationship and the result is what I would have expected:
exec sp_executesql N'SELECT COUNT(*)
FROM (
SELECT TOP(#__p_0) [c].[Id], [c].[Name], [c0].[Name] AS [Name0], [c0].[Id]
AS [Id0]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]
) AS [t]',N'#__p_0 int',#__p_0=2147483647
Edit 2021/02/20 - Latest Version
Seems behaviour is the same in the latest version.
FYI: We have a scenario where records are imported on a regular basis from another application. We were hoping to use the inner join to exclude the records that don't have a matching record in another table. Then those records would be updated at a later point by the import process.
But from the application point of view it should always ignore those records hence the inner join and the status being mandatory. But we will have to manually exclude them (as per atiyar's solution) using the where to prevent paging from returning blown out page count numbers.
Edit 2021/02/20 - Further Digging
This does appear to be a design choice by the EF team and an optimisation. The assumption here is that if the relationship is non-null able. Then the join wont be included as a performance boost. The way around this is as suggested by #atiyar. Thanks for the help everyone #atiyar & #Lucian-Bargaoanu.
I have tested your code in .NET Core 3.1 with Entity Framework Core 3.1 and AutoMapper 10.1.1. And -
your first query generates a LEFT JOIN, not an INNER JOIN like you posted. So, the result from that query will not exclude any customer with a null StatusId. And, the generated SQL is same with ProjectTo<> and manual EF projection. I'd suggest to check your query and generated SQL again to make sure.
your second query generates the same SQL, the SQL you have posted, with ProjectTo<> and manual EF projection.
A solution for you :
If I understand correctly, you are trying to get -
a list of Customer, within the specified range, who has a related Status
the count of all such customers in your database.
Try the following -
Add a nullable foreign-key property in your Customer model -
public Guid? StatusId { get; set; }
This will help to simplify your queries and the SQL they generate.
To get your expected list, modify the first query as -
var viewModels = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.Where(p => p.StatusId != null)
.ProjectTo<CustomerViewModel>(_Mapper.ConfigurationProvider)
.ToList();
It will generate the following SQL -
exec sp_executesql N'SELECT [t].[Code], [t].[Id], [t].[Name], [s].[Name] AS [StatusName], [t].[Website]
FROM (
SELECT [c].[Id], [c].[Code], [c].[Name], [c].[StatusId], [c].[Website]
FROM [Customers] AS [c]
ORDER BY (SELECT 1)
OFFSET #__p_0 ROWS FETCH NEXT #__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [Statuses] AS [s] ON [t].[StatusId] = [s].[Id]
WHERE [t].[StatusId] IS NOT NULL',N'#__p_0 int,#__p_1 int',#__p_0=0,#__p_1=25
To get your expected count, modify the second query as -
var count = Db.Customers
.Where(p => p.StatusId != null)
.Count();
It will generate the following SQL -
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL
Related
I've got a relatively basic model - Users and Tags. There is a fixed list of Tags. A User can have multiple Tags and a Tag can be used by multiple users.
I had gone with structure below and finding performance issues when returning results.
public class User
{
public string Id {get; set;}
public virtual List<UserTag> UserTags {get; set}
}
public class UserTag
{
public string UserId { get; set; }
public User User { get; set; }
public int TagId { get; set; }
public Tag Tag{ get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
public string Name { get; set; }
public virtual List<UserTag> UserTags { get; set; }
}
I have the following query which is takings a long time (several seconds):
var x = db.Users.Include(u => u.UserTags).ThenInclude(u => u.Trait).ToList<User>();
I have tried writing it as such, which has improved the time, however it is still taking too long:
db.UserTags.Load();
db.Tags.Load();
var x = db.Users.ToList<User>();
Is there any other way to speed this up? Running a query directly in SQL SMS is almost instant (e.g.
select * from Users u left outer join UserTags t on t.UserId = u.Id)
In terms of data rows, it is apx Tags: 100, UserTags:50,000, Users: 5,000
First you can check how EF translates your request to SQL Server - therefore use the "SQL Server Profiler"
Then you could use the genereated query to check if there might be an missing index which speeds up the query
You also can try to write a Join instead of ThenInclude and see how the query then behaves
best regards
Jimmy
I've got an ASP.NET MVC application using EF in C#. I can write and run my query correctly in SQL Server. In EF, I'm not sure how to accomplish the same thing.
Tables A, B and C. C references B which References A.
The query looks like this:
Select *
From C
Where C.bID in (Select B.bID
From B
Where B.aID = '<unique Key In A>')
The sub-query returns multiple B keys. Which then pass it through and look up the ID's in C. In short I'm looking up all data in C related to a key in A.
I just don't know how to put that into EF language. Or if it's even possible. The IN operator is what's throwing me on the conversion.
Example:
var exampleList = _context.C
.Where(l => l.bId in (_context.B
.Where(p => p.aId = keyInA)));
"in" doesn't work here. Obviously. After I wrote this post I made sure of it.
Note: A:B has a 1:Many relation. B:C has a 1 to many relation. All IDs and keys are GUIDs
Set up the navigation property between C & B, and between A & B. This is pretty much the whole point of using Entity Framework rather than just an alternative to ADO and SQL queries.
C contains a BId, so set up a B navigation property:
public class C
{
public int CId { get; set; }
[ForeignKey("B")]
public int BId { get; set; }
public virtual B B { get; set; }
}
B contains an AId, so similar:
public class B
{
public int BId { get; set; }
[ForeignKey("A")]
public int AId { get; set; }
public virtual A A { get; set; }
}
Now, to write your query:
var cs = context.Cs.Where(x => x.B.AId == id);
or
var cs = context.Cs.Where(x => x.B.A.AId == id); // can filter on other "A" fields this way.
I mean, to avoid the use of navigation properties, you may as well just write Sprocs and use ADO. It is possible in cases where you have unrelated tables or absolutely need to avoid a navigation property. Use a Join between context.Cs and context.Bs:
var cs = context.Cs.Join(context.Bs, c => c.BId, b => b.BId, (c, b) => new { C = c, AId = b.AId })
.Where(x => x.AId == aid)
.Select(x => x.C);
IMO seriously ugly working with joins in EF, but sometimes necessary. If you can use a navigation property I'd highly recommend it over using something like that regularly.
What I'm trying to achieve is like this:
Fill up a form in view
Save data into table1 with columns:
Id|OperationNumber|Name|ContactNo
table1 Id is the primary key
Save data into table2 with columns:
ReferenceId|OperationNumber
ReferenceId is the primary key of datatype uniqueidentifier
"OperationNumber" column in both table is related. when data is saved in table1, OperationNumber will also save in table2 together with autogenerated ReferenceId (uniqueidentifier)
Retrieving process is:
input ReferenceId as search
Display all the details from table1 and table2 in result view
So that is my problem, how do i save and retrieve data in two tables?
Here's what I currently have:
Controller:
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(RefViewModel myViewModel)
{
db.Table1.Add(myViewModel.Ref);
db.SaveChanges();
return RedirectToAction("Index");
}
Model:
public class RefViewModel
{
public Table1 Ref { get; set; }
public string OperationNumber { get; set; }
public string Name { get; set; }
public string ContactNo { get; set; }
}
In the above code can save only in table1. so what to do in order for me to save two tables?
(oh and btw. as you may have noticed the "Bind(Include =" is nowhere, I have disabled it because I'm getting null values when saving into database. so, anyways. that's not the problem anymore here. just mentioning)
Database is SQL Server
Edit: as for retrieving data
SearchController:
public ActionResult Search(string searchString)
{
var myRef = (from x in db.Table1
where x.OperationNumber.Contains(searchString)
select x).FirstOrDefault();
return View(myRef);
}
That returns details when I input the operationnumber. So the problem is how to retrieve details from two tables when I input only referenceId?
You can easily do this by using stored procedures. Define two property classes to represent two tables and use inheritance as follows.
public class Tablel
{
public string ID { get; set; }
public string OperationNumber { get; set; }
public string Name { get; set; }
public string ContactNo { get; set; }
}
public class Table2 extend Table1
{
public string ReferenceID { get; set; }
public string OperationNumber { get; set; }
}
Now you can use stored procedures to save and retrieve data from database. Use join when you retrieve data from database and you can use two separate queries when save data to two tables.
From c# metadata of DbContext.SaveChanges():-
Returns:
// The number of objects written to the underlying database.
so basically you will do something like this:-
public ActionResult Create(RefViewModel myViewModel)
{
db.Table1.Add(myViewModel.Ref);
var t1 = db.SaveChanges();
Table2 t2 = new Table2
{
OperationNumber = t1.OperationNumber
};
db.Table2.Add(t2);
db.SaveChanges();
return RedirectToAction("Index");
}
when the SaveChanges is called, it returns all the objects saved to the database so when you call SaveChanges for the first time, it will return a Table1 object and with that Table1 object you will populate the OperationNumber property of Table2 and call SaveChanges again.
if your operationnumber is unique then use this query on store procedure, you can found data using ReferenceID
Select * from table1 t1
inner join table2 t2 on t1.OperationNumber = t2.OperationNumber
where t2.ReferenceID = #pReferenceID
I'm new to LINQ and understand little enough to do some simple LINQ. But now, what I have is more complex than what I can do. I have created the SQL statement, tried it and it works. But I can't translate it to LINQ.
EDIT: I tried to code the linq and created a model to show it to a view. However, I'm not getting the data I want. I can't figure out where did I get the linq wrong. Please help.
This are the tables involved, the output using SQL and output using LINQ
This is the SQL:
select oh.date, oh.id,
od.TotalAmount,
CASE when oh.Discount_level = 2
then ((od.TotalAmount / 1.12) * .2)
END as Discount,
(od.TotalAmount / 1.12) as VATSale,
((od.TotalAmount / 1.12) * .12) as VAT,
CASE when oh.Discount_level = 2
then ((od.TotalAmount / 1.12) * .8)
when oh.Discount_level = 1
then od.TotalAmount
END as AmountDue
from Order_Header oh
inner join
(
select Order_Header_id, SUM(price * quantity) as TotalAmount
from Order_Details
group by Order_Header_id
) od
on oh.id = od.Order_Header_id;
The linq I have:
var list = (from oh in db.Order_Header
join iq in (
from t in db.Order_Details
group t by t.Order_Header_id
into g
select new
{
ohId = g.Key,
totalAmount = ((from t2 in db.Order_Details select t2.price * t2.quantity)).Max()
}
)
on oh.id equals iq.ohId
select new Report
{
date = oh.date,
ohId = iq.ohId,
totalAmount = iq.totalAmount,
discount = oh.Discount_level == 2 ? (iq.totalAmount /1.12) * 2 : 0 ,
VATSale = iq.totalAmount / 1.12,
VAT = (iq.totalAmount / 1.12) * .12,
amountDue = oh.Discount_level == 2 ? ((iq.totalAmount / 1.12) * .8) * 2 : iq.totalAmount
}).ToList();
The Report model:
public class Report
{
public DateTime? date { get; set; }
public int ohId { get; set; }
public double totalAmount { get; set; }
public double discount { get; set; }
public double VATSale { get; set; }
public double VAT { get; set; }
public double amountDue { get; set; }
}
Assuming your POCO models looked like these:
public class Order_Header
{
public Guid Id { get; set; }
public int Discount_Level { get; set; }
public Order_Detail Order_Detail {get;set;}
}
public class Order_Detail
{
public Guid Order_Header_Id { get; set; }
public double Price { get; set; }
public int Quantity { get; set; }
public Order_Header Order_Header { get; set; }
}
and let ods as a IDbSet<Order_Detail> instance within a DbContext.
You can translate your query above into Lambda LINQ like this.
var odproj = ods.GroupBy(x => x.Order_Header_Id)
.Select(x => new { Order_Header = x.First().Order_Header,
TotalAmount = x.Sum(y => y.Price * y.Quantity)})
.Select(x => new { Order_Header = x.Order_Header,
Id = x.Order_Header.Id,
TotalAmount = x.TotalAmount,
Discount = (x.Order_Header.Discount_Level == 2 ? ((x.TotalAmount / 1.12) * 0.2) : 0),
VATSale = (x.TotalAmount / 1.12),
VAT = ((x.TotalAmount / 1.12) * 0.12),
AmountDue = ((x.Order_Header.Discount_Level == 2) ? ((x.TotalAmount / 1.12) * 0.8)
: (x.Order_Header.Discount_Level == 1 ? x.TotalAmount : 0)
)
});
Explanation
First, we group the Order_Detail (many) based on Order_Header (one) to define a single transaction (in POS system sense thats it). After grouping, we 'calculate' the total amount based on price and quantity of the orders. The total price calculation is done in the first Select, it generates an anonymous class with 2 field, one to keep the Order_Header information and the other one the one we want. The next Select we compute the discount, VATSale, etc -- your complex if-else in the first clause.
Ps. I have not tried this in EF yet, my RAM just too small to kick up SQL Server. Take this idea with grain and salt. The key corner of this problem, which i'm a bit hesitant is whether EF will recognize Order_Header = x.First().Order_Header or not. Nevertheless, using the LINQ is bad enough to guarantee you with heavy OUTER JOIN that might end up make your query dropped by SQL Execution Planner. If its too complex, just make a STORED PROCEDURE and pull up an edmx to map it (wait, does Code First can do this? I do wonder).
If I had a User model that looks like this:
public class User
{
public Guid Id { get; set; }
public DateTime CreatedAtUtc { get; set; }
public string Username { get; set; }
public string Country { get; set; }
}
...and I'd perform a query which starts at a given row and fetches a limited amount of further rows (basically for paginating over the results) that looks like this:
var spaniards = connection.Query<User>(
"select * from Users where Country=#Country OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY",
new { Country = "Spain" }).ToList();
.. using Dapper(.Net), would it be possible to get that particular, limited result set AND the total count of rows in one single query and if so.. how?
One way to do solve this is using the splitOn functionality, together with count(1) over()
Let's assume the following sql string:
var sql = "select *, overall_count = COUNT(1) OVER() from Users ORDER BY Id Asc OFFSET 5 ROWS;"
together with the following dapper.net call:
HashSet<int> hashSet = new HashSet<int>();
Func<User, int, User> map = (result, count) =>
{
hashSet.Add(count);
return result;
};
await connection.QueryAsync(sql, map, "overall_count").ConfigureAwait(false);
This will split the result of the dapper call into two parts (just like with joining different tables) and it will call the map functor each time, in this case we simply store the count in a hashset (you could then check if the hashSet.Count is 1)
Hope this helps
PS: I'm not certain that your OFFSET works without any ORDER BY