Can I clone an IQueryable in linq? For UNION purposes? - c#

I have a table of WorkOrders. The table has a PrimaryWorker & PrimaryPay field. It also has a SecondaryWorker & SecondaryPay field (which can be null).
I wish to run 2 very similar queries & union them so that it will return a Worker Field & Pay field. So if a single WorkOrder record had both the PrimaryWorker and SecondaryWorker field populated I would get 2 records back.
The "where clause" part of these 2 queries is very similar and long to construct. Here's a dummy example
var q = ctx.WorkOrder.Where(w => w.WorkDate >= StartDt && w.WorkDate <= EndDt);
if(showApprovedOnly)
{
q = q.Where(w => w.IsApproved);
}
//...more filters applied
Now I also have a search flag called hideZeroPay. If that's true I don't want to include the record if the worker was payed $0. But obviously for 1 query I need to compare the PrimaryPay field and in the other I need to compare the SecondaryPay field.
So I'm wondering how to do this.
Can I clone my base query q and make a primary & secondary worker query out of it and then union those 2 queries together?

Hmm, I'm not sure that I understand you intention. But I think cloning is not neccessary. Why don't you split two new queries from your base query?
var baseQuery = ctx.WorkOrder.Where(w => w.WorkDate >= StartDt && w.WorkDate <= EndDt);
IQueryable<WorkOrder> query1;
if (showApprovedOnly)
{
query1 = baseQuery.Where(w => w.IsApproved);
}
//more filters on query1
...
IQueryable<WorkOrder> query2;
if (/*something*/)
query2 = baseQuery.Where(w => w.SomeThing);
After defining your queries you can interpret them (per enumeration) and retrieve your different results.
var res1 = query1.ToList();
var res2 = query2.ToList();

When you do your second Where you are actually cloning your query.
Here you create your initial queryable object.
var q = ctx.WorkOrder.Where(w => w.WorkDate >= StartDt && w.WorkDate <= EndDt);
Here you create a new queryable with the where associated
if(showApprovedOnly)
{
q = q.Where(w => w.IsApproved);
}
//...more filters applied
All you need to do is create a new variable to store the ammended query.
var qw = q.Where(w=> w.IsApproved);
This works because the queryable is created as an object and the query itself is only run once you enumerate it.

Related

Why does AsEnumerable not work when using FromSqlRaw

