LINQ Where clause from array of strings - c#

I have a list of class Products:
class Products
{
public string Name { get; set; }
public string Size { get; set; }
public string ProductId { get; set; }
public string Category { get; set; }
}
I would like to use one TextBox to search through any matching products utilizing a wildcard value. This would return me a list of items where all values in the search string are found somewhere in the four properties listed above.
As of now, I'm using string[] values = searchText.Split("*".ToCharArray) to seperate the values of the search string into an array of strings (based on an asterisk wildcard). From there, I get stumped, since I want to search for all values of the search string in all properties of the class.
I tried to figure it out using a complex LINQ statement, but I have not been able to figure it out how to make this work. I don't know how to build a Where statement when I don't know how many values I'm going need to test against my four properties.

So, if you're breaking search up into separate keywords, using * as the delimiter, which you've described in the comments, then this is how you do it:
var products = new List<Products>()
{
new Products()
{
Name = "theo frederick smith",
Size = "",
ProductId = "",
Category = "brown",
}
};
var searchText = "fred*brown";
var splits = searchText.Split("*".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
var results =
products
.Where(p => splits.All(s =>
p.Name.Contains(s)
|| p.Size.Contains(s)
|| p.ProductId.Contains(s)
|| p.Category.Contains(s)));
That matches the input.
Alternatively, if you really want a wildcard search, such as "fred*smith" (meaning that any one field must contain "fred" followed by zero or more characters and followed by "smith"), then this works:
var products = new List<Products>()
{
new Products()
{
Name = "theo frederick smith",
Size = "",
ProductId = "",
Category = "brown",
}
};
var searchText = "fred*smith";
var wildcard =
new Regex(
String.Join(".*",
searchText
.Split('*')
.Select(x => Regex.Escape(x))));
var results =
products
.Where(p => new []
{
p.Name, p.Size, p.ProductId, p.Category
}.Any(x => wildcard.IsMatch(x)));

Naively, you could write
products.Where(x=>x.Name.Contains(search)
|| x.Size.Contains(search)
|| x.ProductId.Contains(search)
|| x.Category.Contains(search))
You would be better off putting that logic in your Product class.
So you would have:
class Products
{
public bool Contains(string term) {
return Name.Contains(search) || Size.Contains(search) ||
ProductId.Contains(search) || Category.Contains(search)
}
public string Name { get; set; }
public string Size { get; set; }
public string ProductId { get; set; }
public string Category { get; set; }
}
And then simply products.Where(x=>x.Contains(search))
You could also use reflection to get all the property names and do a for each on each string and check for Contains.

Related

Search and Order results by keywords

I'm trying to create a search on a collection of Organisations (I'm using LINQ to Entities).
public class Organisation
{
public string OrgName { get; set; }
public string ContactName { get; set; }
public string OverviewOfServices { get; set; }
public string Address1 { get; set; }
public string Town { get; set; }
public string PostCode { get; set; }
public string Keywords { get; set; }
}
The user inputs some keywords, and then I want to return all Organisations where all the keywords exist in any of the Organisation fields above.
// 1. Remove special characters and create an array of keywords
string[] Keywords = CleanKeyWordString(model.keyword)
// 2 . Get organisations
orglist = _UoW.OrganisationRepo.All();
(OrganisationRepo.All returns an IQueryable of Organisation. There are further queries on the search prior to this)
// 3. Filter results by keywords
orglist = (from org in orglist
where Keywords.All(s =>
org.OrgName.ToLower().Contains(s)
|| org.OverviewOfServices.ToLower().Contains(s)
|| org.ContactName.Contains(s)
|| org.Address1.ToLower().Contains(s)
|| org.Town.ToLower().Contains(s)
|| org.PostCode.ToLower().Contains(s)
|| org.Keywords.ToLower().Contains(s))
orderby searchTerms.Any(s => org.OrgName.ToLower().Contains(s)) ? 1 : 2
select org);
This brings back the required results, however, I would now like to order them so that those records with the keywords in the title are ordered first and then the rest after.
Is this possible without adding some kind of ranking algorithm to the results?
var withRank = orglist
.Select(o =>
new {
Org = o,
Rank = o.OrgName.ToLower().Contains(s)
});
var orderedOrgList = withRank
.OrderBy(o => o.Rank)
.Select(o => o.Org);

cannot implicity convert type System.Collections.GenericList<AnonymousType#1>

