Skip to content

Commit 46cca26

Browse files
committed
parameterize for perf
1 parent e136b0c commit 46cca26

80 files changed

Lines changed: 691 additions & 128 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CS1591;NU5104;CS1573;CS9107;NU1608;NU1109</NoWarn>
5-
<Version>34.2.0</Version>
5+
<Version>34.2.2</Version>
66
<LangVersion>preview</LangVersion>
77
<AssemblyVersion>1.0.0</AssemblyVersion>
88
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>

src/GraphQL.EntityFramework/GlobalUsings.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
global using Microsoft.EntityFrameworkCore;
55
global using Microsoft.EntityFrameworkCore.Metadata;
66
global using Microsoft.EntityFrameworkCore.Query;
7-
global using Microsoft.Extensions.DependencyInjection;
7+
global using Microsoft.Extensions.DependencyInjection;
8+
global using System.Runtime.CompilerServices;

src/GraphQL.EntityFramework/SelectProjection/SelectExpressionBuilder.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ static class SelectExpressionBuilder
1919

2020
static ConcurrentDictionary<Type, EntityTypeMetadata> entityMetadataCache = new();
2121

22-
record PropertyMetadata(PropertyInfo Property, bool CanWrite, MemberExpression PropertyAccess, MemberBinding? Binding, MethodInfo OrderByMethod);
22+
record PropertyMetadata(PropertyInfo Property, bool CanWrite, bool IsAutoProperty, MemberExpression PropertyAccess, MemberBinding? Binding, MethodInfo OrderByMethod);
2323