I'm currently creating a site that showcases all my patients within a data table and I have to use FromSqlRaw in order to get the data from my database. I have a search funtion that allows me to search the patients within the table but upon entering the page I get this error when I use AsQueryable and no data is displayed in the table. It recommends me to use AsEnumerable but when I do I get an intellisense error. Any ideas on how to fix?
public async Task<IActionResult> Search(StaySearchViewModel model)
{
if (model.Cleared)
{
return Json(new
{
draw = model.Draw,
data = new object[] { },
recordsFiltered = 0,
recordsTotal = 0,
total = 0
});
}
var records = getSearchData(model);
//var records = System.Linq.Enumerable.AsEnumerable(getSearchData(model)); // Hard coding this an enumerable will break line 55, 57, and 64
//Sorting
if (!string.IsNullOrEmpty(model.SortOrder))
records = records.OrderBy(model.SortOrder);
var count = await records.CountAsync().ConfigureAwait(false);
records = records.Skip(model.Start);
if (model.Length != -1) records = records.Take(model.Length);
// Create models
var result = new List<SpStaySearchResultViewModel>();
try
{
await records.ForEachAsync(r =>
{
result.Add(new SpStaySearchResultViewModel()
{
BuildingName = r.BuildingName,
CaseManager = r.CaseManager,
CaseManagerId = r.CaseManagerId,
OccupantFileAs = r.OccupantFileAs,
StayOCFSNumber = r.StayOCFSNumber,
StayId = r.StayId,
MaxOfBillSentDate = r.MaxOfBillSentDate,
CountOfChildren = r.CountOfChildren,
StartDate = r.StartDate,
EndDate = r.EndDate,
OccupantId = r.OccupantId,
IsActive = r.IsActive,
});
}).ConfigureAwait(false);
}
catch (Exception e) { }
return Json(new
{
draw = model.Draw,
data = result,
recordsFiltered = count,
recordsTotal = count,
});
}
private IQueryable<spStaysSearch> getSearchData(StaySearchViewModel model)
{
var records = db.SpStaySearches.FromSqlRaw("dbo.spStaysSearch").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;
}
Try this
var records = getSearchData(model).ToList();
var count = records.Count;
.....
You can't order records by model.SortOrder since it has nothing to do with records.
You can only do something like this
if (!string.IsNullOrEmpty(model.SortOrder)) records = records.OrderBy(r=> r.Id);
because your source data is a Stored Procedure, you cannot compose additional query expressions over the top of it. Instead you must load it into memory, as the error suggests, by enumerating the result set.
Including Related Data
SQL Server doesn't allow composing over stored procedure calls, so any attempt to apply additional query operators to such a call will result in invalid SQL. Use AsEnumerable or AsAsyncEnumerable method right after FromSqlRaw or FromSqlInterpolated methods to make sure that EF Core doesn't try to compose over a stored procedure.
The obvious way to interpret this in the code is to call .ToList() on the results from the SP, then to match your existing code pattern you can cast that result back to IQueryable:
var records = db.SpStaySearches.FromSqlRaw("dbo.spStaysSearch")
.ToList()
.AsQueryable()
Using AsEnumerable() is sometimes problematic as there are many different libraries that you may have implemented that might all provide an AsEnumerable() extension method.
We have to do this because even in SQL you cannot simply select from an SP and then add where clauses to it, you first have to read the results into a temporary table or a table variable, then you can re-query from the result set, that is what we are effectively doing now, we are reading the results into a C# variable (.ToList()) and then composing an in-memory query over the top of that result.
If your search logic must be encapsulated in a stored procedure, then given the technical limitations, the usual expectation is that you would add the search arguments as optional parameters to the stored procedure, rather then tring to add filter clauses on top of the results in C#.
We can help with how to move your filter logic into dbo.spStaysSearch but you'll have to post the content of that SP, ideally as a new question.
Instead of using an SP at all, where we lose practically all the goodness that EF can offer us, an alternative approach is to replace your SP entirely with a raw SQL then the rest of your logic will work as expected.
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);
In this way the SP is providing the structure of the resultset, which might be necessary if you are using the Database-First approach but you are no longer executing the SP at all.
The SQL in this answer is provided as a guide to the syntax only, there is not enough information available to determine the validity of the query or that the results conform to your business requirements.

LINQ: Is there a way to combine these queries into one?

I have a database that contains 3 tables:
Phones
PhoneListings
PhoneConditions
PhoneListings has a FK from the Phones table(PhoneID), and a FK from the Phone Conditions table(conditionID)
I am working on a function that adds a Phone Listing to the user's cart, and returns all of the necessary information for the user. The phone make and model are contained in the PHONES table, and the details about the Condition are contained in the PhoneConditions table.
Currently I am using 3 queries to obtain all the neccesary information. Is there a way to combine all of this into one query?
public ActionResult phoneAdd(int listingID, int qty)
{
ShoppingBasket myBasket = new ShoppingBasket();
string BasketID = myBasket.GetBasketID(this.HttpContext);
var PhoneListingQuery = (from x in myDB.phoneListings
where x.phonelistingID == listingID
select x).Single();
var PhoneCondition = myDB.phoneConditions
.Where(x => x.conditionID == PhoneListingQuery.phonelistingID).Single();
var PhoneDataQuery = (from ph in myDB.Phones
where ph.PhoneID == PhoneListingQuery.phonePageID
select ph).SingleOrDefault();
}
You could project the result into an anonymous class, or a Tuple, or even a custom shaped entity in a single line, however the overall database performance might not be any better:
var phoneObjects = myDB.phoneListings
.Where(pl => pl.phonelistingID == listingID)
.Select(pl => new
{
PhoneListingQuery = pl,
PhoneCondition = myDB.phoneConditions
.Single(pc => pc.conditionID == pl.phonelistingID),
PhoneDataQuery = myDB.Phones
.SingleOrDefault(ph => ph.PhoneID == pl.phonePageID)
})
.Single();
// Access phoneObjects.PhoneListingQuery / PhoneCondition / PhoneDataQuery as needed
There are also slightly more compact overloads of the LINQ Single and SingleOrDefault extensions which take a predicate as a parameter, which will help reduce the code slightly.
Edit
As an alternative to multiple retrievals from the ORM DbContext, or doing explicit manual Joins, if you set up navigation relationships between entities in your model via the navigable join keys (usually the Foreign Keys in the underlying tables), you can specify the depth of fetch with an eager load, using Include:
var phoneListingWithAssociations = myDB.phoneListings
.Include(pl => pl.PhoneConditions)
.Include(pl => pl.Phones)
.Single(pl => pl.phonelistingID == listingID);
Which will return the entity graph in phoneListingWithAssociations
(Assuming foreign keys PhoneListing.phonePageID => Phones.phoneId and
PhoneCondition.conditionID => PhoneListing.phonelistingID)
You should be able to pull it all in one query with join, I think.
But as pointed out you might not achieve alot of speed from this, as you are just picking the first match and then moving on, not really doing any inner comparisons.
If you know there exist atleast one data point in each table then you might aswell pull all at the same time. if not then waiting with the "sub queries" is nice as done by StuartLC.
var Phone = (from a in myDB.phoneListings
join b in myDB.phoneConditions on a.phonelistingID equals b.conditionID
join c in ph in myDB.Phones on a.phonePageID equals c.PhoneID
where
a.phonelistingID == listingID
select new {
Listing = a,
Condition = b,
Data = c
}).FirstOrDefault();
FirstOrDefault because single throws error if there exists more than one element.

LINQ Groupby query on a single column

I have a situation where I have a Job which has multiple tests which run at specific intervals. A job run generates a unique TestRunId which is a GUID, which is used to reference multiple results, basically grouping a particular run with a unique RunId(GUID).
Now the problem is that I need to select unique runs which have been generated, but my LINQ query picks up every run.
I tried something like this
var testRunIds = ((from tests in context.testresults
where tests.JobId == jobId
select new
{
tests.TestRunId
}).GroupBy(t=>t.TestRunId).OrderBy(t=>t.Key).Skip((pagenum - 1) * pagesize).Take(pagesize)).ToList();
But as I said, this query picks up each and every testResult. Not sure how I do this now. I tried Distinct(), but that too didnt work. Sample data below.
Thanks
I believe the problem is that I have multiple TestRunId values as its essentially a grouping. Inorder to achieve what I need, I tried using (got using Linqer)
from Queries in db.TestResult
where
Queries.JobId == 1
group Queries by new {
Queries.TestRunId,
Queries.StartTime,
Queries.EndTime
} into g
orderby
g.Key.TestRunId
select new {
_ID = (int?)g.Max(p => p.Id),
g.Key.TestRunId,
g.Key.StartTime,
g.Key.EndTime
}
But this works only for MSSQL datasource which is essentially a
SELECT max(id)[ ID],
TestRunId,
StartTime,
Endtime
FROM dbo.query where jobid = 1 group by TestRunId,StartTime,Endtime order by StartTime;
But what I need is
SELECT TestRunId,StartTime,Endtime FROM testresult where jobid = 1 group by TestRunId order by StartTime;
for MySQL.
Try this:-
var jobs = context.testresults;
var query2 = jobs.Where(x => x.TestID == 1).OrderBy(x => x.StartTime).Select(x => x.TestRunID).Distinct();
Working Fiddle.
I think you're possibly looking for this:
var testRunIds = context.testresults.Where(t => t.JobId == jobId).OrderBy(t => t.starttime)
.Select(t => t.TestRunId).Distinct().Skip((pagenum - 1) * pagesize).Take(pagesize)
.ToList();
Do the filtering and ordering first, then select the single field needed, then use Distinct() for uniqueness, then skip/take as required. Selecting the single field first then attempting to order or filter on other fields in the table won't work as those fields are no longer part of the query.
Thanks for helping me out. I managed to do this in a two step process.
var testRunIds = (from tests in context.testresults
where tests.JobId == jobId
select new
{
tests.TestRunId,
tests.StartTime
}).OrderBy(x => x.StartTime).Skip((pagenum - 1) * pagesize).Take(pagesize).GroupBy(x=>x.TestRunId).ToList();
var resultData = testRunIds.Select(testRunId => (context.testresults.Where(
items => items.TestRunId == testRunId.Key)).FirstOrDefault()).ToList();