Please have patience with me I'm new to linq and have a question about a couple of errors that I am not understanding any help would be greatly appreciated.
the error I am receiving in the title of my question.
my childproduct variable returns every single character indiviuallly I would like them to be a string version of the productId and Childtext parameters.
Code:
public class AOAPlusChildModel
{
public List<string> LongName { get; set; }
public List<string> Text { get; set; }
public List<string> ProductId { get; set; }
public static List<AOAPlusChildModel> GetChildProducts()
{
List<AOAPlusChildModel> cp = new List<AOAPlusChildModel>();
List<AoaUserDefinedVWGetAOAPlusProducts> ChildProductsLists = AoaSvcClient.Client.Context.AoaUserDefinedVWGetAOAPlusProductss.Where(a => a.MasterProductFlag == false && a.Affiliate == "VA").ToList();
var childProducts = ChildProductsLists.SelectMany(p => p.LongName, (id, childtext) =>
new { ProductId = id.ProductId, Text = childtext }).ToList();
cp = childProducts.ToList();
return cp;
}
}
Your variable cp is a List<AOAPlusChildModel> but the linq query is projecting an anonymous type. Instead of creating a new anonymous type create a new AOAPlusChildModel
return ChildProductsLists.SelectMany(p => p.LongName,
(id, childtext) =>
new AOAPlusChildModel {
ProductId = id.ProductId,
Text = childtext }).ToList();
Reason for following errors are that you perform ChildProductsLists.SelectMany(p => p.LongName) which basically now returns a collection of strings - this collection of strings you are trying to assign as a new AOAPlusChildModel object which does not hold string properties but List<string> properties.
I think your model should look like:
public string LongName { get; set; }
public string Text { get; set; }
public string ProductId { get; set; }

Making sure a value is equal to one of many in LINQ

I have an IEnumerable containing values like "ABC", "DEF", etc.
I'm trying to form a LINQ query where the value of a property in my object may be equal to one of the many values contained in my IEnumerable. How do I form that LINQ query?
var statesFilter = new List<string>();
statesFilter.Add("NY");
statesFilter.Add("CA");
var employees = new List<Employee>();
employees = getDataFromSomewhere();
// Code below is not working. Just wanted to give you an idea about my LINQ query
var myFilteredList = employees.Where(x => x.State.Contains(statesFilter));
Employee class could be something like this:
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string State { get; set; }
}
If State property is of type string, you should change your Where to the following:
var myFilteredList = employees.Where(x => statesFilter.Contains(x.State));
Also, you can do this:
var myFilteredList = employees.Where(x => statesFilter.Any(s=>s==x.State));
Condition should be
x=>statesFilter.Contains(x.State))

C# splitting comma delimited string in a foreach loop over anonymous type

