Combine results from multiple queries to as single queryable - c#

I want to search through a user database and order my results according to how precise the match is. Exact matches on a users name should appear in the result before single word matches, as an example.
This is what i have (the variable 'value' contains a search term and 'query' contains an initial queryable i want to modify)
var values = value.Split(new [] {' ','\t', '\n', '\r'});
var q1 = query.Where(u => u.Id == valueAsInt || u.ExternalId == valueAsInt);
var q2 = query.Where(u => u.Name.Contains(value) || u.Username.Contains(value));
var q3 = query.Where(u => values.All(i => u.Name.Contains(i)) || values.All(i => u.Username.Contains(i)));
var q4 = query.Where(u => values.Any(i => u.Name.Contains(i)) || values.Any(i => u.Username.Contains(i)));
However, I now want to combine the results of q1 through q4 and have a new queryable which i can pass along. I also want to preserve the order of my queries, and frankly I have no idea how to go about doing this..

Answered by Silas Hansen in the comments

You should use ranking. e.g.
var result = query.Select(u =>
{
if (u.Id == valueAsInt || u.ExternalId == valueAsInt)
return new {Rank = 1, Item = u};
if (u.Name.Contains(value) || u.UserName.Contains(value) )
return new {Rank = 2, Item = u};
//Add the other conditions in here
return new {Rank = 3, Item = u};
}).OrderBy(u => u.Rank).Select(u => u.Item);

Related

SQLite and LINQ: find all objects that have a sub list with all ids present in a supplied list of IDs

