Skip to content

Improve EF Core SQL caching, parameter binding, and tests#1

Merged
riflosnake merged 2 commits intomainfrom
cache-ef-core-queries-and-improve-api
Dec 15, 2025
Merged

Improve EF Core SQL caching, parameter binding, and tests#1
riflosnake merged 2 commits intomainfrom
cache-ef-core-queries-and-improve-api

Conversation

@riflosnake
Copy link
Owner

  • Add dual-mode EF Core query execution with SQL template caching
  • Parameter binding now uses name analysis (no value collision)
  • QueryDefinition supports both DbParameter[] and anonymous params
  • TypedQueryResult: new result accessors (GetAll, GetSingle, etc.)
  • Add full integration tests and performance benchmarks
  • Update README with new performance data and usage docs
  • Pin Dapper to >=2.0.4, misc bug fixes and API cleanup

- Add dual-mode EF Core query execution with SQL template caching
- Parameter binding now uses name analysis (no value collision)
- QueryDefinition supports both DbParameter[] and anonymous params
- TypedQueryResult: new result accessors (GetAll, GetSingle, etc.)
- Add full integration tests and performance benchmarks
- Update README with new performance data and usage docs
- Pin Dapper to >=2.0.4, misc bug fixes and API cleanup
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces significant improvements to TypedQuery's EF Core integration by adding SQL template caching and dual-mode query execution. The changes enable TypedQuery to compile LINQ queries once via EF Core and then execute subsequent calls directly through Dapper using cached SQL templates, resulting in 2.5-3× performance improvements over sequential EF Core queries.

