So I'm trying to run a Linq query that's analogous to the SQL query:
SELECT COUNT(*)
FROM table
WHERE ww >= [wwStartSelected]
AND ww <= [wwEndSelected]
AND manager='[managerName]'
AND status='Done'
GROUP BY ww;
To get a count of the number of rows (tasks) within the given ww range that are marked as done under a particular manager and grouped by ww. I've tried to create a LINQ query that would return something similar (wwStartSelected && wwEndSelected are global vars):
protected List<int> getManagerDoneCount(string managerName)
{
using (var context = new InfoDBContext())
{
List<int> managerDoneCount = context.InfoSet.Where(x => x.ww >= wwStartSelected && x.ww <= wwEndSelected && x.manager == managerName && x.status == "Done").GroupBy(x => x.ww).Count().ToList();
return managerDoneCount;
}
}
This query would then feed into a chart:
var testChart = new Chart(width: 600, height: 400)
.AddTitle("Test")
.AddSeries(
name: "Done",
chartType: "StackedColumn100",
xValue: new[] { WWList },
yValues: new[] { getManagerDoneCount("someManager") })
However I'm running into an issue with my Linq line and it says:
'int' does not contain a definition for 'ToList' and no extension method 'ToList'
accepting a first argument of type 'int' could be found (are you
missing a using directive or an assembly reference?)
Is there way to get fix this easily or do I have to convert to string, then convert back to int for the chart series (the latter of which seems a bit silly [but if so, best way to do so])?
.Count().ToList()
You're asking it to count the items in the list (which results in a number) and then convert that single number into a list, which makes no sense.
Either return a list and count it later (omit the .Count()) or, change the method to return an int not a List<int> and omit the .ToList()
protected int getManagerDoneCount(string managerName)
{
using (var context = new InfoDBContext())
{
int managerDoneCount = context.InfoSet
.Where(x => x.ww >= wwStartSelected &&
x.ww <= wwEndSelected &&
x.manager == managerName &&
x.status == "Done")
.GroupBy(x => x.ww)
.Count();
return managerDoneCount;
}
}
As an aside, to save you writing hundreds of these methods, you can pass the Where clause in as a parameter...
using System.Linq.Expressions;
protected int getManagerCount(string managerName, Expression<Info> predicate)
{
using (var context = new InfoDBContext())
{
int managerDoneCount = context.InfoSet
.Where(predicate)
.GroupBy(x => x.ww)
.Count();
return managerDoneCount;
}
}
Then call it like this...
var count = getManagerCount("...", x => x.ww >= wwStartSelected &&
x.ww <= wwEndSelected &&
x.manager == managerName &&
x.status == "Done");
Edit Re: Comments
To return a count of each group, List<int> is a bad idea as you aren't ordering the groups so the counts will be in an undefined order. The ideal solution is to have a class that has an appropriate Key and Count property, but to simplify the example, I'll use a Tuple.
//I'm assuming `ww` is an `int`, change the first type parameter of the tuple as appropriate
List<Tuple<int, int>> counts = context.InfoSet
.Where(x => x.ww >= wwStartSelected &&
x.ww <= wwEndSelected &&
x.manager == managerName &&
x.status == "Done")
.GroupBy(x => x.ww)
.Select(x => new Tuple<int, int>(x.Key, x.Count())).ToList();
Note that after you've done the group, the next Select is against the group, which has a Key property for the thing you've grouped on and a lot of aggregate methods for counting, summing, etc..
If you really just want a list of ints, change the last Select to be...
.Select(x => x.Count())
If you weren't passing it out of the method, I'd just use an anonymous class...
.Select(x => new {Ww = x.Key, Count = x.Count()})
But that's no use in a method signature. If you created a CountResult class with Ww and Count properties...
.Select(x => new CountResult{Ww = x.Key, Count = x.Count()})
Edit Re: Comments #2
Linq-To-Entities builds an expression tree which is executed against SQL server, whereas Linq-To-Objects runs in-memory on the client and has more features (as it doesn't need to work out equivalent SQL). In this case, when it gets results from SQL it creates a special proxy class that looks/behaves the same as your entities but handle additional things like tracking which properties have changed. Because of this, you can only use classes which can be constructed (with a parameterless constructor) and then have their properties set (and tracked).
(Although you didn't specify Linq-To-Entities, it's obvious from your question so I should've caught this).
LINQ doesn't deal in lists, but IQueryables, which support lazy evaluation.
Eg If you do...
var a = dbcontext.SomeSet.Where(x => true); //You could omit the Where entirely, just for illustration purposes
var b = a.Where(x => x.Id < 100);
var c = b.ToList();
The query is only executed on the last line and at most 100 records will be returned by the database. a and b are both IQueryable<SomeSet> and "just" contain the expression tree (basically a hierarchy representing the constrains/operations applied so far).
So, to be able to use parameterised constructors / other Linq-To-Object features, we can force the evaluation ...
List<Tuple<int, int>> counts = context.InfoSet
.Where(x => x.ww >= wwStartSelected &&
x.ww <= wwEndSelected &&
x.manager == managerName &&
x.status == "Done")
.GroupBy(x => x.ww)
.ToList() // <<<< Force execution of SQL query
.Select(x => new Tuple<int, int>(x.Key, x.Count())).ToList();
Which should allow you to use constructors, should you wish.
That said, getting a zero count is difficult - the same as it would be getting it from a database (if you group by a field, it doesn't show any 0 counts). There are a number of ways to approach this and which one you use depends on how much data you're playing with. All of them require some way of defining all possible values. I'll use a List<string> as it works well with LINQ
You could, for example get a list of all values and run a different count for each. This is easy but requires multiple queries. If there are lots of groups, it might be expensive...
var groups = new List<string> {"ww1", "ww2", ...};
var counts = groups.Select(g => context.InfoSet.Where(x => x.ww == g &&
x.manager == managerName &&
x.status == "Done").Count());
(This will only return counts, but in the same order as your groups list. As before, you can Select anything you like, including a CountResult...)
var counts = groups.Select(g => new CountResult {
Ww = g,
Count = context.InfoSet.Where(x => x.ww == g &&
x.manager == managerName &&
x.status == "Done").Count();
});
Alternatively, you can run the query you were doing previously and add the missing groups with a count of 0. This has the benefit of running a single SQL query and letting the database do all the heavy lifting (Ok, handling a few counts isn't too expensive but it's worth bearing in mind for other problems - you don't want to get the whole DB table in memory and do the processing there!).
var groups = new List<string> {"ww1", "ww2", ...};
var previousQuery = ... //(I'll assume a List<CountResult> but tuple would work the same)
var finalList = previousQuery.Concat(
groups.Where(g => ! previousQuery.Exists(p => p.Ww == g))
.Select(g => new CountResult {Ww=g, Count=0})
);
In short, take the previous results set, and concatenate (join) it with the result of; (take a list of all groups, remove those already in set 1, for the remainder create a new object with the appropriate ww and a count of 0)
Related
Need to query many columns for possible matches and then see how many did match and add that to a column in the Select projection. I could have two column of the four match but how many did? Then want to sort the result by how many matches. Do I need to group them first? Do a separate aggregation? Kind of stumped as which way to go because the real one would have a lot more tests of fields in production. Could possibly match as many as 8 tests in the where clause.
var results = _RList
.Where(d => d.RMI15Min == RMI.ConfirmedBottom || d.RMI15Min == RMI.InPlaceBottomConfirmed
|| d.RMI30Min == RMI.ConfirmedBottom || d.RMI30Min == RMI.InPlaceBottomConfirmed)
.OrderBy(d => d.Instrument)
.Select(d => new
{
d.Instrument,
d.Description,
d.RMI15Min,
d.RMI30Min,
NewColumn with the total of the matches in the .Where clause above.
}).ToList();
Assuming _RList does not tie back to a database table
var confirmedList = new List<int> { RMI.ConfirmedBottom, RMI.InPlaceBottomConfirmed };
var results = _RList
.OrderBy(d => d.Instrument)
.Select(d => new
{
d.Instrument,
d.Description,
d.RMI15Min,
d.RMI30Min,
Count = (new List<int> { d.RMI15Min, d.RMI30Min }).Count(c => confirmedList.Contains(c))
})
.Where(d => d.Count > 0)
.ToList();
If it ties back to a database table, it depends on whether your library can convert the above LINQ statement.
I would consider using ternary operators to add 1/0 to the total;
Count = (d.RMI15Min == RMI.ConfirmedBottom ? 1 : 0)
+ (d.RMI15Min == RMI.InPlaceBottomConfirmed ? 1 : 0)
+ (d.RMI30Min == RMI.ConfirmedBottom ? 1 : 0)
+ (d.RMI30Min == RMI.InPlaceBottomConfirmed ? 1 : 0)
Then filter the list after calculating the count.
I have a list that I want to generate a query from. I need to get back the items that match each entry in my list and the list uses two values to match against the database. Manually created code would be like this pattern...
from x in Context.Items
where (x.Prop1 == 5 && x.Prop2 == "Foo") ||
(x.Prop1 == 2 && x.Prop2 == "Bar") ||
(x.Prop1 == 9 && x.Prop2 == "Etc")
select x
If I only wanted to compare a single property I would just use the 'list.Contains(x => x.Prop1)' approach but I need to compare on two values and not one. Any ideas?
There is a few easy ways to do it and there are better answers if you search for it. This is my version based on, if the "list" model contains Prop1 and Prop2.
First Solution - This gets Prop1 and Prop2 into their own lists.
var getProp1List = list.Select(i => i.Prop1).ToList();
var getProp2List = list.Select(i => i.Prop2).ToList();
var results = queryList.Where(i => getProp1List.Contains(i.Prop1) && getProp2List.Contains(i.Prop2)).ToList();
Second Solution - This selects multiple objects into one list.
var getSearchTerms = list.Select(i => new { i.Prop1, i.Prop2 }).ToList();
var results = queryList.Where(i => getSearchTerms.Select(x=>x.Prop1).Contains(i.Prop1) && getSearchTerms.Select(x => x.Prop2).Contains(i.Prop2)).ToList();
Third Solution - Simplest - Uses gets the lists of data using select from original list.
var results = queryList.Where(i => list.Select(x=>x.Prop1).Contains(i.Prop1) &&
list.Select(x => x.Prop2).Contains(i.Prop2)).ToList();
And that's if I have, understood the question properly!!
Here is my working SQL query I want write LINQ to
I have no idea to convert write 3 level nested query
Select *
from demo.dbo.Account
where accType = 3
and LinkAcc IN (
select accNum
from demo.dbo.Account
here inkAcc IN (
select accNum
from demo.dbo.Account
where memberid = 20
and accType= 0
)
and accType = 2
)
When writing LINQ equivalents to a SQL IN(), you have to think about it in reverse.
Rather than the SQL
where entity-value IN sub-values
the LINQ expression becomes
where sub-values contains entity-value
Because writing this in one monolithic LINQ statement is mind-bending, I have broken each subquery into a separate variable.
using System.Linq;
public IEnumerable<Account> FilterAccounts(IEnumerable<Account> accounts)
// start with the deepest subquery first
var memberAccountNums = accounts
.Where(x => x.MemberId == 20 && x.AccType == 0)
.Select(x => x.AccNum)
.ToArray();
var linkAccountNums = accounts
.Where(x => x.AccType == 2 && memberAccountNums.Contains(x.AccNum))
.Select(x => x.AccNum)
.ToArray();
var result = accounts
.Where(x => x.AccType == 3 && linkAccountNums.Contains(x.AccNum))
.ToArray();
return result;
}
I have used a method here to demonstrate a compilable version of the code (assuming the class and property names are correct). You would obviously want to parameterise this to meet your needs.
If you want to combine them for some reason (say LINQ-to-SQL), then you could write it as one query, or you could instead use a series of IQueryable variables instead of calling .ToArray().
I have created a working demo here: https://dotnetfiddle.net/pg0WLC
I assume that the logic is you want to return all accounts with AccType 3 where there is also a matching AccNum for AccType 0 and 2? This assumes that the MemberId property will match if the AccNum properties do.
Another way of doing this with LINQ would be to use group by:
int[] types = new int[] { 0, 2, 3 };
return accounts
.Where(x => x.MemberId == 20 && types.Contains(x.AccType))
.GroupBy(x => x.AccNum)
.Where(x => x.Count() == types.Count())
.SelectMany(x => x)
.Where(x => x.AccType == 3);
Ihave the following datagridView :
I want to make a group by these three columns :
annee and mois and jour
I tried this:
bool isActiveDuplicate = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["isActive"].Value.ToString() == "2")
.GroupBy(u => new { u.Cells["annee"], u.Cells["jour"], u.Cells["mois"] })
.Any(g => g.Count() > 1);
And this:
bool isActiveDuplicate = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["isActive"].Value.ToString() == "2")
.GroupBy(u => u.Cells["annee"].Value, u=> u.Cells["jour"].Value, u=>u.Cells["fd"].Value)
.Any(u => u.Count() > 1);
but i am stuck with this following error:
Anonymous type projection initializer should be simple name or member
access expression
whereas i don't want to create a specific class to match the result of the query because i will only use it once.
Can i achieve this group by without being forced to use member access to a type of a class that i will be creating?
I have to mention that it works fine when i group by one or at most 2 cells like following:
bool duplicate_ista2 = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["annee"].Value.ToString() == "2000")
.GroupBy(x => x.Cells["mois"].Value)
.Any(g => g.Count() > 1);
EDIT: this is different from this suggested duplicate question :
because here also we are doing group by 2 columns not more
I want to make a group by these three columns : annee and mois and jour
Let's look at your first implementation.
bool isActiveDuplicate = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["isActive"].Value.ToString() == "2")
.GroupBy(u => new { u.Cells["annee"], u.Cells["jour"], u.Cells["mois"] })
.Any(g => g.Count() > 1);
Before even compiling it you should get a few errors, specifically:
CS0746 Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access.
In order to fix this problem, add some simple names for those members, for example:
.GroupBy(u => new { annee = u.Cells["annee"], jour = u.Cells["jour"], mois = u.Cells["mois"] })
Also, I don't think you may understand, that linq statement is trying to group by DataGridViewRow.Cells object, not what you may want, you are more than likely looking for the value of the cells.
Your second implementation breakdown.
bool isActiveDuplicate = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["isActive"].Value.ToString() == "2")
.GroupBy(u => u.Cells["annee"].Value, u=> u.Cells["jour"].Value, u=>u.Cells["fd"].Value)
.Any(u => u.Count() > 1);
You should get a little different error from the first one. It may be along the lines of:
Can't convert lambda expression to type 'IEqualityComparer<object>...`
The reason is because the GroupBy expression is trying to convert the lambda expression to a IEqualityComparer<object>.
Here's one way to get your results: (comments in code)
// Get all records that match your where clause into a IEnumerable<DataGridViewRow>.
var records = dg_PanRejet.Rows.Cast<DataGridViewRow>().Where(r => r.Cells["annee"].Value.ToString() == "2000");
// Get an IEnumerable<IGrouping<a, DataGridViewRow>> of anonymous type that matches our group by.
var ienRecords =
from rec in records
group rec by new
{
annee = rec.Cells["annee"].Value,
jour = rec.Cells["jour"].Value,
mois = rec.Cells["mois"].Value
};
// Finally do we have duplicates?
bool dup = ienRecords.Count() > 0;
In the above code, ienRecords contains all records (IEnumerable>) that are grouped by those three fields. dup just holds the operator check of making sure there are duplicates.
I'm using this code to rank players in a game.
private void RecalculateUserRanks(GWDatabase db)
{
// RankedScore is a precalculated Double.
var users = db.UserStatistics.Where(x => x.RankedScore > 0);
var usersWithRank = users.OrderByDescending(x => x.RankedScore)
.Select(x => new
{
x.Id,
x.RankedScore
});
int position = 0;
foreach (var u in usersWithRank)
{
position++;
db.UserStatistics.First(x => x.Id == u.Id).Rank = position;
}
db.SaveChanges();
}
It's not the prettiest and as the number of players grows this will probably take some time and use a bit of memory.
I could do this in pure TSQL like this:
;WITH r AS
(
SELECT
[Id]
,[RankedScore]
,ROW_NUMBER() OVER(ORDER BY [RankedScore] DESC) AS Rnk
FROM [dbo].[UsersStatistics]
)
UPDATE u
SET u.Rank = r.Rnk
FROM [dbo].[UsersStatistics] u
INNER JOIN r ON r.Id = u.Id
But I would prefer to keep all my logic in the C# code as the database gets rebuilt all the time right now (and all other logic is there as well).
So my question is if there is a smarter way to do this in C# LINQ (or Lambda if thats your thing) without iterating over it in a for loop, and without dragging all the data outside of the SQL?
I assume by 'efficient' you mean 'efficient to read'. For a faster calculation you might consider to use a sorted list for db.UserStatistics; Those keep themselves sorted, while using log n time to insert a new member.
This is pretty much the same you posted, except lazy-evaluation might save a little time:
//get sorted list of IDs
var SortedIds = db.UserStatistics
.OrderByDescending(x => x.RankedScore)
.Select(x => x.Id);
//Fill in Values into result-set
db.UserStatistics = db.UserStatistics
.Where(x => x.RankedScore > 0)
.ForEach(x => u.Rank = SortedIds.IndexOf(x.id));
It seems a little inconsistent to have ranked and unranked players together.
This will give unranked players the rank -1 while saving a step. The downside would be, that all user will be altered, instead just those with a rank:
db.UserStatistics = db.UserStatistics.ForEach(u =>
u.Rank = db.UserStatistics
.Where(x => x.RankedScore > 0)
.OrderByDescending(x => x.RankedScore)
.IndexOf(u.id));