- 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?
|
#
¿
Jul 11, 2023 15:06
|
|
- Adbot
-
ADBOT LOVES YOU
|
|
#
¿
May 17, 2024 16:52
|
|
- 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...
|
#
¿
Jul 12, 2023 14:06
|
|
- 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?
|
#
¿
Apr 3, 2024 21:15
|
|