Row_number over (partição por xxx) em Linq?

Eu tenho um DataTable que tem essa estrutura e dados:

 id |  inst |  nome
 ------------------------
  1 |  guitarra |  John
  2 |  guitarra |  george
  3 |  guitarra |  Paulo
  4 |  tambores |  ringo
  5 |  tambores |  pete

Eu posso recuperar os registros assim:

 IEnumerable ... class Beatle { int id; string inst; string name; } 

Eu gostaria de obter a ordem interna daqueles que tocam os diferentes instrumentos. No MSSQL eu usaria

 SELECT * ,Row_Number() OVER (PARTITION BY inst ORDER BY id) AS rn FROM Beatles 

Esta consulta retorna

 id |  inst |  nome |  rn
 -----------------------------
  1 |  guitarra |  john |  1
  2 |  guitarra |  george |  2
  3 |  guitarra |  paul |  3
  4 |  tambores |  ringo |  1
  5 |  tambores |  pete |  2

Como posso fazer isso no Linq ?

Editar (depois da resposta aceita)

Código de trabalho completo:

 var beatles = (new[] { new { id=1 , inst = "guitar" , name="john" }, new { id=2 , inst = "guitar" , name="george" }, new { id=3 , inst = "guitar" , name="paul" }, new { id=4 , inst = "drums" , name="ringo" }, new { id=5 , inst = "drums" , name="pete" } }); var o = beatles.OrderBy(x => x.id).GroupBy(x => x.inst) .Select(g => new { g, count = g.Count() }) .SelectMany(t => tgSelect(b => b) .Zip(Enumerable.Range(1, t.count), (j, i) => new { j.inst, j.name, rn = i })); foreach (var i in o) { Console.WriteLine("{0} {1} {2}", i.inst, i.name, i.rn); } 

B “H

Eu sei que isso é velho. Mas por que a solução não é simplesmente?

 var o = beatles.GroupBy(x => x.inst) .SelectMany(g => g.Select((j, i) => new { j.inst, j.name, rn = i + 1 }) ); 

Experimente este forro:

 var o = beatles .OrderBy( x => x.id ) .GroupBy( x => x.inst ) .Select( group => new { Group = group, Count = group.Count() } ) .SelectMany( groupWithCount => groupWithCount.Group.Select( b => b) .Zip( Enumerable.Range( 1, groupWithCount.Count ), ( j, i ) => new { j.inst, j.name, RowNumber = i } ) ); foreach (var i in o) { Console.WriteLine( "{0} {1} {2}", i.inst, i.name, i.RowNumber ); } 

Saída:

 Guitar John 1 Guitar George 2 Guitar Paul 3 drums Ringo 1 drums Pete 2 

Outra ideia é usar uma visão, se possível.

Como @The_Smallest aponta, o número da linha não é suportado pelo LINQ. Veja como você pode obter o que está procurando:

 var grouped = beatles.OrderBy( x => x.id ) .ToList() // required because SelectMany below doesn't evaluate to SQL .GroupBy( x => x.inst ); var rns = grouped.ToDictionary( x => x.Key, x => 1 ); var result = grouped .SelectMany( x => x.Select( y => new { inst = y.inst, name = y.name, rn = rns[y.inst]++ } ) ); 

Outra solução para o Linq para objects é:

 var result = beatles .GroupBy(g => g.inst) // PARTITION BY ^^^^ .Select(c => c.OrderBy(o => o.id).Select((v, i) => new { i, v }).ToList()) // ORDER BY ^^ .SelectMany(c => c) .Select(c => new { cvid, cvinst, cvname, rn = ci + 1 }) .ToList(); 

[ C# Demo ]

Alguns podem achar útil usar em seu código para obter o índice adequado

 .Select((item, i) => new { Item = item, Index = i }) 

Outra solução para fazer o equivalente de RANK () OVER (PARTIÇÃO BY “partitionBy” ORDER BY “orderBy” DESC):

  DataTable Rank(DataTable dt, string partitionBy, string orderBy, int whichRank) { DataView dv = new DataView(dt); dv.Sort = partitionBy + ", " + orderBy + " DESC"; DataTable rankDt = dv.ToTable(); rankDt.Columns.Add("Rank"); int rank = 1; for (int i = 0; i < rankDt.Rows.Count - 1; i++) { rankDt.Rows[i]["Rank"] = rank; DataRow thisRow = rankDt.Rows[i]; DataRow nextRow = rankDt.Rows[i + 1]; if (thisRow[partitionBy].ToString() != nextRow[partitionBy].ToString()) rank = 1; else rank++; } DataView selectRankdv = new DataView(rankDt); selectRankdv.RowFilter = "rank = " + whichRank; return selectRankdv.ToTable(); }