【发布时间】:2021-09-24 21:52:51
【问题描述】:
我有一个 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);
}
代码正在运行并产生预期的结果,但是我担心使用表达式和一些反射会降低性能。所以我使用内存数据库和 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);
}
}
就像我说的,我预计使用反射会导致性能下降。但基准测试表明,通过我的扩展方法构建的查询比直接在 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 |
谁能解释一下?
【问题讨论】:
-
你生成的 SQL 是什么?另一个生成的是什么 SQL?
-
内存数据库总是会更快地进行基准测试。反射与此无关。您是否在问为什么 In-Memory 数据库在基准测试中比传统的 SQL 支持的 DBContext 执行得更快?
-
不知道,应该会慢一点。检查一下,也许 JIT 优化了整个测试体?
标签: c# .net-core reflection entity-framework-core expression