Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(SqlExpressionFactory): compiled lambda expression cache #311

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions src/Dommel/DommelMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Dapper;
Expand All @@ -15,6 +16,7 @@ namespace Dommel;
public static partial class DommelMapper
{
private static readonly Func<Type, SqlMapper.ITypeMap> DefaultTypeMapProvider;
private static readonly ConcurrentDictionary<Type, Func<ISqlBuilder, object>> ExpressionCache = new();

internal static ConcurrentDictionary<QueryCacheKey, string> QueryCache { get; } = new ConcurrentDictionary<QueryCacheKey, string>();
internal static IPropertyResolver PropertyResolver = new DefaultPropertyResolver();
Expand Down Expand Up @@ -139,12 +141,35 @@ public static ISqlBuilder GetSqlBuilder(IDbConnection connection)
}

/// <summary>
/// The factory to create <see cref="SqlExpression{TEntity}"/> or custom instances.
/// A compiled expression cache factory to create <see cref="SqlExpression{TEntity}"/>.
/// Creates a compiled lambda expression to instantiate the generic type, passing the
/// parameter and constructor an <see cref="ISqlBuilder"/>. Delegate for a given type
/// is compiled only once and stored in the ExpressionCache. All subsequent calls use the cached delegate.
/// </summary>
public static Func<Type, ISqlBuilder, object> SqlExpressionFactory = (type, sqlBuilder) =>
{
var expr = typeof(SqlExpression<>).MakeGenericType(type);
return Activator.CreateInstance(expr, sqlBuilder)!;
var compiledFactory = ExpressionCache.GetOrAdd(type, t =>
{
// Create the type `SqlExpression<TEntity>`
var sqlExpressionType = typeof(SqlExpression<>).MakeGenericType(t);

// Parameter: ISqlBuilder sqlBuilder
var sqlBuilderParam = Expression.Parameter(typeof(ISqlBuilder), "sqlBuilder");

// Constructor that takes ISqlBuilder
var ctor = sqlExpressionType.GetConstructor([typeof(ISqlBuilder)])
?? throw new InvalidOperationException($"No suitable constructor found for type {sqlExpressionType.Name}");

// Expression: new SqlExpression<TEntity>(sqlBuilder)
var newExpression = Expression.New(ctor, sqlBuilderParam);

// Compile: (ISqlBuilder sqlBuilder) => new SqlExpression<TEntity>(sqlBuilder)
var lambda = Expression.Lambda<Func<ISqlBuilder, object>>(newExpression, sqlBuilderParam);
return lambda.Compile();
});

// Execute the compiled delegate
return compiledFactory(sqlBuilder);
};

internal static SqlExpression<TEntity> CreateSqlExpression<TEntity>(ISqlBuilder sqlBuilder)
Expand Down
1 change: 1 addition & 0 deletions test/Dommel.Tests/Dommel.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
21 changes: 21 additions & 0 deletions test/Dommel.Tests/SqlExpressions/SqlExpressionFactoryBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using BenchmarkDotNet.Attributes;

namespace Dommel.Tests.SqlExpressions;

[MemoryDiagnoser(true)]
public class SqlExpressionFactoryBenchmarks
{
private static readonly ISqlBuilder DummySqlBuilder = new SqlServerSqlBuilder();

[Benchmark(Baseline = true)]
public object ActivatorBasedFactory()
{
return DommelMapper.SqlExpressionFactory(typeof(Product), DummySqlBuilder);
}

[Benchmark]
public object CompiledExpressionFactory()
{
return DommelMapper.CompiledSqlExpressionFactory(typeof(Product), DummySqlBuilder);
}
}
31 changes: 31 additions & 0 deletions test/Dommel.Tests/SqlExpressions/SqlExpressionFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;
using Xunit;
using Xunit.Abstractions;

namespace Dommel.Tests.SqlExpressions;
public class SqlExpressionFactoryTests
{
private readonly ITestOutputHelper _outputHelper;

public SqlExpressionFactoryTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}

[Fact]
public void Run_Benchmarks()
{
var logger = new AccumulationLogger();

var config = ManualConfig.Create(DefaultConfig.Instance)
.AddLogger(logger)
.WithOptions(ConfigOptions.Default);

BenchmarkRunner.Run<SqlExpressionFactoryBenchmarks>(config);

// write benchmark summary
_outputHelper.WriteLine(logger.GetLog());
}
}