LINQ: How to Write INNER JOIN and WHERE Condition - c#

I have two database tables--
ReconFinalCost (Id, LeaseDocNumber, SnapshotId)
ReconSnapshot (Id, FiscalYear, FiscalQuarter)
ReconFinalCost.SnapshotId is a foreign-key to ReconSnapshot.Id.
In my C# code, I have a List<ReconFinalCost> leaselist. I want to write a LINQ command that inner joins the ReconFinalCost and ReconSnapshot tables on (ReconFinalCost.SnapshotId == ReconSnapshot.Id) and KEEPS all ReconFinalCost items in List<ReconFinalCost> leaselist for which there DOES NOT EXIST another ReconFinalCost item with an identical LeaseDocNumber but for which there is a SnapshotId associated with a ReconSnapshot with identical FiscalYear, but larger FiscalQuarter.
Here's that query, which I'm trying to write in LINQ, in SQL. I want to KEEP all items in List<ReconFinalCost> leaselist that are returned by the SQL query.
select * from "ReconFinalCost"
inner join "ReconSnapshot" on "ReconFinalCost"."SnapshotId" = "ReconSnapshot"."Id"
where NOT EXISTS (
SELECT 1 FROM "ReconFinalCost" B
inner join "ReconSnapshot" RS2 on RS2."Id" = B."SnapshotId"
where "ReconFinalCost"."LeaseDocNumber" = B."LeaseDocNumber" and
( RS2."FiscalYear" = "ReconSnapshot"."FiscalYear" and
RS2."FiscalQuarter" > "ReconSnapshot"."FiscalQuarter" )
)
So far I have:
List<ReconFinalCost> leaselist;
List<ReconFinalCost> originalLeaseList = leaselist;
leaselist = leaselist
.Where(x => !originalLeaseList.Any(
y => y.LeaseDocNumber == x.LeaseDocNumber &&
getIdsOfFutureQuarterSnapshots(x.SnapshotId).Any(
futureSnapshotId => futureSnapshotId == y.SnapshotId) ) )
.OrderBy(x => x.LeaseDocNumber).ToList();
...
private List<long> getIdsOfFutureQuarterSnapshots(long? snapshotId)
{
// this function returns a list of Ids of ReconSnapshots with the same (FiscalYear) as that of snapshotId, except in a FUTURE quarter
ReconSnapshot givenSnapshot = repository.GetReconSnapshotByID( (int)snapshotId );
return _context.ReconSnapshots.Where(r => r.FiscalYear == givenSnapshot.FiscalYear &&
r.FiscalQuarter > givenSnapshot.FiscalQuarter ).Select(r => r.Id).ToList();
}
What is the LINQ syntax for inner joining the ReconFinalCost and ReconSnapshot tables on (ReconFinalCost.SnapshotId == ReconSnapshot.Id), and then specifying the where clause for identical FiscalYear but larger FiscalQuarter, instead of having to call a separate function like I've done above?

Related

How to pass parameters into this stored procedure

