LINQ multiple keyword search to PagedList - c#

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);

Related

Select many over complex model and multiple collections

I am trying to flatten out my model and I am coming across an issue I can't seem to understand.
Inspection Item Model:
public class InspectionItem
{
public string InspectionItemName { get; set; }
public ICollection<Inspection> Inspections { get; set; }
}
Inspection Model:
public class Inspection
{
public InspectionItem InspectionItem { get; set; }
public string InspectionItemId { get; set; }
public string PassFail { get; set; }
public ICollection<Answer> Answers { get; set; }
public ICollection<InspectionInputAnswer> InspectionInputAnswers { get; set; }
}
The Query:
So my issue is, if an Inspection does not contain a data for BOTH Answers and InspectionInputAnswers, the whole list returns 0 records? If the Inspection contains a record for both it then returns data. I don't understand why my select many clause is working this way? I am trying to flatten Inspections and both Answers/InspectionInputAnswers.
var inspectionItemInspections = await context.InspectionItems
.Include("Inspections")
.Include("Inspections.Answers")
.Include("Inspections.InspectionInputAnswers")
.Where(a => a.InspectionItemTypeId == idFilter)
.SelectMany(x => x.Inspections
.SelectMany(y => y.Answers
.SelectMany(z => y.InspectionInputAnswers,(answer,inputAnswer) => new { answer, inputAnswer })
.Select(d => new {
Date = y.CreatedAt,
InspectionItemName = x.InspectionItemName,
InspectionItemTypeId = x.InspectionItemTypeId,
InspectedById = y.InspectedById,
PassFail = y.PassFail,
QuestionId = d.answer.QuestionId,
ValueMeasured = d.inputAnswer.ValueMeasured
})
)).ToListAsync();
So how can I write this so that the Inspections relationship does not require an entry for both Answers and InspectionInputAnswers?
Thanks!

Error when using Select() instead of Include() in a query

