Skip to content

Commit 397a545

Browse files
committed
The collection parameter have got TOP clause and a DISTINCT so SQL Server is able to build better execution plans.
1 parent 3674643 commit 397a545

File tree

7 files changed

+151
-78
lines changed

7 files changed

+151
-78
lines changed

samples/Thinktecture.EntityFrameworkCore.SqlServer.Samples/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static async Task Main(string[] args)
7979

8080
private static async Task DoScalarCollectionParameterAsync(DemoDbContext ctx, List<Guid> customerIds)
8181
{
82-
var customerIdsQuery = ctx.ToScalarCollectionParameter(customerIds);
82+
var customerIdsQuery = ctx.CreateScalarCollectionParameter(customerIds);
8383

8484
var customers = await ctx.Customers.Where(c => customerIdsQuery.Contains(c.Id)).ToListAsync();
8585

@@ -88,7 +88,7 @@ private static async Task DoScalarCollectionParameterAsync(DemoDbContext ctx, Li
8888

8989
private static async Task DoComplexCollectionParameterAsync(DemoDbContext ctx, Guid customerId)
9090
{
91-
var parameters = ctx.ToComplexCollectionParameter(new[] { new MyParameter(customerId, 42) });
91+
var parameters = ctx.CreateComplexCollectionParameter(new[] { new MyParameter(customerId, 42) });
9292

9393
var customers = await ctx.Customers.Join(parameters, c => c.Id, t => t.Column1, (c, t) => new { Customer = c, Number = t.Column2}).ToListAsync();
9494

src/Thinktecture.EntityFrameworkCore.BulkOperations/EntityFrameworkCore/Parameters/ICollectionParameterFactory.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,19 @@ public interface ICollectionParameterFactory
1212
/// </summary>
1313
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="values"/> with.</param>
1414
/// <param name="values">A collection of <paramref name="values"/> to create a query from.</param>
15+
/// <param name="applyDistinct">Indication whether the query should apply 'DISTINCT' on <paramref name="values"/>.</param>
1516
/// <typeparam name="T">Type of the <paramref name="values"/>.</typeparam>
1617
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="values"/>.</returns>
17-
IQueryable<T> CreateScalarQuery<T>(DbContext ctx, IEnumerable<T> values);
18+
IQueryable<T> CreateScalarQuery<T>(DbContext ctx, IReadOnlyCollection<T> values, bool applyDistinct);
1819

1920
/// <summary>
2021
/// Creates an <see cref="IQueryable{T}"/> out of provided <paramref name="objects"/>.
2122
/// </summary>
2223
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="objects"/> with.</param>
2324
/// <param name="objects">A collection of <paramref name="objects"/> to create a query from.</param>
25+
/// <param name="applyDistinct">Indication whether the query should apply 'DISTINCT' on <paramref name="objects"/>.</param>
2426
/// <typeparam name="T">Type of the <paramref name="objects"/>.</typeparam>
2527
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="objects"/>.</returns>
26-
IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IEnumerable<T> objects)
28+
IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IReadOnlyCollection<T> objects, bool applyDistinct)
2729
where T : class;
2830
}

src/Thinktecture.EntityFrameworkCore.BulkOperations/Extensions/BulkOperationsDbContextExtensions.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,25 +312,33 @@ public static Task TruncateTableAsync<T>(
312312
/// </summary>
313313
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="values"/> with.</param>
314314
/// <param name="values">A collection of <paramref name="values"/> to create a query from.</param>
315+
/// <param name="applyDistinct">
316+
/// Indication whether the query should apply 'DISTINCT' on <paramref name="values"/>.
317+
/// It is highly recommended to set this parameter to <c>true</c> to get better execution plans.
318+
/// </param>
315319
/// <typeparam name="T">Type of the <paramref name="values"/>.</typeparam>
316320
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="values"/>.</returns>
317-
public static IQueryable<T> ToScalarCollectionParameter<T>(this DbContext ctx, IEnumerable<T> values)
321+
public static IQueryable<T> CreateScalarCollectionParameter<T>(this DbContext ctx, IReadOnlyCollection<T> values, bool applyDistinct = true)
318322
{
319323
return ctx.GetService<ICollectionParameterFactory>()
320-
.CreateScalarQuery(ctx, values);
324+
.CreateScalarQuery(ctx, values, applyDistinct);
321325
}
322326

323327
/// <summary>
324328
/// Converts the provided <paramref name="objects"/> to a "parameter" to be used in queries.
325329
/// </summary>
326330
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="objects"/> with.</param>
327331
/// <param name="objects">A collection of <paramref name="objects"/> to create a query from.</param>
332+
/// <param name="applyDistinct">
333+
/// Indication whether the query should apply 'DISTINCT' on <paramref name="objects"/>.
334+
/// It is highly recommended to set this parameter to <c>true</c> to get better execution plans.
335+
/// </param>
328336
/// <typeparam name="T">Type of the <paramref name="objects"/>.</typeparam>
329337
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="objects"/>.</returns>
330-
public static IQueryable<T> ToComplexCollectionParameter<T>(this DbContext ctx, IEnumerable<T> objects)
338+
public static IQueryable<T> CreateComplexCollectionParameter<T>(this DbContext ctx, IReadOnlyCollection<T> objects, bool applyDistinct = true)
331339
where T : class
332340
{
333341
return ctx.GetService<ICollectionParameterFactory>()
334-
.CreateComplexQuery(ctx, objects);
342+
.CreateComplexQuery(ctx, objects, applyDistinct);
335343
}
336344
}

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Parameters/JsonCollectionParameter.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ namespace Thinktecture.EntityFrameworkCore.Parameters;
66
/// <summary>
77
/// Represents a collection parameter.
88
/// </summary>
9-
public class JsonCollectionParameter<T, TConverted> : JsonCollectionParameter
9+
internal class JsonCollectionParameter<T, TConverted> : JsonCollectionParameter
1010
{
11-
private readonly IEnumerable<T> _values;
11+
private readonly IReadOnlyCollection<T> _values;
1212
private readonly Func<object?, object?> _convertValue;
1313
private readonly JsonSerializerOptions _jsonSerializerOptions;
1414

@@ -19,7 +19,7 @@ public class JsonCollectionParameter<T, TConverted> : JsonCollectionParameter
1919
/// <param name="convertValue">EF value converter.</param>
2020
/// <param name="jsonSerializerOptions">JSON serialization settings.</param>
2121
public JsonCollectionParameter(
22-
IEnumerable<T> values,
22+
IReadOnlyCollection<T> values,
2323
JsonSerializerOptions jsonSerializerOptions,
2424
Func<object?, object?> convertValue)
2525
{
@@ -28,22 +28,26 @@ public JsonCollectionParameter(
2828
_convertValue = convertValue ?? throw new ArgumentNullException(nameof(convertValue));
2929
}
3030

31-
/// <inheritdoc />
3231
public override string ToString(IFormatProvider? provider)
3332
{
3433
var convert = _convertValue;
3534
var values = _values.Select(i => (TConverted)convert(i)!);
3635

3736
return JsonSerializer.Serialize(values, typeof(IEnumerable<TConverted>), _jsonSerializerOptions);
3837
}
38+
39+
public override long ToInt64(IFormatProvider? provider)
40+
{
41+
return _values.Count;
42+
}
3943
}
4044

4145
/// <summary>
4246
/// Represents a collection parameter.
4347
/// </summary>
44-
public class JsonCollectionParameter<T> : JsonCollectionParameter
48+
internal class JsonCollectionParameter<T> : JsonCollectionParameter
4549
{
46-
private readonly IEnumerable<T> _values;
50+
private readonly IReadOnlyCollection<T> _values;
4751
private readonly JsonSerializerOptions _jsonSerializerOptions;
4852

4953
/// <summary>
@@ -52,27 +56,31 @@ public class JsonCollectionParameter<T> : JsonCollectionParameter
5256
/// <param name="values">Values to serialize.</param>
5357
/// <param name="jsonSerializerOptions">JSON serialization settings.</param>
5458
public JsonCollectionParameter(
55-
IEnumerable<T> values,
59+
IReadOnlyCollection<T> values,
5660
JsonSerializerOptions jsonSerializerOptions)
5761
{
5862
_values = values ?? throw new ArgumentNullException(nameof(values));
5963
_jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions));
6064
}
6165