I a previous question I tried to call a Stored Procedure via EntityFramework and then filter on the results.
How would I move my filter logic into dbo.spStaysSearch so I can modify the stored procedure to accept parameters?
CREATE PROCEDURE [dbo].[spStaysSearch]
AS
BEGIN
SELECT
tblOccupantStays.StayID,
COUNT(tblOccupantStays.OccupantStayID) AS CountOfOccupantStayID
INTO
#OccupantStays_CountOfChildren
FROM
tblOccupantStays
INNER JOIN
tblOccupant ON tblOccupantStays.OccupantID = tblOccupant.OccupantID
WHERE
(((tblOccupant.OccupantType) LIKE 'Child'))
GROUP BY
tblOccupantStays.StayID;
SELECT
tblOccupant.OccupantID, tblOccupant.OccupantType
INTO
#OccupantsAdults
FROM
tblOccupant
WHERE
(((tblOccupant.OccupantType) = 'Adult'));
SELECT
tblStayBillingHx.StayID,
MAX(tblStayBillingHx.BillSentDate) AS MaxOfBillSentDate
INTO
#StaysMaxBillSentDate
FROM
tblStayBillingHx
GROUP BY
tblStayBillingHx.StayID;
SELECT
tblStays.*, tblOccupant.OccupantID,
tblOccupant.FileAs AS OccupantFileAs,
IIF(tblStays.BuildingName LIKE 'Main Shelter',
tblOccupant.OCFSMainNumber,
tblOccupant.OCFSNorthNumber) AS StayOCFSNumber,
COALESCE([CountOfOccupantStayID], 0) AS CountOfChildren,
tblCaseManager.FileAs AS CaseManager,
#StaysMaxBillSentDate.MaxOfBillSentDate
FROM
(((((tblStays
LEFT JOIN
tblOccupantStays ON tblStays.StayID = tblOccupantStays.StayID)
LEFT JOIN
tblOccupant ON tblOccupantStays.OccupantID = tblOccupant.OccupantID)
LEFT JOIN
#OccupantStays_CountOfChildren ON tblStays.StayID = #OccupantStays_CountOfChildren.StayID)
LEFT JOIN
#OccupantsAdults ON tblOccupant.OccupantID = #OccupantsAdults.OccupantID)
LEFT JOIN
tblCaseManager ON tblStays.CaseManagerID = tblCaseManager.CaseManagerID)
LEFT JOIN
#StaysMaxBillSentDate ON tblStays.StayID = #StaysMaxBillSentDate.StayID
ORDER BY
tblStays.StartDate, tblOccupant.FileAs;
END
Currently calling from this C#
private IQueryable<spStaysSearch> getSearchData(StaySearchViewModel model)
{
var records = db.SpStaySearches.FromSqlRaw("dbo.spStaysSearch").ToList().AsQueryable();
if (model.OccupantId.HasValue)
records = records.Where(x => x.OccupantId == model.OccupantId);
if (!string.IsNullOrWhiteSpace(model.OccupantFileAs))
records = records.Where(x => x.OccupantFileAs == model.OccupantFileAs);
if (!string.IsNullOrWhiteSpace(model.BuildingName))
records = records.Where(x => x.BuildingName == model.BuildingName);
if (!string.IsNullOrWhiteSpace(model.CaseManager))
records = records.Where(x => x.CaseManager == model.CaseManager);
if (!string.IsNullOrWhiteSpace(model.BuildingName))
records = records.Where(x => x.BuildingName == model.BuildingName);
if (model.IntakeDateStart.HasValue && model.IntakeDateEnd.HasValue)
{
records = records.Where(x => x.StartDate >= model.IntakeDateStart && x.StartDate <= model.IntakeDateEnd);
}
else
{
if (model.IntakeDateStart.HasValue)
records = records.Where(x => x.StartDate >= model.IntakeDateStart);
if (model.IntakeDateEnd.HasValue)
records = records.Where(x => x.StartDate <= model.IntakeDateEnd);
}
if (model.ExitDateStart.HasValue && model.ExitDateEnd.HasValue)
{
records = records.Where(x => x.EndDate >= model.ExitDateStart && x.EndDate <= model.ExitDateEnd);
}
else
{
if (model.ExitDateStart.HasValue)
records = records.Where(x => x.EndDate >= model.ExitDateStart);
if (model.ExitDateEnd.HasValue)
records = records.Where(x => x.EndDate <= model.ExitDateEnd);
}
if (model.IsActive.HasValue)
records = records.Where(x => x.IsActive == model.IsActive);
return records;
}
There's a few issues to unpack
Adding parameters to an SP is as simple as declaring them according to this guide: SQL Parameters
Passing parameters to an SP from C# is described here EF Raw SQL Queries
Your SP can be improved and replaced with a composable query which mean you no longer have to pass parameters through at all
How to Define the Parameters in your SP:
Lets do just the first 2 for now...
CREATE PROCEDURE [dbo].[spStaysSearch]
#OccupantId INT,
#OccupantFileAs VARCHAR(10)
AS
BEGIN
...
But then you need to use those parameters in your query, one way to do that is adding filter clauses like this to your select statement:
WHERE (#OccupantId IS NULL OR tblOccupant.OccupantID = #OccupantId)
AND (#OccupantFileAs IS NULL OR tblOccupant.FileAs = #OccupantFileAs)
How to Pass C# parameters to an SP via EF
var records = db.SpStaySearches.FromSqlRaw("EXECUTE dbo.spStaysSearch #OccupantId, #OccupantFileAs")
,new SqlParameter("OccupantId", model.OccupantId)
,new SqlParameter("OccupantFileAs", model.OccupantFileAs)
.ToList()
.AsQueryable();
Replace the SP altogether
There is no benefit in this case to using a stored procedure at all, the SP is not performing any logical operations, it is merely a wrapper around a very simple query. There are 2 ways we can replace this stored procedure call:
replace the SP with actual raw SQL:
NOTE: To be composable we cannot use CTEs, it must be a valid SQL expression that can be called as a nested or inline query
var sql = #"
SELECT
tblStays.*, tblOccupant.OccupantID,
tblOccupant.FileAs AS OccupantFileAs,
IIF(tblStays.BuildingName LIKE 'Main Shelter',
tblOccupant.OCFSMainNumber,
tblOccupant.OCFSNorthNumber) AS StayOCFSNumber,
COALESCE([CountOfOccupantStayID], 0) AS CountOfChildren,
tblCaseManager.FileAs AS CaseManager,
StaysMaxBillSentDate.MaxOfBillSentDate
FROM tblStays
LEFT JOIN tblOccupantStays ON tblStays.StayID = tblOccupantStays.StayID
LEFT JOIN tblOccupant ON tblOccupantStays.OccupantID = tblOccupant.OccupantID
LEFT JOIN (
SELECT lkpOccStays.StayID
, COUNT(tblOccupantStays.OccupantStayID) AS CountOfOccupantStayID
FROM tblOccupantStays lkpOccStays
INNER JOIN tblOccupant lkpChild ON lkpOccStays.OccupantID = lkpChild.OccupantID
WHERE lkpChild.OccupantType LIKE 'Child'
GROUP BY lkpOccStays.StayID
) OccupantStays_CountOfChildren ON tblStays.StayID = OccupantStays_CountOfChildren.StayID
LEFT JOIN tblCaseManager ON tblStays.CaseManagerID = tblCaseManager.CaseManagerID
LEFT JOIN (SELECT tblStayBillingHx.StayID
, MAX(tblStayBillingHx.BillSentDate) AS MaxOfBillSentDate
FROM tblStayBillingHx
GROUP BY tblStayBillingHx.StayID
) StaysMaxBillSentDate ON tblStays.StayID = StaysMaxBillSentDate.StayID
";
var records = db.SpStaySearches.FromSqlRaw(sql);
You could do this entirely in Linq:
A LOT of assumptions are made here, your EF schema has not been provided so some conventions have been assumed, this specific query may not match your schema but it should be close enough to understand the concept.
var records = db.Stays.SelectMany(s => s.Occupants.Select(o => new spStaysSearch {
StayId = s.StayId,
... (other stays properties)
OccupantId = o.OccupantId,
OccupantFileAs = o.FileAs,
StayOCFSNumber = s.BuildingName == "Main Shelter" ? o.OCFSMainNumber : o.OCFSNorthNumber,
CountOfChildren = s.Occupants.Count(child => child.OccupantType == "Child"),
CaseManager = s.CaseManager.FileAs,
MaxOfBillSentDate = s.BillingHistory.Max(h => h.BillSentDate)
}));

Improving LINQ query for many-to-many relation

I have a database with the following schema:
Now, I'm trying to pull all landingpages for a domain and sort those by the first UrlFilter's FilterType that matches a certain group. This is the LINQ I've come up with so far:
var baseQuery = DbSet.AsNoTracking()
.Where(e => EF.Functions.Contains(EF.Property<string>(e, "Url"), $"\"{searchTerm}*\""))
.Where(e => e.DomainLandingPages.Select(lp => lp.DomainId).Contains(domainId));
var count = baseQuery.Count();
var page = baseQuery
.Select(e => new
{
LandingPage = e,
UrlFilter = e.LandingPageUrlFilters.FirstOrDefault(f => f.UrlFilter.GroupId == groupId)
})
.Select(e => new
{
e.LandingPage,
FilterType = e.UrlFilter == null ? UrlFilterType.NotCovered : e.UrlFilter.UrlFilter.UrlFilterType
})
.OrderBy(e => e.FilterType)
.Skip(10).Take(75).ToList();
Now, while this technically works, it's quite slow with execution times ranging from 10-30 seconds, which is not good enough for the use case. The LINQ is translated to the following SQL:
SELECT [l1].[Id], [l1].[LastUpdated], [l1].[Url], CASE
WHEN (
SELECT TOP(1) [l].[LandingPageId]
FROM [LandingPageUrlFilters] AS [l]
INNER JOIN [UrlFilters] AS [u] ON [l].[UrlFilterId] = [u].[Id]
WHERE ([l1].[Id] = [l].[LandingPageId]) AND ([u].[GroupId] = #__groupId_3)) IS NULL THEN 4
ELSE (
SELECT TOP(1) [u0].[UrlFilterType]
FROM [LandingPageUrlFilters] AS [l0]
INNER JOIN [UrlFilters] AS [u0] ON [l0].[UrlFilterId] = [u0].[Id]
WHERE ([l1].[Id] = [l0].[LandingPageId]) AND ([u0].[GroupId] = #__groupId_3))
END AS [FilterType]
FROM [LandingPages] AS [l1]
WHERE CONTAINS([l1].[Url], #__Format_1) AND #__domainId_2 IN (
SELECT [d].[DomainId]
FROM [DomainLandingPages] AS [d]
WHERE [l1].[Id] = [d].[LandingPageId]
)
ORDER BY CASE
WHEN (
SELECT TOP(1) [l2].[LandingPageId]
FROM [LandingPageUrlFilters] AS [l2]
INNER JOIN [UrlFilters] AS [u1] ON [l2].[UrlFilterId] = [u1].[Id]
WHERE ([l1].[Id] = [l2].[LandingPageId]) AND ([u1].[GroupId] = #__groupId_3)) IS NULL THEN 4
ELSE (
SELECT TOP(1) [u2].[UrlFilterType]
FROM [LandingPageUrlFilters] AS [l3]
INNER JOIN [UrlFilters] AS [u2] ON [l3].[UrlFilterId] = [u2].[Id]
WHERE ([l1].[Id] = [l3].[LandingPageId]) AND ([u2].[GroupId] = #__groupId_3))
END
OFFSET #__p_4 ROWS FETCH NEXT #__p_5 ROWS ONLY
Now my question is, how can I improve the execution time of this? Either by SQL or LINQ
EDIT: So I've been tinkering with some raw SQL and this is what I've come up with:
with matched_urls as (
select l.id, min(f.urlfiltertype) as Filter
from landingpages l
join landingpageurlfilters lpf on lpf.landingpageid = l.id
join urlfilters f on lpf.urlfilterid = f.id
where f.groupid = #groupId
and contains(Url, '"barz*"')
group by l.id
) select l.id, 5 as Filter
from landingpages l
where #domainId in (
select domainid
from domainlandingpages dlp
where l.id = dlp.landingpageid
) and l.id not in (select id from matched_urls ) and contains(Url, '"barz*"')
union select * from matched_urls
order by Filter
offset 10 rows fetch next 30 rows only
This performs somewhat okay, cutting the execution time down to ~5 seconds. As this is to be used for a table search I would however like to get it down even further. Is there any way to improve this SQL?
You're right to have a look at the generated SQL. In general, I would advise to learn SQL, write a performing SQL query and work your way back (either use a stored procedure or raw SQL, or design your LINQ query with that same philosophy.
I suspect this will be better (not tested):
var page = (
from e in baseQuery
let urlFilter = e.LandingPageUrlFilters.OrderBy(f => f.UrlFilterType).FirstOrDefault(f => f.UrlFilter.GroupId == groupId)
let filterType = urlFilter == null ? UrlFilterType.NotCovered : e.UrlFilter.UrlFilter.UrlFilterType
select new
{
LandingPage = e,
FilterType = filterType
}
).Skip(10).Take(75).ToList();
one of the way to improve the execution time is see execution plan in SSMS (SQL Server Management Studio).
After look on the execution plan you can design some indexes, or if you have no experiences with this, you can see if SSMS recommends some indexes.
Next try to create the indexes and execute the query again and see if execution time was improved.
Note: this is only one of many possible ways to improve execution time...

How to remove duplicate row in a table in C# asp.net query

I have two rows that have similar data in one table. And I want to remove the duplicate data between that two row which is from one table, how to remove it using C# ASP.NET query?
I use distinct() to remove the duplicate data, but it shows an error:
'The image data type cannot be selected as DISTINCT because it is not comparable'
Here is my code:
var query = (from user in context.Users
join group in context.Groups on user.ID equals group.UserID
orderby user.Name
where group.GroupID == nameID && user.IsDeleted == false ||
userGroup.GroupID == fullID && user.IsDeleted == false
where user.IsDeleted == false
select user);
This is the expected result:
but the result is like this:
It duplicate because of this, I need to show the data between the two row, and combine it as one.
where group.GroupID == nameID && userMaster.IsDeleted == false || userGroup.GroupID == fullID && user.IsDeleted == false
In SQL Server, I successfully removed the duplicate data as follows:
WITH CTE ([ID], [GroupID], [NameID], [Name], DuplicateCount) AS
(
SELECT TOP (10)
[Group].ID, [GroupID], [NameID], [Name],
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Name asc) AS DuplicateCount
FROM
[SCMD3].[dbo].[Group]
JOIN
User ON Group.UserID = User.ID
WHERE
(NameID LIKE '1' OR NameID LIKE '2')
)
SELECT *
FROM CTE
WHERE DuplicateCount = 1
ORDER BY Name ASC
but how to remove it in C# code?
You can use SelectMany like so:
var partitioned = query.GroupBy(x => x.ID, x.GroupID, x.NameID, x.Name)
.SelectMany(g =>
g.Select((j, i) => new { j.ID, j.GroupID, j.NameID, j.Name, row_num = i + 1 }))
.Where(r => r.row_num = 1);
Group by all (or the required) columns,
Create a row_num column,
Filter all row_nums != 1
If you have duplicate row then you can use "Skip" to remove the same, like the below
select user.Skip(1)
If you have more than two duplicate records, then you can use "Take(1)" also.
Hope this will help you.
Happy coding!!!!

How do I write an Entity Framework 4 query for the following SQL?

I have an Entity Framework 4 model in my application. I need it to generate the following SQL:
SELECT *
FROM Reads AS R
INNER JOIN Images AS I ON R.ReadId = I.ReadId AND I.ImageTypeId = 2
LEFT OUTER JOIN Alarms AS A ON R.ReadId = A.ReadId AND A.DomainId IN (103,102,101,2,1,12) AND A.i_active = 1
LEFT OUTER JOIN ListDetails AS L ON L.ListDetailId = A.ListDetailId
WHERE R.i_active = 1
Here's what I have now for this query in my code:
var items = from read in query
join plate in context.Images on read.ReadId equals plate.ReadId
join temp1 in context.Alarms on read.ReadId equals temp1.ReadId into alarms from alarm in alarms.DefaultIfEmpty()
join temp2 in context.ListDetails on alarm.ListDetailId equals temp2.ListDetailId into entries from entry in entries.DefaultIfEmpty()
where plate.ImageTypeId == 2 && plate.IActive == 1
select new { read, plate, alarm, entry.OfficerNotes };
How do I modify the Entity Framework query to get the SQL query I need?
For Left Joins - I suggest using a from with a where instead of join, I find it makes it cleaner
For Inner Joins with multiple join conditions - you need to join on two matching anonymous types, or use a from with where conditions
For x IN list, you need to specify your list outside the query and use the Any method or Contains method
List<int> domainIds = new List<int>() { 103,102,101,2,1,12 };
var items = from read in query
join plate in context.Images
on new { read.ReadId, ImageTypeId = 2 } equals new { plate.ReadId, plate.ImageTypeId }
from alarm in context.Alarms
.Where(x => x.IActive == 1)
.Where(x => domainIds.Any(y => y == x.DomainId))
.Where(x => read.ReadId == x.ReadId)
.DefaultIfEmpty()
from entry in context.ListDetails
.Where(x => alarm.ListDetailId == x.ListDetailId)
.DefaultIfEmpty()
select new { read, plate, alarm, entry.OfficerNotes };

LINQ to Entities - Multiple Joins - Null Reference Exception on 'Select'

I am trying to convert a SQL query to a LINQ to entities query, but am having some problems with the LINQ select block.
Here is the SQL query which performs as expected:
SELECT distinct( p.PendingID,
p.Description,
p.Date,
f.Status,
u.UserName,
m.MapID
FROM Pending p
JOIN Users u
ON p.UserID = u.UserID
LEFT JOIN Forks f
ON p.PendingID = f.PendingID
LEFT JOIN Maps m
ON f.ForkID = m.ForkID
ORDER BY p.Date DESC
Here is the LINQ to entities query as I have it thus far:
var pList = (from pending in pendingItems
// JOIN
from user in userList.Where(u => pending.UserID == u.UserID)
// LEFT OUTER JOIN
from fork in forkList.Where(f => pending.ID == f.PendingID)
.DefaultIfEmpty()
// LEFT OUTER JOIN
from map in mapList.Where(m => fork.ID == m.ForkID)
.DefaultIfEmpty()
orderby pending.Date descending
select new
{
ItemID = pending.ID, // Guid
Description = pending.Description, // String
Date = pending.Date, // DateTime
Status = fork.Status, // Int32 (*ERROR HERE*)
UserName = user.UserName, // String
MapID = map.ID // Guid (*ERROR HERE*)
})
.Distinct()
.ToList();
The LINQ query fails on either of the following 2 lines, which attempt to assign values retrieved from left outer join results. If the following lines are omitted, the LINQ query completes without errors:
Status = fork.Status,
MapID = map.ID
Why are those 2 property assignments failing within the LINQ query's select block?
The problem is that due to your outer joins, fork and map may be null. Of course when they're null, you can't access their properties. You may need something like this:
Status = (fork == null) ? null : fork.Status,
MapID = (map == null) ? null : map.ID

Categories