3

I am using linq2db and while it works well enough for most CRUD operations I have encountered many expressions that it just cannot translate into SQL.

It has gotten to the point where unless I know in advance exactly what kinds of expressions will be involved and have successfully invoked them before, I am worried that any benefit derived from linq2db will be outweighed by the cost of trying to find and then remove (or move away from the server side) the offending expressions.

If I knew how to tell linq2db how to parse an Expression<Func<T,out T>> or whatnot into SQL whenever on an ad-hoc, as-it-is-needed basis, then I would be much more confident and I could do many things using this tool.

Take, for instance, String.Split(char separator), the method that takes a string and a char to return a string[] of each substring between the separator.

Suppose my table Equipment has a nullable varchar field Usages that contains lists of different equipment usages separated by commas.

I need to implement IList<string> GetUsages(string tenantCode, string needle = null) that will give provide a list of usages for a given tenant code and optional search string.

My query would then be something like:

var listOfListOfStringUsages =
    from et in MyConnection.GetTable<EquipmentTenant>()
    join e in MyConnection.GetTable<Equipment>() on et.EquipmentId = e.EquipmentId
    where (et.TenantCode == tenantCode)
    where (e.Usages != null)
    select e.Usages.Split(','); // cannot convert to sql here

var flattenedListOfStringUsages = 
    listOfListOfStringUsages.SelectMany(strsToAdd => strsToAdd)
                            .Select(str => str.Trim())
                            .Distinct();

var list = flattenedListOfStringUsages.ToList();

However, it would actually bomb out at runtime on the line indicated by comment.

I totally get that linq2db's creators cannot possibly be expected to ship with every combination of string method and major database package.

At the same time I feel as though could totally tell it how to handle this if I could just see an example of doing just that (someone implementing a custom expression).

So my question is: how do I instruct linq2db on how to parse an Expression that it cannot parse out of the box?

John Saunders
  • 160,644
  • 26
  • 247
  • 397
tacos_tacos_tacos
  • 10,277
  • 11
  • 73
  • 126
  • What SQL has to be generated ? – IT. May 03 '15 at 20:25
  • Basically I think this question requires a knowledge of LinqToDB in particular. When parsing the expression tree it halts on "select e.Usages.Split` and says it doesn't know what to do with `String.Split` function – tacos_tacos_tacos May 03 '15 at 22:18

2 Answers2

4

A few years ago I wrote something like this:

public class StoredFunctionAccessorAttribute : LinqToDB.Sql.FunctionAttribute
{
    public StoredFunctionAccessorAttribute()
    {
        base.ServerSideOnly = true;
    }

    // don't call these properties, they are made private because user of the attribute must not change them
    // call base.* if you ever need to access them
    private new bool ServerSideOnly { get; set; }
    private new int[] ArgIndices { get; set; }
    private new string Name { get; set; }
    private new bool PreferServerSide { get; set; }

    public override ISqlExpression GetExpression(System.Reflection.MemberInfo member, params ISqlExpression[] args)
    {
        if (args == null)
            throw new ArgumentNullException("args");

        if (args.Length == 0)
        {
            throw new ArgumentException(
                "The args array must have at least one member (that is a stored function name).");
        }

        if (!(args[0] is SqlValue))
            throw new ArgumentException("First element of the 'args' argument must be of SqlValue type.");

        return new SqlFunction(
            member.GetMemberType(),
            ((SqlValue)args[0]).Value.ToString(),
            args.Skip(1).ToArray());
    }
}

public static class Sql
{
    private const string _serverSideOnlyErrorMsg = "The 'StoredFunction' is server side only function.";

    [StoredFunctionAccessor]
    public static TResult StoredFunction<TResult>(string functionName)
    {
        throw new InvalidOperationException(_serverSideOnlyErrorMsg);
    }

    [StoredFunctionAccessor]
    public static TResult StoredFunction<TParameter, TResult>(string functionName, TParameter parameter)
    {
        throw new InvalidOperationException(_serverSideOnlyErrorMsg);
    }
}

...

[Test]
public void Test()
{
    using (var db = new TestDb())
    {
        var q = db.Customers.Select(c => Sql.StoredFunction<string, int>("Len", c.Name));
        var l = q.ToList();
    }
}

(and of course you can write your wrappers around Sql.StoredFunction() methods to get rid of specifying function name as a string every time)

Generated sql (for the test in the code above):

SELECT
    Len([t1].[Name]) as [c1]
FROM
    [dbo].[Customer] [t1]

PS. We use linq2db extensively in our projects and completely satisfied with it. But yes, there is a learning curve (as with almost everything serious we learn) and one needs to spend some time learning and playing with the library in order to feel comfortable with it and see all the benefits it can give.

nightcoder
  • 13,149
  • 16
  • 64
  • 72
1

Try listOfListOfStringUsages.AsEnumerable(). It will enforce executing SelectMany on client side.

UPDATE:

I used the following code to reproduce the issue:

var q =
    from t in db.Table
    where t.StringField != null
    select t.StringField.Split(' ');

var q1 = q
    //.AsEnumerable()
    .SelectMany(s => s)
    .Select(s => s.Trim())
    .Distinct()
    .ToList();

It's not working. But if I uncomment .AsEnumerable(), it works just fine.

IT.
  • 859
  • 4
  • 11
  • I did already... that's not the issue. I guess something infers that `string[]` **is** `IEnumerable` (I know that probably isn't **technically** true but it has the same effect as `AsEnumerable()`) – tacos_tacos_tacos May 03 '15 at 23:29
  • 1
    The issue is that when it does get around to being enumerated, ie making the query and running it, it cannot determine what to do with that portion of the query. If I move the `.Split` out of the first variable and into the second, putting a `ToList()` in between the two, it "works" in a crappy way - it does it **in memory** only after getting thousands or millions of records. – tacos_tacos_tacos May 03 '15 at 23:32