Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>Ooh, this looks like a <em>fun</em> problem :)</p> <p>So first, let's set up our faux-source, since I don't have your DB handy:</p> <pre><code>// 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 }; </code></pre> <p>Ok, so now we've got a set of data to work with, let's look at what we want...we want something with a "shape" like:</p> <pre><code>public Expression&lt;Func&lt;Journal, ????&gt;&gt; GetThingToGroupByWith( string[] someMagicStringNames, ????) </code></pre> <p>That has roughly the same functionality as (in pseudo code):</p> <pre><code>GroupBy(x =&gt; new { x.magicStringNames }) </code></pre> <p>Let's dissect it one piece at a time. First, how the heck do we do this dynamically?</p> <pre><code>x =&gt; new { ... } </code></pre> <p>The compiler does the magic for us normally - what it does is define a new <code>Type</code>, and we can do the same:</p> <pre><code> 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 =&gt; sourceType.GetProperty(name)) .Cast&lt;MemberInfo&gt;(); var fields = groupByNames .Select(name =&gt; sourceType.GetField(name)) .Cast&lt;MemberInfo&gt;(); var propFields = properties .Concat(fields) .Where(pf =&gt; 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(); </code></pre> <p>So what we've done here is define a custom, throwaway type that has one field for each name we pass in, which is the same type as the (either Property or Field) on the source type. Nice!</p> <p>Now how do we give LINQ what it wants?</p> <p>First, let's set up an "input" for the func we'll return:</p> <pre><code>// Create and return an expression that maps T =&gt; dynamic type var sourceItem = Expression.Parameter(sourceType, "item"); </code></pre> <p>We know we'll need to "new up" one of our new dynamic types...</p> <pre><code>Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)) </code></pre> <p>And we'll need to initialize it with the values coming in from that parameter...</p> <pre><code>Expression.MemberInit( Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), </code></pre> <p>But what the heck are we going to use for <code>bindings</code>? Hmm...well, we want something that binds to the corresponding properties/fields in the source type, but remaps them to our <code>dynamicType</code> fields...</p> <pre><code> var bindings = dynamicType .GetFields() .Select(p =&gt; Expression.Bind( p, Expression.PropertyOrField( sourceItem, p.Name))) .OfType&lt;MemberBinding&gt;() .ToArray(); </code></pre> <p>Oof...nasty looking, but we're still not done - so we need to declare a return type for the <code>Func</code> we're creating via Expression trees...when in doubt, use <code>object</code>! </p> <pre><code>Expression.Convert( expr, typeof(object)) </code></pre> <p>And finally, we'll bind this to our "input parameter" via <code>Lambda</code>, making the whole stack:</p> <pre><code> // Create and return an expression that maps T =&gt; dynamic type var sourceItem = Expression.Parameter(sourceType, "item"); var bindings = dynamicType .GetFields() .Select(p =&gt; Expression.Bind(p, Expression.PropertyOrField(sourceItem, p.Name))) .OfType&lt;MemberBinding&gt;() .ToArray(); var fetcher = Expression.Lambda&lt;Func&lt;T, object&gt;&gt;( Expression.Convert( Expression.MemberInit( Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), typeof(object)), sourceItem); </code></pre> <p>For ease of use, let's wrap the whole mess up as an extension method, so now we've got:</p> <pre><code>public static class Ext { // Science Fact: the "Grouper" (as in the Fish) is classified as: // Perciformes Serranidae Epinephelinae public static Expression&lt;Func&lt;T, object&gt;&gt; Epinephelinae&lt;T&gt;( this IEnumerable&lt;T&gt; 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 =&gt; sourceType.GetProperty(name)) .Cast&lt;MemberInfo&gt;(); var fields = groupByNames .Select(name =&gt; sourceType.GetField(name)) .Cast&lt;MemberInfo&gt;(); var propFields = properties .Concat(fields) .Where(pf =&gt; 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 =&gt; dynamic type var sourceItem = Expression.Parameter(sourceType, "item"); var bindings = dynamicType .GetFields() .Select(p =&gt; Expression.Bind( p, Expression.PropertyOrField(sourceItem, p.Name))) .OfType&lt;MemberBinding&gt;() .ToArray(); var fetcher = Expression.Lambda&lt;Func&lt;T, object&gt;&gt;( Expression.Convert( Expression.MemberInit( Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), typeof(object)), sourceItem); return fetcher; } } </code></pre> <p>Now, to use it:</p> <pre><code>// What you had originally (hand-tooled query) var db = entries.AsQueryable(); var query = db.GroupBy(x =&gt; new { Year = x.CreatedAt.Year, Month = x.CreatedAt.Month }, prj =&gt; prj.AuthorName) .Select(data =&gt; new { Key = data.Key.Year * 100 + data.Key.Month, // very ugly code, I know Details = data.GroupBy(y =&gt; y).Select(z =&gt; new { z.Key, Count = z.Count() }) }); var func = db.Epinephelinae(new[]{"CreatedAt", "AuthorName"}); var dquery = db.GroupBy(func, prj =&gt; prj.AuthorName); </code></pre> <p>This solution lacks the flexibility of "nested statements", like "CreatedDate.Month", but with a bit of imagination, you could possibly extend this idea to work with any freeform query.</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload