How to negate a Where clause of an IQueryable - c#

I created an extension method to encapsule some where logic like this (this is a very simplified version):
public static IQueryable<Cargo> ReadyToCarry(this IQueryable<Cargo> q)
{
VehicleType[] dontNeedCouple = new VehicleType[] { VehicleType.Sprinter, VehicleType.Van, VehicleType.Truck };
return q.Where(c => c.DriverId > 0 && c.VehicleId > 0)
.Where(c => c.CoupleId > 0 || dontNeedCouple.Contains(c.Vehicle.Type));
}
So I can use it like this:
using (var ctx = new MyNiceContext())
{
var readyCargo = ctx.Cargos.ReadyToCarry().OrderBy(c => c.Id).ToList();
// ...more code
}
Which works nicely, this is translated to SQL and executed by Entity Framework. Now, I have another place I need cargos which are not ready to carry, which means I need exactly the opposite.
My idea was something like this:
public static IQueryable<Cargo> NotReadyToCarry(this IQueryable<Cargo> q)
{
return !q.ReadyToCarry(); // ofc this doesn't work...
}
using (var ctx = new MyNiceContext())
{
var readyCargo = ctx.Cargos.NotReadyToCarry().OrderBy(c => c.Id).ToList();
// OR maybe
var readyCargo = ctx.Cargos.ReadyToCarry(false).OrderBy(c => c.Id).ToList(); // somehow use that bool param to reverse the logic when false
}
I didn't want to recreate the reverse logic from scratch, so if I needed to change it one day, I'd change in one unique place.
I'm accepting alternatives to this approach, since it's a new project.

You can use Except() method:
var readyCargo = ctx.Cargos.ReadyToCarry().OrderBy(c => c.Id);
var notReadyCargo = ctx.Cargos.Except(readyCargo);
OR
you can add some parameter to ReadyToCarry():
public static IQueryable<Cargo> ReadyToCarry(this IQueryable<Cargo> q, bool ready = true)
{
VehicleType[] dontNeedCouple = new VehicleType[] { VehicleType.Sprinter, VehicleType.Van, VehicleType.Truck };
if (ready)
{
return q.Where(c => c.DriverId > 0 && c.VehicleId > 0)
.Where(c => c.CoupleId > 0 || dontNeedCouple.Contains(c.Vehicle.Type));
}
else
{
// logic to get not ready for carrying
}
}
OR
you can combine these two options:
public static IQueryable<Cargo> ReadyToCarry(this IQueryable<Cargo> q, bool ready = true)
{
VehicleType[] dontNeedCouple = new VehicleType[] { VehicleType.Sprinter, VehicleType.Van, VehicleType.Truck };
var readyToCarry = q.Where(c => c.DriverId > 0 && c.VehicleId > 0)
.Where(c => c.CoupleId > 0 || dontNeedCouple.Contains(c.Vehicle.Type));
if (ready)
{
return readyToCarry;
}
else
{
return q.Except(readyToCarry);
}
}
In the last case when you change logic to get ready to carry entities you don't need to change negation of that condition. You should change only one query.

Related

How do you call a function to an Linq List query that uses the exist function

I have this method with a linq statement below. I'm not a fan of multiple if statement and I'm trying to find what is the best way to not have these if statement and have a private method.
My field values is being set as such:
var fieldValues = await GetFields // then it's being passed to my method.
public static AppraisalContactBorrower BuildCoBorrower(List<LoanFieldValue> fieldValues) {
var coborrower = new AppraisalContactBorrower();
if (fieldValues.Exists(f => f.FieldId == "CX.OS.AO.COBORRNAME")) {
coborrower.Name = fieldValues.First(v => v.FieldId == "CX.OS.AO.COBORRNAME").Value;
}
if (fieldValues.Exists(f => f.FieldId == "CX.OS.AO.BORRCONTACTZIP")) {
borrower.Zip = fieldValues.First(v => v.FieldId == "CX.OS.AO.BORRCONTACTZIP").Value;
}
if (fieldValues.Exists(f => f.FieldId == "CX.OS.AO.BORRCONTACTZIP")) {
borrower.Zip = fieldValues.First(v => v.FieldId == "CX.OS.AO.BORRCONTACTZIP").Value;
}
What I'm trying to do is instead of this:
coborrower.Name = fieldValues.First(v => v.FieldId == "CX.OS.AO.COBORRNAME").Value;
Is having something similar to this.
if (fieldValues.Exists(f => f.FieldId == "CX.OS.AO.BORRCONTACTZIP")) {
coborrower.Name = SETVALUE("CX.OS.AO.BORRCONTACTZIP")}
First, try using Enumerable.ToDictionary to have the field values grouped by FieldId, then use IDictionary.TryGetValue to get the existing values:
public static AppraisalContactBorrower BuildCoBorrower(List<LoanFieldValue> fieldValues) {
var groupedFieldValues = fieldValues.ToDictionary(f => f.FieldId)
var coborrower = new AppraisalContactBorrower();
if (groupedFieldValues.TryGetValue("CX.OS.AO.COBORRNAME", out var name)) {
coborrower.Name = name.Value;
}
if (groupedFieldValues.TryGetValue("CX.OS.AO.BORRCONTACTZIP", out var zip)) {
borrower.Zip = zip.Value;
}
}
Using Dictionary makes it faster to check the appropriate field existence as it is O(1) and with TryGetValue you combine two operations into one (existence check + obtaining the value).
Your two last statements are almost identitical. The equivalent of :
if (groupedFieldValues.TryGetValue("CX.OS.AO.COBORRNAME", out var name)) {
coborrower.Name = name.Value;
}
is:
coborrower.Name = fieldValues.FirstOrDefault(v => v.FieldId == "CX.OS.AO.COBORRNAME")
?? coborrower.Name;
In the original code, coborrower.Name is not updated if the field doesn't exist in the list.

C# LINQ Contains() query for autocomplete search box is slow

I've got a search box that I'm providing autocomplete suggestions for but it's really slow, it takes multiple seconds for suggestions to appear. I'm pretty sure my code is inefficient but I'm not sure the best way to improve it, any suggestions?
[HttpPost]
[Route("search")]
public virtual JsonResult Search(string term)
{
var result = new List<SearchResult>();
if (!String.IsNullOrWhiteSpace(term))
{
var searchTerms = term.ToLower().Split(' ');
List<Card> resultList = null;
foreach (var query in searchTerms)
{
if (resultList == null)
{
resultList = CardRepository.FindAll().Where(x => x.Name.ToLower().Contains(query) || x.Set.SetName.ToLower().Contains(query) || x.Variant.ToLower().Contains(query)
|| x.CardNumber.ToLower().Contains(query) || (query == "holo" && x.IsHolo)).ToList();
}
else
{
resultList = resultList.Where(x => x.Name.ToLower().Contains(query) || x.Set.SetName.ToLower().Contains(query) || x.Variant.ToLower().Contains(query)
|| x.CardNumber.ToLower().Contains(query) || (query == "holo" && x.IsHolo)).ToList();
}
}
foreach (var item in resultList.Take(10))
{
result.Add(new SearchResult()
{
label = item.FullCardName,
value = item.CardId.ToString()
});
}
}
return Json(result);
}
EDIT: Added the FindAll() code.
private readonly IDatabase _database;
public IQueryable<Card> FindAll()
{
return _database.CardDataSource.OrderBy(a => a.Name).AsQueryable();
}
SOLUTION: Going on the advice from the comments and with reference to this post Full Text Search with LINQ I moved my searching to the repository as a method and the result is almost instant autocomplete suggestions. I'm not sure how much better I could make the performance but it's easily usable in its current state.
public Card[] Search(string[] searchTerms)
{
IQueryable<Card> cardQuery = _database.CardDataSource;
foreach(var term in searchTerms)
{
var currentTerm = term.Trim();
cardQuery = cardQuery.Where(p => (p.Name.Contains(currentTerm) ||
p.Variant.Contains(currentTerm) ||
p.CardNumber.Contains(currentTerm) ||
p.Set.SetName.Contains(currentTerm) ||
(term == "holo" && p.IsHolo) ||
(term == "reverse" && p.IsHolo))
);
}
return cardQuery.Take(10).ToArray();
}
[HttpPost]
[Route("search")]
public virtual JsonResult Search(string term)
{
var result = new List<SearchResult>();
if (!String.IsNullOrWhiteSpace(term))
{
var searchTerms = term.ToLower().Split(' ');
var resultList = CardRepository.Search(searchTerms);
foreach (var item in resultList)
{
result.Add(new SearchResult()
{
label = item.FullCardName,
value = item.CardId.ToString()
});
}
}
return Json(result);
}
I think that the main problem is that you're using .FindAll() which returns a List<T>.
This means that when you say CardRepository.FindAll() it gets all of the records into an in-memory list and then your subsequent refining queries (e.g. Where(x => x.Name.ToLower().Contains(query)) and so on) are all run against the entire list. So it's right that it's returning really slowly.
You could try rewriting it by simply removing the .FindAll() and see what happens.
Please note, I'm just giving you the main problem, there are other issues, but none is as important as this one.
You could use multi-threading like this (pseudo-C# code):
var allCards = CardRepository.FindAll().ToArray(); // Ensure array.
query = query.ToUpper();
var nameTask = Task.StartNew(() => allCards.Where(x => x.Name.ToUpper().Contains(query)).ToArray());
var setTask = Task.StartNew(() => allCards.Where(x => x.Set.SetName.ToUpper().Contains(query)).ToArray());
var variantTask = Task.StartNew(() => allCards.Where(x => x.Variant.ToUpper().Contains(query)).ToArray());
var cardNumberTask = Task.StartNew(() => allCards.Where(x => x.CardNumber.ToUpper().Contains(query)).ToArray());
var holoTask = Task.StartNew(() => allCards.Where(x => query == "holo" && x.IsHolo).ToArray());
Task.WaitAll(new Task[] {nameTask, setTask, variantTask, cardNumberTask, holoTask});
var result = (nameTask.Result + setTask.Result + variantTask.Result + cardNumberTask.Result + halaTask.Result).Distinct().ToArray();

Using Where predicate to filter detail.List with another list

I'm trying to get a set of Master-Detail records on the basis of a list of strings that I need to match with each detail's reference number column for each master. So, for example, I have this as a list of strings:
string[] listToFilterFor = new [] { "2729113", "2732623", "2734483", "2735355", "2752260" };
Anf the DAL function to filter:
public async Task<IQueryable<BILL_INFO>> GetBills(IDictionary<string, object> filterCriteria, string operationGuid)
{
var callerInfo = Shared.CommonAcross.Helper.GetCaller();
Logger.Info($"{LayerName} -> {callerInfo.MethodName} -> Started");
try
{
IList<BILL_INFO> intermResult;
using (var context = new FinanceConnection())
{
var result = context.BILL_INFOS
.Include(i => i.MASTER_ACCOUNT)
.Include(i => i.MASTER_PAY_MODE)
.Include(i => i.MASTER_BANK)
.Include(i => i.MASTER_CREDIT_CARD_TYPE)
.Include(i => i.MASTER_EDIRHAM_CARD_TYPE);
if (filterCriteria != null && filterCriteria.Any())
{
#region Keys
var billNumberKey = "BillNumber";
var cashierNumberKey = "AssignedCashiers";
var payModeIdKey = "PayModeId";
var depositIdKey = "DepositId";
var dateFromKey = "DateFrom";
var dateToKey = "DateTo";
var accountsKey = "Account";
var accountGroupsKey = "AccountGroups";
var referenceNumber = "ReferenceNumber";
var referenceNumbers = "ReferenceNumbers";
#endregion
if (filterCriteria.ContainsKey(billNumberKey) && filterCriteria.TryGetValue(billNumberKey, out var actualFilterBillNumber))
result = result.Where(where => where.BILL_NUMBER.Contains(actualFilterBillNumber.ToString()));
if (filterCriteria.ContainsKey(referenceNumbers) && filterCriteria.TryGetValue(referenceNumbers, out var actualReferenceNumbers))
{
result = result.Include(i => i.BILL_INFO_DETAIL);
result = result.Where(where => where.BILL_INFO_DETAIL.Any(p=>p.));
}
#region From/To Dates
DateTime? tempDateFrom = null;
DateTime? tempDateTo = null;
if (filterCriteria.ContainsKey(dateFromKey) && filterCriteria.TryGetValue(dateFromKey, out var actualDateFrom))
{
tempDateFrom = ((DateTime?)actualDateFrom)?.Date;
}
if (filterCriteria.ContainsKey(dateToKey) && filterCriteria.TryGetValue(dateToKey, out var actualDateTo))
{
tempDateTo = ((DateTime?)actualDateTo)?.Date.AddDays(1).AddMilliseconds(-1);
}
if (tempDateFrom.HasValue && tempDateTo.HasValue)
{
result = result.Where(where => where.BILL_DATE != null && where.BILL_DATE >= tempDateFrom && where.BILL_DATE <= tempDateTo);
}
else if (tempDateFrom.HasValue && !tempDateTo.HasValue)
{
result = result.Where(where => where.BILL_DATE != null && where.BILL_DATE >= tempDateFrom && where.BILL_DATE <= tempDateFrom);
}
else if (!tempDateFrom.HasValue && tempDateTo.HasValue)
{
result = result.Where(where => where.BILL_DATE != null && where.BILL_DATE >= tempDateTo.Value.Date && where.BILL_DATE <= tempDateTo);
}
#endregion
}
intermResult = await result.OrderByDescending(o => o.BILL_DATE).Take(10000).ToListAsync();
}
Logger.Info($"{LayerName} -> {callerInfo.MethodName} -> Returning");
return intermResult.AsQueryable();
}
catch (Exception exp)
{
Logger.Error($"{LayerName} -> {callerInfo.MethodName} -> Exception [{exp.Message}]", exp);
throw;
}
}
Task:
I need to go through the master records (BILL_INFOS) and look into each master's detail records (BILL_INFO_DETAILS) and try to match AnyOf listToFilterFor against BILL_INFO_DETAIL.REFERENCE_NUMBER
If i understand this correct, i apologize if i have not. Could you use an includes statement.
BILL_INFO.where(x => listToFilterFor.Includes(x.BILL_INFO_DETAILS.REFERENCE_NUMBER)
I finally sorted this one out as here were composite primary keys involved and a proper tuple was not possible. I found the solution through this question: Cannot create a relation between two tables with three primary keys
I was able to extract a subset.

C# Ordering event dates with null values last

I'm working on a course listing in C# and an course can have up to 5 dates of when they are running. Ideally, the next date after today in the future would be selected, and ordered accordingly in a list.
What i have so far is a course list that gets the next date, and displays it, but it displays all the events without dates first (Null/Blank). I'm trying to show the courses with next dates first, and then those without after this.
C# Code:
public ActionResult FilterList(string role = null, string category = null)
{
return View("~/Views/FilterList.cshtml", GetCourses(role, category));
}
[NonAction]
public List<IEnumerable<Course>> GetCourses(string role = null, string category = null)
{
var collection = new List<IEnumerable<Course>>();
var items = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CourseRoot)
.Children.Where(m => m.TemplateID == Course.TemplateID)
.Select(m => (Course)m).ToList();
var dates = new List<FilterDates>();
items.ForEach(m => dates.Add(new FilterDates
{
Dates = new List<DateTime>{ m.Date1, m.Date2, m.Date3, m.Date4, m.Date5 },
Name = m.Name
}));
dates.ForEach(m => m.Dates.RemoveAll(n => n == new DateTime(0001, 01, 01)));
dates.ForEach(m => m.Dates.Sort((a, b) => a.CompareTo(b)));
dates = dates.OrderBy(m => m.Dates.AsQueryable().FirstOrDefault(n => n - DateTime.Now >= TimeSpan.Zero)).ToList();
var model = new List<Course>();
dates.ForEach(m => model.Add(items.AsQueryable().FirstOrDefault(n => n.Name == m.Name)));
if (!string.IsNullOrEmpty(role) || !string.IsNullOrEmpty(category))
{
var currentRole = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CategoryRoot)
.Children.AsQueryable().FirstOrDefault(m => m.Fields["Key"].Value == role);
if (!string.IsNullOrEmpty(category))
{
var currentCategory = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.SeriesRoot)
.Children.AsQueryable().FirstOrDefault(m => m.Fields["Key"].Value == category);
model = model.Where(m => m.Series == currentCategory.Name).ToList();
if (string.IsNullOrEmpty(role))
{
collection.Add(model);
}
}
if (!string.IsNullOrEmpty(role))
{
model = model.Where(m => m.InnerItem.Children.Where(n => n.Fields["Key"].Value == currentRole.Name).Any()).ToList();
List<Course> required = new List<Course>(), recommended = new List<Course>(), refresh = new List<Course>();
foreach (var item in model)
{
foreach (Item inner in item.InnerItem.Children)
{
if (inner.Fields["Key"].Value == currentRole.Name)
{
switch (inner.Fields["Severity"].Value)
{
case "Required":
required.Add(item);
break;
case "Recommended":
recommended.Add(item);
break;
case "Refresh":
refresh.Add(item);
break;
}
}
}
}
collection.Add(required);
collection.Add(recommended);
collection.Add(refresh);
}
}
else
{
collection.Add(model);
}
return collection;
}
I've tried different orderbys, but can't seem to get the ordering right. Any help would be greatly appreciated.
Andy
The code you posted has some extra stuff that seems unrelated to your question about sorting. I am ignoring that and just addressing the question at hand: how to sort your courses so that the ones with the nearest future date are first.
I would create a little method to return the next future date or DateTime.MaxValue as the "null" value.
private DateTime GetNextFutureDate(Course course)
{
var dates =
new[] {course.Date1, course.Date2, course.Date3, course.Date4, course.Date5}.Where(d => d > DateTime.Now).ToArray();
return dates.Length == 0 ? DateTime.MaxValue : dates[0];
}
Then in your GetCourses method you could use it like this:
[NonAction]
public List<IEnumerable<Course>> GetCourses(string role = null, string category = null)
{
var collection = new List<IEnumerable<Course>>();
var model = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CourseRoot)
.Children.Where(m => m.TemplateID == Course.TemplateID)
.Select(m => (Course)m).OrderBy(m => GetNextFutureDate(m));
if (!string.IsNullOrEmpty(role) || !string.IsNullOrEmpty(category))
// ... the rest of your code ...
return collection;
}
You might also want to consider making GetNextFutureDate a member or extension method on your Course class.

