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.
Related
I'm trying to essentially copy any list items in fileManifest but only those that don't contain in any of the items from exclusionFilters to a newly initialized list. I haven't figured out an elegant way to do this other than a nested foreach loop.
Does someone by chance have a better solution for this problem? Maybe LINQ?
var fileManifest = new List<string>()
{
#"C:\Test\Directory1\File1.xml",
#"C:\Test\Directory1\File2.xml",
#"C:\Test\Directory1\Directory2\File1.xml",
};
var exclusionFilters = new List<string>()
{
#"Directory2\"
};
var filteredList = new List<string>();
Expected Output of filteredList:
C:\Test\Directory1\File1.xml
C:\Test\Directory1\File2.xml
var filteredList = fileManifest.Where(x => exclusionFilters.All(y => !x.Contains(y))).ToList();
Description:
Enumerable.Where Method: Filters a sequence of values based on a predicate.
Enumerable.All Method: Determines whether all elements of a sequence satisfy a condition.
String.Contains(String) Method: Returns a value indicating whether a specified substring occurs within this string.
Enumerable.ToList Method: Creates a List from an IEnumerable.
I have a list of items (InputList). Along with this I have a list of names (Names). Now I need only those items from InputList whose names are present in the Names list.
One solution is that I do a loop on Names list and check if it is present in InputList or not.
foreach (var name in names)
{
var result = InputList.FirstOrDefault(i => i.Name == name);
if (result != null)
outputList.Add(result);
}
There is IN operator in SQL in which we provide list and query returns desired result. Can I achieve the same using Linq?
That you want is the following:
var result = inputList.Where(x=>Names.Contains(x.Name));
the above query selects all the elements of inputList, whose name is contained in the Names list.
The signature of Contains method is bool Contains(T item), where T is the type of objects that are contained in your list. If the item you pass to this method is contained in your list returns true. Otherwise, it returns false.
For further documentation on the Contains method, please have a look here.
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 am using an array of string to sort a Linq string list according to the number of words matched from the array.
var targets = new string[] { ... }; // obtained via code
var list = = new List<string>(); // obtained via code
list = list.OrderByDescending(u => targets.Count(u.Contains)).ToList();// exception if null
The code works perfectly until it encounter a null string in list. I don't want to omit the nulls just want them sorted at the bottom of list. Is it possible to do this without using another list or intersections?
If you want to have null values be at the end of the list then you need to map them to the lowest value. In this case the key is int so use int.MinValue.
list = list
.OrderByDescending(u => u == null ? int.MinValue : targets.Count(u.Contains))
.ToList();
Rather than using this pattern though I would suggest you use List.Sort directly. The Sort method can be done in place and doesn't require the allocation of a brand new List<string>
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.