Dynamic lists in Linq Query - solr - Sitecore - c#

So here is the summary:
Sitecore - SOLR index query
I have items that I am trying to retrieve using a set of sites that can vary.
I have a query:
query = query.Where(x => x.Language == this.ItemLanguage)
.Where(x => x.Templates.Contains(new Guid("94c1f3e5ac174a319cc5bbb942fe80c6")));
this will return all of the items correctly.
What I need is to add a dynamic list call for this query.
Something like:
.Where(x => x.Site.Any(y => siteNames.Contains(y)));
I have tried adding this line of code and I get an error:
System.ArgumentException: 'Argument must be array'
(some details)
Items returned (x) have a field "Site" of List<string>
siteNames is a List<String> of variable site names
The following code works in other places:
.Where(x => x.Site.Contains("somesite"));
Is there a way to manage a dynamic list or will I need to manually generate this expression based on the number of items in the siteNames list?
.Where(x => x.Site.Contains("dynamic") || x.Site.Contains("dynamic")
|| x.Site.Contains("dynamic") || x.Site.Contains("dynamic")
|| and so on);
Here is the full code example:
using (var context = Sitecore.ContentSearch.ContentSearchManager.GetIndex(AccelConstants.SEARCH_INDEX).CreateSearchContext())
{
IQueryable<SearchResultModel> query = context.GetQueryable<LMSSearchResultModel>();
SearchResults<SearchResultModel> results = null;
List<string> siteNames = new List<string>();
siteNames = this.SiteNames;
// Define the base search
query = query.Where(x => x.Language == this.ItemLanguage)
.Where(x => x.Templates.Contains(new Guid("94c1f3e5ac174a319cc5bbb942fe80c6")))
.Where(x => x.Site.Any(p => siteNames.Contains(p)));
// Execute the query
results = query.GetResults();
}
the site field is a solr field of Site_SM which outputs like this:
The reason the name "Site" is used is that Sites is also a field.
This is not code that I have control over so I am working with what I have.
"site_sm":["login",
"admin",
"service",
"covid19",
"scheduler",
"system",
"publisher"],
The search results model simply converts computed solr fields to c#
public class SearchResultModel : SearchResultItem
{
[IndexField("_templates")]
public List<Guid> Templates { get; set; }
[IndexField("site_sm")]
public List<string> Site { get; set; }
}

Use Predicate Builder to create a dinamic OR (I just use it to fix this issue).
Sitecore have a implementation for this is:
//using Sitecore.ContentSearch.Linq.Utilities
private IQueryable<LMSSearchResultModel> LimitSearchBySite(IQueryable<LMSSearchResultModel> query, IEnumerable<string> sites)
{
if (sites != null && sites.Any())
{
// https://www.albahari.com/nutshell/predicatebuilder.aspx
var predicate = PredicateBuilder.False<StoreLocatorResult>();
foreach (string s in site)
predicate = predicate.Or(p => p.Site.Contains(s));
return query.Where(predicate);
}
return query;
}
if you are using Solr and C# directly the best way is to use this site as reference:
https://www.albahari.com/nutshell/predicatebuilder.aspx
At the end is the same development I did but you need to add the PredicateBuilder class to your project
So this will be what you need to replace:
using (var context = Sitecore.ContentSearch.ContentSearchManager.GetIndex(AccelConstants.SEARCH_INDEX).CreateSearchContext())
{
IQueryable<SearchResultModel> query = context.GetQueryable<LMSSearchResultModel>();
SearchResults<SearchResultModel> results = null;
List<string> siteNames = new List<string>();
siteNames = this.SiteNames;
// Define the base search
query = query.Where(x => x.Language == this.ItemLanguage)
.Where(x => x.Templates.Contains(new Guid("94c1f3e5ac174a319cc5bbb942fe80c6")));
query = LimitSearchBySite(query, siteNames);
// Execute the query
results = query.GetResults();
}

OK, so here is a rather hacked way of making my search work.
using (var context = Sitecore.ContentSearch.ContentSearchManager.GetIndex(AccelConstants.SEARCH_INDEX).CreateSearchContext())
{
IQueryable<SearchResultModel> query = context.GetQueryable<LMSSearchResultModel>();
SearchResults<SearchResultModel> results = null;
List<string> siteNames = new List<string>();
siteNames = this.SiteNames;
// Define the base search
// remove site filtering from query
query = query.Where(x => x.Language == this.ItemLanguage)
.Where(x => x.Templates.Contains(new Guid("94c1f3e5ac174a319cc5bbb942fe80c6")));
// Execute the query
results = query.GetResults();
// Get the results
foreach (var hit in results.Hits)
{
//move site filter code to here
if (hit.Document != null && hit.Document.Site.Any(p => siteNames.Contains(p)))
{
// Add the model to the results.
}
else
{
}
}
}
By moving the site filter code to after the query, the Site List<string> is instantiated and has value at the time of the call. Which seems to have been my issue.
This then allows me to filter by the sites in the siteNames List<string> and get my final result set.
I don't know if this is the best way of making this section of code work but it does work.

