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);
Related
I have many to many relationship between entities user and group, I also have joining table GroupParticipants.
public class User
{
public string Id {get; set;}
public ICollection<GroupParticipant> Group { get; set;}
}
public class Group
{
public int Id { get; set; }
public ICollection<GroupParticipant> Participants { get; set; }
}
public class GroupParticipant
{
public int GroupId { get; set; }
public string ParticipantId { get; set; }
public User Participant { get; set; }
public Group Group { get; set; }
}
I need to select groups which user specified user did not join. I want to do something like:
string userId = 5;
var groupsAvailableToJoin = await _context.Groups
.Where(group => group.Participants.Id != userId);
Thanks!
A query like:
_context.Groups.Where(g =>
!_context.GroupParticipants.Any(gp => gp.UserId == userId && gp.GroupId == g.I'd
);
Should translate to:
SELECT * FROM Groups g
WHERE NOT EXISTS(SELECT null FROM groupParticipants gp WHERE gp.UserId = 5 AND gp.GroupId = g.Id)
Which should be a reasonably performant way of getting you what you're looking for.. I'm sure that the GroupParticipants columns are indexed..
There are various ways to write this - if you find a two step approach easier to understand, it's effectively the same as:
var joined = _context.GroupParticipants.Where(gp => gp.UserId == 5).Select(gp => gp.GroupId).ToList();
var notJoined = _context.Groups.Where(g => !joined.Contains(g.Id));
This one translates as a NOT IN (list,of,groups,they,are,in) for a similar effect
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.
I’m trying to filter a list based on this classes - The result should be a list with all the machines where there are sessions that match the search condition, but only those sessions.
class Machine {
public string collectionName { get; set; }
public string machineName { get; set; }
public bool status { get; set; }
public List<Session> sessionList { get; set; }
}
class Session {
public string userId { get; set; }
public string userName { get; set; }
public string computerName { get; set; }
public IPAddress ipAddress { get; set; }
}
List<Machine> allMachineData;
var auxfindResults = (from machine_item in allMachineData
from session_item in machine_item.sessionList
where (session_item.userId.ToUpperInvariant().Contains(searchTerm.ToUpperInvariant())
|| session_item.userName.ToUpperInvariant().Contains(searchTerm.ToUpperInvariant()))
select machine_item).ToList();
I get a list of all machines with sessions matching the condition, but i also get results that I don't want i.e. sessions that don't match the conditions.
If instead I try:
var auxfindResults = (from machine_item in allMachineData
from session_item in machine_item.sessionList
where (session_item.userId.ToUpperInvariant().Contains(searchTerm.ToUpperInvariant())
|| session_item.userName.ToUpperInvariant().Contains(searchTerm.ToUpperInvariant()))
select session_item).ToList();
I get all the sessions matching the condition, but obviously i loose the "machine" part
I have a working solution using loops, but I don't like it.
Is there any way of doing this using linq - I’m sure there is, but I can’t find it.
Any suggestions/pointers will be greatly appreciated.
You need to also filter the sessions you include in your selection.
Using Linq method syntax:
searchTerm = searchTerm.ToLower();
var result = allMachineData
.Where(m => m.sessionList.Any(s => s.userId.ToLower().Contains(searchTerm) || s.userName.ToLower().Contains(searchTerm)))
.Select(m => new Machine
{
collectionName = m.collectionName,
machineName = m.machineName,
status = m.status,
sessionList = m.sessionList.Where(s => s.userId.ToLower().Contains(searchTerm) || s.userName.ToLower().Contains(searchTerm)).ToList(),
}).ToList();
I have an application that matches customers to vehicles in inventory. There are 3 main tables: Customer, Match, and Inventory. The match record contains the estimated monthly payment for a specific Customer and Inventory record. A customer can be matched to multiple vehicles in Inventory.
The match record contains a CustomerId and an InventoryId along with a MonthlyPayment field and a few other miscellaneous fields.
There is a 1 to Many relationship between Customer and Match.
There is a 1 to Many relationship between Inventory and Match.
For each customer, I want to select the Customer record, the match record with the lowest monthly payment, and the Inventory record for that match.
What is the best way to do this? Can it be done with a single query?
I tried this code, but entity framework can't evaluate it and it executes it locally which kills the performance.
var bestMatches = _matchRepository.GetAll(customerMatchSummaryRequest)
.Where(match =>
(_matchRepository.GetAll(customerMatchSummaryRequest)
.GroupBy(m => new { m.Bpid, m.BuyerId, m.CurrentVehicleId })
.Select(g => new
{
g.Key.Bpid,
g.Key.BuyerId,
g.Key.CurrentVehicleId,
LowestMonthlyPayment = g.Min(m => m.MonthlyPayment)
})
.Where(m => m.Bpid == match.Bpid
&& m.BuyerId == match.BuyerId
&& m.CurrentVehicleId == match.CurrentVehicleId
&& m.LowestMonthlyPayment == match.MonthlyPayment)
).Any())
.Include(m => m.Buyer)
.Include(m => m.Inventory);
I receive the following Output when stepping through the debugger:
Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'GroupBy(new <>f__AnonymousType2`3(Bpid = [<generated>_2].Bpid, BuyerId = [<generated>_2].BuyerId, CurrentVehicleId = [<generated>_2].CurrentVehicleId), [<generated>_2])' could not be translated and will be evaluated locally.
Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'GroupBy(new <>f__AnonymousType2`3(Bpid = [<generated>_2].Bpid, BuyerId = [<generated>_2].BuyerId, CurrentVehicleId = [<generated>_2].CurrentVehicleId), [<generated>_2])' could not be translated and will be evaluated locally.
Assuming your model is something like this
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Match> Matches { get; set; }
}
public class Inventory
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Match> Matches { get; set; }
}
public class Match
{
public int CustomerId { get; set; }
public Customer Custmer { get; set; }
public int InventoryId { get; set; }
public Inventory Inventory { get; set; }
public decimal MonthlyPayment { get; set; }
}
the query in question could be something like this:
var query =
from customer in db.Set<Customer>()
from match in customer.Matches
where !customer.Matches.Any(m => m.MonthlyPayment > match.MonthlyPayment)
select new
{
Customer = customer,
Match = match,
Inventory = match.Inventory
};
Note that it could return more than one match for a customer if it contains more than one inventory record with the lowest payment. If the data allows that and you want to get exactly 0 or 1 result per customer, change the
m.MonthlyPayment > match.MonthlyPayment
criteria to
m.MonthlyPayment > match.MonthlyPayment ||
(m.MonthlyPayment == match.MonthlyPayment && m.InventoryId < match.InventoryId)
P.S. The above LINQ query is currently the only way which translates to single SQL query. Unfortinately the more natural ways like
from customer in db.Set<Customer>()
let match = customer.Matches.OrderBy(m => m.MonthlyPayment).FirstOrDefault()
...
or
from customer in db.Set<Customer>()
from match in customer.Matches.OrderBy(m => m.MonthlyPayment).Take(1)
...
lead to client evaluation.
I'm a bit lost here and I've tried a few different ways to tackle it. So far I'm having a hard time writing out the LINQ to do what I want.
I want to take the user input string which can be multiple keywords split either by whitespace or ",".
This here works grabs the whole search term and compares it to the title in the Post or any tag I may have. I want the user to type in "HTML Preview" which would match a post called, "Preview the World" with the tags "HTML", "CSS", etc....
This query won't work...but I'm trying to modify it so that it does work.
public IPagedList<Post> SearchResultList(string searchTerm, int resultsPerPage, int page)
{
string[] terms = searchTerm.Split(null);
TNDbContext context = DataContext;
return context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.Where(c => (c.Title.Contains(searchTerm) || c.Tags.Any(d => d.Name.StartsWith(searchTerm))) || searchTerm == null)
.OrderByDescending(x => x.Views)
.ToPagedList(page, resultsPerPage);
}
I tried writing this instead of the other "Where" statement
.Where(x => (terms.All(y => x.Title.Contains(y))) || terms == null)
but it keeps throwing this error
Cannot compare elements of type 'System.String[]'. Only primitive types, enumeration types and entity types are supported.
FOR REFERENCE:
public class Post
{
public Post()
{
Tags = new HashSet<Tag>();
Comments = new HashSet<Comment>();
}
public int Id { get; set; }
public string Title { get; set; }
public string UrlTitle { get; set; }
public DateTime Date { get; set; }
public DateTime DateEdited { get; set; }
public string Body { get; set; }
public string Preview { get; set; }
public string PhotoPath { get; set; }
public int Views { get; set; }
//Navigational
public ICollection<Tag> Tags { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Tag
{
public Tag()
{
Post = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; }
public int TimesTagWasUsed { get; set; }
//Navigational
public ICollection<Post> Post { get; set; }
}
You need to start with a base query, and then keep adding where clauses to it for each search term. Try this:
TNDbContext context = DataContext;
//Create the base query:
var query = context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.OrderByDescending(x => x.Views);
//Refine this query by adding "where" filters for each search term:
if(!string.IsNullOrWhitespace(searchTerm))
{
string[] terms = searchTerm.Split(" ,".ToCharArray(),
StringSplitOptions.RemoveEmptyEntries);
foreach(var x in terms)
{
string term = x;
query = query.Where(post => (post.Title.Contains(term) ||
post.Tags.Any(tag => tag.Name.StartsWith(term))));
}
}
//Run the final query to get some results:
var result = query.ToPagedList(page, resultsPerPage);
return result;
You can nest queries with additional 'from' statements, so something like this should work:
var list = (from post in context.Posts.Include(a => a.Tags).Include(b => b.Comments)
from term in terms
where post.Title.Contains(term) || post.Tags.Any(d => d.Name.StartsWith(term))
select post).OrderByDescending(x => x.Views);