Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Linq.Expressions;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using EntityFrameworkCore.Projectables.Benchmarks.Helpers;
using EntityFrameworkCore.Projectables.Services;

namespace EntityFrameworkCore.Projectables.Benchmarks
{
/// <summary>
/// Micro-benchmarks <see cref="ProjectionExpressionResolver.FindGeneratedExpression"/> in
/// isolation (no EF Core overhead) to directly compare the static registry path against
/// the reflection-based path (<see cref="ProjectionExpressionResolver.FindGeneratedExpressionViaReflection"/>).
/// </summary>
[MemoryDiagnoser]
public class ExpressionResolverBenchmark
{
private static readonly MemberInfo _propertyMember =
typeof(TestEntity).GetProperty(nameof(TestEntity.IdPlus1))!;

private static readonly MemberInfo _methodMember =
typeof(TestEntity).GetMethod(nameof(TestEntity.IdPlus1Method))!;

private static readonly MemberInfo _methodWithParamMember =
typeof(TestEntity).GetMethod(nameof(TestEntity.IdPlusDelta), new[] { typeof(int) })!;

private readonly ProjectionExpressionResolver _resolver = new();

// ── Registry (source-generated) path ─────────────────────────────────

[Benchmark(Baseline = true)]
public LambdaExpression? ResolveProperty_Registry()
=> _resolver.FindGeneratedExpression(_propertyMember);

[Benchmark]
public LambdaExpression? ResolveMethod_Registry()
=> _resolver.FindGeneratedExpression(_methodMember);

[Benchmark]
public LambdaExpression? ResolveMethodWithParam_Registry()
=> _resolver.FindGeneratedExpression(_methodWithParamMember);

// ── Reflection path ───────────────────────────────────────────────────

[Benchmark]
public LambdaExpression? ResolveProperty_Reflection()
=> ProjectionExpressionResolver.FindGeneratedExpressionViaReflection(_propertyMember);

[Benchmark]
public LambdaExpression? ResolveMethod_Reflection()
=> ProjectionExpressionResolver.FindGeneratedExpressionViaReflection(_methodMember);

[Benchmark]
public LambdaExpression? ResolveMethodWithParam_Reflection()
=> ProjectionExpressionResolver.FindGeneratedExpressionViaReflection(_methodWithParamMember);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ public class TestEntity

[Projectable]
public int IdPlus1Method() => Id + 1;

[Projectable]
public int IdPlusDelta(int delta) => Id + delta;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace EntityFrameworkCore.Projectables.Benchmarks
{
[MemoryDiagnoser]
public class PlainOverhead
{
[Benchmark(Baseline = true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace EntityFrameworkCore.Projectables.Benchmarks
{
[MemoryDiagnoser]
public class ProjectableExtensionMethods
{
const int innerLoop = 10000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace EntityFrameworkCore.Projectables.Benchmarks
{
[MemoryDiagnoser]
public class ProjectableMethods
{
const int innerLoop = 10000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace EntityFrameworkCore.Projectables.Benchmarks
{
[MemoryDiagnoser]
public class ProjectableProperties
{
const int innerLoop = 10000;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Linq;
using BenchmarkDotNet.Attributes;
using EntityFrameworkCore.Projectables.Benchmarks.Helpers;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCore.Projectables.Benchmarks
{
/// <summary>
/// Measures the per-DbContext cold-start cost of resolver lookup by creating a new
/// <see cref="TestDbContext"/> on every iteration. The previous benchmarks reuse a single
/// DbContext for 10 000 iterations, so the resolver cache is warm after the first query —
/// these benchmarks expose the cost of the very first query per context.
/// </summary>
[MemoryDiagnoser]
public class ResolverOverhead
{
const int Iterations = 1000;

/// <summary>Baseline: no projectables, new DbContext per query.</summary>
[Benchmark(Baseline = true)]
public void WithoutProjectables_FreshDbContext()
{
for (int i = 0; i < Iterations; i++)
{
using var dbContext = new TestDbContext(false);
dbContext.Entities.Select(x => x.Id + 1).ToQueryString();
}
}

/// <summary>
/// New DbContext per query with a projectable property.
/// After the registry is in place this should approach baseline overhead.
/// </summary>
[Benchmark]
public void WithProjectables_FreshDbContext_Property()
{
for (int i = 0; i < Iterations; i++)
{
using var dbContext = new TestDbContext(true, false);
dbContext.Entities.Select(x => x.IdPlus1).ToQueryString();
}
}

/// <summary>
/// New DbContext per query with a projectable method.
/// After the registry is in place this should approach baseline overhead.
/// </summary>
[Benchmark]
public void WithProjectables_FreshDbContext_Method()
{
for (int i = 0; i < Iterations; i++)
{
using var dbContext = new TestDbContext(true, false);
dbContext.Entities.Select(x => x.IdPlus1Method()).ToQueryString();
}
}

/// <summary>
/// New DbContext per query with a projectable method that takes a parameter,
/// exercising parameter-type disambiguation in the registry key.
/// </summary>
[Benchmark]
public void WithProjectables_FreshDbContext_MethodWithParam()
{
for (int i = 0; i < Iterations; i++)
{
using var dbContext = new TestDbContext(true, false);
dbContext.Entities.Select(x => x.IdPlusDelta(5)).ToQueryString();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Polyfill for C# 9 record types when targeting netstandard2.0 or netstandard2.1
// The compiler requires this type to exist in order to use init-only setters (used by records).
namespace System.Runtime.CompilerServices
{
internal sealed class IsExternalInit { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Collections.Immutable;
using System.Linq;

namespace EntityFrameworkCore.Projectables.Generator
{
/// <summary>
/// Incremental-pipeline-safe representation of a single projectable member.
/// Contains only primitive types and an equatable wrapper around <see cref="ImmutableArray{T}"/>
/// so that structural value equality works correctly across incremental generation steps.
/// </summary>
sealed internal record ProjectableRegistryEntry(
string DeclaringTypeFullName,
string MemberKind,
string MemberLookupName,
string GeneratedClassFullName,
bool IsGenericClass,
bool IsGenericMethod,
EquatableImmutableArray ParameterTypeNames
);

/// <summary>
/// A structural-equality wrapper around <see cref="ImmutableArray{T}"/> of strings.
/// <see cref="ImmutableArray{T}"/> uses reference equality by default, which breaks
/// Roslyn's incremental-source-generator caching when the same logical array is
/// produced by two different steps. This wrapper provides element-wise equality so
/// that incremental steps are correctly cached and skipped.
/// </summary>
internal readonly struct EquatableImmutableArray : System.IEquatable<EquatableImmutableArray>
{
public static readonly EquatableImmutableArray Empty = new(ImmutableArray<string>.Empty);

public readonly ImmutableArray<string> Array;

public EquatableImmutableArray(ImmutableArray<string> array)
{
Array = array;
}

public bool IsDefaultOrEmpty => Array.IsDefaultOrEmpty;

public bool Equals(EquatableImmutableArray other) =>
Array.SequenceEqual(other.Array);

public override bool Equals(object? obj) =>
obj is EquatableImmutableArray other && Equals(other);

public override int GetHashCode()
{
unchecked
{
var hash = 17;
foreach (var s in Array)
hash = hash * 31 + (s?.GetHashCode() ?? 0);
return hash;
}
}

public static implicit operator ImmutableArray<string>(EquatableImmutableArray e) => e.Array;
public static implicit operator EquatableImmutableArray(ImmutableArray<string> a) => new(a);
}
}
Loading