I ran across this exact problem and found this StackOverflow answer searching for the error. It turns out you have to take the error literally and change the List<string> Site to a string[] Site in order to use .Any() in a predicate.
public class SearchResultModel : SearchResultItem
{
[IndexField("_templates")]
public List<Guid> Templates { get; set; }
[IndexField("site_sm")]
public string[] Site { get; set; }
}

Related

Using for loop on where() method

So I have a search-input and checkboxes that passes the values to the controller when there are inputs. And I want to use these values to get something back from the database. The search-input is a string and it works and intended. Here is the code for the search-input:
public async Task<ViewResult> Index(string searchString, List<int> checkedTypes)
{
var products = from p in _db.Products select p;
ViewData["CurrentFilter"] = searchString;
if (!string.IsNullOrEmpty(searchString))
{
products = products.Where(p => p.Name.ToLower().Contains(searchString));
}
return View(products);
}
However the checkboxes values are stored in a list. So basically I want to do the same as the code above, but with a list. So basically an idea is like this:
if(checkedTypes != null)
{
foreach (var i in checkedTypes)
{
products = products.Where(p => p.TypeId == i));
}
}
If I do it like the code above, I just get the last (i) from the loop. Another solution I did was this:
if(checkedTypes != null)
{
var temp = new List<Product>();
foreach (var i in checkedTypes)
{
temp.AddRange(products.Where(p => p.TypeId == i));
}
products = temp.AsQueryable();
}
But when I did it like that I get this error:
InvalidOperationException: The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IAsyncQueryProvider can be used for Entity Framework asynchronous operations.
So anyone have a solution that I can use? Or is there a better way to handle checkboxes in the controller?
Assuming you are using EF Core (also the same is true for linq2db) - it supports translating filtering with local collection, i.e. Where(x => checkedTypes.Contains(x.SomeId)).
If you have "and" logic to filter by searchString and checkedTypes than you can conditionally add Where clause:
if (!string.IsNullOrEmpty(searchString))
{
products = products.Where(p => p.Name.ToLower().Contains(searchString));
}
if(checkedTypes != null)
{
products = products.Where(p => checkedTypes.Contains(p.TypeId));
}
P.S.
Also you should be able to change your first line to:
var products = _db.Products.AsQueryable();

How do I search a dynamic object IEnumerable<dynamic> using a dynamically built lambda expression?

Previously, I had great help on my previous question, thank you vyrp
,
How do I create and populate a dynamic object using a dynamically built lambda expression
I'm now looking to search the dynamic object, and as before, I don't know the objects properties, and therefore what I'm searching until runtime.
Here's the code that builds the dynamic object:
// Get list of optional fields
var optFieldList = await _tbList_FieldRepository.GetAsync(lf => lf.ListID == listId && lf.DisplayInList == true);
// order list of optional fields
optFieldList.OrderBy(lf => lf.DisplayOrder);
// Get base Data excluding Inactive if applicable
IEnumerable<tbList_Data> primaryData = await _tbList_DataRepository.GetAsync(ld => ld.ListID == listId && (ld.IsActive == includeInactive ? ld.IsActive : true));
// Build IEnumerable<dynamic> from base results plus any optional fields to be displayed in table
var results = primaryData.Select(pd => {
dynamic result = new System.Dynamic.ExpandoObject();
result.Id = pd.ID;
result.PrimaryData = pd.PrimaryData;
result.DisplayOrder = pd.DisplayOrder;
result.IsActive = pd.IsActive;
foreach (var optField in optFieldList)
{
switch (optField.FieldType.ToLower()) {
case "text":
((IDictionary<string, object>)result).Add(optField.FieldName, pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == optField.ID).Select(ld => ld.DataField).DefaultIfEmpty("").First());
break;
}
}
return result;
});
For the purpose of testing, I have 2 dynamic fields, "PhoneNumber" and "FuelType"
I can search the known field(s) i.e. PrimaryData, no problem, as below.
results = results.Where(r => r.PrimaryData.Contains(searchString));
And the following will work if I know the field PhoneNumber at design time
results = results.Where(r => r.PhoneNumber.Contains(searchString));
but what I want to do, is something like:
results = results.Where(r => r.PrimaryData.Contains(searchString)
|| foreach(var optField in optFieldList)
{
r.optField.FieldName.Contains(searchString)
})
ending up with
results = results.Where(r =>
r.PrimaryData.Contains(searchString)
|| r.PhoneNumber.Contains(searchString) ||
r.FuelType.Contains(searchString));
but obviously that code doesn't work. I've tried a bunch of different attempts, none successful, so I'm looking for suggestions. Thanks
Since you know that the dynamic element of your query is actually ExpandoObject, hence IDictionary<string, object>>, you can safely cast it to dictionary interface and use it to access the property values by name, while Enumerable.Any method can be used to simulate dynamic || condition:
results = results.Where(r => r.PrimaryData.Contains(searchString)
|| optFieldList.Any(f =>
{
object value;
return ((IDictionary<string, object>)r).TryGetValue(f.FieldName, out value)
&& value is string && ((string)value).Contains(searchString);
}));

