So, I know I'm very rusty, but I never thought this would be so difficult in spite of spending hours trying different solutions. I'm trying to select the newest record from each group, after eliminating a particular clause and then binding it to a grid. Having essentially no luck... this is where I left off:
var widgets = db.Updates
.GroupBy(c => c.widgetType)
.SelectMany(s => s)
.Where(c => c.Sold.Equals(false))
.OrderByDescending(x => x.TimeStamp)
.FirstOrDefault();
var list = new List<Update>() {widgets};
widgetsGrid.DataSource = list;
widgetsGrid.DataBind();
I added the cast to list since I was getting a data type error, and at present it returns only the last item of all records, rather than the last item from each group.
Thanks in advance for any help!!!
The OrderByDescending should be on the level of each group. You also don't need SelectMany because this will join the groups back into a flat list.
var widgets = Updates
.GroupBy(c => c.widgetType)
.Where(c => c.Sold.Equals(false))
.Select(x => x.OrderByDescending(y => y.TimeStamp).First());
var widgets = db.Updates
.GroupBy(c => c.widgetType)
.SelectMany(s => s)
.Where(c => c.Sold.Equals(false))
.OrderByDescending(x => x.TimeStamp)
.Select(y=>y.First());
var list = new List<Update>() {widgets};
widgetsGrid.DataSource = list;
widgetsGrid.DataBind();
It seems the key thing you need to do is order by and select the first item from the list. Here's an example program:
class Program
{
static void Main(string[] args)
{
var foodOrders = new List<FoodOrder>
{
new FoodOrder { FoodName = "hotdog", OrderDate = new DateTime(2016, 7, 7) },
new FoodOrder { FoodName = "hamburger", OrderDate = new DateTime(2016, 7, 6) },
new FoodOrder { FoodName = "taco", OrderDate = new DateTime(2016, 7, 5) },
};
var mostRecentFoodOrder = foodOrders.OrderByDescending(f => f.OrderDate).First().FoodName;
Console.WriteLine(mostRecentFoodOrder);
//cmd
//hotdog
//Press any key to continue . . .
}
}
class FoodOrder
{
public string FoodName { get; set; }
public DateTime OrderDate { get; set; }
}
var widgets = db.Updates
.Where(c => c.Sold.Equals(false))
.GroupBy(c => c.widgetType)
.Select(x => x.OrderByDescending(y => y.TimeStamp).First()).ToList();
widgetGrid.DataSource = widgets;
widgetGrid.DataBind();
I've seen this question asked a hundred times all over the web... this worked for me. Courtesy of, and many thanks to #Ahmad Ibrahim
Related
I have this document, a post :
{Content:"blabla",Tags:["test","toto"], CreatedOn:"2019-05-01 01:02:01"}
I want to have a page that displays themost used tags since the last 30 days.
So far I tried to create an index like this
public class Toss_TagPerDay : AbstractIndexCreationTask<TossEntity, TagByDayIndex>
{
public Toss_TagPerDay()
{
Map = tosses => from toss in tosses
from tag in toss.Tags
select new TagByDayIndex()
{
Tag = tag,
CreatedOn = toss.CreatedOn.Date,
Count = 1
};
Reduce = results => from result in results
group result by new { result.Tag, result.CreatedOn }
into g
select new TagByDayIndex()
{
Tag = g.Key.Tag,
CreatedOn = g.Key.CreatedOn,
Count = g.Sum(i => i.Count)
};
}
}
And I query it like that
await _session
.Query<TagByDayIndex, Toss_TagPerDay>()
.Where(i => i.CreatedOn >= firstDay)
.GroupBy(i => i.Tag)
.OrderByDescending(g => g.Sum(i => i.Count))
.Take(50)
.Select(t => new BestTagsResult()
{
CountLastMonth = t.Count(),
Tag = t.Key
})
.ToListAsync()
But this gives me the error
Message: System.NotSupportedException : Could not understand expression: from index 'Toss/TagPerDay'.Where(i => (Convert(i.CreatedOn, DateTimeOffset) >= value(Toss.Server.Models.Tosses.BestTagsQueryHandler+<>c__DisplayClass3_0).firstDay)).GroupBy(i => i.Tag).OrderByDescending(g => g.Sum(i => i.Count)).Take(50).Select(t => new BestTagsResult() {CountLastMonth = t.Count(), Tag = t.Key})
---- System.NotSupportedException : GroupBy method is only supported in dynamic map-reduce queries
Any idea how can I make this work ? I could query for all the index data from the past 30 days and do the groupby / order / take in memory but this could make my app load a lot of data.
The results from the map-reduce index you created will give you the number of tags per day. You want to have the most popular ones from the last 30 days so you need to do the following query:
var tagCountPerDay = session
.Query<TagByDayIndex, Toss_TagPerDay>()
.Where(i => i.CreatedOn >= DateTime.Now.AddDays(-30))
.ToList();
Then you can the the client side grouping by Tag:
var mostUsedTags = tagCountPerDay.GroupBy(x => x.Tag)
.Select(t => new BestTagsResult()
{
CountLastMonth = t.Count(),
Tag = t.Key
})
.OrderByDescending(g => g.CountLastMonth)
.ToList();
#Kuepper
Based on your index definition. You can handle that by the following index:
public class TrendingSongs : AbstractIndexCreationTask<TrackPlayedEvent, TrendingSongs.Result>
{
public TrendingSongs()
{
Map = events => from e in events
where e.TypeOfTrack == TrackSubtype.song && e.Percentage >= 80 && !e.Tags.Contains(Podcast.Tags.FraKaare)
select new Result
{
TrackId = e.TrackId,
Count = 1,
Timestamp = new DateTime(e.TimestampStart.Year, e.TimestampStart.Month, e.TimestampStart.Day)
};
Reduce = results => from r in results
group r by new {r.TrackId, r.Timestamp}
into g
select new Result
{
TrackId = g.Key.TrackId,
Count = g.Sum(x => x.Count),
Timestamp = g.Key.Timestamp
};
}
}
and the query using facets:
from index TrendingSongs where Timestamp between $then and $now select facet(TrackId, sum(Count))
The reason for the error is that you can't use 'GroupBy' in a query made on an index.
'GroupBy' can be used when performing a 'dynamic query',
i.e. a query that is made on a collection, without specifying an index.
See:
https://ravendb.net/docs/article-page/4.1/Csharp/client-api/session/querying/how-to-perform-group-by-query
I solved a similar problem, by using AdditionalSources that uses dynamic values.
Then I update the index every morning to increase the Earliest Timestamp. await IndexCreation.CreateIndexesAsync(new AbstractIndexCreationTask[] {new TrendingSongs()}, _store);
I still have to try it in production, but my tests so far look like it's a lot faster than the alternatives. It does feel pretty hacky though and I'm surprised RavenDB does not offer a better solution.
public class TrendingSongs : AbstractIndexCreationTask<TrackPlayedEvent, TrendingSongs.Result>
{
public DateTime Earliest = DateTime.UtcNow.AddDays(-16);
public TrendingSongs()
{
Map = events => from e in events
where e.TypeOfTrack == TrackSubtype.song && e.Percentage >= 80 && !e.Tags.Contains(Podcast.Tags.FraKaare)
&& e.TimestampStart > new DateTime(TrendingHelpers.Year, TrendingHelpers.Month, TrendingHelpers.Day)
select new Result
{
TrackId = e.TrackId,
Count = 1
};
Reduce = results => from r in results
group r by new {r.TrackId}
into g
select new Result
{
TrackId = g.Key.TrackId,
Count = g.Sum(x => x.Count)
};
AdditionalSources = new Dictionary<string, string>
{
{
"TrendingHelpers",
#"namespace Helpers
{
public static class TrendingHelpers
{
public static int Day = "+Earliest.Day+#";
public static int Month = "+Earliest.Month+#";
public static int Year = "+Earliest.Year+#";
}
}"
}
};
}
}
I am trying to create an Iqueryable method which returns the number of connections to a service for each day. this data is read from a SQL Server database.
Here is ConnectionItem class
public class ConnectionItem
{
public DateTime CreatedDate { get; set; }
public int NumberOfConnections { get; set; }
}
And here is my iqueryable
private IQueryable<ConnectionItem> ListItems(DataContext dataContext)
{
return dataContext.Connections
.Join(dataContext.Configurations,
connections => connections.ConfigID,
config => config.ConfigID,
(connections, config) => new { cx = connections, cf = config })
.Join(dataContext.Users,
config => config.cf.UserID,
users => users.UserID,
(config, users) => new { cf = config, su = users})
.Where(q => q.su.AccountEventID == 123 && q.cf.cx.Successful == true)
.GroupBy(g => g.cf.cx.CreatedDate.ToShortDateString())
.Select(s => new ConnectionItem
{
CreatedDate = ????,
NumberOfConnections = ????
});
}
How do I access the grouped date value and the number of items per group?
Also, is there an easier way to write this kind of statements? I am not 100% sure on how the aliases cx,cf etc work.
Any input is appreciated.
Group by the Date portion of the DateTime objects. The Date property simply drops the time part. You're converting your dates to strings so you're losing the fidelity of a DateTime object.
var eventId = 123;
return dataContext.Connections.Join(dataContext.Configurations,
conn => conn.ConfigID,
cfg => cfg.ConfigID,
(conn, cfg) => new { conn, cfg })
.Join(dataContext.Users,
x => x.cfg.UserID,
u => u.UserID,
(x, u) => new { x.conn, u })
.Where(x => x.conn.Successful && x.u.AccountEventID == eventId)
.GroupBy(x => x.conn.CreatedDate.Date)
.Select(g => new ConnectionItem
{
CreatedDate = g.Key,
NumberOfConnections = g.Count(),
});
The above could be more nicely expressed using query syntax.
var eventId = 123;
return
from conn in dataContext.Connections
join cfg in dataContext.Configurations on conn.ConfigID equals cfg.ConfigID
join u in dataContext.Users on cfg.UserID equals u.UserID
where conn.Successful && u.AccountEventID == eventId
group 1 by conn.CreatedDate.Date into g
select new ConnectionItem
{
CreatedDate = g.Key,
NumberOfConnections = g.Count(),
};
The .GroupBy linq method returns an IGrouping<TKey, TValue>, which is basically a List with a Key property that you've just grouped by.
So here
Select(s => new ConnectionItem
{
CreatedDate = ????,
NumberOfConnections = ????
});
Your iterating through a IEnumerable<IGrouping<TKey,TValue>>so you can do this
Select(s => new ConnectionItem
{
CreatedDate = s.Key
NumberOfConnections = s.Count()
});
edited as per comment I realized your looking for the number not an actual list
Just call s.Key and s.Count() ,You can get it like:
private IQueryable<ConnectionItem> ListItems(DataContext dataContext)
{
return dataContext.Connections
.Join(dataContext.Configurations,
connections => connections.ConfigID,
config => config.ConfigID,
(connections, config) => new {cx = connections, cf = config})
.Join(dataContext.Users,
config => config.cf.UserID,
users => users.UserID,
(config, users) => new {cf = config, su = users})
.Where(q => q.su.AccountEventID == 123 && q.cf.cx.Successful == true)
.GroupBy(g => g.cf.cx.CreatedDate.ToShortDateString())
.Select(s => new ConnectionItem
{
CreatedDate = s.Key,
NumberOfConnections = s.Count()
});
}
The group clause returns a sequence of IGrouping
objects that contain zero or more items that match the key value for
the group. For example, you can group a sequence of strings according
to the first letter in each string. In this case, the first letter is
the key and has a type char, and is stored in the Key property of each
IGrouping object. The compiler infers the type of the
key.
Group clause docs
I have a table having TeamName and CurrentStatus fields. I am making a linq query to get for each team and for each status the count of records:
var teamStatusCounts = models.GroupBy(x => new { x.CurrentStatus, x.TeamName })
.Select(g => new { g.Key, Count = g.Count() });
The results of this query returns all the counts except where count is 0. I need to get the rows where there is no record for a specific team and a specific status (where count = 0).
You could have a separate collection for team name and statuses you are expecting and add the missing ones to the result set
//assuming allTeamNamesAndStatuses is a cross joing of all 'CurrentStatus' and 'TeamNames'
var teamStatusCounts = models.GroupBy(x => new { x.CurrentStatus, x.TeamName })
.Select(g => new { g.Key, Count = g.Count() })
.ToList();
var missingTeamsAndStatuses = allTeamNamesAndStatuses
.Where(a=>
!teamStatusCounts.Any(b=>
b.Key.CurrentStatus == a.CurrentStatus
&& b.Key.TeamName == a.TeamName))
.Select(a=>new {
Key = new { a.CurrentStatus, a.TeamName },
Count = 0
});
teamStatusCounts.AddRange(emptyGroups);
I've created a fiddle demonstrating the answer as well
I would select the team names and status first:
var teams = models.Select(x => x.TeamName).Distinct().ToList();
var status = models.Select(x => x.CurrentStatus).Distinct().ToList();
You can skip this if you know the list entries already.
Then you can select for each team and each state the number of models:
var teamStatusCounts = teams.SelectMany(team => states.Select(state =>
new
{
TeamName = team,
CurrentStatus = state,
Count = models.Count(model =>
model.TeamName == team && model.CurrentStatus == state)
}));
I'm looking to encapsulate the Select section of the GroupBy to have a more readable, understandable and nicer code. There is anyway to do that?
This is what I have now (And I have a lot of GroupsBy in my code, is not only one, that's another reason to encapsulate as much as possible):
var xTypeAggregatedTransactions = xTypeTrtansactions.
.GroupBy(x => new {x.TypeId, x.AccountId})
.Select(y => new PayTransactionsCommand
{
Id = Guid.NewGuid(),
Narration = item.Name,
AccountId = y.Key.AccountId,
Credit = y.Sum(z => z.Credit),
});
This is what I want:
var xTypeAggregatedTransactions = xTypeTrtansactions.
.GroupBy(x => new {x.TypeId, x.AccountId})
.AsEnumerable().ToPayTransaction();
Thanks
Write an extension class:
public static class TypeTrtansactionExtensions
{
public static IEnumerable<PayTransactionsCommand> ToPayTransaction(this IQueryable<TypeTrtansaction> query, string itemName)
{
return query.GroupBy(x => new { x.TypeId, x.AccountId })
.Select(y => new PayTransactionsCommand
{
Id = Guid.NewGuid(),
Narration = itemName,
AccountId = y.Key.AccountId,
Credit = y.Sum(z => z.Credit),
});
}
}
And call it:
var xTypeAggregatedTransactions = xTypeTrtansactions.ToPayTransaction(item.Name);
Also, you can add more parameters, if you need.
How can I do GroupBy multiple columns in LINQ
Something similar to this in SQL:
SELECT * FROM <TableName> GROUP BY <Column1>,<Column2>
How can I convert this to LINQ:
QuantityBreakdown
(
MaterialID int,
ProductID int,
Quantity float
)
INSERT INTO #QuantityBreakdown (MaterialID, ProductID, Quantity)
SELECT MaterialID, ProductID, SUM(Quantity)
FROM #Transactions
GROUP BY MaterialID, ProductID
Use an anonymous type.
Eg
group x by new { x.Column1, x.Column2 }
Procedural sample:
.GroupBy(x => new { x.Column1, x.Column2 })
Ok got this as:
var query = (from t in Transactions
group t by new {t.MaterialID, t.ProductID}
into grp
select new
{
grp.Key.MaterialID,
grp.Key.ProductID,
Quantity = grp.Sum(t => t.Quantity)
}).ToList();
For Group By Multiple Columns, Try this instead...
GroupBy(x=> new { x.Column1, x.Column2 }, (key, group) => new
{
Key1 = key.Column1,
Key2 = key.Column2,
Result = group.ToList()
});
Same way you can add Column3, Column4 etc.
Since C# 7 you can also use value tuples:
group x by (x.Column1, x.Column2)
or
.GroupBy(x => (x.Column1, x.Column2))
C# 7.1 or greater using Tuples and Inferred tuple element names (currently it works only with linq to objects and it is not supported when expression trees are required e.g. someIQueryable.GroupBy(...). Github issue):
// declarative query syntax
var result =
from x in inMemoryTable
group x by (x.Column1, x.Column2) into g
select (g.Key.Column1, g.Key.Column2, QuantitySum: g.Sum(x => x.Quantity));
// or method syntax
var result2 = inMemoryTable.GroupBy(x => (x.Column1, x.Column2))
.Select(g => (g.Key.Column1, g.Key.Column2, QuantitySum: g.Sum(x => x.Quantity)));
C# 3 or greater using anonymous types:
// declarative query syntax
var result3 =
from x in table
group x by new { x.Column1, x.Column2 } into g
select new { g.Key.Column1, g.Key.Column2, QuantitySum = g.Sum(x => x.Quantity) };
// or method syntax
var result4 = table.GroupBy(x => new { x.Column1, x.Column2 })
.Select(g =>
new { g.Key.Column1, g.Key.Column2 , QuantitySum= g.Sum(x => x.Quantity) });
You can also use a Tuple<> for a strongly-typed grouping.
from grouping in list.GroupBy(x => new Tuple<string,string,string>(x.Person.LastName,x.Person.FirstName,x.Person.MiddleName))
select new SummaryItem
{
LastName = grouping.Key.Item1,
FirstName = grouping.Key.Item2,
MiddleName = grouping.Key.Item3,
DayCount = grouping.Count(),
AmountBilled = grouping.Sum(x => x.Rate),
}
Though this question is asking about group by class properties, if you want to group by multiple columns against a ADO object (like a DataTable), you have to assign your "new" items to variables:
EnumerableRowCollection<DataRow> ClientProfiles = CurrentProfiles.AsEnumerable()
.Where(x => CheckProfileTypes.Contains(x.Field<object>(ProfileTypeField).ToString()));
// do other stuff, then check for dups...
var Dups = ClientProfiles.AsParallel()
.GroupBy(x => new { InterfaceID = x.Field<object>(InterfaceField).ToString(), ProfileType = x.Field<object>(ProfileTypeField).ToString() })
.Where(z => z.Count() > 1)
.Select(z => z);
var Results= query.GroupBy(f => new { /* add members here */ });
A thing to note is that you need to send in an object for Lambda expressions and can't use an instance for a class.
Example:
public class Key
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
}
This will compile but will generate one key per cycle.
var groupedCycles = cycles.GroupBy(x => new Key
{
Prop1 = x.Column1,
Prop2 = x.Column2
})
If you wan't to name the key properties and then retreive them you can do it like this instead. This will GroupBy correctly and give you the key properties.
var groupedCycles = cycles.GroupBy(x => new
{
Prop1 = x.Column1,
Prop2= x.Column2
})
foreach (var groupedCycle in groupedCycles)
{
var key = new Key();
key.Prop1 = groupedCycle.Key.Prop1;
key.Prop2 = groupedCycle.Key.Prop2;
}
group x by new { x.Col, x.Col}
.GroupBy(x => (x.MaterialID, x.ProductID))
.GroupBy(x => x.Column1 + " " + x.Column2)
For VB and anonymous/lambda:
query.GroupBy(Function(x) New With {Key x.Field1, Key x.Field2, Key x.FieldN })