Skip to content

Commit 39e1c6a

Browse files
committed
Merge branch 'feature/collection-params'
2 parents 5df763d + 397a545 commit 39e1c6a

File tree

21 files changed

+779
-16
lines changed

21 files changed

+779
-16
lines changed

samples/Thinktecture.EntityFrameworkCore.SqlServer.Samples/Database/DemoDbContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2929
modelBuilder.ConfigureTempTable<Guid>();
3030
modelBuilder.ConfigureTempTable<Guid, Guid>();
3131

32+
modelBuilder.ConfigureComplexCollectionParameter<MyParameter>();
33+
3234
modelBuilder.Entity<Customer>(builder =>
3335
{
3436
builder.Property(c => c.FirstName).HasMaxLength(100);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Thinktecture.Database;
2+
3+
public record MyParameter(Guid Column1, int Column2);

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public static async Task Main(string[] args)
4343
await DoBulkDeleteAsync(ctx);
4444
ctx.ChangeTracker.Clear();
4545

46+
await DoScalarCollectionParameterAsync(ctx, new List<Guid> { customerId });
47+
await DoComplexCollectionParameterAsync(ctx, customerId);
48+
ctx.ChangeTracker.Clear();
49+
4650
// Bulk insert into temp tables
4751
await DoBulkInsertEntitiesIntoTempTableAsync(ctx);
4852
ctx.ChangeTracker.Clear();
@@ -73,6 +77,24 @@ public static async Task Main(string[] args)
7377
Console.WriteLine("Exiting samples...");
7478
}
7579

80+
private static async Task DoScalarCollectionParameterAsync(DemoDbContext ctx, List<Guid> customerIds)
81+
{
82+
var customerIdsQuery = ctx.CreateScalarCollectionParameter(customerIds);
83+
84+
var customers = await ctx.Customers.Where(c => customerIdsQuery.Contains(c.Id)).ToListAsync();
85+
86+
Console.WriteLine($"Found customers: {String.Join(", ", customers.Select(c => c.Id))}");
87+
}
88+
89+
private static async Task DoComplexCollectionParameterAsync(DemoDbContext ctx, Guid customerId)
90+
{
91+
var parameters = ctx.CreateComplexCollectionParameter(new[] { new MyParameter(customerId, 42) });
92+
93+
var customers = await ctx.Customers.Join(parameters, c => c.Id, t => t.Column1, (c, t) => new { Customer = c, Number = t.Column2}).ToListAsync();
94+
95+
Console.WriteLine($"Found customers: {String.Join(", ", customers.Select(c => c.Customer.Id))}");
96+
}
97+
7698
private static async Task DoTenantQueriesAsync(DemoDbContext ctx)
7799
{
78100
await ctx.Customers

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public IServiceProvider CreateServiceProvider(string? schema = null)
5050
sqlOptions.AddRowNumberSupport()
5151
.AddTenantDatabaseSupport<DemoTenantDatabaseProviderFactory>()
5252
.AddBulkOperationSupport()
53+
.AddCollectionParameterSupport()
5354
.UseThinktectureSqlServerMigrationsSqlGenerator();
5455
})
5556
.EnableSensitiveDataLogging()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Linq;
2+
3+
namespace Thinktecture.EntityFrameworkCore.Parameters;
4+
5+
/// <summary>
6+
/// Factory for creation of an <see cref="IQueryable{T}"/> out of provided values.
7+
/// </summary>
8+
public interface ICollectionParameterFactory
9+
{
10+
/// <summary>
11+
/// Creates an <see cref="IQueryable{T}"/> out of provided <paramref name="values"/>.
12+
/// </summary>
13+
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="values"/> with.</param>
14+
/// <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>
16+
/// <typeparam name="T">Type of the <paramref name="values"/>.</typeparam>
17+
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="values"/>.</returns>
18+
IQueryable<T> CreateScalarQuery<T>(DbContext ctx, IReadOnlyCollection<T> values, bool applyDistinct);
19+
20+
/// <summary>
21+
/// Creates an <see cref="IQueryable{T}"/> out of provided <paramref name="objects"/>.
22+
/// </summary>
23+
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="objects"/> with.</param>
24+
/// <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>
26+
/// <typeparam name="T">Type of the <paramref name="objects"/>.</typeparam>
27+
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="objects"/>.</returns>
28+
IQueryable<T> CreateComplexQuery<T>(DbContext ctx, IReadOnlyCollection<T> objects, bool applyDistinct)
29+
where T : class;
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Thinktecture.EntityFrameworkCore.Parameters;
2+
3+
/// <summary>
4+
/// Represents a collection of items with 1 column/property.
5+
/// </summary>
6+
/// <param name="Value">Value of the column.</param>
7+
/// <typeparam name="T">Type of the column.</typeparam>
8+
public record ScalarCollectionParameter<T>(T Value);

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Linq.Expressions;
22
using Microsoft.EntityFrameworkCore.Infrastructure;
33
using Thinktecture.EntityFrameworkCore.BulkOperations;
4+
using Thinktecture.EntityFrameworkCore.Parameters;
45
using Thinktecture.EntityFrameworkCore.TempTables;
56

67
// ReSharper disable once CheckNamespace
@@ -305,4 +306,39 @@ public static Task TruncateTableAsync<T>(
305306
return ctx.GetService<ITruncateTableExecutor>()
306307
.TruncateTableAsync<T>(cancellationToken);
307308
}
309+
310+
/// <summary>
311+
/// Converts the provided <paramref name="values"/> to a "parameter" to be used in queries.
312+
/// </summary>
313+
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="values"/> with.</param>
314+
/// <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>
319+
/// <typeparam name="T">Type of the <paramref name="values"/>.</typeparam>
320+
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="values"/>.</returns>
321+
public static IQueryable<T> CreateScalarCollectionParameter<T>(this DbContext ctx, IReadOnlyCollection<T> values, bool applyDistinct = true)
322+
{
323+
return ctx.GetService<ICollectionParameterFactory>()
324+
.CreateScalarQuery(ctx, values, applyDistinct);
325+
}
326+
327+
/// <summary>
328+
/// Converts the provided <paramref name="objects"/> to a "parameter" to be used in queries.
329+
/// </summary>
330+
/// <param name="ctx">An instance of <see cref="DbContext"/> to use the <paramref name="objects"/> with.</param>
331+
/// <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>
336+
/// <typeparam name="T">Type of the <paramref name="objects"/>.</typeparam>
337+
/// <returns>An <see cref="IQueryable{T}"/> giving access to the provided <paramref name="objects"/>.</returns>
338+
public static IQueryable<T> CreateComplexCollectionParameter<T>(this DbContext ctx, IReadOnlyCollection<T> objects, bool applyDistinct = true)
339+
where T : class
340+
{
341+
return ctx.GetService<ICollectionParameterFactory>()
342+
.CreateComplexQuery(ctx, objects, applyDistinct);
343+
}
308344
}

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.EntityFrameworkCore.Infrastructure;
22
using Microsoft.EntityFrameworkCore.Metadata.Builders;
3+
using Thinktecture.EntityFrameworkCore.Parameters;
34
using Thinktecture.EntityFrameworkCore.TempTables;
45

56
// ReSharper disable once CheckNamespace
@@ -21,7 +22,38 @@ public static class BulkOperationsModelBuilderExtensions
2122
public static EntityTypeBuilder<T> ConfigureTempTableEntity<T>(this ModelBuilder modelBuilder, bool isKeyless = true)
2223
where T : class
2324
{
24-
return modelBuilder.Configure<T>(isKeyless);
25+
return modelBuilder.ConfigureTempTableInternal<T>(isKeyless);
26+
}
27+
28+
/// <summary>
29+
/// Introduces and configures a scalar parameter.
30+
/// </summary>
31+
/// <param name="modelBuilder">A model builder.</param>
32+
/// <typeparam name="T">Type of the column.</typeparam>
33+
/// <returns>An entity type builder for further configuration.</returns>
34+
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/> is <c>null</c>.</exception>
35+
public static EntityTypeBuilder<ScalarCollectionParameter<T>> ConfigureScalarCollectionParameter<T>(this ModelBuilder modelBuilder)
36+
{
37+
return modelBuilder.Entity<ScalarCollectionParameter<T>>()
38+
.ToTable(typeof(ScalarCollectionParameter<T>).ShortDisplayName(),
39+
tableBuilder => tableBuilder.ExcludeFromMigrations())
40+
.HasNoKey();
41+
}
42+
43+
/// <summary>
44+
/// Introduces and configures a complex parameter.
45+
/// </summary>
46+
/// <param name="modelBuilder">A model builder.</param>
47+
/// <typeparam name="T">Type of the parameter.</typeparam>
48+
/// <returns>An entity type builder for further configuration.</returns>
49+
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/> is <c>null</c>.</exception>
50+
public static EntityTypeBuilder<T> ConfigureComplexCollectionParameter<T>(this ModelBuilder modelBuilder)
51+
where T : class
52+
{
53+
return modelBuilder.Entity<T>()
54+
.ToTable(typeof(T).ShortDisplayName(),
55+
tableBuilder => tableBuilder.ExcludeFromMigrations())
56+
.HasNoKey();
2557
}
2658