I have the following class:
public class Article
{
long Id;
List<Category> Categories;
}
I am using EF Core 5 and What I need is a LINQ query against SQLite that returns all the articles that have all the categories that I specify.
I tried the following code:
List<long> cIds = c.Select (x => x.Id).ToList ();
query.Where (art => cIds.All (cId => art.Categories.Select (c => c.Id).Contains (cId)));
but the compiler says
InvalidOperationException: The LINQ expression 'DbSet<Article>()
.Where(a => __cIds_0
.All(cId => DbSet<Dictionary<string, object>>("ArticleCategory")
.Where(a0 => EF.Property<Nullable<long>>(a, "Id") != null && object.Equals(
objA: (object)EF.Property<Nullable<long>>(a, "Id"),
objB: (object)EF.Property<Nullable<long>>(a0, "ArticlesId")))
.Join(
inner: DbSet<Category>(),
outerKeySelector: a0 => EF.Property<Nullable<long>>(a0, "CategoriesId"),
innerKeySelector: c => EF.Property<Nullable<long>>(c, "Id"),
resultSelector: (a0, c) => new TransparentIdentifier<Dictionary<string, object>, Category>(
Outer = a0,
Inner = c
))
.Select(ti => ti.Inner.Id)
.Any(p => p == cId)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
How can I obtain it?
A possible workaround I found is the following:
List<long> cIds = c.Select (x => x.Id).ToList ();
query = query.Where (art => art.Categories.Select (c => c.Id).Any (x => cIds.Contains (x)));
query = query.Include (x => x.Categories);
result = await query.ToListAsync ();
result = result.Where (art => cIds.All (cId => art.Categories.Select (c => c.Id).Contains (cId))).ToList ();
But I was wondering if I could obtain the same result with a single LINQ query.
Thanks in advance
UPDATE:
I'll just add the function where this code will be used and make make an example to make things clearer:
This is the function where the code will be used:
public async Task<List<Article>> SearchAsync (string search, Section s, Website w,
List<Category> c)
{
List<Article> result = new List<Article> ();
if (
search == ""
&& s == null
&& w == null
&& c.Count == 0
)
return result;
IQueryable<Article> query = dbSet.AsQueryable ();
if (search != "")
query = query.Where (x => x.Title.Contains (search) || x.Summary.Contains (search));
if (s != null)
query = query.Where (x => x.SectionId == s.Id);
if (w != null)
query = query.Where (x => x.WebsiteId == w.Id);
if (c.Count > 0)
{
List<long> cIds = c.Select (x => x.Id).ToList ();
query = query.Where (art => art.Categories.Select (c => c.Id).Any (x => cIds.Contains (x)));
}
query = query.Include (x => x.Categories);
result = await query.ToListAsync ();
if (c.Count > 0)
{
List<long> cIds = c.Select (x => x.Id).ToList ();
result = result.Where (art => cIds.All (cId => art.Categories.Select (c => c.Id).Contains (cId))).ToList ();
}
return result;
}
And here is an example:
Let's say c will contain ids 9,10,11 and the articles collection is the following pseudo code:
List<article> articles = new List<Article> ()
{
new Article () {Id = 1, Categories = "12,44,55"}
new Article () {Id = 2, Categories = "7,8,9,10,11"}
new Article () {Id = 3, Categories = "9,10,11"}
}
The linq query should return Article with Id 2 and 3 because both contains all of the ids present in c.
One of the solutions using Intersect, but we have to prepare data for intersection.
// articles query
var query = ...
var cIds = c.Select(x => x.Id).ToList();
var idsCount = cIds.Count();
// translating list of IDs to IQueryable
var categoryIdsQuery = dbContext.Categories
.Where(c => cIds.Contains(c.Id))
.Select(c => c.Id);
query = query
.Where(art => art.Categories
.Select(c => c.Id)
.Intersect(categoryIdsQuery)
.Count() == idsCount
)
.Include(x => x.Categories);
What I need is a LINQ query against SQLite that returns all the articles that have all the categories that I specify.
So you have a sequence of Category Ids and you want all Articles, each Article with only the Categories that are in your sequence of Category Ids.
I'm not sure what your variable 'c' is, but it seems to me that the following statement returns the Ids of all c:
List<long> cIds = c.Select (x => x.Id).ToList ();
If c is your sequence of Categories, then you will have the Ids of all existing categories. This will mean that you will have all Articles, each with all Categories.
If you have a local sequence of Category Ids, with a limited count (say about 250), then you should use Contains:
IEnumerable<long> categoryIds = ...
var articlesWithTheseCategories = dbContext.Articles.Select(article => new
{
Id = article.Id,
Categories = article.Categories
.Where(category => categoryIds.Contains(category.Id)
.ToList(),
})
So if you have CategoryIds 2, 3, and 12, this query will give you all Articles with only the Categories with ids 2, 3, 12.
If Article 40 has only Categories 20, 21, 21, then Article 40 will be in your result, but it will have an empty Categories list.
If you don't have your Category Ids locally, but you have a predicate to select the Category Ids, then your query will be like:
IQueryable<long> categoryIds = dbContext.Categories
.Where(category => category.Status == StatusCode.Obsolete); // predicate
var articlesWithTheseCategories = dbContext.Articles.Select(article => new
{
Id = article.Id,
Categories = article.Categories
.Where(category => categoryIds.Contains(category.Id)
.ToList(),
});
Because your first query is an IQueryable<...> it is not executed yet. If you want you can make it one big statement:
var articlesWithTheseCategories = dbContext.Articles.Select(article => new
{
Id = article.Id,
Categories = article.Categories
.Where(category => dbContext.Categories
.Where(category => category.Status == StatusCode.Obsolete)
.Contains(category.Id))
.ToList(),
});
Although this will not improve efficiency, it surely deteriorates readability.

returning multiple column and sum using linq expression

I need to return two fields using a lambda expression. The first one is the sum of the amount field and the second one is CurrentFinancial year. Below is the code that I have written, how do I include CurrentFinancialYear?
var amount = dealingContext.vw_GetContribution
.Where(o => o.ContactID == contactId)
.Sum(o => o.Amount);
return new Contribution { Amount = amount ?? 0, CurrentFinancialYear = };
Grouping by Year should do the trick:
from entry in ledger.Entries
where entry.ContactID == contactId
&& entry.Time.Year == currentFinancialYear
group entry by entry.Time.Year
into g
select new Contribution ()
{
Amount = g.ToList ().Sum (e => e.Amount),
CurrentFinancialYear = g.Key
};
UPDATE - just return the first/default result...
(from entry in ledger.Entries
where entry.ContactID == contactId
&& entry.Time.Year == currentFinancialYear
group entry by entry.Time.Year
into g
select new Contribution ()
{
Amount = g.ToList ().Sum (e => e.Amount),
CurrentFinancialYear = g.Key
}).FirstOrDefault();
First of all use a simple select
var contribution = dealingContext.vw_GetContribution
.Where(o => o.ContactID == contactId).ToList();
It will give you a list of type vw_GetContribution
Then use groupby on this list as
var groupedContribution = contribution.GroupBy(b => b.CurrentFinancialYear).ToList();
Now you can iterate through or use this list as
foreach(var obj in groupedContribution.SelectMany(result => result).ToList())
{
var amount = obj.Amount;
var Year = obj.CurrentFinancialYear;
}
OR
In single line, you can do all the above as
var contList = context.vw_GetContribution
.Select(a => new { a.Amount, a.CurrentFinancialYear })
.GroupBy(b => b.CurrentFinancialYear)
.SelectMany(result => result).ToList();
I hope this will solve your problem.
Can you try this:
var amount = dealingContext.vw_GetContribution
.Where(o => o.ContactID == contactId)
.GroupBy(o=> new { o.CurrentFinancialYear, o.Amount})
.Select(group =>
new {
year= group.Key.CurrentFinancialYear,
sum= group.Sum(x=>x.Amount)
});

How to use LINQ to select and transform a subset of a List<T>

I have a list List<UserRoles> roles that has this structure
{r:1,u:1,v:3},
{r:1,u:1,v:5},
{r:2,u:1,v:9},
{r:3,u:2,v:10}
I am trying to write a LINQ statement that will filter out only the "r"s that have values 1 & 2 and return a collection of ints/strings of "v"s
This is what I am trying to do and my problem is in the part where I want to transform the into that holds only the corresponding "v"s.
List<Int32> = roles.Where(r => r.r == 1 || r.r == 2)
.Select(i => new Int32{id = i.v});
This doesn't compile with an error that 'id' is unknown.
the end result that I need is this:
List<Int32>
{v:3},
{v:5},
{v:9}
Sound like you need a list of int:
List<int> result = roles.Where(r => r.r == 1 || r.r == 2)
.Select(i => i.v)
.ToList();
In case you have a list of int to filter, you can use Contains method to avoid lots of ||:
var filters = new[] { 1, 2};
List<int> result = roles.Where(r => filters.Contains(r.r))
.Select(i => i.v)
.ToList();
Or maybe you need {v:9}, you can use anonymous type with var keyword:
var result = roles.Where(r => filters.Contains(r.r))
.Select(i => new { i.v })
.ToList();
I guess v is already an int.
So the solution would be as simple as :
var result = roles.Where(r => r.r == 1 || r.r == 2).Select(i => i.v).ToList();
If what you want is an array of anonymous objects, use this:
var res = roles.Where(r => r.r == 1 || r.r == 2).Select(i => new{i.v}).ToList();
This would produce a list of objects with a single property called v.
If you are looking for a list of integers, and v is an int in the original class, use this:
var res = roles.Where(r => r.r == 1 || r.r == 2).Select(i => i.v ).ToList();
// note that there's no new here ^^^^^
You've used an annoymous type but then added Int32 in front of it which is illegal.
List<Int32> results = roles.Where(r => r.r == 1 || r.r == 2)
.Select(i => new { i.v }).ToList();

Filtering this Linq query

I want to use this query:
var queryData = from va in xdoc.Descendants("language")
select new
{
StringID = va.Parent.Parent.Attribute("id").Value,
Language = va.Attribute("name").Value,
LanguageData = va.Element("value").Value,
};
var organizedData = from x in queryData
group x by x.StringID into xg
select new
{
StringID = xg.Key,
English = xg.SingleOrDefault(x => x.Language == "ENGLISH_US").LanguageData,
Custom = xg.SingleOrDefault(x => x.Language == languageBox.SelectedItem.ToString()).LanguageData,
};
mainView.DataSource = organizedData.ToList();
mainView.Refresh();
except that as an additional condition for what is retrieved for the Custom anonymous type, its value must be equal to "*".
Why can't I figure this out? I guess I don't know enough about anonymous types or the => operator.
Is that what you want?
mainView.DataSource = organizedData.Where(x => x.Custom == "*").ToList();
I think this is what you're looking for. I put the value in a temp variable so it doesn't have to be computed twice.
var organizedData = from x in queryData
group x by x.StringID into xg
let temp = xg.SingleOrDefault(x => x.Language == languageBox.SelectedItem.ToString()).LanguageData
where temp == "*"
select new
{
StringID = xg.Key,
English = xg.SingleOrDefault(x => x.Language == "ENGLISH_US").LanguageData,
Custom = temp,
};

Help creating a LINQ to SQL query

I'm populating a FormView by setting the datasource to an IQueryable object that I get by doing a LINQ query. Basically it returns the number of employees that hold a certain "Position" within a certain "Shift".
int shiftID = 1;
var shiftCount = from x in context.Employees.Take(1)
select new
{
ManagerCount = ((from p in context.Persons
where p.PositionID == 1 && p.ShiftID == shiftID && p.IsEmployee == true
select p.PersonId).Count(),
PartTimeCount = ((from p in context.Persons
where (p.PositionID == 2 || p.PositionID == 3) && p.ShiftID == shiftID && p.IsEmployee == true
select p.PersonId).Count(),
etc, etc...
};
That part works fine. However, when I want to get the number of employees for all shifts, I can't quite figure out how to do it:
//Get all shifts 1, 2, and 3
var shiftCount = from x in context.Employees.Take(1)
select new
{
ManagerCount = ((from p in context.Persons
where p.PositionID == 1 && (p.ShiftID == 1 || p.ShiftID == 2 || p.ShiftID == 3) && p.IsEmployee == true
select p.PersonId).Count()
};
That doesn't work though because it of course returns 3 values and gives the Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression. error.
So I need to get the sum of the three values returned (it's not always three though, it depends on the position).
I've looked at various ways using Sum and LINQ grouping, but can't quite seem to work it out. Can anybody point me in the right direction?
I've got a few answers for you, but first it seems like you have something wrong with your first part of your query. You're doing .Take(1) on Employees which will give you only one x value. Since you don't use x in the remainder of your query then using Employees is redundant.
Now, since all of your queries are quite similar I've tried to remove repetition. The first thing to do is to get a common filter for the shifts you are filtering on.
If you have a single shift, use this:
int shiftID = 1;
var shifts = new [] { shiftID, };
If you have multiple shifts, use this:
var shifts = new [] { 1, 2, 3, };
Either way you end up with an array of integers representing the shifts you want to filter against. All of the below answers require this shifts array.
Then define a query for the employees in those shift, regardless of position for now.
var employeesInShifts =
from p in context.Persons
where p.IsEmployee
where shifts.Contains(p.ShiftID)
select p;
So you can then get the shift counts like so:
var shiftCount =
new
{
ManagerCount = employeesInShifts
.Where(p => p.PositionID == 1)
.Count(),
PartTimeCount = employeesInShifts
.Where(p => p.PositionID == 2 || p.PositionID == 3)
.Count(),
// etc
};
Perhaps a better alternative for you though, would be to turn the employeesInShifts query into a dictionary and then just pluck the values from the dictionary.
var employeesInShifts =
(from p in context.Persons
where p.IsEmployee
where shifts.Contains(p.ShiftID)
group p by p.PositionID into gps
select new
{
PositionID = gps.Key,
Count = gps.Count(),
})
.ToDictionary(pc => pc.PositionID, pc.Count);
var shiftCount =
new
{
ManagerCount = employeesInShifts[1],
PartTimeCount = employeesInShifts[2] + employeesInShifts[3],
// etc
};
The downside to this approach is that you really should check that the dictionary has values for each PositionID before getting the values.
That can be fixed by introducing an array of position ids that you want the dictionary to have and joining your results on that.
var positionIDs = new [] { 1, 2, 3, };
var employeesInShifts =
(from p in context.Persons
where p.IsEmployee
where shifts.Contains(p.ShiftID)
where positionIDs.Contains(p.PositionID)
select p).ToArray();
var allPositionEmployeesInShifts =
from pid in positionIDs
join p in employeesInShifts on pid equals p.PersonId into gps
select new
{
PositionID = pid,
Count = gps.Count(),
};
var countOfPositionID =
allPositionEmployeesInShifts
.ToDictionary(x => x.PositionID, x => x.Count);
var shiftCount =
new
{
ManagerCount = countOfPositionID[1],
PartTimeCount = countOfPositionID[2] + countOfPositionID[3],
// etc
};
Now that guarantees that your final dictionary will contain the counts of all of the position ids that you are wanting to query.
Let me know if this works or if you really needed to join on the Employees table, etc.
I would recommend looking at the Group By examples here:

Categories