MrW*_*rus 2 c# reflection expression entity-framework-core .net-core
我有一个 IQueryable 扩展方法,用于减少在 EF Core DbContext 模型中搜索多个字段所需的样板代码量:
public static IQueryable<TEntity> WherePropertyIsLikeIfStringIsNotEmpty<TEntity>(this IQueryable<TEntity> query,
string searchValue, Expression<Func<TEntity, string>> propertySelectorExpression)
{
if (string.IsNullOrEmpty(searchValue) || !(propertySelectorExpression.Body is MemberExpression memberExpression))
{
return query;
}
// get method info for EF.Functions.Like
var likeMethod = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new []
{
typeof(DbFunctions),
typeof(string),
typeof(string)
});
var searchValueConstant = Expression.Constant($"%{searchValue}%");
var dbFunctionsConstant = Expression.Constant(EF.Functions);
var propertyInfo = typeof(TEntity).GetProperty(memberExpression.Member.Name);
var parameterExpression = Expression.Parameter(typeof(TEntity));
var propertyExpression = Expression.Property(parameterExpression, propertyInfo);
var callLikeExpression = Expression.Call(likeMethod, dbFunctionsConstant, propertyExpression, searchValueConstant);
var lambda = Expression.Lambda<Func<TEntity, bool>>(callLikeExpression, parameterExpression);
return query.Where(lambda);
}
Run Code Online (Sandbox Code Playgroud)
代码正在运行并产生了预期的结果,但是我担心使用表达式和一些反射会影响性能。所以我使用内存数据库和 BenchmarkDotNet nuget 包设置了一个基准测试。这是基准:
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;
class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<Benchmark>();
}
}
public class Benchmark
{
private Context _context;
private string SearchValue1 = "BCD";
private string SearchValue2 = "FG";
private string SearchValue3 = "IJ";
[GlobalSetup]
public void Setup()
{
_context = new Context(new DbContextOptionsBuilder<Context>().UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options);
_context.TestModels.Add(new TestModel(1, "ABCD", "EFGH", "HIJK"));
_context.SaveChanges();
}
[GlobalCleanup]
public void Cleanup()
{
_context.Dispose();
}
[Benchmark]
public void FilterUsingExtension()
{
var _ = _context.TestModels
.WherePropertyIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value)
.WherePropertyIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue)
.WherePropertyIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue)
.ToList();
}
[Benchmark]
public void FilterTraditionally()
{
var query = _context.TestModels.AsQueryable();
if (!string.IsNullOrEmpty(SearchValue1))
{
query = query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"));
}
if (!string.IsNullOrEmpty(SearchValue2))
{
query = query.Where(x => EF.Functions.Like(x.OtherValue, $"%{SearchValue2}%"));
}
if (!string.IsNullOrEmpty(SearchValue3))
{
query = query.Where(x => EF.Functions.Like(x.ThirdValue, $"%{SearchValue3}%"));
}
var _ = query.ToList();
}
}
public class TestModel
{
public int Id { get; }
public string Value { get; }
public string OtherValue { get; }
public string ThirdValue { get; }
public TestModel(int id, string value, string otherValue, string thirdValue)
{
Id = id;
Value = value;
OtherValue = otherValue;
ThirdValue = thirdValue;
}
}
public class Context : DbContext
{
public Context(DbContextOptions<Context> options)
: base(options)
{
}
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public DbSet<TestModel> TestModels { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TestModel>().ToTable("test_class", "test");
modelBuilder.Entity<TestModel>().Property(x => x.Id).HasColumnName("id").HasColumnType("int");
modelBuilder.Entity<TestModel>().Property(x => x.Value).HasColumnName("value").HasColumnType("varchar")
.ValueGeneratedNever();
modelBuilder.Entity<TestModel>().Property(x => x.OtherValue).HasColumnName("other_value").HasColumnType("varchar")
.ValueGeneratedNever();
modelBuilder.Entity<TestModel>().Property(x => x.ThirdValue).HasColumnName("third_value").HasColumnType("varchar")
.ValueGeneratedNever();
modelBuilder.Entity<TestModel>().HasKey(x => x.Id);
}
}
Run Code Online (Sandbox Code Playgroud)
就像我说的,我期待使用反射的性能损失。但基准测试表明,由我的扩展方法构建的查询比直接在 Where 方法中编写表达式快 10 倍以上:
| Method | Mean | Error | StdDev | Median |
|--------------------- |------------:|----------:|----------:|------------:|
| FilterUsingExtension | 73.73 us | 1.381 us | 3.310 us | 72.36 us |
| FilterTraditionally | 1,036.60 us | 20.494 us | 22.779 us | 1,032.69 us |
Run Code Online (Sandbox Code Playgroud)
任何人都可以对此做出解释吗?
简而言之,不同之处在于pattern参数的不同表达式EF.Functions.Like,以及 LINQ to Objects(由 EF Core InMemory 提供程序使用)处理IQueryable表达式树的方式。
首先,使用 EF Core InMemory 提供程序针对小数据集进行性能测试是无关紧要的,因为它基本上测量的是查询表达式树的构建,而在真实数据库的情况下,大部分时间是执行生成的 SQL 查询,返回和物化结果数据集。
二、关于
我担心我会因为使用表达式和一些反思而受到影响
这两种方法都使用类方法在运行时构建查询表达式树Expression。唯一的区别是 C# 编译器在编译时为您生成该代码,因此没有反射调用。但是您的代码也可以轻松修改以避免反射,从而使生成完全等效。
更重要的区别是您的代码正在发出ConstantExpression,而当前 C# 编译器无法从变量生成常量表达式,因此它总是发出闭包,而闭包又由 EF Core 查询转换器绑定为查询参数。通常建议将其用于 SQL 查询,因此您最好在方法中执行相同的操作,或者可以选择这样做。
因此,简要回顾一下,您的方法绑定了常量表达式,而编译器方法绑定了闭包。但不仅如此。看这里
query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"))
Run Code Online (Sandbox Code Playgroud)
SearchValue1变量被转换成闭合,但由于$"%{SearchValue1}%"是的一部分表达,这不是在该点进行评价,但被记录为MethodCallExpression至string.Format。
这两者在 LINQ to Objects 中提供了很大的性能差异,因为它通过首先将表达式编译为委托,然后运行它来执行查询表达式树。所以最后你的代码传递常量值,编译器生成查询代码调用string.Format。并且两者在编译/执行时间上存在很大差异。在您的测试中乘以 3。
说了这么多,让我们看看它的实际效果。
第一,优化的扩展方法,具有一次性静态反射信息缓存和使用常量或变量的选项:
public static IQueryable<TEntity> WhereIsLikeIfStringIsNotEmpty<TEntity>(
this IQueryable<TEntity> query,
string searchValue,
Expression<Func<TEntity, string>> selector,
bool useVariable = false)
{
if (string.IsNullOrEmpty(searchValue)) return query;
var parameter = selector.Parameters[0];
var pattern = Value($"%{searchValue}%", useVariable);
var body = Expression.Call(LikeMethod, DbFunctionsArg, selector.Body, pattern);
var predicate = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
return query.Where(predicate);
}
static Expression Value(string value, bool variable)
{
if (!variable) return Expression.Constant(value);
return Expression.Property(
Expression.Constant(new StringVar { Value = value }),
StringVar.ValueProperty);
}
class StringVar
{
public string Value { get; set; }
public static PropertyInfo ValueProperty { get; } = typeof(StringVar).GetProperty(nameof(Value));
}
static Expression DbFunctionsArg { get; } = Expression.Constant(EF.Functions);
static MethodInfo LikeMethod { get; } = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new[]
{
typeof(DbFunctions),
typeof(string),
typeof(string)
});
Run Code Online (Sandbox Code Playgroud)
请注意,我Property从方法名称中删除了和 的要求MemberExpression,因为它不需要 - 该方法将适用于任何string返回表达式。
其次,为其添加两个新的基准测试方法:
[Benchmark]
public void FilterUsingExtensionOptimizedUsingConstant()
{
var _ = _context.TestModels
.WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, false)
.WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, false)
.WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, false)
.ToList();
}
[Benchmark]
public void FilterUsingExtensionOptimizedUsingVariable()
{
var _ = _context.TestModels
.WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, true)
.WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, true)
.WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, true)
.ToList();
}
Run Code Online (Sandbox Code Playgroud)
最后,为string.Format在表达式树中避免的“传统方式”的优化版本添加基准测试(但仍然绑定变量):
[Benchmark]
public void FilterTraditionallyOptimized()
{
var query = _context.TestModels.AsQueryable();
if (!string.IsNullOrEmpty(SearchValue1))
{
var pattern = $"%{SearchValue1}%";
query = query.Where(x => EF.Functions.Like(x.Value, pattern));
}
if (!string.IsNullOrEmpty(SearchValue2))
{
var pattern = $"%{SearchValue2}%";
query = query.Where(x => EF.Functions.Like(x.OtherValue, pattern));
}
if (!string.IsNullOrEmpty(SearchValue3))
{
var pattern = $"%{SearchValue3}%";
query = query.Where(x => EF.Functions.Like(x.ThirdValue, pattern));
}
var _ = query.ToList();
}
Run Code Online (Sandbox Code Playgroud)
结果:
| 方法 | 意思 | 错误 | 标准差 |
|---|---|---|---|
| 过滤器使用扩展 | 51.84 美元 | 0.089 美元 | 0.079 美元 |
| FilterUsingExtensionOptimizedUsingConstant | 48.95 美元 | 0.061 美元 | 0.054 美元 |
| FilterUsingExtensionOptimizedUsingVariable | 58.40 美元 | 0.354 美元 | 0.331 美元 |
| 传统过滤 | 625.40 美元 | 1.269 美元 | 1.187 美元 |
| 过滤器传统优化 | 60.09 美元 | 0.491 美元 | 0.435 美元 |
正如我们所看到的,使用常量的优化扩展方法最快,但非常接近您的原始方法(这意味着反射不是必需的)。
带变量的变体有点慢,但在用于真实数据库时通常会更好。
优化后的“传统”方法比前两种慢一点,这有点令人惊讶,但差异可以忽略不计。
由于上述原因,原始的“传统”方法比以前所有方法都慢。但是对于真实的数据库,它在整个查询执行中可以忽略不计。
| 归档时间: |
|
| 查看次数: |
60 次 |
| 最近记录: |