I'm trying to split the title of a product from something like, Nitric Acid 70% , ACS REAGENT GRADE, 2.5 Liter(7 Lbs) Bottle to Nitric Acid 70%. In my code I first joined two collections with the customer name as the key. Then I did a foreach loop over the class spreadlist and added the members from customers and product. However, in Title = record.Title, I don't want the full title name just the first string before the first comma. How can I make this happen? I was trying to use .split but I don't think I'm understanding it correctly
private static IEnumerable<SpreadList> Combine(IEnumerable<Address> addresses, IEnumerable<Address> products)
{
var customersandproducts = addresses.Select(x => new { x.Name, x.AddressLine1, x.AddressLine2, x.City, x.State, x.S_OrderId, x.PostalCode })
.Join(products, custs => custs.S_OrderId, prod => prod.P_OrderId,
(custs, prod) => new { custs.Name, prod.Title, prod.ShippingPrice, prod.ItemPrice, prod.Quantity });
var records = new List<SpreadList>();
foreach (var record in customersandproducts)
{
records.Add(new SpreadList()
{
Name = WebUtility.HtmlDecode(record.Name),
Title = record.Title,
ItemPrice = record.ItemPrice,
ShippingPrice = record.ShippingPrice,
Quantity = record.Quantity,
});
}
return records;
}
public class SpreadList
{
public string Name { get; set; }
public string Title { get; set; }
public string ShippingPrice { get; set; }
public string ItemPrice { get; set; }
public string Quantity { get; set; }
}
Untested, but in your records.Add(new SpreadLIst()) code block, I'd suggest:
Title = record.Title.Substring(0,record.Title.IndexOf(',')-1),
Another option is:
Title = record.Title.Split(',')[0]
Using split is correct - split will create an array of sub-strings between the specified delimiter and all you have to do is grab the first item in that array.
Title = record.Title.Split(',')[0].Trim();
I'd write a little function. This could be a named function, or just a Func<string, string>:
Func<string, string> GetFirstStringPart = title =>
{
if(string.IsNullOrEmpty(title))
return null; // or string.Empty;
string[] stringParts = title.Split(',');
if(stringParts.Any())
return stringParts.First().Trim();
else
return null; // or string.Empty;
};
foreach (var record in customersandproducts)
{
records.Add(new SpreadList()
{
Name = WebUtility.HtmlDecode(record.Name),
Title = GetFirstStringPart(record.Title),
...
. etc .
The advantage of using a function is that you can deal with any special cases that need separate handling, like empty strings or null strings, or any additional logic you might find you need.
From https://msdn.microsoft.com/en-us/library/system.string.split(v=vs.110).aspx:
Returns a string array that contains the substrings in this instance that are delimited by elements of a specified string or Unicode character array.
So if you want the first part of the string, you could just use string result = record.Title.split(',')[0]

Map reduce in RavenDb over 2 collections with child collection

I have 2 different object types stored in RavenDb, which are a parent/child type relationship, like this in JSON:
Account/1
{
"Name": "Acc1",
}
Items/1
{
"Account": "Account/1",
"Value" : "100",
"Tags": [
"tag1",
"tag2"]
}
Items/2
{
"Account": "Account/1",
"Value" : "50",
"Tags": [
"tag2"]
}
Note that I don't want to store these in the same document, as an account may have thousands of items.
I am trying to write a map/reduce index that will return me something like:
{
"Account": "Acc1",
"TagInfo": [
{ "TagName" : "tag1",
"Count" : "1", //Count of all the "tag1" occurrences for acc1
"Value" : "100" //Sum of all the Values for acc1 which are tagged 'tag1'
},
{ "TagName" : "tag2",
"Count" : "2", //Two items are tagged "tag2"
"Value" : "150"
}]
}
i.e. a list of all the distinct tag names along with the number of each and their value.
I think I need to use a multi-map to map the Account and Items collections together, but I can't figure out the reduce part to create the "TagInfo" part of the result.
Is this possible, or am I modelling this all wrong in Raven?
EDIT:
The class I want to retrieve from this query would look something like this:
public class QueryResult
{
public string AccountId {get;set;}
public TagInfo Tags {get;set;}
}
public class TagInfo
{
public string TagName {get;set;}
public int Count {get;set;}
public int TotalSum {get;set;}
}
You can't use a Multi Map/Reduce index for that because you want one map on the tags and the other on the account. They don't have a common property, so you can't have a multi maps/reduce here.
However, you can use TransformResult instead. Here's how to do it:
public class Account
{
public string Id { get; set; }
public string Name { get; set; }
}
public class Item
{
public string Id { get; set; }
public string AccountId { get; set; }
public int Value { get; set; }
public List<string> Tags { get; set; }
}
public class TagsWithCountAndValues : AbstractIndexCreationTask<Item, TagsWithCountAndValues.ReduceResult>
{
public class ReduceResult
{
public string AccountId { get; set; }
public string AccountName { get; set; }
public string Tag { get; set; }
public int Count { get; set; }
public int TotalSum { get; set; }
}
public TagsWithCountAndValues()
{
Map = items => from item in items
from tag in item.Tags
select new
{
AccountId = item.AccountId,
Tag = tag,
Count = 1,
TotalSum = item.Value
};
Reduce = results => from result in results
group result by result.Tag
into g
select new
{
AccountId = g.Select(x => x.AccountId).FirstOrDefault(),
Tag = g.Key,
Count = g.Sum(x => x.Count),
TotalSum = g.Sum(x => x.TotalSum)
};
TransformResults = (database, results) => from result in results
let account = database.Load<Account>(result.AccountId)
select new
{
AccountId = result.AccountId,
AccountName = account.Name,
Tag = result.Tag,
Count = result.Count,
TotalSum = result.TotalSum
};
}
}
Later then, you can query like this:
var results = session.Query<TagsWithCountAndValues.ReduceResult, TagsWithCountAndValues>()
.Where(x => x.AccountId == "accounts/1")
.ToList();
OK, so I figured out a way to do this in an acceptable manner that builds on Daniel's answer, so I'll record it here for any future travellers (probably myself!).
I changed from trying to return one result per account, to one result per account/tag combination, so the index had to change as follows (note the group by in the reduce is on 2 properties):
public class TagsWithCountAndValues : AbstractIndexCreationTask<Item, TagsWithCountAndValues.ReduceResult>
{
public class ReduceResult
{
public string AccountId { get; set; }
public string AccountName { get; set; }
public string TagName { get; set; }
public int TagCount { get; set; }
public int TagValue { get; set; }
}
public TagsWithCountAndValues()
{
Map = items => from item in items
from tag in item.Tags
select new ReduceResult
{
AccountId = item.AccountId,
TagName = tag,
TagCount = 1,
TagValue = item.Value
};
Reduce = results => from result in results
where result.TagName != null
group result by new {result.AccountId, result.TagName}
into g
select new ReduceResult
{
AccountId = g.Key.AccountId,
TagName = g.Key.TagName,
TagCount = g.Sum(x => x.TagCount),
TagValue = g.Sum(x => x.TagValue),
};
TransformResults = (database, results) => from result in results
let account = database.Load<Account>(result.AccountId)
select new ReduceResult
{
AccountId = result.AccountId,
AccountName = account.Name,
TagName = result.TagName,
TagCount = result.TagCount,
TagValue = result.TagValue,
};
}
}
As before, querying this is just:
var results = session
.Query<TagsWithCountAndValues.ReduceResult, TagsWithCountAndValues>()
.ToList();
The result of this can then be transformed into the object I originally wanted by an in-memory LINQ query. At this point the number of results that could be returned would be relatively small, so performing this at the client end is easily acceptable. The LINQ statement is:
var hierachicalResult = from result in results
group new {result.TagName, result.TagValue} by result.AccountName
into g
select new
{
Account = g.Key,
TagInfo = g.Select(x => new { x.TagName, x.TagValue, x.TagCount })
};
Which gives us one object per account, with a child list of TagInfo objects - one for each unique tag.

Categories