Linq Conditional .Any() Select

How can I perform a conditional select on a column value, where I have a preference over which value is returned. If I can't find the top choice, I settle on the next, if available, and then if not the next, etc. As it looks right now, it would take 3 total queries. Is there a way to simplify this further?
var myResult = string.Empty;
if (myTable.Where(x => x.ColumnValue == "Three").Any())
{
myResult = "Three"; // Can also be some list.First().Select(x => x.ColumnValue) if that makes it easier;
}
else if (myTable.Where(x => x.ColumnValue == "One").Any())
{
myResult = "One";
}
else if (myTable.Where(x => x.ColumnValue == "Two").Any())
{
myResult = "Two";
}
else
{
myResult = "Four";
}
You could use a string[] for your preferences:
string[] prefs = new[]{ "One", "Two", "Three" };
string myResult = prefs.FirstOrDefault(p => myTable.Any(x => x.ColumnValue == p));
if(myResult == null) myResult = "Four";
Edit Enumerable.Join is a very efficient hash table method, it also needs only one query:
string myResult = prefs.Select((pref, index) => new { pref, index })
.Join(myTable, xPref => xPref.pref, x => x.ColumnValue, (xPref, x) => new { xPref, x })
.OrderBy(x => x.xPref.index)
.Select(x => x.x.ColumnValue)
.DefaultIfEmpty("Four")
.First();
Demo
I wrote an extension method that effectively mirrors Tim Schmelter's answer (was testing this when he posted his update. :-()
public static T PreferredFirst<T>(this IEnumerable<T> data, IEnumerable<T> queryValues, T whenNone)
{
var matched = from d in data
join v in queryValues.Select((value,idx) => new {value, idx}) on d equals v.value
orderby v.idx
select new { d, v.idx };
var found = matched.FirstOrDefault();
return found != null ? found.d : whenNone;
}
// usage:
myResult = myTable.Select(x => x.ColumnValue)
.PreferredFirst(new [] {"Three", "One", "Two"}, "Four");
I've written one that will quit a little more early:
public static T PreferredFirst<T>(this IEnumerable<T> data, IList<T> orderBy, T whenNone)
{
// probably should consider a copy of orderBy if it can vary during runtime
var minIndex = int.MaxValue;
foreach(var d in data)
{
var idx = orderBy.IndexOf(d);
if (idx == 0) return d; // best case; quit now
if (idx > 0 && idx < minIndex) minIndex = idx;
}
// return the best found or "whenNone"
return minIndex == int.MaxValue ? whenNone : orderBy[minIndex];
}
I use a weighted approach in SQL where I assign a weight to each conditional value. The solution would then be found by finding the highest or lowest weight depending on your ordering scheme.
Below would be the equivalent LINQ query. Note that in this example I am assigning a lower weight a higher priority:
void Main()
{
// Assume below list is your dataset
var myList =new List<dynamic>(new []{
new {ColumnKey=1, ColumnValue ="Two"},
new {ColumnKey=2, ColumnValue ="Nine"},
new {ColumnKey=3, ColumnValue ="One"},
new {ColumnKey=4, ColumnValue ="Eight"}});
var result = myList.Select(p => new
{
ColVal = p.ColumnValue,
OrderKey = p.ColumnValue == "Three" ? 1 :
p.ColumnValue == "One" ? 2 :
p.ColumnValue == "Two" ? 3 : 4
}).Where(i=> i.OrderKey != 4)
.OrderBy(i=>i.OrderKey)
.Select(i=> i.ColVal)
.FirstOrDefault();
Console.WriteLine(result ?? "Four");
}
How about something like this:
var results = myTable.GroupBy(x => x.ColumnValue).ToList();
if (results.Contains("Three")) {
myResult = "Three";
} else if (results.Contains("One")) {
myResult = "One";
} else if (results.Contains("Two")) {
myResult = "Two";
} else {
myResult = "Four";
}

Categories

Resources