How to use Linq to check if a list of strings contains any string in a list

I'm constructing a linq query that will check is a string in the DB contains any of the strings in a list of strings.
Something like.
query = query.Where(x => x.tags
.Contains(--any of the items in my list of strings--));
I'd also like to know how many of the items in the list were matched.
Any help would be appreciated.
Update: I should have mentioned that tags is a string not a list. And I am adding on a couple more wheres that are not related to tags before the query actually runs. This is running against entity framework.
EDIT: This answer assumed that tags was a collection of strings...
It sounds like you might want:
var list = new List<string> { ... };
var query = query.Where(x => x.tags.Any(tag => list.Contains(tag));
Or:
var list = new List<string> { ... };
var query = query.Where(x => x.tags.Intersect(list).Any());
(If this is using LINQ to SQL or EF, you may find one works but the other doesn't. In just LINQ to Objects, both should work.)
To get the count, you'd need something like:
var result = query.Select(x => new { x, count = x.tags.Count(tag => list.Contains(tag)) })
.Where(pair => pair.count != 0);
Then each element of result is a pair of x (the item) and count (the number of matching tags).
I've done something like this before:
var myList = new List<string>();
myList.Add("One");
myList.Add("Two");
var matches = query.Where(x => myList.Any(y => x.tags.Contains(y)));
like this:
List<string> list = new List<string>();
list.Add("One");
list.Add("Two");
var result = query.Where(x => list.Contains(x.tags));
I am not quite sure from your question if x.tags is a string or list, if it is a list Jon Skeet's answer is correct. If I understand you correctly though x.tags is a string of strings. If so then the solution is:
list.Any(x => x.tags.IndexOf(x) > -1)
to count them do
list.Count(x => x.tags.IndexOf(x) > -1)
var t = new List<string> { "a", "b", "c" };
var y = "a b d";
var res = y.Count(x => t.Contains(x.ToString()));
I faced a similar problem recently and here's how I managed to work it out:
var list = [list of strings];
if (list != null && list.Any())
{
queryable = queryable.Where(x => x.tags != null);
var tagQueries = new List<IQueryable<WhateverTheDbModelIs>>();
foreach (var element in list)
{
tagQueries.Add(queryable.Where(x => x.tags.Contains(element)));
}
IQueryable<WhateverTheDbModelIs> query = tagQueries.FirstOrDefault();
foreach (var tagQuery in tagQueries)
{
query = query.Union(tagQuery);
}
queryable = queryable.Intersect(query);
}
probably not the best option but something a less experienced developer can understand and use

Cant use Linq with nested class List<> on MongoDb C#

I have the following classes:
public class Company
{
[BsonId]
public string dealerId = null;
public List<Dealer> dealers = new List<Dealer>();
}
public class Dealer
{
public string dId = null;
public int dIndex = -1;
public List<AutoStore> stores = new List<AutoStore>();
}
public class AutoStore
{
public string type = null;
public Dictionary<string, object> data = new Dictionary<string, object>();
}
I am able to store the Company class objects in Mongo with Insert(). The problem is when I search for a document and try to use LINQ on the List<> items. I constantly get an exception .
var query = collection.AsQueryable<Company>()
.Where(cpy =>
cpy.dealers.Where(dlr =>
dlr.stores.Count == 1).Count() > 0) ;
Running this code I get:
System.NotSupportedException: Unable to determine the serialization
information for the expression: Enumerable.Count
I just started using Mongo today, but I thought the LINQ support was more mature. Can anyone tell me if I can do a nested array query like I've done with C# andLINQ ?
As soon as I remove the Where() on any of the List<> , that exception isn't thrown
Going by your exception the problem area is within where you are doing Where statements.
As I said in my comment. Try to do:
var v = collection.AsQueryable<Company>().Where(cpy => cpy.Dealers.Any(dlr => dlr.Stores.Count == 1));
You are currently doing something like:
var dealers = collection.AsQueryable<Company>().Select(cpy => cpy.Dealers);
var dealersWithStores = dealers.Where(dealer => dealer.Stores.Count == 1);
You are then checking if there are any dealers with stores by calling count and checking if that is more than 0 to get your bool in the where. All of this is the same as calling IEnumerable.Any(). See if this works? :)
You could write you query more efficiently as
var query = collection.AsQueryable<Company>()
.Where(c => c.dealers.Any(d => d.stores.Count == 1);
If the Mongo querty provider is struggling to support IList, you might find
var query = collection.AsQueryable<Company>()
.Where(c => c.dealers.Any(d => d.stores.Count() == 1);
works better. If so, reports of the maturity of MongoDBs query provider are exaggerated.

Why does this LINQ-to-SQL query get a NotSupportedException?

The following LINQ statement:
public override List<Item> SearchListWithSearchPhrase(string searchPhrase)
{
List<string> searchTerms = StringHelpers.GetSearchTerms(searchPhrase);
using (var db = Datasource.GetContext())
{
return (from t in db.Tasks
where searchTerms.All(term =>
t.Title.ToUpper().Contains(term.ToUpper()) &&
t.Description.ToUpper().Contains(term.ToUpper()))
select t).Cast<Item>().ToList();
}
}
gives me this error:
System.NotSupportedException: Local
sequence cannot be used in LINQ to SQL
implementation of query operators
except the Contains() operator.
Looking around it seems my only option is to get all my items first into a generic List, then do a LINQ query on that.
Or is there a clever way to rephrase the above LINQ-to-SQL statement to avoid the error?
ANSWER:
Thanks Randy, your idea helped me to build the following solution. It is not elegant but it solves the problem and since this will be code generated, I can handle up to e.g. 20 search terms without any extra work:
public override List<Item> SearchListWithSearchPhrase(string searchPhrase)
{
List<string> searchTerms = StringHelpers.GetSearchTerms(searchPhrase);
using (var db = Datasource.GetContext())
{
switch (searchTerms.Count())
{
case 1:
return (db.Tasks
.Where(t =>
t.Title.Contains(searchTerms[0])
|| t.Description.Contains(searchTerms[0])
)
.Select(t => t)).Cast<Item>().ToList();
case 2:
return (db.Tasks
.Where(t =>
(t.Title.Contains(searchTerms[0])
|| t.Description.Contains(searchTerms[0]))
&&
(t.Title.Contains(searchTerms[1])
|| t.Description.Contains(searchTerms[1]))
)
.Select(t => t)).Cast<Item>().ToList();
case 3:
return (db.Tasks
.Where(t =>
(t.Title.Contains(searchTerms[0])
|| t.Description.Contains(searchTerms[0]))
&&
(t.Title.Contains(searchTerms[1])
|| t.Description.Contains(searchTerms[1]))
&&
(t.Title.Contains(searchTerms[2])
|| t.Description.Contains(searchTerms[2]))
)
.Select(t => t)).Cast<Item>().ToList();
default:
return null;
}
}
}
Ed, I've run into a similiar situation. The code is below. The important line of code is where I set the memberList variable. See if this fits your situation. Sorry if the formatting didn't come out to well.
Randy
// Get all the members that have an ActiveDirectorySecurityId matching one in the list.
IEnumerable<Member> members = database.Members
.Where(member => activeDirectoryIds.Contains(member.ActiveDirectorySecurityId))
.Select(member => member);
// This is necessary to avoid getting a "Queries with local collections are not supported"
//error in the next query.
memberList = members.ToList<Member>();
// Now get all the roles associated with the members retrieved in the first step.
IEnumerable<Role> roles = from i in database.MemberRoles
where memberList.Contains(i.Member)
select i.Role;
Since you cannot join local sequence with linq table, the only way to translate the above query into SQL woluld be to create WHERE clause with as many LIKE conditions as there are elements in searchTerms list (concatenated with AND operators). Apparently linq doesn't do that automatically and throws an expception instead.
But it can be done manually by iterating through the sequence:
public override List<Item> SearchListWithSearchPhrase(string searchPhrase)
{
List<string> searchTerms = StringHelpers.GetSearchTerms(searchPhrase);
using (var db = Datasource.GetContext())
{
IQueryable<Task> taskQuery = db.Tasks.AsQueryable();
foreach(var term in searchTerms)
{
taskQuery = taskQuery.Where(t=>t.Title.ToUpper().Contains(term.ToUpper()) && t.Description.ToUpper().Contains(term.ToUpper()))
}
return taskQuery.ToList();
}
}
Mind that the query is still executed by DBMS as a SQL statement. The only drawback is that searchTerms list shouldn't be to long - otherwise the produced SQL statement won'tbe efficient.

Categories

Resources