2424
record EntityTypeMetadata(
2525
ParameterExpression Parameter,
@@ -67,9 +67,15 @@ public static bool TryBuild<TEntity>(
6767
foreach (var keyName in projection.KeyNames)
6868
{
6969
if (properties.TryGetValue(keyName, out var metadata) &&
70-
metadata.CanWrite &&
7170
addedProperties.Add(keyName))
7271
{
72+
if (!metadata.CanWrite || !metadata.IsAutoProperty)
73+
{
74+
// Key property has a custom setter that may throw during materialization,
75+
// or is read-only. Fall back to full entity loading so EF can use backing fields.
76+
return null;
77+
}
78+
7379
bindings.Add(metadata.Binding!);
7480
}
7581
}
@@ -269,9 +275,16 @@ static bool TryBuildNavigationBindings(
269275
foreach (var keyName in projection.KeyNames)
270276
{
271277
if (properties.TryGetValue(keyName, out var metadata) &&
272-
metadata.CanWrite &&
273278
addedProperties.Add(keyName))
274279
{
280+
if (!metadata.CanWrite || !metadata.IsAutoProperty)
281+
{
282+
// Key property has a custom setter that may throw during materialization,
283+
// or is read-only. Can't use projection.
284+
bindings = null;
285+
return false;
286+
}
287+
275288
bindings.Add(Expression.Bind(metadata.Property, Expression.Property(sourceExpression, metadata.Property)));
276289
}
277290
}
@@ -435,9 +448,12 @@ static EntityTypeMetadata GetEntityMetadata(Type type) =>
435448
foreach (var property in properties)
436449
{
437450
var propertyAccess = Expression.Property(parameter, property);
438-
var binding = property.CanWrite ? Expression.Bind(property, propertyAccess) : null;
451+
var canWrite = property.CanWrite;
452+
var isAutoProperty = canWrite &&
453+
property.SetMethod!.IsDefined(typeof(CompilerGeneratedAttribute), false);
454+
var binding = canWrite ? Expression.Bind(property, propertyAccess) : null;
439455
var orderByMethod = SelectExpressionBuilder.orderByMethod.MakeGenericMethod(type, property.PropertyType);
440-
dictionary[property.Name] = new(property, property.CanWrite, propertyAccess, binding, orderByMethod);
456+
dictionary[property.Name] = new(property, canWrite, isAutoProperty, propertyAccess, binding, orderByMethod);
441457
}
442458

443459
var newInstance = Expression.New(type);

src/GraphQL.EntityFramework/Where/ExpressionBuilder.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,8 @@ static Expression GetExpression(string path, Comparison comparison, string?[]? v
228228

229229
static MethodCallExpression MakeObjectListInComparision(string[] values, Property<T> property)
230230
{
231-
// Attempt to convert the string values to the object type
232231
var objects = TypeConverter.ConvertStringsToList(values, property.Info);
233-
// Make the object values a constant expression
234-
var constant = Expression.Constant(objects);
235-
// Build and return the expression body
232+
var constant = MakeParameterizedConstant(objects, objects.GetType());
236233
return Expression.Call(constant, property.SafeListContains, property.Left);
237234
}
238235

@@ -244,14 +241,14 @@ static MethodCallExpression MakeStringListInComparison(string[] values, Property
244241
var itemEvaluate = Expression.Lambda<Func<string, bool>>(equalsBody, ExpressionCache.StringParam);
245242

246243
// Build Expression body to check if any string values match the property value
247-
return Expression.Call(null, ReflectionCache.StringAny, Expression.Constant(values), itemEvaluate);
244+
return Expression.Call(null, ReflectionCache.StringAny, MakeParameterizedConstant(values, typeof(string[])), itemEvaluate);
248245
}
249246

250247
static Expression MakeSingleStringComparison(Comparison comparison, string? value, Property<T> property)
251248
{
252249
var left = property.Left;
253250

254-
var valueConstant = Expression.Constant(value, typeof(string));
251+
var valueConstant = MakeParameterizedConstant(value, typeof(string));
255252
var nullCheck = Expression.NotEqual(left, ExpressionCache.Null);
256253

257254
switch (comparison)
@@ -280,7 +277,7 @@ static Expression MakeSingleStringComparison(Comparison comparison, string? valu
280277
static Expression MakeSingleObjectComparison(Comparison comparison, object? value, Property<T> property)
281278
{
282279
var left = property.Left;
283-
var constant = Expression.Constant(value, left.Type);
280+
var constant = MakeParameterizedConstant(value, left.Type);
284281

285282
return comparison switch
286283
{
@@ -294,6 +291,15 @@ static Expression MakeSingleObjectComparison(Comparison comparison, object? valu
294291
};
295292
}
296293

294+
// Wraps a value in a field access on a holder object so that EF Core
295+
// emits a SQL parameter (@p0) instead of inlining the value as a literal.
296+
static Expression MakeParameterizedConstant(object? value, Type targetType)
297+
{
298+
var holder = new ValueHolder(value);
299+
Expression access = Expression.Field(Expression.Constant(holder), "Value");
300+
return Expression.Convert(access, targetType);
301+
}
302+
297303
static bool HasListPropertyInPath(string path) =>
298304
path.Contains('[');
299305

@@ -310,4 +316,9 @@ static Expression NegateExpression(Expression expression) =>
310316

311317
[GeneratedRegex(@"\[(.*)\]")]
312318
private static partial Regex ListPropertyRegex();
319+
}
320+
321+
class ValueHolder(object? value)
322+
{
323+
public object? Value = value;
313324
}

src/SampleWeb.Tests/GraphQlControllerTests.Single_not_found.verified.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
Message:
44
Not found.
55
Query:
6+
DECLARE @p1 int = 99;
7+
68
SELECT [c].[Id]
79
FROM [Companies] AS [c]
8-
WHERE [c].[Id] = 99,
10+
WHERE [c].[Id] = @p1,
911
Query:
12+
DECLARE @p1 int = 99;
13+
1014
SELECT [c].[Id]
1115
FROM [Companies] AS [c]
12-
WHERE [c].[Id] = 99
16+
WHERE [c].[Id] = @p1
1317
}

src/SampleWeb.Tests/SchemaPrint.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using GraphQL;
2-
using GraphQL.Types;
32

43
public class SchemaPrint
54
{
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
public class GuardedKeyEntity
2+
{
3+
Guid id;
4+
5+
public Guid Id
6+
{
7+
get => id;
8+
set => throw new InvalidOperationException("Id is generated from initial EmailAddress");
9+
}
10+
11+
public string? EmailAddress { get; set; }
12+
13+
public GuardedKeyEntity(string emailAddress)
14+
{
15+
id = CreateDeterministicGuid(emailAddress);
16+
EmailAddress = emailAddress;
17+
}
18+
19+
// EF Core needs a parameterless constructor; it will set the backing field directly
20+
GuardedKeyEntity()
21+
{
22+
}
23+
24+
static Guid CreateDeterministicGuid(string input)
25+
{
26+
var bytes = Encoding.UTF8.GetBytes(input);
27+
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
28+
return new(hash.AsSpan(0, 16));
29+
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public class GuardedKeyEntityGraphType :
2+
EfObjectGraphType<IntegrationDbContext, GuardedKeyEntity>
3+
{
4+
public GuardedKeyEntityGraphType(IEfGraphQLService<IntegrationDbContext> graphQlService) :
5+
base(graphQlService) =>
6+
AutoMap();
7+
}

src/Tests/IntegrationTests/IntegrationDbContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder builder) =>
5454
public DbSet<TphDerivedNavBaseEntity> TphDerivedNavBaseEntities { get; set; } = null!;
5555
public DbSet<CategoryEntity> CategoryEntities { get; set; } = null!;
5656
public DbSet<RegionEntity> RegionEntities { get; set; } = null!;
57+
public DbSet<GuardedKeyEntity> GuardedKeyEntities { get; set; } = null!;
5758

5859
protected override void OnModelCreating(ModelBuilder modelBuilder)
5960
{
@@ -173,6 +174,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
173174
.HasOne(_ => _.Region)
174175
.WithMany()
175176
.HasForeignKey(_ => _.RegionId);
177+
modelBuilder.Entity<GuardedKeyEntity>(entity =>
178+
{
179+
entity.Property(_ => _.Id).HasField("id");
180+
entity.OrderBy(_ => _.EmailAddress);
181+
});
176182
modelBuilder.Entity<CategoryEntity>()
177183
.OrderBy(_ => _.Name);
178184
modelBuilder.Entity<RegionEntity>()

src/Tests/IntegrationTests/IntegrationTests.AddSingleField_with_filter_accessing_navigation_denies_when_no_match.verified.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ from FilterReferenceEntities as f
1515
inner join
1616
FilterBaseEntities as f0
1717
on f.BaseEntityId = f0.Id
18-
where f.Id = 'Guid_1'
18+
where f.Id = @p1,
19+
Parameters: {
20+
@p1: Guid_1
21+
}
1922
}
2023
}

0 commit comments

Comments
 (0)