Key Changes:

  • Dual-mode execution model: First call compiles LINQ to SQL via EF Core and caches the template; subsequent calls bypass EF Core entirely and execute via Dapper with cached SQL
  • Parameter binding improvements: Hybrid parameter matching strategy using name analysis (extracting field names from EF Core's @__fieldName_0 pattern) with fallback to value matching
  • Anonymous object parameter support: QueryDefinition now accepts Dapper-style anonymous objects (new { id }) as the recommended approach instead of DbParameter arrays

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/TypedQuery/TypedQueryResult.cs Added new result accessor methods (GetAll, GetSingle, GetFirst, etc.) and marked Get() as obsolete
src/TypedQuery/TypedQuery.csproj Bumped minimum Dapper version from 2.0.0 to 2.0.4
src/TypedQuery/SqlBatchBuilder.cs Added RewriteAnonymousParametersInPlace to support anonymous object parameters
src/TypedQuery/DapperExecutionHelper.cs Removed ToList materialization to return IEnumerable directly from Dapper
src/TypedQuery.Tests/TypedQuery.Tests.csproj Added new console-based integration test project
src/TypedQuery.Tests/Program.cs Comprehensive integration tests covering raw SQL, EF Core, caching, and edge cases
src/TypedQuery.EntityFrameworkCore/Interceptor/EfCoreInterceptor.cs Implemented SQL template compilation and caching with hybrid parameter binding
src/TypedQuery.EntityFrameworkCore/Interceptor/CapturedQuery.cs Added CompiledSqlTemplate, ParameterBinding, and ParameterNameAnalyzer classes
src/TypedQuery.EntityFrameworkCore/ITypedQuery.cs Implemented dual-mode execution logic with template lookup and fallback
src/TypedQuery.EntityFrameworkCore/Extensions.cs Added generic UseTypedQuery overload
src/TypedQuery.Abstractions/QueryDefinition.cs Added constructor accepting anonymous objects as parameters
benchmarks/TypedQuery.Benchmarks/TypedQuery.Benchmarks.csproj Downgraded EF Core packages from 9.0.0 to 8.0.0
benchmarks/TypedQuery.Benchmarks/Benchmarks/EfCoreCachingBenchmarks.cs Added comprehensive benchmarks comparing cold/warm execution modes
TypedQuery.slnx Added test and benchmark projects to solution
README.md Updated with new performance benchmarks and SQL caching documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


var queryType = queryInstance.GetType();
var fields = queryType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
var fieldsByName = fields.ToDictionary(f => f.Name.ToLowerInvariant(), f => f);
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ToDictionary call on line 171 could throw an ArgumentException if there are multiple fields with the same name (after converting to lowercase). This could happen with certain inheritance scenarios or compiler-generated fields. Consider using a more robust approach that handles duplicate keys, such as using a Dictionary with Add checks or grouping fields by name and selecting the most appropriate one.

Suggested change
var fieldsByName = fields.ToDictionary(f => f.Name.ToLowerInvariant(), f => f);
var fieldsByName = fields
.GroupBy(f => f.Name.ToLowerInvariant())
.ToDictionary(g => g.Key, g => g.First());

Copilot uses AI. Check for mistakes.
/// Useful for queries that should return a single item.
/// Gets all results as an enumerable sequence for the specified result type.
/// This is the recommended method for queries that return multiple items.
/// Results are buffered by Dapper but not materialized to a list.
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "Results are buffered by Dapper but not materialized to a list" but this is misleading. According to the changes in DapperExecutionHelper.cs, the results are already enumerated and buffered when they come back from Dapper's ReadAsync. GetAll returns an IEnumerable but it's already a materialized collection (likely a List from Dapper), not a lazy enumerable. Consider clarifying the documentation or returning the data more explicitly as a collection type.

Suggested change
/// Results are buffered by Dapper but not materialized to a list.
/// Results are already fully materialized into a collection (typically a List) by Dapper.

Copilot uses AI. Check for mistakes.
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test project is configured with OutputType as "Exe" which suggests it's an executable console application rather than a proper test project. Consider using a test framework like xUnit, NUnit, or MSTest instead of a console application with manual assertions. This would provide better test discovery, reporting, and integration with CI/CD pipelines.

Suggested change
<OutputType>Exe</OutputType>

Copilot uses AI. Check for mistakes.
/// Measures the benefit of EF Core SQL caching in TypedQuery.
///
/// Key scenarios:
/// 1. Cold (first call) - EF Core compiles LINQ ? SQL
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment uses "?" instead of "→" (arrow) in "EF Core compiles LINQ ? SQL". This appears to be a typo or encoding issue. Should be "EF Core compiles LINQ → SQL" to match the consistent usage of arrows elsewhere in the codebase.

Suggested change
/// 1. Cold (first call) - EF Core compiles LINQ ? SQL
/// 1. Cold (first call) - EF Core compiles LINQ SQL

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EF Core packages were downgraded from version 9.0.0 to 8.0.0. While this may be intentional for compatibility reasons, consider documenting the reason in a comment or the PR description. If the library should support both EF Core 8.x and 9.x, ensure that the version range is properly tested, or consider using a version range like [8.0.0,10.0.0) if appropriate.

Suggested change
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<!-- Use a version range to support both EF Core 8.x and 9.x for compatibility and future-proofing. -->
<PackageReference Include="Microsoft.Data.Sqlite" Version="[8.0.0,10.0.0)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="[8.0.0,10.0.0)" />

Copilot uses AI. Check for mistakes.
var cleanName = originalName.TrimStart('@', ':', '?');
var newName = $"@tql{queryIndex}_{cleanName}";

modifiedSql = modifiedSql.Replace(paramNameInSql, newName);
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string.Replace for parameter name substitution can lead to incorrect replacements when parameter names are substrings of other identifiers. For example, if you have "@id" and "@idtype" as parameters, replacing "@id" could partially match "@idtype". Consider using a regex with word boundaries or a more precise replacement strategy that ensures you're only replacing actual parameter references.

Copilot uses AI. Check for mistakes.
Comment on lines +200 to +204
if (kvp.Key.Contains(extractedName.ToLowerInvariant()))
{
matchedField = kvp.Value;
break;
}
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback matching logic using string.Contains is too broad and could match the wrong field. For example, if you have fields "id" and "userId", extracting "id" would match both. This could lead to incorrect parameter bindings. Consider using a more precise matching strategy, such as checking if the extracted name is a complete suffix or using edit distance for fuzzy matching with a threshold.

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +205
foreach (var kvp in fieldsByName)
{
if (usedFields.Contains(kvp.Value)) continue;

// Check if field name contains the extracted name
if (kvp.Key.Contains(extractedName.ToLowerInvariant()))
{
matchedField = kvp.Value;
break;
}
}
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +219 to +229
foreach (var field in fields)
{
if (usedFields.Contains(field)) continue;

var fieldValue = field.GetValue(queryInstance);
if (fieldValue != null && AreValuesEqual(paramValue, fieldValue))
{
matchedField = field;
matchCount++;
}
}
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
{
}
// Trigger EF Core pipeline - interceptor captures SQL and suppresses execution
try { _ = query.ToList(); } catch { }
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Suggested change
try { _ = query.ToList(); } catch { }
try { _ = query.ToList(); } catch (Exception ex) { Console.Error.WriteLine($"Exception during EF Core pipeline trigger: {ex}"); }

Copilot uses AI. Check for mistakes.
@riflosnake riflosnake merged commit 24e3cd2 into main Dec 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant