Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
Admiral Snackbar
Mar 13, 2006

OUR SNEEZE SHIELDS CANNOT REPEL A HUNGER OF THAT MAGNITUDE
I have a question about including an expression in an EF select operation. If I run this code against SQL Server:
code:
var tc = ctx.TestClasses
            .Select(x => new
            {
                FullName = x.GroupName != null && x.DisplayName != null
                           ? $"{x.GroupName.Substring(0, 3).ToUpper()}: {x.DisplayName}"
                           : string.Empty
            })
            .ToList();
much of the work of the lambda is translated into SQL and run server-side:
code:
SELECT CASE
    WHEN ([t].[GroupName] IS NOT NULL) AND ([t].[DisplayName] IS NOT NULL) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END, UPPER(SUBSTRING([t].[GroupName], 0 + 1, 3)), [t].[DisplayName]
FROM [TestClasses] AS [t]
Now that's great for this one case, but if I want to reuse the contents of the lambda elsewhere, I don't see a way to factor it out of this query but keep it running server-side. I think I should be able to do it with an expression, but doing this:
code:
Expression<Func<TestClass, string>> fullName = x => x.GroupName != null && x.DisplayName != null
                                                  ? $"{x.GroupName.Substring(0, 3).ToUpper()}: {x.DisplayName.Substring(0, 3)}"
                                                  : string.Empty;

var tc = ctx.TestClasses
            .Select(x => new
            {
                FullName = fullName.Compile()(x)
            })
            .ToList();
causes the lambda to run client-side instead:
code:
SELECT [t].[Id], [t].[DisplayName], [t].[GroupName]
FROM [TestClasses] AS [t]
Is there a way to incorporate my expression into the EF expression tree to push the work back to the server?

Adbot
ADBOT LOVES YOU

Admiral Snackbar
Mar 13, 2006

OUR SNEEZE SHIELDS CANNOT REPEL A HUNGER OF THAT MAGNITUDE
I appreciate everyone's feedback on this. I wanted to see if I could make this work by manipulating expression trees, and I put together a visitor that actually works (at least for anonymous types):
code:
internal class PropertyExpressionVisitor<T> : ExpressionVisitor
{
    private readonly Dictionary<string, Expression> propertyExpressions;

    public PropertyExpressionVisitor(Dictionary<string, Expression> propertyExpressions)
    {
        this.propertyExpressions = propertyExpressions;
    }

    [return: NotNullIfNotNull("node")]
    public override Expression? Visit(Expression? node)
    {
        if (node is not LambdaExpression lambda 
         || lambda.ReturnType != typeof(T)
         || lambda.Body is not NewExpression oldConstructorExp
         || oldConstructorExp.Constructor?.DeclaringType != typeof(T)
         || oldConstructorExp.Arguments == null)
            return base.Visit(node);

        var arguments = new List<Expression>(oldConstructorExp.Arguments);            

        foreach (var pe in propertyExpressions.Keys)
        {
            var membIndex = oldConstructorExp.Members?
                                             .Select((m, i) => new { m, i })
                                             .Where(mi => mi.m.Name == pe)
                                             .SingleOrDefault()?.i;

            if (!membIndex.HasValue)
                continue;

            var propExp = Expression.Invoke(propertyExpressions[pe], lambda.Parameters);
            arguments[membIndex.Value] = propExp;
        }

        var newConstructorExp = Expression.New(oldConstructorExp.Constructor, arguments);
        var newLambda = Expression.Lambda(newConstructorExp, lambda.Parameters);

        return newLambda;
    }
}
That paired with this thing successfully convice EF to do the operations server-side:
code:
public static IQueryable<TResult> SelectWithExpressionProperties<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, params KeyValuePair<string, Expression>[] propertySelectors)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));
    if (propertySelectors == null) throw new ArgumentNullException(nameof(propertySelectors));

    var select = source.Select(selector);
    var visitor = new PropertyExpressionVisitor<TResult>(propertySelectors.ToDictionary(ps => ps.Key, ps => ps.Value));            
    var updatedSelect = select.InterceptWith(visitor);

    return updatedSelect;
}
Calling it looks like this (the anonymous type needs placeholder properties for this to work):
code:
var ctx = new Context();

Expression adder = (TestClass x) => x.Id + 17;

var tc = ctx.TestClasses
            .SelectWithExpressionProperties(x => new
                {
                    Id = 0,
                    FullName = ""
                }, 
                new KeyValuePair<string, Expression>("FullName", ExpressionsHelper.FullName),
                new KeyValuePair<string, Expression>("Id", adder))
            .ToList();
Which generates this SQL:
code:
SELECT [t].[Id] + 17, CASE
    WHEN ([t].[GroupName] IS NOT NULL) AND ([t].[DisplayName] IS NOT NULL) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END, UPPER(SUBSTRING([t].[GroupName], 0 + 1, 3)), SUBSTRING([t].[DisplayName], 0 + 1, 3)
FROM [TestClasses] AS [t]
Now, whether or not this is actually a good idea is another question...

Admiral Snackbar
Mar 13, 2006

OUR SNEEZE SHIELDS CANNOT REPEL A HUNGER OF THAT MAGNITUDE
I've been learning how to write analyzers and code fix providers, and I have a question about bundling them in NuGet packages. For example, I have an existing NuGet package that is used by some other projects. I have written some analyzers and fixers to address some common issues that have come up when using the NuGet package. Is there a way to update the existing package to include the new analyzers and have those analyzers automatically run when a client project updates its package version?

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply