Skip to content

Commit 286151e

Browse files
committed
Merge branch 'releases/4.x.x'
2 parents 1cd81ba + 4d5eb20 commit 286151e

File tree

38 files changed

+3943
-151
lines changed

38 files changed

+3943
-151
lines changed

samples/Thinktecture.EntityFrameworkCore.SqlServer.Samples/Thinktecture.EntityFrameworkCore.SqlServer.Samples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>

samples/Thinktecture.EntityFrameworkCore.Sqlite.Samples/Thinktecture.EntityFrameworkCore.Sqlite.Samples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ internal static IReadOnlyList<PropertyWithNavigations> ConvertToEntityProperties
7272

7373
private static IProperty? FindProperty(IEntityType entityType, MemberInfo memberInfo)
7474
{
75-
return entityType.GetProperties().FirstOrDefault(property => property.PropertyInfo == memberInfo || property.FieldInfo == memberInfo);
75+
return entityType.GetProperties().FirstOrDefault(property => property.PropertyInfo?.MetadataToken == memberInfo.MetadataToken
76+
|| property.FieldInfo?.MetadataToken == memberInfo.MetadataToken);
7677
}
7778

7879
private static INavigation? FindOwnedProperty(
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
using Microsoft.EntityFrameworkCore.Storage;
2+
13
namespace Thinktecture.EntityFrameworkCore;
24

35
/// <summary>
46
/// Represents a table hint.
57
/// </summary>
68
public interface ITableHint
79
{
8-
}
10+
/// <summary>
11+
/// Returns a string representing the table hint.
12+
/// </summary>
13+
/// <param name="sqlGenerationHelper">SQL generation helper.</param>
14+
/// <returns>Table hint.</returns>
15+
string ToString(ISqlGenerationHelper sqlGenerationHelper);
16+
}

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections.Concurrent;
21
using System.Data;
32
using System.Data.Common;
43
using System.Linq.Expressions;
@@ -9,16 +8,32 @@
98

109
namespace Thinktecture.EntityFrameworkCore.Testing;
1110

11+
/// <summary>
12+
/// Provides instances of <see cref="DbContext"/> for testing purposes.
13+
/// </summary>
14+
public abstract class SqlServerTestDbContextProvider
15+
{
16+
private static readonly object _sharedLock = new();
17+
18+
/// <summary>
19+
/// Provides a lock object for database-wide operations like creation of tables.
20+
/// </summary>
21+
/// <param name="ctx">Current database context.</param>
22+
/// <returns>A lock object.</returns>
23+
protected virtual object GetSharedLock(DbContext ctx)
24+
{
25+
return _sharedLock;
26+
}
27+
}
28+
1229
/// <summary>
1330
/// Provides instances of <see cref="DbContext"/> for testing purposes.
1431
/// </summary>
1532
/// <typeparam name="T">Type of the database context.</typeparam>
16-
public class SqlServerTestDbContextProvider<T> : ITestDbContextProvider<T>
33+
public class SqlServerTestDbContextProvider<T> : SqlServerTestDbContextProvider, ITestDbContextProvider<T>
1734
where T : DbContext
1835
{
19-
// ReSharper disable once StaticMemberInGenericType because the locks are all for the same database context but for different schemas.
20-
private static readonly ConcurrentDictionary<string, object> _locks = new(StringComparer.OrdinalIgnoreCase);
21-
36+
private readonly object _instanceWideLock;
2237
private readonly ITestIsolationOptions _isolationOptions;
2338
private readonly DbContextOptions<T> _masterDbContextOptions;
2439
private readonly DbContextOptions<T> _dbContextOptions;
@@ -35,6 +50,7 @@ public class SqlServerTestDbContextProvider<T> : ITestDbContextProvider<T>
3550
private IDbContextTransaction? _tx;
3651
private bool _isAtLeastOneContextCreated;
3752
private readonly IsolationLevel _sharedTablesIsolationLevel;
53+
private readonly IsolationLevel _migrationAndCleanupIsolationLevel;
3854
private bool _isDisposed;
3955

4056
/// <inheritdoc />
@@ -70,8 +86,10 @@ protected internal SqlServerTestDbContextProvider(SqlServerTestDbContextProvider
7086
{
7187
ArgumentNullException.ThrowIfNull(options);
7288

89+
_instanceWideLock = new object();
7390
Schema = options.Schema ?? throw new ArgumentException($"The '{nameof(options.Schema)}' cannot be null.", nameof(options));
7491
_sharedTablesIsolationLevel = ValidateIsolationLevel(options.SharedTablesIsolationLevel);
92+
_migrationAndCleanupIsolationLevel = options.MigrationAndCleanupIsolationLevel ?? IsolationLevel.Serializable;
7593
_isolationOptions = options.IsolationOptions;
7694
_masterConnection = options.MasterConnection ?? throw new ArgumentException($"The '{nameof(options.MasterConnection)}' cannot be null.", nameof(options));
7795
_masterDbContextOptions = options.MasterDbContextOptions ?? throw new ArgumentException($"The '{nameof(options.MasterDbContextOptions)}' cannot be null.", nameof(options));
@@ -117,7 +135,7 @@ public T CreateDbContext(bool useMasterConnection)
117135

118136
bool isFirstCtx;
119137

120-
lock (_locks.GetOrAdd(Schema, _ => new object()))
138+
lock (_instanceWideLock)
121139
{
122140
isFirstCtx = !_isAtLeastOneContextCreated;
123141
_isAtLeastOneContextCreated = true;
@@ -204,6 +222,18 @@ protected virtual IDbContextTransaction BeginTransaction(T ctx)
204222
return ctx.Database.BeginTransaction(_sharedTablesIsolationLevel);
205223
}
206224

225+
/// <summary>
226+
/// Starts a new transaction for migration and cleanup.
227+
/// </summary>
228+
/// <param name="ctx">Database context.</param>
229+
/// <returns>An instance of <see cref="IDbContextTransaction"/>.</returns>
230+
protected virtual IDbContextTransaction? BeginMigrationAndCleanupTransaction(T ctx)
231+
{
232+
ArgumentNullException.ThrowIfNull(ctx);
233+
234+
return ctx.Database.BeginTransaction(_migrationAndCleanupIsolationLevel);
235+
}
236+
207237
/// <summary>
208238
/// Runs migrations for provided <paramref name="ctx" />.
209239
/// </summary>
@@ -214,15 +244,28 @@ protected virtual void RunMigrations(T ctx)
214244
ArgumentNullException.ThrowIfNull(ctx);
215245

216246
// concurrent execution is not supported by EF migrations
217-
lock (_locks.GetOrAdd(Schema, _ => new object()))
247+
lock (GetSharedLock(ctx))
218248
{
219249
var logLevel = LogLevelSwitch.MinimumLogLevel;
220250

221251
try
222252
{
223253
LogLevelSwitch.MinimumLogLevel = _testingLoggingOptions.MigrationLogLevel;
224254

225-
_migrationExecutionStrategy.Migrate(ctx);
255+
IDbContextTransaction? migrationTx = null;
256+
257+
if (ctx.Database.CurrentTransaction is null)
258+
migrationTx = BeginMigrationAndCleanupTransaction(ctx);
259+
260+
try
261+
{
262+
_migrationExecutionStrategy.Migrate(ctx);
263+
migrationTx?.Commit();
264+
}
265+
finally
266+
{
267+
migrationTx?.Dispose();
268+
}
226269
}
227270
finally
228271
{
@@ -279,12 +322,20 @@ protected virtual async ValueTask DisposeAsync(bool disposing)
279322
if (!disposing)
280323
return;
281324

282-
if (_isAtLeastOneContextCreated)
325+
var isAtLeastOneContextCreated = false;
326+
327+
lock (_instanceWideLock)
283328
{
284-
await DisposeContextsAndRollbackMigrationsAsync(default);
285-
_isAtLeastOneContextCreated = false;
329+
if (_isAtLeastOneContextCreated)
330+
{
331+
isAtLeastOneContextCreated = true;
332+
_isAtLeastOneContextCreated = false;
333+
}
286334
}
287335

336+
if (isAtLeastOneContextCreated)
337+
await DisposeContextsAndRollbackMigrationsAsync(default);
338+
288339
_masterConnection.Dispose();
289340
_testingLoggingOptions.Dispose();
290341
}

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProviderBuilder.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class SqlServerTestDbContextProviderBuilder<T> : TestDbContextProviderBui
2727
private bool _useThinktectureSqlServerMigrationsSqlGenerator = true;
2828
private string? _sharedTablesSchema;
2929
private IsolationLevel? _sharedTablesIsolationLevel;
30+
private IsolationLevel? _migrationAndCleanupIsolationLevel;
3031
private Func<DbContextOptions<T>, IDbDefaultSchema, T?>? _contextFactory;
3132
private Func<SqlServerTestDbContextProviderOptions<T>, SqlServerTestDbContextProvider<T>?>? _providerFactory;
3233

@@ -68,6 +69,19 @@ public SqlServerTestDbContextProviderBuilder<T> UseSharedTablesIsolationLevel(Is
6869
return this;
6970
}
7071

72+
/// <summary>
73+
/// Specifies the isolation level to use when migrating and cleaning up the database.
74+
/// Default is <see cref="IsolationLevel.Serializable"/>.
75+
/// </summary>
76+
/// <param name="migrationAndCleanupIsolationLevel">Isolation level to use.</param>
77+
/// <returns>Current builder for chaining</returns>
78+
public SqlServerTestDbContextProviderBuilder<T> UseMigrationAndCleanupIsolationLevel(IsolationLevel migrationAndCleanupIsolationLevel)
79+
{
80+
_migrationAndCleanupIsolationLevel = migrationAndCleanupIsolationLevel;
81+
82+
return this;
83+
}
84+
7185
/// <summary>
7286
/// Specifies the migration strategy to use.
7387
/// Default is <see cref="IMigrationExecutionStrategy.Migrations"/>.
@@ -357,7 +371,8 @@ public SqlServerTestDbContextProvider<T> Build()
357371
IsolationOptions = _isolationOptions,
358372
ContextFactory = _contextFactory,
359373
ExecutedCommands = state.CommandCapturingInterceptor?.Commands,
360-
SharedTablesIsolationLevel = _sharedTablesIsolationLevel
374+
SharedTablesIsolationLevel = _sharedTablesIsolationLevel,
375+
MigrationAndCleanupIsolationLevel = _migrationAndCleanupIsolationLevel
361376
};
362377

363378
return _providerFactory?.Invoke(options) ?? new SqlServerTestDbContextProvider<T>(options);

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProviderOptions.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ namespace Thinktecture.EntityFrameworkCore.Testing;
1111
public class SqlServerTestDbContextProviderOptions<T> : TestDbContextProviderOptions<T>
1212
where T : DbContext
1313
{
14-
1514
/// <summary>
1615
/// Indication whether the current <see cref="SqlServerTestDbContextProvider{T}"/> is using its own tables with a new schema
1716
/// or shares the tables with others.
@@ -46,9 +45,16 @@ public ITestIsolationOptions IsolationOptions
4645

4746
/// <summary>
4847
/// Isolation level to be used with shared tables.
48+
/// Default is <see cref="IsolationLevel.ReadCommitted"/>.
4949
/// </summary>
5050
public IsolationLevel? SharedTablesIsolationLevel { get; set; }
5151

52+
/// <summary>
53+
/// Isolation level to use when migrating and cleaning up the database.
54+
/// Default is <see cref="IsolationLevel.Serializable"/>.
55+
/// </summary>
56+
public IsolationLevel? MigrationAndCleanupIsolationLevel { get; set; }
57+
5258
/// <summary>
5359
/// Initializes new instance of <see cref="SqlServerTestDbContextProviderOptions{T}"/>.
5460
/// </summary>
Lines changed: 70 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,70 @@
1-
namespace Thinktecture.EntityFrameworkCore.BulkOperations;
2-
3-
/// <summary>
4-
/// Bulk insert or update options for SQL Server.
5-
/// </summary>
6-
public sealed class SqlServerBulkInsertOrUpdateOptions : ISqlServerMergeOperationOptions, IBulkInsertOrUpdateOptions
7-
{
8-
/// <inheritdoc />
9-
public IEntityPropertiesProvider? PropertiesToInsert { get; set; }
10-
11-
/// <inheritdoc />
12-
public IEntityPropertiesProvider? PropertiesToUpdate { get; set; }
13-
14-
/// <inheritdoc />
15-
public IEntityPropertiesProvider? KeyProperties { get; set; }
16-
17-
/// <inheritdoc />
18-
public List<SqlServerTableHintLimited> MergeTableHints { get; }
19-
20-
/// <inheritdoc />
21-
public SqlServerBulkOperationTempTableOptions TempTableOptions { get; }
22-
23-
/// <summary>
24-
/// Initializes new instance of <see cref="SqlServerBulkUpdateOptions"/>.
25-
/// </summary>
26-
/// <param name="optionsToInitializeFrom">Options to initialize from.</param>
27-
public SqlServerBulkInsertOrUpdateOptions(IBulkInsertOrUpdateOptions? optionsToInitializeFrom = null)
28-
{
29-
if (optionsToInitializeFrom is not null)
30-
{
31-
PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert;
32-
PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate;
33-
KeyProperties = optionsToInitializeFrom.KeyProperties;
34-
}
35-
36-
if (optionsToInitializeFrom is ISqlServerMergeOperationOptions mergeOptions)
37-
{
38-
TempTableOptions = new SqlServerBulkOperationTempTableOptions(mergeOptions.TempTableOptions);
39-
MergeTableHints = mergeOptions.MergeTableHints.ToList();
40-
}
41-
else
42-
{
43-
TempTableOptions = new SqlServerBulkOperationTempTableOptions();
44-
MergeTableHints = new List<SqlServerTableHintLimited> { SqlServerTableHintLimited.HoldLock };
45-
}
46-
}
47-
48-
/// <summary>
49-
/// Gets the options for bulk insert into a temp table.
50-
/// </summary>
51-
public SqlServerTempTableBulkOperationOptions GetTempTableBulkInsertOptions()
52-
{
53-
var options = new SqlServerTempTableBulkInsertOptions
54-
{
55-
PropertiesToInsert = PropertiesToInsert is null || PropertiesToUpdate is null
56-
? null
57-
: CompositeTempTableEntityPropertiesProvider.CreateForInsertOrUpdate(PropertiesToInsert, PropertiesToUpdate, KeyProperties),
58-
Advanced = { UsePropertiesToInsertForTempTableCreation = true }
59-
};
60-
61-
TempTableOptions.Populate(options);
62-
63-
return options;
64-
}
65-
}
1+
using Microsoft.Data.SqlClient;
2+
3+
namespace Thinktecture.EntityFrameworkCore.BulkOperations;
4+
5+
/// <summary>
6+
/// Bulk insert or update options for SQL Server.
7+
/// </summary>
8+
public sealed class SqlServerBulkInsertOrUpdateOptions : ISqlServerMergeOperationOptions, IBulkInsertOrUpdateOptions
9+
{
10+
/// <inheritdoc />
11+
public IEntityPropertiesProvider? PropertiesToInsert { get; set; }
12+
13+
/// <inheritdoc />
14+
public IEntityPropertiesProvider? PropertiesToUpdate { get; set; }
15+
16+
/// <inheritdoc />
17+
public IEntityPropertiesProvider? KeyProperties { get; set; }
18+
19+
/// <inheritdoc />
20+
public List<SqlServerTableHintLimited> MergeTableHints { get; }
21+
22+
/// <inheritdoc />
23+
public SqlServerBulkOperationTempTableOptions TempTableOptions { get; }
24+
25+
/// <summary>
26+
/// Initializes new instance of <see cref="SqlServerBulkUpdateOptions"/>.
27+
/// </summary>
28+
/// <param name="optionsToInitializeFrom">Options to initialize from.</param>
29+
public SqlServerBulkInsertOrUpdateOptions(IBulkInsertOrUpdateOptions? optionsToInitializeFrom = null)
30+
{
31+
if (optionsToInitializeFrom is not null)
32+
{
33+
PropertiesToInsert = optionsToInitializeFrom.PropertiesToInsert;
34+
PropertiesToUpdate = optionsToInitializeFrom.PropertiesToUpdate;
35+
KeyProperties = optionsToInitializeFrom.KeyProperties;
36+
}
37+
38+
if (optionsToInitializeFrom is ISqlServerMergeOperationOptions mergeOptions)
39+
{
40+
TempTableOptions = new SqlServerBulkOperationTempTableOptions(mergeOptions.TempTableOptions);
41+
MergeTableHints = mergeOptions.MergeTableHints.ToList();
42+
}
43+
else
44+
{
45+
TempTableOptions = new SqlServerBulkOperationTempTableOptions
46+
{
47+
SqlBulkCopyOptions = SqlBulkCopyOptions.KeepIdentity
48+
};
49+
MergeTableHints = new List<SqlServerTableHintLimited> { SqlServerTableHintLimited.HoldLock };
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Gets the options for bulk insert into a temp table.
55+
/// </summary>
56+
public SqlServerTempTableBulkOperationOptions GetTempTableBulkInsertOptions()
57+
{
58+
var options = new SqlServerTempTableBulkInsertOptions
59+
{
60+
PropertiesToInsert = PropertiesToInsert is null || PropertiesToUpdate is null
61+
? null
62+
: CompositeTempTableEntityPropertiesProvider.CreateForInsertOrUpdate(PropertiesToInsert, PropertiesToUpdate, KeyProperties),
63+
Advanced = { UsePropertiesToInsertForTempTableCreation = true }
64+
};
65+
66+
TempTableOptions.Populate(options);
67+
68+
return options;
69+
}
70+
}

0 commit comments

Comments
 (0)