2759
/// <summary>
@@ -34,7 +66,7 @@ public static EntityTypeBuilder<T> ConfigureTempTableEntity<T>(this ModelBuilder
3466
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/> is <c>null</c>.</exception>
3567
public static EntityTypeBuilder<TempTable<TColumn1>> ConfigureTempTable<TColumn1>(this ModelBuilder modelBuilder, bool isKeyless = true)
3668
{
37-
var builder = modelBuilder.Configure<TempTable<TColumn1>>(isKeyless);
69+
var builder = modelBuilder.ConfigureTempTableInternal<TempTable<TColumn1>>(isKeyless);
3870

3971
if (!isKeyless)
4072
{
@@ -58,7 +90,7 @@ public static EntityTypeBuilder<TempTable<TColumn1, TColumn2>> ConfigureTempTabl
5890
{
5991
ArgumentNullException.ThrowIfNull(modelBuilder);
6092

61-
var builder = modelBuilder.Configure<TempTable<TColumn1, TColumn2>>(isKeyless);
93+
var builder = modelBuilder.ConfigureTempTableInternal<TempTable<TColumn1, TColumn2>>(isKeyless);
6294

6395
if (!isKeyless)
6496
{
@@ -70,7 +102,7 @@ public static EntityTypeBuilder<TempTable<TColumn1, TColumn2>> ConfigureTempTabl
70102
return builder;
71103
}
72104

73-
private static EntityTypeBuilder<T> Configure<T>(this ModelBuilder modelBuilder, bool isKeyless)
105+
private static EntityTypeBuilder<T> ConfigureTempTableInternal<T>(this ModelBuilder modelBuilder, bool isKeyless)
74106
where T : class
75107
{
76108
ArgumentNullException.ThrowIfNull(modelBuilder);

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Globalization;
3+
using System.Text;
4+
using System.Text.Json;
35
using Microsoft.EntityFrameworkCore.Infrastructure;
6+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
47
using Microsoft.EntityFrameworkCore.Migrations;
58
using Microsoft.EntityFrameworkCore.Query;
69
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
10+
using Microsoft.EntityFrameworkCore.Storage;
711
using Microsoft.Extensions.DependencyInjection;
812
using Microsoft.Extensions.DependencyInjection.Extensions;
13+
using Microsoft.Extensions.ObjectPool;
914
using Thinktecture.EntityFrameworkCore.BulkOperations;
1015
using Thinktecture.EntityFrameworkCore.Migrations;
16+
using Thinktecture.EntityFrameworkCore.Parameters;
1117
using Thinktecture.EntityFrameworkCore.Query;
1218
using Thinktecture.EntityFrameworkCore.TempTables;
1319

@@ -89,6 +95,9 @@ public bool AddCustomQuerySqlGeneratorFactory
8995
/// </summary>
9096
public bool AddBulkOperationSupport { get; set; }
9197

98+
private JsonSerializerOptions? _collectionParameterJsonSerializerOptions;
99+
private bool _addCollectionParameterSupport;
100+
92101
/// <summary>
93102
/// Changes the implementation of <see cref="IMigrationsSqlGenerator"/> to <see cref="ThinktectureSqlServerMigrationsSqlGenerator"/>.
94103
/// </summary>
@@ -133,6 +142,16 @@ public void ApplyServices(IServiceCollection services)
133142
services.TryAddScoped<ITruncateTableExecutor>(provider => provider.GetRequiredService<SqlServerBulkOperationExecutor>());
134143
}
135144

145+
if (_addCollectionParameterSupport)
146+
{
147+
var jsonSerializerOptions = _collectionParameterJsonSerializerOptions ?? new JsonSerializerOptions();
148+
149+
services.AddSingleton<ICollectionParameterFactory>(serviceProvider => new SqlServerCollectionParameterFactory(jsonSerializerOptions,
150+
serviceProvider.GetRequiredService<ObjectPool<StringBuilder>>(),
151+
serviceProvider.GetRequiredService<ISqlGenerationHelper>()));
152+
services.Add<IConventionSetPlugin, SqlServerCollectionParameterConventionSetPlugin>(GetLifetime<IConventionSetPlugin>());
153+
}
154+
136155
if (UseThinktectureSqlServerMigrationsSqlGenerator)
137156
AddWithCheck<IMigrationsSqlGenerator, ThinktectureSqlServerMigrationsSqlGenerator, SqlServerMigrationsSqlGenerator>(services);
138157

@@ -163,6 +182,15 @@ public void Register(Type serviceType, object implementationInstance)
163182
_relationalOptions.Register(serviceType, implementationInstance);
164183
}
165184

185+
/// <summary>
186+
/// Enables and disables support for queryable parameters.
187+
/// </summary>
188+
public void AddCollectionParameterSupport(bool addCollectionParameterSupport, JsonSerializerOptions? jsonSerializerOptions)
189+
{
190+
_addCollectionParameterSupport = addCollectionParameterSupport;
191+
_collectionParameterJsonSerializerOptions = jsonSerializerOptions;
192+
}
193+
166194
/// <inheritdoc />
167195
public void Validate(IDbContextOptions options)
168196
{
@@ -181,6 +209,7 @@ private class SqlServerDbContextOptionsExtensionInfo : DbContextOptionsExtension
181209
'Custom QuerySqlGeneratorFactory'={_extension.AddCustomQuerySqlGeneratorFactory},
182210
'Custom RelationalParameterBasedSqlProcessorFactory'={_extension.AddCustomRelationalParameterBasedSqlProcessorFactory},
183211
'BulkOperationSupport'={_extension.AddBulkOperationSupport},
212+
'CollectionParameterSupport'={_extension._addCollectionParameterSupport},
184213
'TenantDatabaseSupport'={_extension.AddTenantDatabaseSupport},
185214
'TableHintSupport'={_extension.AddTableHintSupport},
186215
'UseThinktectureSqlServerMigrationsSqlGenerator'={_extension.UseThinktectureSqlServerMigrationsSqlGenerator}
@@ -196,13 +225,19 @@ public SqlServerDbContextOptionsExtensionInfo(SqlServerDbContextOptionsExtension
196225
/// <inheritdoc />
197226
public override int GetServiceProviderHashCode()
198227
{
199-
return HashCode.Combine(_extension.AddCustomQueryableMethodTranslatingExpressionVisitorFactory,
200-
_extension.AddCustomQuerySqlGeneratorFactory,
201-
_extension.AddCustomRelationalParameterBasedSqlProcessorFactory,
202-
_extension.AddBulkOperationSupport,
203-
_extension.AddTenantDatabaseSupport,
204-
_extension.AddTableHintSupport,
205-
_extension.UseThinktectureSqlServerMigrationsSqlGenerator);
228+
var hashCode = new HashCode();
229+
230+
hashCode.Add(_extension.AddCustomQueryableMethodTranslatingExpressionVisitorFactory);
231+
hashCode.Add(_extension.AddCustomQuerySqlGeneratorFactory);
232+
hashCode.Add(_extension.AddCustomRelationalParameterBasedSqlProcessorFactory);
233+
hashCode.Add(_extension.AddBulkOperationSupport);
234+
hashCode.Add(_extension._addCollectionParameterSupport);
235+
hashCode.Add(_extension._collectionParameterJsonSerializerOptions);
236+
hashCode.Add(_extension.AddTenantDatabaseSupport);
237+
hashCode.Add(_extension.AddTableHintSupport);
238+
hashCode.Add(_extension.UseThinktectureSqlServerMigrationsSqlGenerator);
239+
240+
return hashCode.ToHashCode();
206241
}
207242

208243
/// <inheritdoc />
@@ -213,6 +248,8 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
213248
&& _extension.AddCustomQuerySqlGeneratorFactory == otherSqlServerInfo._extension.AddCustomQuerySqlGeneratorFactory
214249
&& _extension.AddCustomRelationalParameterBasedSqlProcessorFactory == otherSqlServerInfo._extension.AddCustomRelationalParameterBasedSqlProcessorFactory
215250
&& _extension.AddBulkOperationSupport == otherSqlServerInfo._extension.AddBulkOperationSupport
251+
&& _extension._addCollectionParameterSupport == otherSqlServerInfo._extension._addCollectionParameterSupport
252+
&& _extension._collectionParameterJsonSerializerOptions == otherSqlServerInfo._extension._collectionParameterJsonSerializerOptions
216253
&& _extension.AddTenantDatabaseSupport == otherSqlServerInfo._extension.AddTenantDatabaseSupport
217254
&& _extension.AddTableHintSupport == otherSqlServerInfo._extension.AddTableHintSupport
218255
&& _extension.UseThinktectureSqlServerMigrationsSqlGenerator == otherSqlServerInfo._extension.UseThinktectureSqlServerMigrationsSqlGenerator;
@@ -225,6 +262,7 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
225262
debugInfo["Thinktecture:CustomQuerySqlGeneratorFactory"] = _extension.AddCustomQuerySqlGeneratorFactory.ToString(CultureInfo.InvariantCulture);
226263
debugInfo["Thinktecture:CustomRelationalParameterBasedSqlProcessorFactory"] = _extension.AddCustomRelationalParameterBasedSqlProcessorFactory.ToString(CultureInfo.InvariantCulture);
227264
debugInfo["Thinktecture:BulkOperationSupport"] = _extension.AddBulkOperationSupport.ToString(CultureInfo.InvariantCulture);
265+
debugInfo["Thinktecture:CollectionParameterSupport"] = _extension._addCollectionParameterSupport.ToString(CultureInfo.InvariantCulture);
228266
debugInfo["Thinktecture:TenantDatabaseSupport"] = _extension.AddTenantDatabaseSupport.ToString(CultureInfo.InvariantCulture);
229267
debugInfo["Thinktecture:TableHintSupport"] = _extension.AddTableHintSupport.ToString(CultureInfo.InvariantCulture);
230268
debugInfo["Thinktecture:UseThinktectureSqlServerMigrationsSqlGenerator"] = _extension.UseThinktectureSqlServerMigrationsSqlGenerator.ToString(CultureInfo.InvariantCulture);

0 commit comments

Comments
 (0)