Skip to content

Commit f04a301

Browse files
committed
Added SQL Server feature to be able to provide a collection of values as a JSON parameter
1 parent 5df763d commit f04a301

File tree

16 files changed

+480
-16
lines changed

16 files changed

+480
-16
lines changed

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

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

46+
await DoContainsUsingCollectionParameterAsync(ctx, new List<Guid> { customerId });
47+
ctx.ChangeTracker.Clear();
48+
4649
// Bulk insert into temp tables
4750
await DoBulkInsertEntitiesIntoTempTableAsync(ctx);
4851
ctx.ChangeTracker.Clear();
@@ -73,6 +76,14 @@ public static async Task Main(string[] args)
7376
Console.WriteLine("Exiting samples...");
7477
}
7578

79+
private static async Task DoContainsUsingCollectionParameterAsync(DemoDbContext ctx, List<Guid> customerIds)
80+
{
81+
var customerIdsQuery = ctx.ToScalarCollectionParameter(customerIds);
82+
83+
var customers = await ctx.Customers.Join(customerIdsQuery, c => c.Id, t => t, (c, t) => c).ToListAsync();
84+
Console.WriteLine($"Found customers: {String.Join(", ", customers.Select(c => c.Id))}");
85+
}
86+
7687
private static async Task DoTenantQueriesAsync(DemoDbContext ctx)
7788
{
7889
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 values with.</param>
14+
/// <param name="values">A collection of values to create a query from.</param>
15+
/// <typeparam name="T">Type of the values.</typeparam>
16+
/// <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+
}
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: 15 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,18 @@ 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 values with.</param>
314+
/// <param name="values">A collection of values to create a query from.</param>
315+
/// <typeparam name="T">Type of the values.</typeparam>
316+
/// <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)
318+
{
319+
var factory = ctx.GetService<ICollectionParameterFactory>();
320+
321+
return factory.CreateScalarQuery(ctx, values);
322+
}
308323
}

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

Lines changed: 20 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,22 @@ 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();
2541
}
2642

2743
/// <summary>
@@ -34,7 +50,7 @@ public static EntityTypeBuilder<T> ConfigureTempTableEntity<T>(this ModelBuilder
3450
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/> is <c>null</c>.</exception>
3551
public static EntityTypeBuilder<TempTable<TColumn1>> ConfigureTempTable<TColumn1>(this ModelBuilder modelBuilder, bool isKeyless = true)
3652
{
37-
var builder = modelBuilder.Configure<TempTable<TColumn1>>(isKeyless);
53+
var builder = modelBuilder.ConfigureTempTableInternal<TempTable<TColumn1>>(isKeyless);
3854

3955
if (!isKeyless)
4056
{
@@ -58,7 +74,7 @@ public static EntityTypeBuilder<TempTable<TColumn1, TColumn2>> ConfigureTempTabl
5874
{
5975
ArgumentNullException.ThrowIfNull(modelBuilder);
6076

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

6379
if (!isKeyless)
6480
{
@@ -70,7 +86,7 @@ public static EntityTypeBuilder<TempTable<TColumn1, TColumn2>> ConfigureTempTabl
7086
return builder;
7187
}
7288

73-
private static EntityTypeBuilder<T> Configure<T>(this ModelBuilder modelBuilder, bool isKeyless)
89+
private static EntityTypeBuilder<T> ConfigureTempTableInternal<T>(this ModelBuilder modelBuilder, bool isKeyless)
7490
where T : class
7591
{
7692
ArgumentNullException.ThrowIfNull(modelBuilder);

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Globalization;
3+
using System.Text.Json;
34
using Microsoft.EntityFrameworkCore.Infrastructure;
5+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
46
using Microsoft.EntityFrameworkCore.Migrations;
57
using Microsoft.EntityFrameworkCore.Query;
68
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
79
using Microsoft.Extensions.DependencyInjection;
810
using Microsoft.Extensions.DependencyInjection.Extensions;
911
using Thinktecture.EntityFrameworkCore.BulkOperations;
1012
using Thinktecture.EntityFrameworkCore.Migrations;
13+
using Thinktecture.EntityFrameworkCore.Parameters;
1114
using Thinktecture.EntityFrameworkCore.Query;
1215
using Thinktecture.EntityFrameworkCore.TempTables;
1316

@@ -89,6 +92,9 @@ public bool AddCustomQuerySqlGeneratorFactory
8992
/// </summary>
9093
public bool AddBulkOperationSupport { get; set; }
9194

95+
private JsonSerializerOptions? _collectionParameterJsonSerializerOptions;
96+
private bool _addCollectionParameterSupport;
97+
9298
/// <summary>
9399
/// Changes the implementation of <see cref="IMigrationsSqlGenerator"/> to <see cref="ThinktectureSqlServerMigrationsSqlGenerator"/>.
94100
/// </summary>
@@ -133,6 +139,14 @@ public void ApplyServices(IServiceCollection services)
133139
services.TryAddScoped<ITruncateTableExecutor>(provider => provider.GetRequiredService<SqlServerBulkOperationExecutor>());
134140
}
135141

142+
if (_addCollectionParameterSupport)
143+
{
144+
var jsonSerializerOptions = _collectionParameterJsonSerializerOptions ?? new JsonSerializerOptions();
145+
146+
services.AddSingleton<ICollectionParameterFactory>(_ => new SqlServerCollectionParameterFactory(jsonSerializerOptions));
147+
services.Add<IConventionSetPlugin, SqlServerCollectionParameterConventionSetPlugin>(GetLifetime<IConventionSetPlugin>());
148+
}
149+
136150
if (UseThinktectureSqlServerMigrationsSqlGenerator)
137151
AddWithCheck<IMigrationsSqlGenerator, ThinktectureSqlServerMigrationsSqlGenerator, SqlServerMigrationsSqlGenerator>(services);
138152

@@ -163,6 +177,15 @@ public void Register(Type serviceType, object implementationInstance)
163177
_relationalOptions.Register(serviceType, implementationInstance);
164178
}
165179

180+
/// <summary>
181+
/// Enables and disables support for queryable parameters.
182+
/// </summary>
183+
public void AddCollectionParameterSupport(bool addCollectionParameterSupport, JsonSerializerOptions? jsonSerializerOptions)
184+
{
185+
_addCollectionParameterSupport = addCollectionParameterSupport;
186+
_collectionParameterJsonSerializerOptions = jsonSerializerOptions;
187+
}
188+
166189
/// <inheritdoc />
167190
public void Validate(IDbContextOptions options)
168191
{
@@ -181,6 +204,7 @@ private class SqlServerDbContextOptionsExtensionInfo : DbContextOptionsExtension
181204
'Custom QuerySqlGeneratorFactory'={_extension.AddCustomQuerySqlGeneratorFactory},
182205
'Custom RelationalParameterBasedSqlProcessorFactory'={_extension.AddCustomRelationalParameterBasedSqlProcessorFactory},
183206
'BulkOperationSupport'={_extension.AddBulkOperationSupport},
207+
'CollectionParameterSupport'={_extension._addCollectionParameterSupport},
184208
'TenantDatabaseSupport'={_extension.AddTenantDatabaseSupport},
185209
'TableHintSupport'={_extension.AddTableHintSupport},
186210
'UseThinktectureSqlServerMigrationsSqlGenerator'={_extension.UseThinktectureSqlServerMigrationsSqlGenerator}
@@ -196,13 +220,19 @@ public SqlServerDbContextOptionsExtensionInfo(SqlServerDbContextOptionsExtension
196220
/// <inheritdoc />
197221
public override int GetServiceProviderHashCode()
198222
{
199-
return HashCode.Combine(_extension.AddCustomQueryableMethodTranslatingExpressionVisitorFactory,
200-
_extension.AddCustomQuerySqlGeneratorFactory,
201-
_extension.AddCustomRelationalParameterBasedSqlProcessorFactory,
202-
_extension.AddBulkOperationSupport,
203-
_extension.AddTenantDatabaseSupport,
204-
_extension.AddTableHintSupport,
205-
_extension.UseThinktectureSqlServerMigrationsSqlGenerator);
223+
var hashCode = new HashCode();
224+
225+
hashCode.Add(_extension.AddCustomQueryableMethodTranslatingExpressionVisitorFactory);
226+
hashCode.Add(_extension.AddCustomQuerySqlGeneratorFactory);
227+
hashCode.Add(_extension.AddCustomRelationalParameterBasedSqlProcessorFactory);
228+
hashCode.Add(_extension.AddBulkOperationSupport);
229+
hashCode.Add(_extension._addCollectionParameterSupport);
230+
hashCode.Add(_extension._collectionParameterJsonSerializerOptions);
231+
hashCode.Add(_extension.AddTenantDatabaseSupport);
232+
hashCode.Add(_extension.AddTableHintSupport);
233+
hashCode.Add(_extension.UseThinktectureSqlServerMigrationsSqlGenerator);
234+
235+
return hashCode.ToHashCode();
206236
}
207237

208238
/// <inheritdoc />
@@ -213,6 +243,8 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
213243
&& _extension.AddCustomQuerySqlGeneratorFactory == otherSqlServerInfo._extension.AddCustomQuerySqlGeneratorFactory
214244
&& _extension.AddCustomRelationalParameterBasedSqlProcessorFactory == otherSqlServerInfo._extension.AddCustomRelationalParameterBasedSqlProcessorFactory
215245
&& _extension.AddBulkOperationSupport == otherSqlServerInfo._extension.AddBulkOperationSupport
246+
&& _extension._addCollectionParameterSupport == otherSqlServerInfo._extension._addCollectionParameterSupport
247+
&& _extension._collectionParameterJsonSerializerOptions == otherSqlServerInfo._extension._collectionParameterJsonSerializerOptions
216248
&& _extension.AddTenantDatabaseSupport == otherSqlServerInfo._extension.AddTenantDatabaseSupport
217249
&& _extension.AddTableHintSupport == otherSqlServerInfo._extension.AddTableHintSupport
218250
&& _extension.UseThinktectureSqlServerMigrationsSqlGenerator == otherSqlServerInfo._extension.UseThinktectureSqlServerMigrationsSqlGenerator;
@@ -225,6 +257,7 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
225257
debugInfo["Thinktecture:CustomQuerySqlGeneratorFactory"] = _extension.AddCustomQuerySqlGeneratorFactory.ToString(CultureInfo.InvariantCulture);
226258
debugInfo["Thinktecture:CustomRelationalParameterBasedSqlProcessorFactory"] = _extension.AddCustomRelationalParameterBasedSqlProcessorFactory.ToString(CultureInfo.InvariantCulture);
227259
debugInfo["Thinktecture:BulkOperationSupport"] = _extension.AddBulkOperationSupport.ToString(CultureInfo.InvariantCulture);
260+
debugInfo["Thinktecture:CollectionParameterSupport"] = _extension._addCollectionParameterSupport.ToString(CultureInfo.InvariantCulture);
228261
debugInfo["Thinktecture:TenantDatabaseSupport"] = _extension.AddTenantDatabaseSupport.ToString(CultureInfo.InvariantCulture);
229262
debugInfo["Thinktecture:TableHintSupport"] = _extension.AddTableHintSupport.ToString(CultureInfo.InvariantCulture);
230263
debugInfo["Thinktecture:UseThinktectureSqlServerMigrationsSqlGenerator"] = _extension.UseThinktectureSqlServerMigrationsSqlGenerator.ToString(CultureInfo.InvariantCulture);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
4+
namespace Thinktecture.EntityFrameworkCore.Parameters;
5+
6+
/// <summary>
7+
/// Represents a collection parameter.
8+
/// </summary>
9+
public class JsonCollectionParameter<T, TConverted> : JsonCollectionParameter
10+
{
11+
private readonly IEnumerable<T> _values;
12+
private readonly Func<object?, object?> _convertValue;
13+
private readonly JsonSerializerOptions _jsonSerializerOptions;
14+
15+
/// <summary>
16+
/// Initializes a new instance of <see cref="JsonCollectionParameter{T}"/>.
17+
/// </summary>
18+
/// <param name="values">Values to serialize.</param>
19+
/// <param name="convertValue">EF value converter.</param>
20+
/// <param name="jsonSerializerOptions">JSON serialization settings.</param>
21+
public JsonCollectionParameter(
22+
IEnumerable<T> values,
23+
JsonSerializerOptions jsonSerializerOptions,
24+
Func<object?, object?> convertValue)
25+
{
26+
_values = values ?? throw new ArgumentNullException(nameof(values));
27+
_jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions));
28+
_convertValue = convertValue ?? throw new ArgumentNullException(nameof(convertValue));
29+
}
30+
31+
/// <inheritdoc />
32+
public override string ToString(IFormatProvider? provider)
33+
{
34+
var convert = _convertValue;
35+
var values = _values.Select(i => (TConverted)convert(i)!);
36+
37+
return JsonSerializer.Serialize(values, typeof(IEnumerable<TConverted>), _jsonSerializerOptions);
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Represents a collection parameter.
43+
/// </summary>
44+
public class JsonCollectionParameter<T> : JsonCollectionParameter
45+
{
46+
private readonly IEnumerable<T> _values;
47+
private readonly JsonSerializerOptions _jsonSerializerOptions;
48+
49+
/// <summary>
50+
/// Initializes a new instance of <see cref="JsonCollectionParameter{T}"/>.
51+
/// </summary>
52+
/// <param name="values">Values to serialize.</param>
53+
/// <param name="jsonSerializerOptions">JSON serialization settings.</param>
54+
public JsonCollectionParameter(
55+
IEnumerable<T> values,
56+
JsonSerializerOptions jsonSerializerOptions)
57+
{
58+
_values = values ?? throw new ArgumentNullException(nameof(values));
59+
_jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions));
60+
}
61+
62+
/// <inheritdoc />
63+
public override string ToString(IFormatProvider? provider)
64+
{
65+
return JsonSerializer.Serialize(_values, typeof(IEnumerable<T>), _jsonSerializerOptions);
66+
}
67+
}
68+
69+
/// <summary>
70+
/// Represents a collection parameter.
71+
/// </summary>
72+
public abstract class JsonCollectionParameter : IConvertible
73+
{
74+
/// <inheritdoc />
75+
public abstract string ToString(IFormatProvider? provider);
76+
77+
#pragma warning disable CS1591
78+
// ReSharper disable ArrangeMethodOrOperatorBody
79+
public TypeCode GetTypeCode() => throw new NotSupportedException();
80+
public bool ToBoolean(IFormatProvider? provider) => throw new NotSupportedException();
81+
public byte ToByte(IFormatProvider? provider) => throw new NotSupportedException();
82+
public char ToChar(IFormatProvider? provider) => throw new NotSupportedException();
83+
public DateTime ToDateTime(IFormatProvider? provider) => throw new NotSupportedException();
84+
public decimal ToDecimal(IFormatProvider? provider) => throw new NotSupportedException();
85+
public double ToDouble(IFormatProvider? provider) => throw new NotSupportedException();
86+
public short ToInt16(IFormatProvider? provider) => throw new NotSupportedException();
87+
public int ToInt32(IFormatProvider? provider) => throw new NotSupportedException();
88+
public long ToInt64(IFormatProvider? provider) => throw new NotSupportedException();
89+
public sbyte ToSByte(IFormatProvider? provider) => throw new NotSupportedException();
90+
public float ToSingle(IFormatProvider? provider) => throw new NotSupportedException();
91+
public object ToType(Type conversionType, IFormatProvider? provider) => throw new NotSupportedException();
92+
public ushort ToUInt16(IFormatProvider? provider) => throw new NotSupportedException();
93+
public uint ToUInt32(IFormatProvider? provider) => throw new NotSupportedException();
94+
public ulong ToUInt64(IFormatProvider? provider) => throw new NotSupportedException();
95+
// ReSharper restore ArrangeMethodOrOperatorBody
96+
#pragma warning restore CS1591
97+
}

0 commit comments

Comments
 (0)