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.
Bạn đã xem LINQ động chưa? – svick
@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 –
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