I have a string:
strCheckedCategories = "2;"
an EntityList representing a SharePoint list, with item IDs from 1 to 21:
EntityList<VendorSearchesItem> vendorSearches =
dataContext.GetList<VendorSearchesItem>("Vendor Searches");
a LINQ query returning fields from two SharePoint lists that are joined to the "Vendor Searches" list:
var vendorSearchesQuery = (from s in vendorSearches
orderby s.VendorID.Title
select new
{
Vendor = s.VendorID.Title,
Website = s.VendorID.VendorWebsite,
VendorID = s.VendorID.Id,
SearchType = s.SearchTypeID.Title,
SearchTypeId = s.SearchTypeID.Id
});
and another LINQ query returning only the items where the item ID is in the list:
var q2 = from m2 in vendorSearchesQuery
where strCheckedCategories.Contains(m2.SearchTypeId.ToString())
select m2
The problem is that, in addition to returning the item with ID 2 (desired result) the query also returns items with ID 12, 20, and 21. How can I fix that?
So, fundamentally, what you want to do here is have an IN clause in which you specify a bunch of values for a field and you want rows who's value for that column is in that set.
CAML does actually have an IN clause which you could use, but sadly LINQ to Sharepoint doesn't provide any means of generating an IN clause; it's simply not supported by the query provider.
You're trying to use a bit of a hack to get around that problem by trying to do a string comparison rather than using the proper operators, and you're running into the pitfals of stringifying all of your operations. It's simply not well suited to the task.
Since, as I said, you cannot get LINQ to SharePoint to use an IN, one option would simply be to not use LINQ, build the CAML manually, and execute it using the standard server object model. But that's no fun.
What we can do is have a series of OR checks. We'll see if that column value is the first value, or the second, or the third, etc. for all values in your set. This is effectively identical to an IN clause, it's just a lot more verbose.
Now this brings us to the problem of how to OR together an unknown number of comparisons. If it were ANDs it'd be easy, we'd just call Where inside of a loop and it would AND those N clauses.
Instead we'll need to use expressions. We can manually build the expression tree ourselves of a dynamic number of OR clauses and then the query provider will be able to parse it just fine.
Our new method, WhereIn, which will filter the query to all items where a given property value is in a set of values, will need to accept a query, a property selector of what property we're using, and a set of values of the same type to compare it to. After we have that it's a simple matter of creating the comparison expression of the property access along with each key value and then ORing all of those expressions.
public static IQueryable<TSource> WhereIn<TSource, TKey>(
this IQueryable<TSource> query,
Expression<Func<TSource, TKey>> propertySelector,
IEnumerable<TKey> values)
{
var t = Expression.Parameter(typeof(TSource));
Expression body = Expression.Constant(false);
var propertyName = ((MemberExpression)propertySelector.Body).Member.Name;
foreach (var value in values)
{
body = Expression.OrElse(body,
Expression.Equal(Expression.Property(t, propertyName),
Expression.Constant(value)));
}
return query.Where(Expression.Lambda<Func<TSource, bool>>(body, t));
}
Now to call it we just need the query, the property we're filtering on, and the collection of values:
var q2 = vendorSearchesQuery.WhereIn(vendor => vendor.SearchTypeId
, strCheckedCategories.Split(';'));
And voila.
While I'd expect that to work as is, you may need to call the WhereIn before the Select. It may not work quite right with the already mapped SearchTypeId.
You should probably use a Regex, but if you want a simpler solution then I would avoid string searching and split those strings to an array:
string strCheckedCategories = "2;4;5;7;12;16;17;19;20;21;";
string[] split = strCheckedCategories.Split(';');
It will create an empty entry in the array for the trailing semicolon delimiter. I would check for that and remove it if this is a problem:
strCheckedCategories.TrimEnd(';');
Finally now you can change your where clause:
where split.Contains(m2.SearchTypeId.ToString())
If you have a very large list it is probably worth comparing integers instead of strings by parsing strCheckedCategories into a list of integers instead:
int[] split = strCheckedCategories.Split(';').Select(x => Convert.ToInt32(x)).ToArray();
Then you can do a quicker equality expression:
where split.Contains(m2.SearchTypeId)
try:
strCheckedCategories.Split(new []{';'}).Any(x => x == m2.SearchTypeId.ToString())
Contains will do a substring match. And "20" has a substring "2".
var q2 = from m2 in vendorSearchesQuery
where strCheckedCategories.Split(';').Contains(m2.SearchTypeId.ToString())
select m2
var q2 = from m2 in vendorSearchesQuery
where strCheckedCategories.Contains(";" + m2.SearchTypeId + ";")
select m2
And your strCheckedCategories should always end with ; and start with ;, for example ;2;, ;2;3;, ...
NOTE: This trick works only when your SearchTypeId should always not contain ;. I think you should use another kind of separator like \n or simply store your checked categories in a list or some array. That's the more standard way to do.
Related
I am looking to optimize some code to less lines and without the need for "for loops" using LINQ if possible. I saw a similar post asking for Select and Where in a single line but it wasn't exactly the same.
Suppose I have:
A list of elements in "fields" which has properties "Id" and "Name" which can be retrieved calling respectively .Id and .Name
Ex.
fields[0] = Element
fields[0].Id = 12345
fields[0].Name = Name01
I want to create a new list "filteredIds" containing the Id properties of selected fields.
This is the for loop version:
List<Id> filteredIds = new List<Id>();
fields = {Element1, Element2, ...}; //List of Elements
List<string> selectedNames = new List<string>() {"Name01", "Name05", "Name10"};
foreach (Element e in fields):
if (selectedNames.Contains(e.Name())
{
filteredIds.Add(e.Id);
}
Can this be done in a single line like this in LINQ?
filteredIds = fields.Select(i => i.Id).Any(o => selectedNames.Contains(o.Name)).ToList();
Any() returns true/false values. You need to call Where() to actually filter results.
filteredIds = fields.Where(o => selectedNames.Contains(o.Name)).Select(i => i.Id).ToList();
Almost correct. You should use Where to filter the list, not Any.
Any returns a boolean which is true if at least one element in the list satisfies the predicate, while Where returns all the elements that satisfy the predicate.
You also need to apply the Where filter before the Select, as the name property is removed by the select.
I have the following code:
var linqResults = (from rst in QBModel.ResultsTable
group rst by GetGroupRepresentation(rst.CallerZipCode, rst.CallerState) into newGroup
select newGroup
).ToList();
With the grouping method:
private string[] GetGroupRepresentation(string ZipCode, string State)
{
string ZipResult;
if (string.IsNullOrEmpty(ZipCode) || ZipCode.Trim().Length < 3)
ZipResult = string.Empty;
else
ZipResult = ZipCode.Substring(0, 3);
return new string[]{ ZipResult, State };
}
This runs just fine but it does not group at all. The QBModel.ResultsTable has 427 records and after the linq has run linqResults still has 427. In debug I can see double-ups of the same truncated zip code and state name. I'm guessing it has to do with the array I'm returning from the grouping method.
What am I doing wrong here?
If I concatenate the return value of the truncated zip code and state name without using an array I get 84 groupings.
If I strip out the rst.CallerState argument and change the grouping method to:
private string GetGroupRepresentation(string ZipCode)
{
if (string.IsNullOrEmpty(ZipCode) || ZipCode.Trim().Length < 3)
return string.Empty;
return ZipCode.Substring(0, 3);
}
It will return me 66 groups
I don't really want to concatenate the group values as I want to use them seperately later, this is wrong as it is based on if the array worked, however, kind of like the following:
List<DataSourceRecord> linqResults = (from rst in QBModel.ResultsTable
group rst by GetGroupRepresentation(rst.CallerZipCode, rst.CallerState) into newGroup
select new MapDataSourceRecord()
{
State = ToTitleCase(newGroup.Key[1]),
ZipCode = newGroup.Key[0],
Population = GetZipCode3Population(newGroup.Key[0])
}).ToList();
Array is reference type, so when the grouping method compare two arrays with same values it can not determine they are the same, because the references are different. you can read more here
One solution would be considering a class instead of using an array for results of function, and use another class to compare your results implementing the IEqualityComparer Interface, and pass it to GroupBy method, so that the grouping method can find which combinations of ZipCode and State are really equatable. read more
Not sure if this will work because I can not replicate your code.
but maybe it will be easier to add a group key and your string[] in seperate variables before you go forth your grouping. like this.
var linqdatacleanup = QBModel.ResultsTable.Select(x=>
new {
value=x,
Representation = GetGroupRepresentation(rst.CallerZipCode, rst.CallerState),
GroupKey= GetGroupRepresentationKey(rst.CallerZipCode, rst.CallerState)
}).ToList();
so GetGroupRepresentationKey returns a single string and your GetGroupRepresentation returns your string[]
this will allow you to do your grouping on this dataset and access your data as you wanted.
but before you spend to much time on this check this stack overflow question. maybe it will help
GroupBy on complex object (e.g. List<T>)
How would I order a list of items where some of the items contain double quotes?
Advance
Access
“Chain free” deal
Binding
Broker
Doing this FaqData = repo.FaqData.OrderBy(q => q.Description) results in the following
“Chain free” deal
Advance
Access
Binding
Broker
Tried this as well
FaqData = repo.FaqData.OrderBy(q => q.QuestionDescription.Replace("”", ""))
FaqData = repo.FaqData.OrderBy(q => q.Description.Replace(#"""",""))
OrderBy calls the delegate once per item contained in the list being sorted. The delegate should return a value which, when compared to values obtained in the same way for other items in the list, will provide a value that can be sorted.
Typically the value returned in the delegate is a property of the listed item - but because its code, it could return anything you like, including values that arn't anything to do with the items in the list.
In this example instead of returning the list property ".Description" the code is returning a new string value derived from the ".Description" property. The derivation is simply to use the .net String.Replace to replace all double-quote values with an empty string.
This means the sorting algorithm sorts on the ".Description" with double-quotes removed.
This is not very efficient if you call this sorting code many times, and could easily be done differently; either by adding a new property to the class being sorted as such;
public string PlainTextDescription
{
get {
return this.Description.Replace(#"""","");
}
}
and sorting like this;
FaqData = repo.FaqData.OrderBy(q => q.PlainTextDescription)
or by pre-populating the PlainTextDescription field using the logic, but only when the .Description value changes; this would be much more efficient because the String.Replace would only be called once each time the .Description changes - with the example above, the String.Replace code must be called every time the sorter needs to evaluate the PlainTextDescription field, which means we're doing the String.Replace many times instead of once.
I'm thinking you not only want to ignore quotes but anything that isn't A-Z.
All you need to do is include a function to Where that strips out anything you don't want. To do that you can use a regular expression, like this:
var filtered = Regex.Replace(s, #"[^A-Za-z0-9]","")
Now to put it in a Where statement:
var tests = new[] { "Advance","Access","Binding","Broker",#"""Chain free"" deal","`Twas the night before Christams","#NotAllMen","Zenit","Quickly"};
var sorted = tests.OrderBy(s => Regex.Replace(s, #"[^A-Za-z0-9]",""));
foreach (var s in sorted)
{
Console.WriteLine(s);
}
Output:
Access
Advance
Binding
Broker
"Chain free" deal
#NotAllMen
Quickly
`Twas the night before Christams
Zenit
Code on DotNetFiddle
I have a list of a custom type called Holdings. I am trying to query the list based on one property of the Holdings object to return a new list of Holdings. The LINQ query below does work correctly but I would like to replace var unitHld with List unitHld but can't get the code to work.
var unitHld = from hld in _holdingList
where hld.FundCode == lookThroList[i].FundCode
select new Holding() { Currency = hld.Currency,
FundCode = lookThroList[i].FundCode,
IdSedol = hld.IdSedol,
Nominal = hld.Nominal * unitWgt,
Price = hld.Price };
This new list is then slightly altered before being added back to the original list (I know the logic sounds strange but please accept this is how it has to be done). However because unitHld is var the line below does not work.
_holdingList.Add(unitHld);
The following call only adds a single item (where the item must be the same type as the list's elements):
_holdingList.Add(unitHld);
But you want to add a range of items, so do it like this:
_holdingList.AddRange(unitHld);
where unitHld is IEnumerable<T> and T is the type of the list's elements.
(This answer assumes that holdingList is of type List<T>, and that T is in fact Holding for your example.)
See List.AddRange() for details.
C# is statically typed.
var is not a type, all it does is a shortcut for in your case typing IEnumerable<Holding>.
If you want the result to be List<Holding> then all you need to do is wrap your query in brackets and put .ToList() at the end.
However, to append this to another list, you don't need to do that. Simply call .AddRange on the other list.
Alternatively, you can use Concat
var bothLists = aList.Concat(anotherList);
I would like to replace var unitHld with List unitHld but can't get the code to work.
You need to call ToList() on the result of the query:
var unitHld = from hld in _holdingList
where hld.FundCode == lookThroList[i].FundCode
select new Holding() { Currency = hld.Currency,
FundCode = lookThroList[i].FundCode,
IdSedol = hld.IdSedol,
Nominal = hld.Nominal * unitWgt,
Price = hld.Price };
List<Holding> unitHldList = unitHld.ToList();
This new list is then slightly altered before being added back to the original list
Once the data is in unitHldList, you can alter it as needed.
the line below does not work. _holdingList.Add(unitHld);
When you add the content of a collection to a List<T>, use AddRange method instead of Add:
_holdingList.AddRange(unitHldList);
Try this:
_holdingList.AddRange(unitHld);
I'm trying to add an extra parameter to a list of ef objects to track processing, but I keep running into having to initialize each list item explicitly. What's the correct linq way to do this? Aside from terseness, is there any advantage to a linq syntax in this case?
List<app_subjects> subjectList = AppMySQLQueries.GetAllSubjects();
List<Tuple<app_subjects, bool>> subjectCollection = new List<Tuple<app_subjects, bool>>(subjectList.Count);
foreach (app_subjects subject in subjectList)
{
subjectCollection.Add(Tuple.Create(subject, false));
}
I have searched the site without success.
You just want to use a projection here ( Select ) which applies the transformation in your lambda expression to each element in the source collection.
List<Tuple<app_subjects, bool>> tuples = subjectList.Select(x => new Tuple<app_subjects, bool>(x, false)).ToList();
The ToList() call is not entirely necessary, if you removed it then the method will return an IEnumerable<Tuple<app_subjects, bool>>. If you're just going to iterate the collection of tuples afterwards the ToList call should be removed as it forces execution (enumerates the IEnumberable) and then your next operation (the foreach) would do the same, making the code perform worse.
Like this?
subjectList.Select(s => Tuple.Create(s, false)).ToList();
With C# 10.0 (.NET 6.0) this is even easier and cleaner. Along with named tuples we can also declare a tuple by simply putting the values in round brackets.
List<(string NamedProperty1, int NamedProperty2)> _tuples = new();
_tuples = _objectList.Select(o => (o.SomeProperty1, o.SomeProperty2)).ToList();
try this.
List<Tuple<app_subjects, bool>> subjectCollection = subjectList.CovertAll( subject => new Tuple<app_subjects, bool>(){
subject,
false
}).ToList();