Calling DB function STRING_AGG from linq using custom mappings

Posts   
 
    
MMStan
User
Posts: 8
Joined: 10-Aug-2016
# Posted on: 29-Feb-2024 22:22:15   

We want to run this query via linq:

SELECT p.SURNAME, STRING_AGG(CONVERT(NVARCHAR(max), p.PHONE), ','), STRING_AGG(CONVERT(NVARCHAR(max), p.EMAIL), ',')
FROM PERSON p
GROUP BY p.SURNAME

We have this mapping

Add(new FunctionMapping(typeof(LinqMethods), "StringAgg", 1, "STRING_AGG(CONVERT(NVARCHAR(max), {0}), ',')"));

How can we write linq? The below one obviously does not work because we do not have an access to PHONE/EMAIL:

var q = from p in db.Person
        group p by p.Surname into g
        orderby g.Key
        select new
               {
                   g.Key,
                   StringAggForPhone = LinqMethods.StringAgg(p.Phone),
                   StringAggForEmail = LinqMethods.StringAgg(p.Email)
               };

We specifically want to use linq and not TypedLists or stored procedure because the linq is more complex containing joins & filters from other part of the system.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 01-Mar-2024 09:35:22   

This is more a C#/linq question and not related to our framework, but here we go: The g in the group by is an IGrouping<> object which is a set. So you can't access a single element's field in the projection as 'g' is the only element available. This is a mismatch between SQL and Linq and also the reason why Group by in linq sucks so much and is so hard to get right (and often fails).

So if you want to use linq, you have to use another projection onto g, or use g.FirstOrDefault().Phone etc. (but that would introduce a select top query which is likely not what you want). Aggregate functions like Sum are defined as public static long? Sum<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, long?>> selector) which require a handler for the tree specified, and that goes beyond a simple function mapping.

To work around this, use QuerySpec, as you can define what you want there without the overdesigned complexity of Linq in this case:

var qf = new QueryFactory();
var q = qf.Create()
          .Select(() => new
                        {
                            CustomerId = OrderFields.CustomerId.ToValue<string>(),
                            F = NorthwindFunctionsQS.HasOrderShipped(OrderFields.OrderId).ToValue<bool>()
                        })
          .OrderBy(OrderFields.OrderId.Ascending())
          .GroupBy(OrderFields.CustomerId);
q.CustomFunctionMappingStore = new NorthwindFunctionMappingsQS();
var results = adapter.FetchQuery(q);

Here I specify a function which is used on a field from the set itself.

Frans Bouma | Lead developer LLBLGen Pro