Skip to content

Commit a18899c

Browse files
committed
Collection parameters: by default, the collections is serialized immediately instead of deferred
This should be the expected behavior, because I can't image why I would create a parameter, then modify the underlying (in-memory-)collection and in the end execute the query expecting it has the last modifications. And there is a regression in 6.0.2 (dotnet/efcore#27427)
1 parent 4563034 commit a18899c

File tree

5 files changed

+72
-12
lines changed

5 files changed

+72
-12
lines changed

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Infrastructure/SqlServerDbContextOptionsExtension.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public bool AddCustomQuerySqlGeneratorFactory
103103
private JsonSerializerOptions? _collectionParameterJsonSerializerOptions;
104104
private bool _addCollectionParameterSupport;
105105
internal bool ConfigureCollectionParametersForPrimitiveTypes { get; private set; }
106+
internal bool UseDeferredCollectionParameterSerialization { get; private set; }
106107

107108
/// <summary>
108109
/// Changes the implementation of <see cref="IMigrationsSqlGenerator"/> to <see cref="ThinktectureSqlServerMigrationsSqlGenerator"/>.
@@ -158,9 +159,7 @@ public void ApplyServices(IServiceCollection services)
158159
{
159160
var jsonSerializerOptions = _collectionParameterJsonSerializerOptions ?? new JsonSerializerOptions();
160161

161-
services.AddSingleton<ICollectionParameterFactory>(serviceProvider => new SqlServerCollectionParameterFactory(jsonSerializerOptions,
162-
serviceProvider.GetRequiredService<ObjectPool<StringBuilder>>(),
163-
serviceProvider.GetRequiredService<ISqlGenerationHelper>()));
162+
services.AddSingleton<ICollectionParameterFactory>(serviceProvider => ActivatorUtilities.CreateInstance<SqlServerCollectionParameterFactory>(serviceProvider, jsonSerializerOptions));
164163
services.Add<IConventionSetPlugin, SqlServerCollectionParameterConventionSetPlugin>(GetLifetime<IConventionSetPlugin>());
165164
}
166165

@@ -200,11 +199,13 @@ public void Register(Type serviceType, object implementationInstance)
200199
public SqlServerDbContextOptionsExtension AddCollectionParameterSupport(
201200
bool addCollectionParameterSupport,
202201
JsonSerializerOptions? jsonSerializerOptions,
203-
bool configureCollectionParametersForPrimitiveTypes)
202+
bool configureCollectionParametersForPrimitiveTypes,
203+
bool useDeferredSerialization)
204204
{
205205
_addCollectionParameterSupport = addCollectionParameterSupport;
206206
_collectionParameterJsonSerializerOptions = jsonSerializerOptions;
207207
ConfigureCollectionParametersForPrimitiveTypes = addCollectionParameterSupport && configureCollectionParametersForPrimitiveTypes;
208+
UseDeferredCollectionParameterSerialization = addCollectionParameterSupport && useDeferredSerialization;
208209

209210
return this;
210211
}
@@ -261,6 +262,7 @@ public override int GetServiceProviderHashCode()
261262
hashCode.Add(_extension.ConfigureTempTablesForPrimitiveTypes);
262263
hashCode.Add(_extension._addCollectionParameterSupport);
263264
hashCode.Add(_extension.ConfigureCollectionParametersForPrimitiveTypes);
265+
hashCode.Add(_extension.UseDeferredCollectionParameterSerialization);
264266
hashCode.Add(_extension._collectionParameterJsonSerializerOptions);
265267
hashCode.Add(_extension.AddTenantDatabaseSupport);
266268
hashCode.Add(_extension.AddTableHintSupport);
@@ -280,6 +282,7 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
280282
&& _extension.ConfigureTempTablesForPrimitiveTypes == otherSqlServerInfo._extension.ConfigureTempTablesForPrimitiveTypes
281283
&& _extension._addCollectionParameterSupport == otherSqlServerInfo._extension._addCollectionParameterSupport
282284
&& _extension.ConfigureCollectionParametersForPrimitiveTypes == otherSqlServerInfo._extension.ConfigureCollectionParametersForPrimitiveTypes
285+
&& _extension.UseDeferredCollectionParameterSerialization == otherSqlServerInfo._extension.UseDeferredCollectionParameterSerialization
283286
&& _extension._collectionParameterJsonSerializerOptions == otherSqlServerInfo._extension._collectionParameterJsonSerializerOptions
284287
&& _extension.AddTenantDatabaseSupport == otherSqlServerInfo._extension.AddTenantDatabaseSupport
285288
&& _extension.AddTableHintSupport == otherSqlServerInfo._extension.AddTableHintSupport

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Infrastructure/SqliteDbContextOptionsExtensionOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ public class SqlServerDbContextOptionsExtensionOptions : IBulkOperationsDbContex
1616
/// </summary>
1717
public bool ConfigureCollectionParametersForPrimitiveTypes { get; private set; }
1818

19+
/// <summary>
20+
/// Indication whether to use deferred serialization or not.
21+
/// </summary>
22+
public bool UseDeferredCollectionParameterSerialization { get; private set; }
23+
1924
/// <inheritdoc />
2025
public void Initialize(IDbContextOptions options)
2126
{
2227
var extension = GetExtension(options);
2328

2429
ConfigureTempTablesForPrimitiveTypes = extension.ConfigureTempTablesForPrimitiveTypes;
2530
ConfigureCollectionParametersForPrimitiveTypes = extension.ConfigureCollectionParametersForPrimitiveTypes;
31+
UseDeferredCollectionParameterSerialization = extension.UseDeferredCollectionParameterSerialization;
2632
}
2733

2834
/// <inheritdoc />
@@ -35,6 +41,9 @@ public void Validate(IDbContextOptions options)
3541

3642
if (extension.ConfigureCollectionParametersForPrimitiveTypes != ConfigureCollectionParametersForPrimitiveTypes)
3743
throw new InvalidOperationException($"The setting '{nameof(SqlServerDbContextOptionsExtension.ConfigureCollectionParametersForPrimitiveTypes)}' has been changed.");
44+
45+
if (extension.UseDeferredCollectionParameterSerialization != UseDeferredCollectionParameterSerialization)
46+
throw new InvalidOperationException($"The setting '{nameof(SqlServerDbContextOptionsExtension.UseDeferredCollectionParameterSerialization)}' has been changed.");
3847
}
3948

4049
private static SqlServerDbContextOptionsExtension GetExtension(IDbContextOptions options)

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.EntityFrameworkCore.Storage;
1010
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
1111
using Microsoft.Extensions.ObjectPool;
12+
using Thinktecture.EntityFrameworkCore.Infrastructure;
1213
using Thinktecture.Internal;
1314

1415
namespace Thinktecture.EntityFrameworkCore.Parameters;
@@ -20,22 +21,26 @@ public class SqlServerCollectionParameterFactory : ICollectionParameterFactory
2021
private readonly JsonSerializerOptions _jsonSerializerOptions;
2122
private readonly ObjectPool<StringBuilder> _stringBuilderPool;
2223
private readonly ISqlGenerationHelper _sqlGenerationHelper;
24+
private readonly SqlServerDbContextOptionsExtensionOptions _options;
2325

2426
/// <summary>
2527
/// Initializes new instance of <see cref="SqlServerCollectionParameterFactory"/>.
2628
/// </summary>
2729
/// <param name="jsonSerializerOptions">JSON serialization options.</param>
2830
/// <param name="stringBuilderPool">String builder pool.</param>
29-
/// <param name="sqlGenerationHelper"></param>
31+
/// <param name="sqlGenerationHelper">SQL generation helper.</param>
32+
/// <param name="options">Options.</param>
3033
/// <exception cref="ArgumentNullException">If <paramref name="jsonSerializerOptions"/> is <c>null</c>.</exception>
3134
public SqlServerCollectionParameterFactory(
3235
JsonSerializerOptions jsonSerializerOptions,
3336
ObjectPool<StringBuilder> stringBuilderPool,
34-
ISqlGenerationHelper sqlGenerationHelper)
37+
ISqlGenerationHelper sqlGenerationHelper,
38+
SqlServerDbContextOptionsExtensionOptions options)
3539
{
3640
_jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions));
3741
_stringBuilderPool = stringBuilderPool ?? throw new ArgumentNullException(nameof(stringBuilderPool));
3842
_sqlGenerationHelper = sqlGenerationHelper ?? throw new ArgumentNullException(nameof(sqlGenerationHelper));
43+
_options = options ?? throw new ArgumentNullException(nameof(options));
3944
_cache = new ConcurrentDictionary<IEntityType, CollectionParameterInfo>();
4045
}
4146

@@ -73,23 +78,29 @@ public IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IReadOnlyCollection<T>
7378
CreateJsonParameter(parameterValue));
7479
}
7580

76-
private static SqlParameter CreateJsonParameter(JsonCollectionParameter parameterValue)
81+
private object CreateJsonParameter(JsonCollectionParameter parameter)
7782
{
83+
if (!_options.UseDeferredCollectionParameterSerialization)
84+
return parameter.ToString(null);
85+
7886
return new SqlParameter
7987
{
8088
DbType = DbType.String,
8189
SqlDbType = SqlDbType.NVarChar,
82-
Value = parameterValue
90+
Value = parameter
8391
};
8492
}
8593

86-
private static SqlParameter CreateTopParameter(JsonCollectionParameter jsonCollection)
94+
private object CreateTopParameter(JsonCollectionParameter parameter)
8795
{
96+
if (!_options.UseDeferredCollectionParameterSerialization)
97+
return parameter.ToInt64(null);
98+
8899
return new SqlParameter
89100
{
90101
DbType = DbType.Int64,
91102
SqlDbType = SqlDbType.BigInt,
92-
Value = jsonCollection
103+
Value = parameter
93104
};
94105
}
95106

src/Thinktecture.EntityFrameworkCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,21 @@ public static SqlServerDbContextOptionsBuilder AddBulkOperationSupport(
4141
/// <param name="jsonSerializerOptions">JSON serialization options.</param>
4242
/// <param name="addCollectionParameterSupport">Indication whether to enable or disable the feature.</param>
4343
/// <param name="configureCollectionParametersForPrimitiveTypes">Indication whether to configure collection parameters for primitive types.</param>
44+
/// <param name="useDeferredSerialization">
45+
/// If <c>true</c> then the provided collection will be serialized when the query is executed.
46+
/// If <c>false</c> then the collection is going to be serialized when "collection parameter" is created,
47+
/// i.e. when calling <see cref="BulkOperationsDbContextExtensions.CreateScalarCollectionParameter{T}"/> pr <see cref="BulkOperationsDbContextExtensions.CreateComplexCollectionParameter{T}"/>.
48+
/// Default is <c>false</c>.
49+
/// </param>
4450
/// <returns>Provided <paramref name="sqlServerOptionsBuilder"/>.</returns>
4551
public static SqlServerDbContextOptionsBuilder AddCollectionParameterSupport(
4652
this SqlServerDbContextOptionsBuilder sqlServerOptionsBuilder,
4753
JsonSerializerOptions? jsonSerializerOptions = null,
4854
bool addCollectionParameterSupport = true,
49-
bool configureCollectionParametersForPrimitiveTypes = true)
55+
bool configureCollectionParametersForPrimitiveTypes = true,
56+
bool useDeferredSerialization = false)
5057
{
51-
return AddOrUpdateExtension(sqlServerOptionsBuilder, extension => extension.AddCollectionParameterSupport(addCollectionParameterSupport, jsonSerializerOptions, configureCollectionParametersForPrimitiveTypes));
58+
return AddOrUpdateExtension(sqlServerOptionsBuilder, extension => extension.AddCollectionParameterSupport(addCollectionParameterSupport, jsonSerializerOptions, configureCollectionParametersForPrimitiveTypes, useDeferredSerialization));
5259
}
5360

5461
/// <summary>

tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/Extensions/DbContextExtensionsTests/CreateScalarCollectionParameter.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,34 @@ public async Task Should_work_with_contains(bool applyDistinct)
8282
Count = 42
8383
});
8484
}
85+
86+
[Fact]
87+
public async Task Should_work_GroupBy_and_aggregate()
88+
{
89+
var testEntity = new TestEntity
90+
{
91+
Id = new Guid("7F8B0E79-2C91-4682-9F61-6FC86B4E5244"),
92+
Name = "Name",
93+
RequiredName = "RequiredName",
94+
Count = 42
95+
};
96+
await ArrangeDbContext.AddAsync(testEntity);
97+
await ArrangeDbContext.SaveChangesAsync();
98+
99+
var collectionParameter = ActDbContext.CreateScalarCollectionParameter(new[] { testEntity.Id });
100+
var loadedEntities = await ActDbContext.TestEntities
101+
.Where(e => collectionParameter.Contains(e.Id))
102+
.GroupBy(e => e.Id)
103+
.Select(g => new { g.Key, Aggregate = g.Count() })
104+
.ToListAsync();
105+
106+
loadedEntities.Should().BeEquivalentTo(new[]
107+
{
108+
new
109+
{
110+
Key = new Guid("7F8B0E79-2C91-4682-9F61-6FC86B4E5244"),
111+
Aggregate = 1
112+
}
113+
});
114+
}
85115
}

0 commit comments

Comments
 (0)