c# only show numbers after decimal point with linq

I have a linq query that executes successfully, one of the columns returned is a decimal type that is used to represent prices in pounds and pence (there will never be any negative values)
I want to be able to strip out the pounds and pence into separate Properties of my projection, however when using functionality such as
var result= from j in context.Products
select
new{
Price = t.Price,
PricePounds = Math.Truncate(t.Price)
};
I get an error that Math.truncate is not supported as it cannot be translated into a store expression. How can I get the pounds value from this query?
If you don't need to do anything else in the database after that, the simplest approach is just to perform the truncation client-side:
var query = context.Products
.AsEnumerable() // Everything from here is LINQ to Objects
.Select(p => new {
p.Price,
PricePounds = Math.Truncate(p.Price)
});
Note that you might also want to just cast to int - and that might be supported in EF already.
EDIT: As noted in comments, you may want to perform a projection first, e.g.
var query = context.Products
.Select(p => new { p.Price, p.SomethingElse })
.AsEnumerable() // Everything from here is LINQ to Objects
.Select(p => new {
p.Price,
PricePounds = Math.Truncate(p.Price),
p.SomethingElse
});
(Where SomethingElse is another property you're interested in, as an example - I doubt that you only want the price.)
This will avoid the entire entity being fetched when you only want a few properties.
You may try:
var result= from j in context.Products
select
new {
Price = t.Price,
PricePounds = EntityFunctions.Truncate(t.Price, 0)
};
The case is Math.Truncate cannot be translated into SQL where EntityFunctions.Truncate should be.

filter a linq query based on the results of another query's results

I am wanting to filter a linq query
I have 2 linq statements
The 1st gets all the stores I want and the 2nd is where I filter information based on the results found in the 1st query.
var stores = ctx.Stores.Where(ps => ps.ParentStoreID == parent.ParentStoreID && ps.StoreID!=storeID);
var query = (from a in ctx.TransactionTable
from b in ctx.MappingTable.Where(x => x.TransactionId== a.TransactionId).DefaultIfEmpty()
where a.StoreID!=storeID
select new
{
Transactions = a,
Mapping = b
}).ToList();
How do I add another where clause into my 2nd query to only return results where a.StoreId is contained within the stores result?
Like this:
var stores = ctx.Stores.Where(ps => ps.ParentStoreID == parent.ParentStoreID && ps.StoreID!=storeID);
var query = (from a in ctx.TransactionTable
from b in ctx.MappingTable.Where(x => x.TransactionId==a.TransactionId).DefaultIfEmpty()
where a.StoreID!=storeID && stores.Select(s => s.StoreID).Contains(a.StoreID)
select new
{
Transactions = a,
Mapping = b
}).ToList();
You can find more info here:
Linq to Entities - SQL "IN" clause

Categories

Resources