I have the following query:
var catInclude = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Include(x => x.CatItems)
.SingleOrDefault(p => p.Id == request.ProvId
cancellationToken: cancellationToken);
As I don't want to get all properties from CatItems with Include(), I have created the following query:
var catSelect = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p ==> new
{ Provider = p,
Items = p.CatItems.Select(x => new List<CatItems> { new CatItems
{ Id = x.Id, Name = x.Name, Price = x.Price } }
})})
SingleOrDefault(cancellationToken: cancellationToken);
But something is wrong in the 2nd query because here return _mapper.ProjectTo<CatDto>(cat) I get the following error:
Argument 1: cannot convert from '<anonymous type: Db.Entities.Cat Prov, System.Colletions.Generic.IEnumerable<System.Colletions.Generic.List<Models.CatItems> > Items>' to 'System.Linq.IQueryable'
Here is my CatDto:
public class CatDto
{
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
Here are my entities:
public class Prov
{
public int Id { get; set; }
public Cat Cat { get; set; }
}
public class Cat
{
public int Id { get; set; }
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
public class CatItems
{
public int Id { get; set; }
public int CatId { get; set; }
public DateTime CreatedOn { get; set; }
}
Is there a way to recreate the 2nd query and use it?
Main difference that instead of returning List of CatItems, your code returns IEnumerable<List<CatItems>> for property Items.
So, just correct your query to project to List:
var catSelect = await _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p => new CatDto
{
ProvId = p.ProvId,
Items = p.CatItems.Select(x => new CatItems
{
Id = x.Id,
Name = x.Name,
Price = x.Price
})
.ToList()
})
.SingleOrDefaultAsync(cancellationToken: cancellationToken);
I mean, even the exception is pretty self-explanatory. Nevertheless:
You are performing a .Select(...). It returns an Anonymous type. So, your catSelect is an anonymous type, thus the AutoMapper fails.
The quickest fix is to just cast (Cat)catSelect before mapping.
Or, you can dig deeper into how does AutoMapper play with anonymous types.
I feel like you can make most of the classes inherent Id and why is public cat CAT {get; set;} i thought you were supposed to initialize some kind of value

How do I negotiate joins and groupings based on nested properties in LINQ?

So I've got a nested data structure like this:
public class ContractTerm
{
public int ContractId { get; set; }
public string SectionId { get; set; }
public string SubsectionId { get; set; }
public string TermId { get; set; }
public int TermOrder { get; set; }
public TermItem TermNavigation { get; set; }
}
public class TermItem
{
public string SectionId { get; set; }
public string SubsectionId { get; set; }
public string TermId { get; set; }
public string Text { get; set; }
public ICollection<ContractTerm> ContractNavigation { get; set; }
}
I've also got a class to map the section/subsection pairings in a more EF-friendly way (IRL this is an enum with attribute values and a helper, but this class abstracts away some work not necessary to reproduce the issue):
public class Section
{
public string Name { get; set; }
public string SectionId { get; set; }
public string SubsectionId { get; set; }
}
Both ContractTerm and TermItem have their own collections in a DbContext, and I'm trying to get a collection of all text entries assigned to specific Sections for a given ContractId. I have the following class to contain it:
public class TextsBySection
{
public string SectionName { get; set; }
public IEnumerable<string> Texts { get; set; }
}
I want to select a collection of TextsBySection, and have something like this:
public class ContractManager
{
//insert constructor initializing MyContext here
private MyContext Context { get; }
public IEnumerable<MyOutputClass> GetTerms(int contractId, IEnumerable<Section> sections)
{
Func<string, string, IEnumerable<string>> getBySection =
(section, subsection) => context.ContractTerms.Include(x => x.TermNavigation)
.Where(x => x.ContractId == contractId
&& x.SectionId == section
&& x.SubsectionId == subsection)
.Select(x => x.TermNavigation.Text);
var result = sections.Select(x => new MyOutputClass
{
SectionName = x.Name,
Texts = getBySection(x.SectionId, x.SubsectionId)
}).ToList();
return result;
}
}
This works fine and dandy, but it hits the database for every Section. I feel like there's got to be a way to use Join and/or GroupBy to make it only query once, but I can't quite see it. Something like this, perhaps:
var result = context.ContractTerms.Include(x => x.TermNavigation)
.Where(x => x.ContractId == contractId)
.Join(sections,
term => //something
section => //something
(term, section) => /*something*/)
If all this were in SQL, selecting the necessary data would be easy:
SELECT sections.name,
term_items.text
FROM contract_terms
JOIN term_items
ON term_items.section_id = contract_terms.section_id
AND term_items.subsection_id = contract_terms.subsection_id
AND term_items.term_id = contract_terms.term_id
JOIN sections --not a real table; just corresponds to sections argument in method
ON sections.section_id = contract_terms.section_id
AND sections.subsection_id = contract_terms.subsection_id
...and then I could group the results in .NET. But I don't understand how to make a single LINQ query that would do the same thing.
I changed my answer, well I would do something like this... maybe this may help you.
public static void Main(string[] args)
{
List<Section> sections = new List<Section>();
List<ContractTerm> contractTerms = new List<ContractTerm>();
List<TermItem> termItens = new List<TermItem>();
//considering lists have records
List<TextsBySection> result = (from contractTerm in contractTerms
join termItem in termItens
on new
{
contractTerm.SectionId,
contractTerm.SubsectionId,
contractTerm.TermId
}
equals new
{
termItem.SectionId,
termItem.SubsectionId,
termItem.TermId
}
join section in sections
on new
{
contractTerm.SectionId,
contractTerm.SubsectionId
} equals new
{
section.SectionId,
section.SubsectionId
}
select
new
{
sectionName = section.Name,
termItemText = termItem.Text
}).GroupBy(x => x.sectionName).Select(x => new TextsBySection()
{
SectionName = x.Key,
Texts = x.Select(i=> i.termItemText)
}).ToList();
}

Get objects whose property does not exist in enumerable

Multiple answers have led me to the following 2 solutions, but both of them do not seem to be working correctly.
What I have are 2 objects
public class DatabaseAssignment : AuditableEntity
{
public Guid Id { get; set; }
public string User_Id { get; set; }
public Guid Database_Id { get; set; }
}
public class Database : AuditableEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Server { get; set; }
public bool IsActive { get; set; }
public Guid DatabaseClientId { get; set; }
}
Now, the front-end will return all selected Database objects (as IEnumerable) for a given user. I am grabbing all current DatabaseAssignments from the database for the given user and comparing them to the databases by the Database.ID property. My goal is to find the DatabaseAssignments that I can remove from the database. However, my solutions keep returning all DatabaseAssignments to be removed.
if (databases != null)
{
var unitOfWork = new UnitOfWork(_context);
var userDatabaseAssignments = unitOfWork.DatabaseAssignments.GetAll().Where(d => d.User_Id == user.Id);
//var assignmentsToRemove = userDatabaseAssignments.Where(ud => databases.Any(d => d.Id != ud.Database_Id));
var assignmentsToRemove = userDatabaseAssignments.Select(ud => userDatabaseAssignments.FirstOrDefault()).Where(d1 => databases.All(d2 => d2.Id != d1.Database_Id));
var assignmentsToAdd = databases.Select(d => new DatabaseAssignment { User_Id = user.Id, Database_Id = d.Id }).Where(ar => assignmentsToRemove.All(a => a.Database_Id != ar.Database_Id));
if (assignmentsToRemove.Any())
{
unitOfWork.DatabaseAssignments.RemoveRange(assignmentsToRemove);
}
if (assignmentsToAdd.Any())
{
unitOfWork.DatabaseAssignments.AddRange(assignmentsToAdd);
}
unitOfWork.SaveChanges();
}
I think u are looking for an Except extension, have a look at this link
LINQ: Select where object does not contain items from list
Or other way is with contains see below Fiddler link :
https://dotnetfiddle.net/lKyI2F

Where condition inside lambda expression c#

I have a entity like
public class Program
{
public int ID { get; set; }
public bool IsActive { get; set; }
public string Title { get; set; }
}
and
public class EMetrics
{
public int ID { get; set; }
public bool IsActive { get; set; }
public string Title { get; set; }
public List<Program> Programs { get; set; }
}
I have repository method like,
IEnumerable<EMetrics> IEmetricsRepository.GetAllByProgram(params int[] programIds)
{
var metrics = EntitySet
.Where(x => programIds.Contains(x.Programs.Select(x => x.ID)))
.ToList();
return metrics;
}
[The above code throwing build error]
Here only where I am facing problem to get the EMetrics based on the program Ids array params.
I want list Emetrics which are associated with the program.
You're incorrectly accessing the same input parameter in your LINQ. It should be refactored by changing your inner Select to use a different parameter:
IEnumerable<EMetrics> IEmetricsRepository.GetAllByProgram(params int[] programIds)
{
var metrics = EntitySet
.Where(x => programIds.Contains(x.Programs.Select(y => y.ID)))
.ToList();
return metrics;
}
So you want to check if all elements of one collection are present in the other. In LINQ that can be done with combination of Except and Any:
var metrics = EntitySet
.Where(x => x.Programs.Select(p => p.ID).Except(programIds).Any())
.ToList();
Fyi - your current code is failing because Array.Contains expects a single item, an int in this case, while you are giving it a whole enumerable

Categories