2013-03-08 38 views
6

Tôi đã gặp sự cố này trong một tuần và không tìm thấy giải pháp nào.Xây dựng truy vấn LINQ GroupBy bằng cách sử dụng các cây biểu thức

Tôi có một POCO như dưới đây:

public class Journal { 
    public int Id { get; set; } 
    public string AuthorName { get; set; } 
    public string Category { get; set; } 
    public DateTime CreatedAt { get; set; } 
} 

Tôi muốn biết trong một khoảng thời gian cụ thể (nhóm theo tháng hoặc năm) số lượng các tạp chí đếm bởi một AUTHORNAME hoặc một loại.

Sau khi tôi gửi các đối tượng queryed để JSON serializer sau đó tạo ra dữ liệu JSON như dưới đây (chỉ sử dụng JSON để chứng minh dữ liệu tôi muốn nhận được, làm thế nào để serializer một đối tượng đến một JSON không phải là vấn đề của tôi)

data: { 
    '201301': { 
     'Alex': 10, 
     'James': 20 
    }, 
    '201302': { 
     'Alex': 1, 
     'Jessica': 9 
    } 
} 

HOẶC

data: { 
    '2012': { 
     'C#': 230 
     'VB.NET': 120, 
     'LINQ': 97 
    }, 
    '2013': { 
     'C#': 115 
     'VB.NET': 29, 
     'LINQ': 36 
    } 
} 

Những gì tôi biết là viết một truy vấn LINQ trong "phương pháp cách" như:

IQueryable<Journal> query = db.GroupBy(x=> new 
    { 
     Year = key.CreatedAt.Year, 
     Month = key.CreatedAt.Month 
    }, prj => prj.AuthorName) 
    .Select(data => new { 
     Key = data.Key.Year * 100 + data.Key.Month, // very ugly code, I know 
     Details = data.GroupBy(y => y).Select(z => new { z.Key, Count = z.Count() }) 
    }); 

Các điều kiện được nhóm theo tháng hoặc năm, AuthorName hoặc Category sẽ được thông qua bởi hai tham số phương thức kiểu chuỗi. Những gì tôi không biết là làm thế nào để sử dụng "Magic String" tham số trong một GroupBy() phương pháp. Sau khi một số googling, có vẻ như tôi không thể nhóm dữ liệu bằng cách đi qua một chuỗi ma thuật như "AuthorName". Những gì tôi cần làm là xây dựng một cây biểu thức và chuyển nó tới phương thức GroupBy().

Bất kỳ giải pháp hoặc đề xuất nào đều được đánh giá cao.

+0

Bạn đã xem LINQ động chưa? – svick

+0

@svick Nếu tôi có tùy chọn khác bên cạnh LINQ to Entity, tôi sẽ chọn Dapper của StackOverflow thay vì LINQ –

+0

LINQ động hoạt động trên đầu trang của 'IQueryable', vì vậy nó không thay thế các thư viện như LINQ thành Entities, nó thực sự yêu cầu một số thư viện như vậy để làm việc. – svick

Trả lời

22

Ooh, điều này có vẻ giống như một vui vấn đề :)

Vì vậy, đầu tiên, chúng ta hãy thiết lập faux-nguồn của chúng tôi, vì tôi không có DB của bạn có ích:

// SETUP: fake up a data source 
var folks = new[]{"Alex", "James", "Jessica"}; 
var cats = new[]{"C#", "VB.NET", "LINQ"}; 
var r = new Random(); 
var entryCount = 100; 
var entries = 
    from i in Enumerable.Range(0, entryCount) 
    let id = r.Next(0, 999999) 
    let person = folks[r.Next(0, folks.Length)] 
    let category = cats[r.Next(0, cats.Length)] 
    let date = DateTime.Now.AddDays(r.Next(0, 100) - 50) 
    select new Journal() { 
     Id = id, 
     AuthorName = person, 
     Category = category, 
     CreatedAt = date };  

Ok, vì vậy bây giờ chúng tôi đã có một bộ dữ liệu để làm việc với, hãy xem những gì chúng tôi muốn ... chúng tôi muốn một cái gì đó với một "hình dạng" như:

public Expression<Func<Journal, ????>> GetThingToGroupByWith(
    string[] someMagicStringNames, 
    ????) 

Điều đó có cùng chức năng ity as (in pseudo code):

GroupBy(x => new { x.magicStringNames }) 

Hãy chia nhỏ từng phần một. Đầu tiên, chúng ta làm điều này bằng cách nào?

x => new { ... } 

Trình biên dịch không sự kỳ diệu cho chúng ta bình thường - những gì nó làm là xác định một mới Type, và chúng ta có thể làm như vậy:

var sourceType = typeof(Journal); 

    // define a dynamic type (read: anonymous type) for our needs 
    var dynAsm = AppDomain 
     .CurrentDomain 
     .DefineDynamicAssembly(
      new AssemblyName(Guid.NewGuid().ToString()), 
      AssemblyBuilderAccess.Run); 
    var dynMod = dynAsm 
     .DefineDynamicModule(Guid.NewGuid().ToString()); 
    var typeBuilder = dynMod 
     .DefineType(Guid.NewGuid().ToString()); 
    var properties = groupByNames 
     .Select(name => sourceType.GetProperty(name)) 
     .Cast<MemberInfo>(); 
    var fields = groupByNames 
     .Select(name => sourceType.GetField(name)) 
     .Cast<MemberInfo>(); 
    var propFields = properties 
     .Concat(fields) 
     .Where(pf => pf != null); 
    foreach (var propField in propFields) 
    {   
     typeBuilder.DefineField(
      propField.Name, 
      propField.MemberType == MemberTypes.Field 
       ? (propField as FieldInfo).FieldType 
       : (propField as PropertyInfo).PropertyType, 
      FieldAttributes.Public); 
    } 
    var dynamicType = typeBuilder.CreateType(); 

Vì vậy, những gì chúng tôi đã thực hiện ở đây là xác định một tùy chỉnh , loại throwaway có một trường cho mỗi tên mà chúng ta truyền vào, cùng kiểu với (hoặc thuộc tính hoặc trường) trên kiểu nguồn. Tốt đẹp!

Bây giờ, làm thế nào để chúng tôi cung cấp cho LINQ những gì nó muốn?

Trước tiên, hãy thiết lập một "đầu vào" cho func chúng tôi sẽ trở lại:

// Create and return an expression that maps T => dynamic type 
var sourceItem = Expression.Parameter(sourceType, "item"); 

Chúng tôi biết chúng tôi sẽ cần phải "mới lên" một trong những loại động mới của chúng tôi ...

Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)) 

Và chúng tôi sẽ cần phải khởi tạo nó với các giá trị đến từ tham số mà ...

Expression.MemberInit(
    Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), 
    bindings), 

Nhưng được cái quái gì chúng ta sẽ sử dụng cho bindings? Hmm ... tốt, chúng tôi muốn một cái gì đó liên kết với các thuộc tính/trường tương ứng trong các loại nguồn, nhưng remaps họ dynamicType lĩnh vực của chúng tôi ...

var bindings = dynamicType 
     .GetFields() 
     .Select(p => 
      Expression.Bind(
       p, 
       Expression.PropertyOrField(
        sourceItem, 
        p.Name))) 
     .OfType<MemberBinding>() 
     .ToArray(); 

OOF ... khó chịu tìm kiếm, nhưng chúng tôi vẫn chưa thực hiện - vì vậy chúng tôi cần khai báo loại trả về cho số Func chúng tôi đang tạo thông qua cây Biểu thức ... khi nghi ngờ, hãy sử dụng object!

Expression.Convert(expr, typeof(object)) 

Và cuối cùng, chúng tôi sẽ liên kết này để "tham số đầu vào" của chúng tôi qua Lambda, làm cho toàn bộ stack:

// Create and return an expression that maps T => dynamic type 
    var sourceItem = Expression.Parameter(sourceType, "item"); 
    var bindings = dynamicType 
     .GetFields() 
     .Select(p => Expression.Bind(p, Expression.PropertyOrField(sourceItem, p.Name))) 
     .OfType<MemberBinding>() 
     .ToArray(); 

    var fetcher = Expression.Lambda<Func<T, object>>(
     Expression.Convert(
      Expression.MemberInit(
       Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), 
       bindings), 
      typeof(object)), 
     sourceItem);     

Để dễ sử dụng, chúng ta hãy bọc toàn bộ mớ hỗn độn lên như một phần mở rộng phương pháp, vì vậy bây giờ chúng tôi đã có:

public static class Ext 
{ 
    // Science Fact: the "Grouper" (as in the Fish) is classified as: 
    // Perciformes Serranidae Epinephelinae 
    public static Expression<Func<T, object>> Epinephelinae<T>(
     this IEnumerable<T> source, 
     string [] groupByNames) 
    { 
     var sourceType = typeof(T); 
    // define a dynamic type (read: anonymous type) for our needs 
    var dynAsm = AppDomain 
     .CurrentDomain 
     .DefineDynamicAssembly(
      new AssemblyName(Guid.NewGuid().ToString()), 
      AssemblyBuilderAccess.Run); 
    var dynMod = dynAsm 
     .DefineDynamicModule(Guid.NewGuid().ToString()); 
    var typeBuilder = dynMod 
     .DefineType(Guid.NewGuid().ToString()); 
    var properties = groupByNames 
     .Select(name => sourceType.GetProperty(name)) 
     .Cast<MemberInfo>(); 
    var fields = groupByNames 
     .Select(name => sourceType.GetField(name)) 
     .Cast<MemberInfo>(); 
    var propFields = properties 
     .Concat(fields) 
     .Where(pf => pf != null); 
    foreach (var propField in propFields) 
    {   
     typeBuilder.DefineField(
      propField.Name, 
      propField.MemberType == MemberTypes.Field 
       ? (propField as FieldInfo).FieldType 
       : (propField as PropertyInfo).PropertyType, 
      FieldAttributes.Public); 
    } 
    var dynamicType = typeBuilder.CreateType(); 

     // Create and return an expression that maps T => dynamic type 
     var sourceItem = Expression.Parameter(sourceType, "item"); 
     var bindings = dynamicType 
      .GetFields() 
      .Select(p => Expression.Bind(
        p, 
        Expression.PropertyOrField(sourceItem, p.Name))) 
      .OfType<MemberBinding>() 
      .ToArray(); 

     var fetcher = Expression.Lambda<Func<T, object>>(
      Expression.Convert(
       Expression.MemberInit(
        Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), 
        bindings), 
       typeof(object)), 
      sourceItem);     
     return fetcher; 
    } 
} 

Bây giờ, để sử dụng nó:

// What you had originally (hand-tooled query) 
var db = entries.AsQueryable(); 
var query = db.GroupBy(x => new 
    { 
     Year = x.CreatedAt.Year, 
     Month = x.CreatedAt.Month 
    }, prj => prj.AuthorName) 
    .Select(data => new { 
     Key = data.Key.Year * 100 + data.Key.Month, // very ugly code, I know 
     Details = data.GroupBy(y => y).Select(z => new { z.Key, Count = z.Count() }) 
    });  

var func = db.Epinephelinae(new[]{"CreatedAt", "AuthorName"}); 
var dquery = db.GroupBy(func, prj => prj.AuthorName); 

Giải pháp này thiếu tính linh hoạt của "câu lệnh lồng nhau", như "CreatedDate.Month", nhưng với một chút trí tưởng tượng, bạn có thể mở rộng ý tưởng này để làm việc với bất kỳ truy vấn dạng tự do nào.

+0

+1 để có giải thích kỹ lưỡng + sự chăm chỉ. –

+0

Tôi ước tôi có thể tặng cho +10 cho số – Jeremy

+0

nhờ @JerKimball, lời giải thích rất hay và thú vị này. –

Các vấn đề liên quan