jeudi 15 juillet 2021

Using Reflection to build EF Core query is faster than to using reflection

I've got an IQueryable Extension method that is being used to reduce the amount of boiler plate code needed to search a number of fields in an EF Core DbContext model:

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);
}

The code is working and producing expected results, however I was worried that I would get a performance hit for using Expressions and a bit of reflection. So I set up a benchmark using an in memory database and the BenchmarkDotNet nuget package. Here is the benchmark:

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);
        }
    }

Like I said, I was expecting performance penalties for using reflection. but the benchmark shows that the query being built by my extension method is more than 10 times faster than just writing the expression directly in the Where method:

|               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 |

Can anyone give an explanation for this?





Aucun commentaire:

Enregistrer un commentaire