62-
/// <inheritdoc />
6366
public override string ToString(IFormatProvider? provider)
6467
{
6568
return JsonSerializer.Serialize(_values, typeof(IEnumerable<T>), _jsonSerializerOptions);
6669
}
70+
71+
public override long ToInt64(IFormatProvider? provider)
72+
{
73+
return _values.Count;
74+
}
6775
}
6876

6977
/// <summary>
7078
/// Represents a collection parameter.
7179
/// </summary>
72-
public abstract class JsonCollectionParameter : IConvertible
80+
internal abstract class JsonCollectionParameter : IConvertible
7381
{
74-
/// <inheritdoc />
7582
public abstract string ToString(IFormatProvider? provider);
83+
public abstract long ToInt64(IFormatProvider? provider);
7684

7785
#pragma warning disable CS1591
7886
// ReSharper disable ArrangeMethodOrOperatorBody
@@ -85,7 +93,6 @@ public abstract class JsonCollectionParameter : IConvertible
8593
public double ToDouble(IFormatProvider? provider) => throw new NotSupportedException();
8694
public short ToInt16(IFormatProvider? provider) => throw new NotSupportedException();
8795
public int ToInt32(IFormatProvider? provider) => throw new NotSupportedException();
88-
public long ToInt64(IFormatProvider? provider) => throw new NotSupportedException();
8996
public sbyte ToSByte(IFormatProvider? provider) => throw new NotSupportedException();
9097
public float ToSingle(IFormatProvider? provider) => throw new NotSupportedException();
9198
public object ToType(Type conversionType, IFormatProvider? provider) => throw new NotSupportedException();

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Parameters/SqlServerCollectionParameterFactory.cs

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,74 +39,109 @@ public SqlServerCollectionParameterFactory(
3939
}
4040

4141
/// <inheritdoc />
42-
public IQueryable<T> CreateScalarQuery<T>(DbContext ctx, IEnumerable<T> values)
42+
public IQueryable<T> CreateScalarQuery<T>(DbContext ctx, IReadOnlyCollection<T> values, bool applyDistinct)
4343
{
4444
var entityType = ctx.Model.GetEntityType(typeof(ScalarCollectionParameter<T>));
45-
var parameterInfo = _cache.GetOrAdd(entityType, GetScalarParameterInfo<T>);
45+
var parameterInfo = _cache.GetOrAdd(entityType,
46+
static (type, args) => GetScalarParameterInfo<T>(args._stringBuilderPool, args._sqlGenerationHelper, type),
47+
(_stringBuilderPool, _sqlGenerationHelper));
4648

47-
var parameter = new SqlParameter
48-
{
49-
DbType = DbType.String,
50-
SqlDbType = SqlDbType.NVarChar,
51-
Value = parameterInfo.ParameterFactory(values, _jsonSerializerOptions)
52-
};
49+
var parameterValue = parameterInfo.ParameterFactory(values, _jsonSerializerOptions);
5350

54-
return ctx.Set<ScalarCollectionParameter<T>>().FromSqlRaw(parameterInfo.Statement, parameter).Select(e => e.Value);
51+
return ctx.Set<ScalarCollectionParameter<T>>()
52+
.FromSqlRaw(applyDistinct ? parameterInfo.StatementWithDistinct : parameterInfo.Statement,
53+
CreateTopParameter(parameterValue),
54+
CreateJsonParameter(parameterValue))
55+
.Select(e => e.Value);
5556
}
5657

5758
/// <inheritdoc />
58-
public IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IEnumerable<T> objects)
59+
public IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IReadOnlyCollection<T> objects, bool applyDistinct)
5960
where T : class
6061
{
6162
var entityType = ctx.Model.GetEntityType(typeof(T));
6263
var parameterInfo = _cache.GetOrAdd(entityType, GetComplexParameterInfo<T>);
6364

64-
var parameter = new SqlParameter
65-
{
66-
DbType = DbType.String,
67-
SqlDbType = SqlDbType.NVarChar,
68-
Value = parameterInfo.ParameterFactory(objects, _jsonSerializerOptions)
69-
};
65+
var parameterValue = parameterInfo.ParameterFactory(objects, _jsonSerializerOptions);
7066

71-
return ctx.Set<T>().FromSqlRaw(parameterInfo.Statement, parameter);
67+
return ctx.Set<T>().FromSqlRaw(applyDistinct ? parameterInfo.StatementWithDistinct : parameterInfo.Statement,
68+
CreateTopParameter(parameterValue),
69+
CreateJsonParameter(parameterValue));
7270
}
7371

74-
private CollectionParameterInfo GetScalarParameterInfo<T>(IEntityType entityType)
72+
private static SqlParameter CreateJsonParameter(JsonCollectionParameter parameterValue)
7573
{
76-
var storeObject = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table) ?? throw new Exception($"Could not create StoreObjectIdentifier for table '{entityType.Name}'.");
74+
return new SqlParameter
75+
{
76+
DbType = DbType.String,
77+
SqlDbType = SqlDbType.NVarChar,
78+
Value = parameterValue
79+
};
80+
}
7781

78-
var property = entityType.GetProperties().Single();
82+
private static SqlParameter CreateTopParameter(JsonCollectionParameter jsonCollection)
83+
{
84+
return new SqlParameter
85+
{
86+
DbType = DbType.Int64,
87+
SqlDbType = SqlDbType.BigInt,
88+
Value = jsonCollection
89+
};
90+
}
7991

80-
var columnName = property.GetColumnName(storeObject) ?? throw new Exception($"The property '{property.Name}' has no column name.");
81-
var escapedColumnName = _sqlGenerationHelper.DelimitIdentifier(columnName);
82-
var columnType = property.GetColumnType(storeObject);
92+
private static CollectionParameterInfo GetScalarParameterInfo<T>(
93+
ObjectPool<StringBuilder> stringBuilderPool,
94+
ISqlGenerationHelper sqlGenerationHelper,
95+
IEntityType entityType)
96+
{
97+
var property = entityType.GetProperties().Single();
8398
var converter = property.GetValueConverter();
84-
var sb = _stringBuilderPool.Get();
99+
100+
return new CollectionParameterInfo(BuildScalarStatement(stringBuilderPool, sqlGenerationHelper, entityType, property, false),
101+
BuildScalarStatement(stringBuilderPool, sqlGenerationHelper, entityType, property, true),
102+
CreateParameterFactory<T>(converter));
103+
}
104+
105+
private static string BuildScalarStatement(
106+
ObjectPool<StringBuilder> stringBuilderPool,
107+
ISqlGenerationHelper sqlGenerationHelper,
108+
IEntityType entityType,
109+
IProperty property,
110+
bool applyDistinct)
111+
{
112+
var sb = stringBuilderPool.Get();
85113

86114
try
87115
{
88-
sb.Append("SELECT ").Append(escapedColumnName).Append(" FROM OPENJSON({0}, '$') WITH (")
89-
.Append(escapedColumnName).Append(" ").Append(columnType).Append(" '$')");
116+
var storeObject = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table) ?? throw new Exception($"Could not create StoreObjectIdentifier for table '{entityType.Name}'.");
117+
var columnType = property.GetColumnType(storeObject);
118+
var columnName = property.GetColumnName(storeObject) ?? throw new Exception($"The property '{property.Name}' has no column name.");
119+
var escapedColumnName = sqlGenerationHelper.DelimitIdentifier(columnName);
120+
121+
sb.Append("SELECT ");
122+
123+
if (applyDistinct)
124+
sb.Append("DISTINCT ");
90125

91-
var parameterFactory = CreateParameterFactory<T>(converter);
126+
sb.Append("TOP({0}) ").Append(escapedColumnName).Append(" FROM OPENJSON({1}, '$') WITH (")
127+
.Append(escapedColumnName).Append(" ").Append(columnType).Append(" '$')");
92128

93-
return new CollectionParameterInfo(sb.ToString(), parameterFactory);
129+
return sb.ToString();
94130
}
95131
finally
96132
{
97-
_stringBuilderPool.Return(sb);
133+
stringBuilderPool.Return(sb);
98134
}
99135
}
100136

101137
private CollectionParameterInfo GetComplexParameterInfo<T>(IEntityType entityType)
102138
{
103-
var sqlStatement = CreateSqlStatementForComplexType(entityType);
104-
var parameterFactory = CreateParameterFactory<T>(null);
105-
106-
return new CollectionParameterInfo(sqlStatement, parameterFactory);
139+
return new CollectionParameterInfo(CreateSqlStatementForComplexType(entityType, false),
140+
CreateSqlStatementForComplexType(entityType, true),
141+
CreateParameterFactory<T>(null));
107142
}
108143

109-
private string CreateSqlStatementForComplexType(IEntityType entityType)
144+
private string CreateSqlStatementForComplexType(IEntityType entityType, bool withDistinct)
110145
{
111146
var sb = _stringBuilderPool.Get();
112147
var withClause = _stringBuilderPool.Get();
@@ -117,6 +152,11 @@ private string CreateSqlStatementForComplexType(IEntityType entityType)
117152

118153
sb.Append("SELECT ");
119154

155+
if (withDistinct)
156+
sb.Append("DISTINCT ");
157+
158+
sb.Append("TOP({0}) ");
159+
120160
var isFirst = true;
121161

122162
foreach (var property in entityType.GetProperties())
@@ -131,14 +171,13 @@ private string CreateSqlStatementForComplexType(IEntityType entityType)
131171
var escapedColumnName = _sqlGenerationHelper.DelimitIdentifier(columnName);
132172
var columnType = property.GetColumnType(storeObject) ?? throw new Exception($"The property '{property.Name}' has no column type.");
133173

134-
sb.Append("[").Append(columnName).Append("]");
135-
174+
sb.Append(escapedColumnName);
136175
withClause.Append(escapedColumnName).Append(" ").Append(columnType).Append($" '$.{property.Name}'");
137176

138177
isFirst = false;
139178
}
140179

141-
sb.Append(" FROM OPENJSON({0}, '$') WITH (").Append(withClause).Append(")");
180+
sb.Append(" FROM OPENJSON({1}, '$') WITH (").Append(withClause).Append(")");
142181

143182
return sb.ToString();
144183
}
@@ -153,7 +192,7 @@ private static Func<IEnumerable, JsonSerializerOptions, JsonCollectionParameter>
153192
ValueConverter? converter)
154193
{
155194
if (converter is null)
156-
return (values, options) => new JsonCollectionParameter<T>((IEnumerable<T>)values, options);
195+
return static (values, options) => new JsonCollectionParameter<T>((IReadOnlyCollection<T>)values, options);
157196

158197
var itemType = typeof(T);
159198
var parameterType = typeof(JsonCollectionParameter<,>).MakeGenericType(itemType, converter.ProviderClrType);
@@ -163,13 +202,13 @@ private static Func<IEnumerable, JsonSerializerOptions, JsonCollectionParameter>
163202

164203
var ctor = parameterType.GetConstructors().Single();
165204
var ctorCall = Expression.New(ctor,
166-
Expression.Convert(valuesParam, typeof(IEnumerable<T>)),
205+
Expression.Convert(valuesParam, typeof(IReadOnlyCollection<T>)),
167206
optionsParam,
168207
Expression.Constant(converter.ConvertToProvider));
169208

170209
return Expression.Lambda<Func<IEnumerable, JsonSerializerOptions, JsonCollectionParameter>>(ctorCall, valuesParam, optionsParam)
171210
.Compile();
172211
}
173212

174-
private readonly record struct CollectionParameterInfo(string Statement, Func<IEnumerable, JsonSerializerOptions, JsonCollectionParameter> ParameterFactory);
213+
private readonly record struct CollectionParameterInfo(string Statement, string StatementWithDistinct, Func<IEnumerable, JsonSerializerOptions, JsonCollectionParameter> ParameterFactory);
175214
}

0 commit comments

Comments
 (0)