From 721fe78170c4e01c6436c2dc1872d6fd70f3de4f Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Fri, 23 Jul 2021 13:17:49 -0700 Subject: [PATCH] Add IColumnModification and IModificationCommand to allow the implementations to be replaced easily Fixes #23027 Fixes #12169 Fixes #17946 Co-authored-by: Kovalenko Dmitry --- ...ntityFrameworkRelationalServicesBuilder.cs | 6 +- .../Internal/MigrationsModelDiffer.cs | 6 +- .../Migrations/MigrationsSqlGenerator.cs | 50 +++-- .../MigrationsSqlGeneratorDependencies.cs | 7 + .../Operations/DeleteDataOperation.cs | 14 +- .../Operations/InsertDataOperation.cs | 15 +- .../Operations/UpdateDataOperation.cs | 22 +- .../Storage/RelationalCommand.cs | 16 +- .../RelationalCommandParameterObject.cs | 8 +- .../Storage/RelationalDataReader.cs | 15 -- .../Update/ColumnModification.cs | 138 ++++++++----- .../Update/ColumnModificationParameters.cs | 191 ++++++++++++++++++ .../Update/IColumnModification.cs | 131 ++++++++++++ .../Update/IModificationCommand.cs | 61 ++++++ .../Update/IMutableModificationCommand.cs | 30 +++ .../IMutableModificationCommandFactory.cs | 31 +++ .../Update/IUpdateSqlGenerator.cs | 6 +- .../Update/Internal/CommandBatchPreparer.cs | 107 +++++----- .../CommandBatchPreparerDependencies.cs | 14 +- .../Internal/ModificationCommandComparer.cs | 4 +- .../MutableModificationCommandFactory.cs | 26 +++ .../Update/ModificationCommand.cs | 144 ++++++------- .../Update/ModificationCommandBatch.cs | 4 +- .../Update/ModificationCommandParameters.cs | 75 +++++++ .../Update/ReaderModificationCommandBatch.cs | 16 +- .../SingularModificationCommandBatch.cs | 2 +- .../Update/UpdateSqlGenerator.cs | 41 ++-- .../Internal/ISqlServerUpdateSqlGenerator.cs | 2 +- .../SqlServerModificationCommandBatch.cs | 8 +- .../Internal/SqlServerUpdateSqlGenerator.cs | 44 ++-- .../Internal/SqliteUpdateSqlGenerator.cs | 2 +- .../Update/UpdateSqlGeneratorTestBase.cs | 73 ++++--- .../MigrationCommandListBuilderTest.cs | 2 + .../Storage/RelationalCommandTest.cs | 6 +- .../TestUtilities/FakeModificationCommand.cs | 25 --- .../FakeProvider/FakeSqlGenerator.cs | 8 +- .../TestModificationCommandBatch.cs | 2 +- .../Update/CommandBatchPreparerTest.cs | 1 + .../Update/ModificationCommandComparerTest.cs | 61 ++++-- .../Update/ModificationCommandTest.cs | 45 +++-- .../ReaderModificationCommandBatchTest.cs | 161 +++++++++------ ...rverModificationCommandBatchFactoryTest.cs | 24 ++- .../SqlServerModificationCommandBatchTest.cs | 20 +- .../Query/BadDataSqliteTest.cs | 6 +- 44 files changed, 1182 insertions(+), 488 deletions(-) create mode 100644 src/EFCore.Relational/Update/ColumnModificationParameters.cs create mode 100644 src/EFCore.Relational/Update/IColumnModification.cs create mode 100644 src/EFCore.Relational/Update/IModificationCommand.cs create mode 100644 src/EFCore.Relational/Update/IMutableModificationCommand.cs create mode 100644 src/EFCore.Relational/Update/IMutableModificationCommandFactory.cs create mode 100644 src/EFCore.Relational/Update/Internal/MutableModificationCommandFactory.cs create mode 100644 src/EFCore.Relational/Update/ModificationCommandParameters.cs delete mode 100644 test/EFCore.Relational.Tests/TestUtilities/FakeModificationCommand.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 26376459b50..a92eb188df8 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -55,7 +55,7 @@ public static readonly IDictionary RelationalServi { { typeof(IKeyValueIndexFactorySource), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IParameterNameGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, - { typeof(IComparer), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IComparer), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMigrationsIdGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ISqlGenerationHelper), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IRelationalAnnotationProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) }, @@ -75,6 +75,7 @@ public static readonly IDictionary RelationalServi { typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IMutableModificationCommandFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -132,7 +133,7 @@ protected override ServiceCharacteristics GetServiceCharacteristics(Type service public override EntityFrameworkServicesBuilder TryAddCoreServices() { TryAdd(); - TryAdd, ModificationCommandComparer>(); + TryAdd, ModificationCommandComparer>(); TryAdd(); TryAdd(); TryAdd(); @@ -149,6 +150,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd(); TryAdd(); TryAdd(); diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index c489a9d32a6..dd941d04a5b 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -2212,7 +2212,6 @@ private IEnumerable GetDataOperations( if (forSource) { - // There shouldn't be any inserts using the source model Check.DebugAssert(false, "Insert using the source model"); break; } @@ -2236,7 +2235,6 @@ private IEnumerable GetDataOperations( if (forSource) { - // There shouldn't be any updates using the source model Check.DebugAssert(false, "Update using the source model"); break; } @@ -2311,10 +2309,10 @@ private IEnumerable GetDataOperations( } } - private object? GetValue(ColumnModification columnModification) + private object? GetValue(IColumnModification columnModification) { var converter = GetValueConverter(columnModification.Property!); - var value = columnModification.UseCurrentValueParameter + var value = columnModification.UseCurrentValue ? columnModification.Value : columnModification.OriginalValue; return converter != null diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs index fb1c9948403..23867a07e52 100644 --- a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs +++ b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs @@ -943,7 +943,7 @@ protected virtual void Generate( /// The data operation to generate commands for. /// The model. /// The commands that correspond to the given operation. - protected virtual IEnumerable GenerateModificationCommands( + protected virtual IEnumerable GenerateModificationCommands( InsertDataOperation operation, IModel? model) { @@ -976,7 +976,8 @@ protected virtual IEnumerable GenerateModificationCommands( for (var i = 0; i < operation.Values.GetLength(0); i++) { - var modifications = new ColumnModification[operation.Columns.Length]; + var modificationCommand = Dependencies.MutableModificationCommandFactory.CreateModificationCommand( + new ModificationCommandParameters(operation.Table, operation.Schema, SensitiveLoggingEnabled)); for (var j = 0; j < operation.Columns.Length; j++) { var name = operation.Columns[j]; @@ -989,14 +990,13 @@ protected virtual IEnumerable GenerateModificationCommands( ? Dependencies.TypeMappingSource.FindMapping(value.GetType(), columnType) : Dependencies.TypeMappingSource.FindMapping(columnType!); - modifications[j] = new ColumnModification( + modificationCommand.AddColumnModification(new ColumnModificationParameters( name, originalValue: null, value, propertyMapping?.Property, columnType, typeMapping, - isRead: false, isWrite: true, isKey: true, isCondition: false, - SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable); + read: false, write: true, key: true, condition: false, + SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable)); } - yield return new ModificationCommand( - operation.Table, operation.Schema, modifications, sensitiveLoggingEnabled: SensitiveLoggingEnabled); + yield return modificationCommand; } } @@ -1034,7 +1034,7 @@ protected virtual void Generate( /// The data operation to generate commands for. /// The model. /// The commands that correspond to the given operation. - protected virtual IEnumerable GenerateModificationCommands( + protected virtual IEnumerable GenerateModificationCommands( DeleteDataOperation operation, IModel? model) { @@ -1067,7 +1067,8 @@ protected virtual IEnumerable GenerateModificationCommands( for (var i = 0; i < operation.KeyValues.GetLength(0); i++) { - var modifications = new ColumnModification[operation.KeyColumns.Length]; + var modificationCommand = Dependencies.MutableModificationCommandFactory.CreateModificationCommand( + new ModificationCommandParameters(operation.Table, operation.Schema, SensitiveLoggingEnabled)); for (var j = 0; j < operation.KeyColumns.Length; j++) { var name = operation.KeyColumns[j]; @@ -1080,14 +1081,13 @@ protected virtual IEnumerable GenerateModificationCommands( ? Dependencies.TypeMappingSource.FindMapping(value.GetType(), columnType) : Dependencies.TypeMappingSource.FindMapping(columnType!); - modifications[j] = new ColumnModification( + modificationCommand.AddColumnModification(new ColumnModificationParameters( name, originalValue: null, value, propertyMapping?.Property, columnType, typeMapping, - isRead: false, isWrite: true, isKey: true, isCondition: true, - SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable); + read: false, write: true, key: true, condition: true, + SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable)); } - yield return new ModificationCommand( - operation.Table, operation.Schema, modifications, sensitiveLoggingEnabled: SensitiveLoggingEnabled); + yield return modificationCommand; } } @@ -1125,7 +1125,7 @@ protected virtual void Generate( /// The data operation to generate commands for. /// The model. /// The commands that correspond to the given operation. - protected virtual IEnumerable GenerateModificationCommands( + protected virtual IEnumerable GenerateModificationCommands( UpdateDataOperation operation, IModel? model) { @@ -1183,7 +1183,8 @@ protected virtual IEnumerable GenerateModificationCommands( for (var i = 0; i < operation.KeyValues.GetLength(0); i++) { - var keys = new ColumnModification[operation.KeyColumns.Length]; + var modificationCommand = Dependencies.MutableModificationCommandFactory.CreateModificationCommand( + new ModificationCommandParameters(operation.Table, operation.Schema, SensitiveLoggingEnabled)); for (var j = 0; j < operation.KeyColumns.Length; j++) { var name = operation.KeyColumns[j]; @@ -1196,13 +1197,12 @@ protected virtual IEnumerable GenerateModificationCommands( ? Dependencies.TypeMappingSource.FindMapping(value.GetType(), columnType) : Dependencies.TypeMappingSource.FindMapping(columnType!); - keys[j] = new ColumnModification( + modificationCommand.AddColumnModification(new ColumnModificationParameters( name, originalValue: null, value, propertyMapping?.Property, columnType, typeMapping, - isRead: false, isWrite: false, isKey: true, isCondition: true, - SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable); + read: false, write: false, key: true, condition: true, + SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable)); } - var modifications = new ColumnModification[operation.Columns.Length]; for (var j = 0; j < operation.Columns.Length; j++) { var name = operation.Columns[j]; @@ -1215,15 +1215,13 @@ protected virtual IEnumerable GenerateModificationCommands( ? Dependencies.TypeMappingSource.FindMapping(value.GetType(), columnType) : Dependencies.TypeMappingSource.FindMapping(columnType!); - modifications[j] = new ColumnModification( + modificationCommand.AddColumnModification(new ColumnModificationParameters( name, originalValue: null, value, propertyMapping?.Property, columnType, typeMapping, - isRead: false, isWrite: true, isKey: true, isCondition: false, - SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable); + read: false, write: true, key: true, condition: false, + SensitiveLoggingEnabled, propertyMapping?.Column.IsNullable)); } - yield return new ModificationCommand( - operation.Table, operation.Schema, keys.Concat(modifications).ToArray(), - sensitiveLoggingEnabled: SensitiveLoggingEnabled); + yield return modificationCommand; } } diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGeneratorDependencies.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGeneratorDependencies.cs index 1b00e7e49f8..f12eac2f203 100644 --- a/src/EFCore.Relational/Migrations/MigrationsSqlGeneratorDependencies.cs +++ b/src/EFCore.Relational/Migrations/MigrationsSqlGeneratorDependencies.cs @@ -61,6 +61,7 @@ public MigrationsSqlGeneratorDependencies( ISqlGenerationHelper sqlGenerationHelper, IRelationalTypeMappingSource typeMappingSource, ICurrentDbContext currentContext, + IMutableModificationCommandFactory modificationCommandFactory, ILoggingOptions loggingOptions, IRelationalCommandDiagnosticsLogger logger, IDiagnosticsLogger migrationsLogger) @@ -79,6 +80,7 @@ public MigrationsSqlGeneratorDependencies( UpdateSqlGenerator = updateSqlGenerator; TypeMappingSource = typeMappingSource; CurrentContext = currentContext; + MutableModificationCommandFactory = modificationCommandFactory; LoggingOptions = loggingOptions; Logger = logger; MigrationsLogger = migrationsLogger; @@ -109,6 +111,11 @@ public MigrationsSqlGeneratorDependencies( /// public ICurrentDbContext CurrentContext { get; init; } + /// + /// The factory. + /// + public IMutableModificationCommandFactory MutableModificationCommandFactory { get; init; } + /// /// The logging options. /// diff --git a/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs index 57831284e2c..6b4dfaf5f4f 100644 --- a/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Migrations.Operations @@ -61,18 +62,23 @@ public virtual IEnumerable GenerateModificationCommands(IMo ? MigrationsModelDiffer.GetMappedProperties(table, KeyColumns) : null; + var modificationCommandFactory = new MutableModificationCommandFactory(); + for (var i = 0; i < KeyValues.GetLength(0); i++) { - var modifications = new ColumnModification[KeyColumns.Length]; + var modificationCommand = modificationCommandFactory.CreateModificationCommand(new ModificationCommandParameters( + Table, Schema, sensitiveLoggingEnabled: false)); for (var j = 0; j < KeyColumns.Length; j++) { - modifications[j] = new ColumnModification( + var columnModificationParameters = new ColumnModificationParameters( KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: properties?[j], - columnType: KeyColumnTypes?[j], isRead: false, isWrite: true, isKey: true, isCondition: true, + columnType: KeyColumnTypes?[j], typeMapping: null, read: false, write: true, key: true, condition: true, sensitiveLoggingEnabled: false); + + modificationCommand.AddColumnModification(columnModificationParameters); } - yield return new ModificationCommand(Table, Schema, modifications, sensitiveLoggingEnabled: false); + yield return (ModificationCommand)modificationCommand; } } } diff --git a/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs index 6b5fcfc323d..250446da2eb 100644 --- a/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Migrations.Operations @@ -59,18 +60,24 @@ public virtual IEnumerable GenerateModificationCommands(IMo ? MigrationsModelDiffer.GetMappedProperties(table, Columns) : null; + var modificationCommandFactory = new MutableModificationCommandFactory(); + for (var i = 0; i < Values.GetLength(0); i++) { - var modifications = new ColumnModification[Columns.Length]; + var modificationCommand = modificationCommandFactory.CreateModificationCommand(new ModificationCommandParameters( + Table, Schema, sensitiveLoggingEnabled: false)); + for (var j = 0; j < Columns.Length; j++) { - modifications[j] = new ColumnModification( + var columnModificationParameters = new ColumnModificationParameters( Columns[j], originalValue: null, value: Values[i, j], property: properties?[j], - columnType: ColumnTypes?[j], isRead: false, isWrite: true, isKey: true, isCondition: false, + columnType: ColumnTypes?[j], typeMapping: null, read: false, write: true, key: true, condition: false, sensitiveLoggingEnabled: false); + + modificationCommand.AddColumnModification(columnModificationParameters); } - yield return new ModificationCommand(Table, Schema, modifications, sensitiveLoggingEnabled: false); + yield return (ModificationCommand)modificationCommand; } } } diff --git a/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs index cc874759e93..7d283806972 100644 --- a/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Migrations.Operations @@ -87,27 +88,34 @@ public virtual IEnumerable GenerateModificationCommands(IMo ? MigrationsModelDiffer.GetMappedProperties(table, Columns) : null; + var modificationCommandFactory = new MutableModificationCommandFactory(); + for (var i = 0; i < KeyValues.GetLength(0); i++) { - var keys = new ColumnModification[KeyColumns.Length]; + var modificationCommand = modificationCommandFactory.CreateModificationCommand(new ModificationCommandParameters( + Table, Schema, sensitiveLoggingEnabled: false)); + for (var j = 0; j < KeyColumns.Length; j++) { - keys[j] = new ColumnModification( + var columnModificationParameters = new ColumnModificationParameters( KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: keyProperties?[j], - columnType: KeyColumnTypes?[j], isRead: false, isWrite: false, isKey: true, isCondition: true, + columnType: KeyColumnTypes?[j], typeMapping: null, read: false, write: false, key: true, condition: true, sensitiveLoggingEnabled: false); + + modificationCommand.AddColumnModification(columnModificationParameters); } - var modifications = new ColumnModification[Columns.Length]; for (var j = 0; j < Columns.Length; j++) { - modifications[j] = new ColumnModification( + var columnModificationParameters = new ColumnModificationParameters( Columns[j], originalValue: null, value: Values[i, j], property: properties?[j], - columnType: ColumnTypes?[j], isRead: false, isWrite: true, isKey: true, isCondition: false, + columnType: ColumnTypes?[j], typeMapping: null, read: false, write: true, key: true, condition: false, sensitiveLoggingEnabled: false); + + modificationCommand.AddColumnModification(columnModificationParameters); } - yield return new ModificationCommand(Table, Schema, keys.Concat(modifications).ToArray(), sensitiveLoggingEnabled: false); + yield return (ModificationCommand)modificationCommand; } } } diff --git a/src/EFCore.Relational/Storage/RelationalCommand.cs b/src/EFCore.Relational/Storage/RelationalCommand.cs index 6320dca1f2b..3ddf95e11b4 100644 --- a/src/EFCore.Relational/Storage/RelationalCommand.cs +++ b/src/EFCore.Relational/Storage/RelationalCommand.cs @@ -24,7 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Storage /// public class RelationalCommand : IRelationalCommand { - private readonly RelationalDataReader _relationalReader; + private RelationalDataReader? _relationalReader; private readonly Stopwatch _stopwatch = new(); /// @@ -51,8 +51,6 @@ public RelationalCommand( Dependencies = dependencies; CommandText = commandText; Parameters = parameters; - - _relationalReader = CreateRelationalDataReader(); } /// @@ -503,6 +501,11 @@ public virtual RelationalDataReader ExecuteReader(RelationalCommandParameterObje reader = new BufferedDataReader(reader, detailedErrorsEnabled).Initialize(readerColumns); } + if (_relationalReader == null) + { + _relationalReader = CreateRelationalDataReader(); + } + _relationalReader.Initialize(parameterObject.Connection, command, reader, commandId, logger); readerOpen = true; @@ -620,6 +623,11 @@ await logger.CommandErrorAsync( .ConfigureAwait(false); } + if (_relationalReader == null) + { + _relationalReader = CreateRelationalDataReader(); + } + _relationalReader.Initialize(parameterObject.Connection, command, reader, commandId, logger); readerOpen = true; @@ -743,7 +751,7 @@ private static async Task CleanupCommandAsync( /// /// The created . protected virtual RelationalDataReader CreateRelationalDataReader() - => new(this); + => new(); /// /// Populates this command from the provided . diff --git a/src/EFCore.Relational/Storage/RelationalCommandParameterObject.cs b/src/EFCore.Relational/Storage/RelationalCommandParameterObject.cs index 16f2ef63f9a..62d84760f17 100644 --- a/src/EFCore.Relational/Storage/RelationalCommandParameterObject.cs +++ b/src/EFCore.Relational/Storage/RelationalCommandParameterObject.cs @@ -28,10 +28,10 @@ public readonly struct RelationalCommandParameterObject /// /// /// The connection on which the command will execute. - /// The SQL parameter values to use, or null if none. - /// The expected columns if the reader needs to be buffered, or null otherwise. - /// The current instance, or null if it is not known. - /// A logger, or null if no logger is available. + /// The SQL parameter values to use, or if none. + /// The expected columns if the reader needs to be buffered, or otherwise. + /// The current instance, or if it is not known. + /// A logger, or if no logger is available. public RelationalCommandParameterObject( IRelationalConnection connection, IReadOnlyDictionary? parameterValues, diff --git a/src/EFCore.Relational/Storage/RelationalDataReader.cs b/src/EFCore.Relational/Storage/RelationalDataReader.cs index 57f8315854a..09946cfff53 100644 --- a/src/EFCore.Relational/Storage/RelationalDataReader.cs +++ b/src/EFCore.Relational/Storage/RelationalDataReader.cs @@ -22,8 +22,6 @@ namespace Microsoft.EntityFrameworkCore.Storage /// public class RelationalDataReader : IDisposable, IAsyncDisposable { - private readonly IRelationalCommand _relationalCommand; - private IRelationalConnection _relationalConnection = default!; private DbCommand _command = default!; private DbDataReader _reader = default!; @@ -36,19 +34,6 @@ public class RelationalDataReader : IDisposable, IAsyncDisposable private bool _disposed; - private static readonly TimeSpan _oneSecond = TimeSpan.FromSeconds(1); - - /// - /// Initializes a new instance of the class. - /// - /// The relational command which owns this relational reader. - public RelationalDataReader(IRelationalCommand relationalCommand) - { - Check.NotNull(relationalCommand, nameof(relationalCommand)); - - _relationalCommand = relationalCommand; - } - /// /// Initializes a new instance of the class. /// diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index e26c8630e60..26e95a07fcd 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -16,23 +15,49 @@ namespace Microsoft.EntityFrameworkCore.Update { /// /// - /// Represents an update, insert, or delete operation for a single column. s - /// contain lists of s. + /// Implementation of interface. + /// + /// + /// Represents an update, insert, or delete operation for a single column. s + /// contain lists of s. /// /// /// This type is typically used by database providers; it is generally not used in application code. /// /// - public class ColumnModification + public class ColumnModification : IColumnModification { private string? _parameterName; private string? _originalParameterName; private readonly Func? _generateParameterName; private readonly object? _originalValue; private object? _value; - private readonly bool _useParameters; private readonly bool _sensitiveLoggingEnabled; - private List? _sharedColumnModifications; + private List? _sharedColumnModifications; + + /// + /// Creates a new instance. + /// + /// Creation parameters. + public ColumnModification(ColumnModificationParameters columnModificationParameters) + { + ColumnName = columnModificationParameters.ColumnName; + _originalValue = columnModificationParameters.OriginalValue; + _value = columnModificationParameters.Value; + Property = columnModificationParameters.Property; + ColumnType = columnModificationParameters.ColumnType; + TypeMapping = columnModificationParameters.TypeMapping; + IsRead = columnModificationParameters.IsRead; + IsWrite = columnModificationParameters.IsWrite; + IsKey = columnModificationParameters.IsKey; + IsCondition = columnModificationParameters.IsCondition; + _sensitiveLoggingEnabled = columnModificationParameters.SensitiveLoggingEnabled; + IsNullable = columnModificationParameters.IsNullable; + _generateParameterName = columnModificationParameters.GenerateParameterName; + Entry = columnModificationParameters.Entry; + + UseParameter = _generateParameterName != null; + } /// /// Creates a new instance. @@ -42,11 +67,12 @@ public class ColumnModification /// The column to be modified. /// A delegate for generating parameter names for the update SQL. /// The relational type mapping to be used for the command parameter. - /// Indicates whether or not a value must be read from the database for the column. - /// Indicates whether or not a value must be written to the database for the column. - /// Indicates whether or not the column part of a primary or alternate key. - /// Indicates whether or not the column is used in the WHERE clause when updating. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + [Obsolete("Use the constructor with columnModificationParameters")] public ColumnModification( IUpdateEntry entry, IProperty property, @@ -78,7 +104,7 @@ public ColumnModification( Entry = entry; _generateParameterName = generateParameterName; - _useParameters = true; + UseParameter = true; } /// @@ -87,13 +113,13 @@ public ColumnModification( /// The that represents the entity that is being modified. /// The property that maps to the column. /// A delegate for generating parameter names for the update SQL. - /// Indicates whether or not a value must be read from the database for the column. - /// Indicates whether or not a value must be written to the database for the column. - /// Indicates whether or not the column part of a primary or alternate key. - /// Indicates whether or not the column is used in the WHERE clause when updating. - /// Indicates whether or not the column is acting as an optimistic concurrency token. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. - [Obsolete("Use the constructor with column")] + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether the column is acting as an optimistic concurrency token. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + [Obsolete("Use the constructor with columnModificationParameters")] public ColumnModification( IUpdateEntry entry, IProperty property, @@ -127,12 +153,13 @@ public ColumnModification( /// The property that maps to the column. /// The database type of the column. /// The relational type mapping to be used for the command parameter. - /// Indicates whether or not a value must be read from the database for the column. - /// Indicates whether or not a value must be written to the database for the column. - /// Indicates whether or not the column part of a primary or alternate key. - /// Indicates whether or not the column is used in the WHERE clause when updating. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. /// A value indicating whether the value could be null. + [Obsolete("Use the constructor with columnModificationParameters")] public ColumnModification( string columnName, object? originalValue, @@ -171,12 +198,12 @@ public ColumnModification( /// Gets or sets the current value of the property mapped to this column. /// The property that maps to the column. /// The database type of the column. - /// Indicates whether or not a value must be read from the database for the column. - /// Indicates whether or not a value must be written to the database for the column. - /// Indicates whether or not the column part of a primary or alternate key. - /// Indicates whether or not the column is used in the WHERE clause when updating. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. - [Obsolete("Use the constructor with type mapping")] + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + [Obsolete("Use the constructor with columnModificationParameters")] public ColumnModification( string columnName, object? originalValue, @@ -210,12 +237,12 @@ public ColumnModification( /// The original value of the property mapped to this column. /// Gets or sets the current value of the property mapped to this column. /// The property that maps to the column. - /// Indicates whether or not a value must be read from the database for the column. - /// Indicates whether or not a value must be written to the database for the column. - /// Indicates whether or not the column part of a primary or alternate key. - /// Indicates whether or not the column is used in the WHERE clause when updating. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. - [Obsolete("Use the constructor with columnType")] + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + [Obsolete("Use the constructor with columnModificationParameters")] public ColumnModification( string columnName, object? originalValue, @@ -261,46 +288,59 @@ public ColumnModification( public virtual bool? IsNullable { get; } /// - /// Indicates whether or not a value must be read from the database for the column. + /// Indicates whether a value must be read from the database for the column. /// public virtual bool IsRead { get; } /// - /// Indicates whether or not a value must be written to the database for the column. + /// Indicates whether a value must be written to the database for the column. /// public virtual bool IsWrite { get; } /// - /// Indicates whether or not the column is used in the WHERE clause when updating. + /// Indicates whether the column is used in the WHERE clause when updating. /// public virtual bool IsCondition { get; } /// - /// Indicates whether or not the column is concurrency token. + /// Indicates whether the column is concurrency token. /// [Obsolete] public virtual bool IsConcurrencyToken { get; } /// - /// Indicates whether or not the column is part of a primary or alternate key. + /// Indicates whether the column is part of a primary or alternate key. /// public virtual bool IsKey { get; } -#pragma warning disable CS8775 // Member must have a non-null value when exiting with 'true'. /// /// Indicates whether the original value of the property must be passed as a parameter to the SQL /// - [MemberNotNullWhen(true, nameof(OriginalParameterName))] public virtual bool UseOriginalValueParameter - => _useParameters && IsCondition; + => UseParameter && UseOriginalValue; /// /// Indicates whether the current value of the property must be passed as a parameter to the SQL /// - [MemberNotNullWhen(true, nameof(ParameterName))] public virtual bool UseCurrentValueParameter - => _useParameters && IsWrite; -#pragma warning restore CS8775 + => UseParameter && UseCurrentValue; + + /// + /// Indicates whether the original value of the property should be used + /// + public virtual bool UseOriginalValue + => IsCondition; + + /// + /// Indicates whether the current value of the property should be used + /// + public virtual bool UseCurrentValue + => IsWrite; + + /// + /// Indicates whether the value of the property must be passed as a parameter to the SQL as opposed to being inlined + /// + public virtual bool UseParameter { get; } /// /// The parameter name to use for the current value parameter (), if needed. @@ -369,14 +409,14 @@ public virtual object? Value /// Adds a modification affecting the same database value. /// /// The modification for the shared column. - public virtual void AddSharedColumnModification(ColumnModification modification) + public virtual void AddSharedColumnModification(IColumnModification modification) { Check.DebugAssert(Entry is not null, "Entry is not null"); Check.DebugAssert(Property is not null, "Property is not null"); Check.DebugAssert(modification.Entry is not null, "modification.Entry is not null"); Check.DebugAssert(modification.Property is not null, "modification.Property is not null"); - _sharedColumnModifications ??= new List(); + _sharedColumnModifications ??= new List(); if (UseCurrentValueParameter && !StructuralComparisons.StructuralEqualityComparer.Equals(Value, modification.Value)) diff --git a/src/EFCore.Relational/Update/ColumnModificationParameters.cs b/src/EFCore.Relational/Update/ColumnModificationParameters.cs new file mode 100644 index 00000000000..71f7c087ec9 --- /dev/null +++ b/src/EFCore.Relational/Update/ColumnModificationParameters.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// Parameters for creating a instance. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + public sealed record ColumnModificationParameters + { + /// + /// A delegate for generating parameter names for the update SQL. + /// + public Func? GenerateParameterName { get; init; } + + /// + /// The original value of the property mapped to column. + /// + public object? OriginalValue { get; init; } + + /// + /// The current value of the property mapped to column. + /// + public object? Value { get; init; } + + /// + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// + public bool SensitiveLoggingEnabled { get; init; } + + /// + /// The that represents the entity that is being modified. + /// + public IUpdateEntry? Entry { get; init; } + + /// + /// The property that maps to the column. + /// + public IProperty? Property { get; init; } + + /// + /// The relational type mapping for the column. + /// + public RelationalTypeMapping? TypeMapping { get; init; } + + /// + /// A value indicating whether the column could contain a null value. + /// + public bool? IsNullable { get; init; } + + /// + /// Indicates whether a value must be read from the database for the column. + /// + public bool IsRead { get; init; } + + /// + /// Indicates whether a value must be written to the database for the column. + /// + public bool IsWrite { get; init; } + + /// + /// Indicates whether the column is used in the WHERE clause when updating. + /// + public bool IsCondition { get; init; } + + /// + /// Indicates whether the column is part of a primary or alternate key. + /// + public bool IsKey { get; init; } + + /// + /// The name of the column. + /// + public string ColumnName { get; init; } + + /// + /// The database type of the column. + /// + public string? ColumnType { get; init; } + + /// + /// Creates a new instance. + /// + /// The name of the column. + /// The original value of the property mapped to this column. + /// The current value of the property mapped to this column. + /// The property that maps to the column. + /// The database type of the column. + /// The relational type mapping to be used for the command parameter. + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// A value indicating whether the value could be null. + public ColumnModificationParameters( + string columnName, + object? originalValue, + object? value, + IProperty? property, + string? columnType, + RelationalTypeMapping? typeMapping, + bool read, + bool write, + bool key, + bool condition, + bool sensitiveLoggingEnabled, + bool? isNullable = null) + { + Check.NotNull(columnName, nameof(columnName)); + + ColumnName = columnName; + OriginalValue = originalValue; + Value = value; + Property = property; + ColumnType = columnType; + TypeMapping = typeMapping; + IsRead = read; + IsWrite = write; + IsKey = key; + IsCondition = condition; + SensitiveLoggingEnabled = sensitiveLoggingEnabled; + IsNullable = isNullable; + + GenerateParameterName = null; + Entry = null; + + //IsConcurrencyToken = false; + } + + /// + /// Creates a new instance. + /// + /// The that represents the entity that is being modified. + /// The property that maps to the column. + /// The column to be modified. + /// A delegate for generating parameter names for the update SQL. + /// The relational type mapping to be used for the command parameter. + /// Indicates whether a value must be read from the database for the column. + /// Indicates whether a value must be written to the database for the column. + /// Indicates whether the column part of a primary or alternate key. + /// Indicates whether the column is used in the WHERE clause when updating. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + public ColumnModificationParameters( + IUpdateEntry entry, + IProperty property, + IColumn column, + Func generateParameterName, + RelationalTypeMapping typeMapping, + bool valueIsRead, + bool valueIsWrite, + bool columnIsKey, + bool columnIsCondition, + bool sensitiveLoggingEnabled) + { + Check.NotNull(entry, nameof(entry)); + Check.NotNull(property, nameof(property)); + Check.NotNull(column, nameof(column)); + Check.NotNull(column.Name, "column.Name"); + Check.NotNull(generateParameterName, nameof(generateParameterName)); + + ColumnName = column.Name; + OriginalValue = null; + Value = null; + Property = property; + ColumnType = column.StoreType; + TypeMapping = typeMapping; + IsRead = valueIsRead; + IsWrite = valueIsWrite; + IsKey = columnIsKey; + IsCondition = columnIsCondition; + SensitiveLoggingEnabled = sensitiveLoggingEnabled; + IsNullable = column.IsNullable; + + GenerateParameterName = generateParameterName; + Entry = entry; + + //IsConcurrencyToken = false; + } + } +} diff --git a/src/EFCore.Relational/Update/IColumnModification.cs b/src/EFCore.Relational/Update/IColumnModification.cs new file mode 100644 index 00000000000..a7d223cace0 --- /dev/null +++ b/src/EFCore.Relational/Update/IColumnModification.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// Represents an update, insert, or delete operation for a single column. s + /// contain lists of s. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + public interface IColumnModification + { + /// + /// The that represents the entity that is being modified. + /// + public IUpdateEntry? Entry { get; } + + /// + /// The property that maps to the column. + /// + public IProperty? Property { get; } + + /// + /// The relational type mapping for the column. + /// + public RelationalTypeMapping? TypeMapping { get; } + + /// + /// A value indicating whether the column could contain a null value. + /// + public bool? IsNullable { get; } + + /// + /// Indicates whether a value must be read from the database for the column. + /// + public bool IsRead { get; } + + /// + /// Indicates whether a value must be written to the database for the column. + /// + public bool IsWrite { get; } + + /// + /// Indicates whether the column is used in the WHERE clause when updating. + /// + public bool IsCondition { get; } + + /// + /// Indicates whether the column is concurrency token. + /// + [Obsolete] + public bool IsConcurrencyToken { get; } + + /// + /// Indicates whether the column is part of a primary or alternate key. + /// + public bool IsKey { get; } + + /// + /// Indicates whether the original value of the property must be passed as a parameter to the SQL + /// + [MemberNotNullWhen(true, nameof(OriginalParameterName))] + public bool UseOriginalValueParameter { get; } + + /// + /// Indicates whether the current value of the property must be passed as a parameter to the SQL + /// + [MemberNotNullWhen(true, nameof(ParameterName))] + public bool UseCurrentValueParameter { get; } + + /// + /// Indicates whether the original value of the property should be used + /// + public bool UseOriginalValue { get; } + + /// + /// Indicates whether the current value of the property should be used + /// + public bool UseCurrentValue { get; } + + /// + /// Indicates whether the value of the property must be passed as a parameter to the SQL as opposed to being inlined + /// + public bool UseParameter { get; } + + /// + /// The parameter name to use for the current value parameter (), if needed. + /// + public string? ParameterName { get; } + + /// + /// The parameter name to use for the original value parameter (), if needed. + /// + public string? OriginalParameterName { get; } + + /// + /// The name of the column. + /// + public string ColumnName { get; } + + /// + /// The database type of the column. + /// + public string? ColumnType { get; } + + /// + /// The original value of the property mapped to this column. + /// + public object? OriginalValue { get; } + + /// + /// Gets or sets the current value of the property mapped to this column. + /// + public object? Value { get; set; } + + /// + /// Adds a modification affecting the same database value. + /// + /// The modification for the shared column. + public void AddSharedColumnModification(IColumnModification modification); + } +} diff --git a/src/EFCore.Relational/Update/IModificationCommand.cs b/src/EFCore.Relational/Update/IModificationCommand.cs new file mode 100644 index 00000000000..2c94c38d688 --- /dev/null +++ b/src/EFCore.Relational/Update/IModificationCommand.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// Represents a conceptual database command to insert/update/delete a row. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + public interface IModificationCommand + { + /// + /// The name of the table containing the data to be modified. + /// + public string TableName { get; } + + /// + /// The schema containing the table, or to use the default schema. + /// + public string? Schema { get; } + + /// + /// The list of needed to perform the insert, update, or delete. + /// + public IReadOnlyList ColumnModifications { get; } + + /// + /// Indicates whether the database will return values for some mapped properties + /// that will then need to be propagated back to the tracked entities. + /// + public bool RequiresResultPropagation { get; } + + /// + /// The that represent the entities that are mapped to the row to update. + /// + public IReadOnlyList Entries { get; } + + /// + /// The that indicates whether the row will be + /// inserted (), + /// updated (), + /// or deleted ((). + /// + public EntityState EntityState { get; } + + /// + /// Reads values returned from the database in the given and + /// propagates them back to into the appropriate + /// from which the values can be propagated on to tracked entities. + /// + /// The buffer containing the values read from the database. + public void PropagateResults(ValueBuffer valueBuffer); + } +} diff --git a/src/EFCore.Relational/Update/IMutableModificationCommand.cs b/src/EFCore.Relational/Update/IMutableModificationCommand.cs new file mode 100644 index 00000000000..415aa733843 --- /dev/null +++ b/src/EFCore.Relational/Update/IMutableModificationCommand.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// Represents a mutable conceptual database command to insert/update/delete a row. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + public interface IMutableModificationCommand : IModificationCommand + { + /// + /// Adds an entry to the command. + /// + /// Entry object. + /// Whether this is the main entry. Only one main entry can be added to a given command. + public void AddEntry(IUpdateEntry entry, bool mainEntry); + + /// + /// Creates a new and add it to this command. + /// + /// Creation parameters. + /// The new instance. + IColumnModification AddColumnModification(ColumnModificationParameters columnModificationParameters); + } +} diff --git a/src/EFCore.Relational/Update/IMutableModificationCommandFactory.cs b/src/EFCore.Relational/Update/IMutableModificationCommandFactory.cs new file mode 100644 index 00000000000..a8f1986ad70 --- /dev/null +++ b/src/EFCore.Relational/Update/IMutableModificationCommandFactory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// A service for creating instances. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + /// The service lifetime is . This means a single instance + /// is used by many instances. The implementation must be thread-safe. + /// This service cannot depend on services registered as . + /// + /// + public interface IMutableModificationCommandFactory + { + /// + /// Creates a new database CUD command. + /// + /// The creation parameters. + /// A new instance. + IMutableModificationCommand CreateModificationCommand( + ModificationCommandParameters modificationCommandParameters); + } +} diff --git a/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs b/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs index 5a845d8963c..0066eabb5eb 100644 --- a/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs +++ b/src/EFCore.Relational/Update/IUpdateSqlGenerator.cs @@ -58,7 +58,7 @@ void AppendNextSequenceValueOperation( /// The for the command. ResultSetMapping AppendDeleteOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition); /// @@ -70,7 +70,7 @@ ResultSetMapping AppendDeleteOperation( /// The for the command. ResultSetMapping AppendInsertOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition); /// @@ -82,7 +82,7 @@ ResultSetMapping AppendInsertOperation( /// The for the command. ResultSetMapping AppendUpdateOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition); } } diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index befcfb83608..22d9c269dc6 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -30,10 +30,6 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal /// public class CommandBatchPreparer : ICommandBatchPreparer { - private readonly IModificationCommandBatchFactory _modificationCommandBatchFactory; - private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; - private readonly IComparer _modificationCommandComparer; - private readonly IKeyValueIndexFactorySource _keyValueIndexFactorySource; private readonly int _minBatchSize; private readonly bool _sensitiveLoggingEnabled; @@ -45,10 +41,6 @@ public class CommandBatchPreparer : ICommandBatchPreparer /// public CommandBatchPreparer(CommandBatchPreparerDependencies dependencies) { - _modificationCommandBatchFactory = dependencies.ModificationCommandBatchFactory; - _parameterNameGeneratorFactory = dependencies.ParameterNameGeneratorFactory; - _modificationCommandComparer = dependencies.ModificationCommandComparer; - _keyValueIndexFactorySource = dependencies.KeyValueIndexFactorySource; _minBatchSize = dependencies.Options.Extensions.OfType().FirstOrDefault()?.MinBatchSize ?? 4; @@ -72,18 +64,18 @@ public virtual IEnumerable BatchCommands( IList entries, IUpdateAdapter updateAdapter) { - var parameterNameGenerator = _parameterNameGeneratorFactory.Create(); + var parameterNameGenerator = Dependencies.ParameterNameGeneratorFactory.Create(); var commands = CreateModificationCommands(entries, updateAdapter, parameterNameGenerator.GenerateNext); var sortedCommandSets = TopologicalSort(commands); foreach (var independentCommandSet in sortedCommandSets) { - independentCommandSet.Sort(_modificationCommandComparer); + independentCommandSet.Sort(Dependencies.ModificationCommandComparer); - var batch = _modificationCommandBatchFactory.Create(); + var batch = Dependencies.ModificationCommandBatchFactory.Create(); foreach (var modificationCommand in independentCommandSet) { - modificationCommand.AssertColumnsNotInitialized(); + (modificationCommand as ModificationCommand)?.AssertColumnsNotInitialized(); if (modificationCommand.EntityState == EntityState.Modified && !modificationCommand.ColumnModifications.Any(m => m.IsWrite)) { @@ -144,10 +136,10 @@ public virtual IEnumerable BatchCommands( private ModificationCommandBatch StartNewBatch( ParameterNameGenerator parameterNameGenerator, - ModificationCommand modificationCommand) + IModificationCommand modificationCommand) { parameterNameGenerator.Reset(); - var batch = _modificationCommandBatchFactory.Create(); + var batch = Dependencies.ModificationCommandBatchFactory.Create(); batch.AddCommand(modificationCommand); return batch; } @@ -158,13 +150,13 @@ private ModificationCommandBatch StartNewBatch( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual IEnumerable CreateModificationCommands( + protected virtual IEnumerable CreateModificationCommands( IList entries, IUpdateAdapter updateAdapter, Func generateParameterName) { - var commands = new List(); - Dictionary<(string Name, string? Schema), SharedTableEntryMap>? sharedTablesCommandsMap = + var commands = new List(); + Dictionary<(string Name, string? Schema), SharedTableEntryMap>? sharedTablesCommandsMap = null; foreach (var entry in entries) { @@ -176,49 +168,50 @@ protected virtual IEnumerable CreateModificationCommands( var mappings = (IReadOnlyCollection)entry.EntityType.GetTableMappings(); var mappingCount = mappings.Count; - ModificationCommand? firstCommand = null; + IMutableModificationCommand? firstCommands = null; foreach (var mapping in mappings) { var table = mapping.Table; var tableKey = (table.Name, table.Schema); - ModificationCommand command; + IMutableModificationCommand command; var isMainEntry = true; if (table.IsShared) { if (sharedTablesCommandsMap == null) { - sharedTablesCommandsMap = new Dictionary<(string, string?), SharedTableEntryMap>(); + sharedTablesCommandsMap = new Dictionary<(string, string?), SharedTableEntryMap>(); } if (!sharedTablesCommandsMap.TryGetValue(tableKey, out var sharedCommandsMap)) { - sharedCommandsMap = new SharedTableEntryMap(table, updateAdapter); + sharedCommandsMap = new SharedTableEntryMap(table, updateAdapter); sharedTablesCommandsMap.Add(tableKey, sharedCommandsMap); } command = sharedCommandsMap.GetOrAddValue( entry, - (n, s, c) => new ModificationCommand(n, s, generateParameterName, _sensitiveLoggingEnabled, c, Dependencies.UpdateLogger)); + (n, s, comparer) => Dependencies.MutableModificationCommandFactory.CreateModificationCommand(new ModificationCommandParameters( + n, s, _sensitiveLoggingEnabled, comparer, generateParameterName, Dependencies.UpdateLogger))); isMainEntry = sharedCommandsMap.IsMainEntry(entry); } else { - command = new ModificationCommand( - table.Name, table.Schema, generateParameterName, _sensitiveLoggingEnabled, comparer: null, Dependencies.UpdateLogger); + command = Dependencies.MutableModificationCommandFactory.CreateModificationCommand(new ModificationCommandParameters( + table.Name, table.Schema, _sensitiveLoggingEnabled, comparer: null, generateParameterName, Dependencies.UpdateLogger)); } command.AddEntry(entry, isMainEntry); commands.Add(command); - if (firstCommand == null) + if (firstCommands == null) { - Check.DebugAssert(firstCommand == null, "firstCommand == null"); - firstCommand = command; + Check.DebugAssert(firstCommands == null, "firstCommand == null"); + firstCommands = command; } } - if (firstCommand == null) + if (firstCommands == null) { throw new InvalidOperationException(RelationalStrings.ReadonlyEntitySaved(entry.EntityType.DisplayName())); } @@ -233,7 +226,7 @@ protected virtual IEnumerable CreateModificationCommands( } private void AddUnchangedSharingEntries( - IEnumerable> sharedTablesCommands, + IEnumerable> sharedTablesCommands, IList entries) { foreach (var sharedCommandsMap in sharedTablesCommands) @@ -277,9 +270,9 @@ private void AddUnchangedSharingEntries( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual IReadOnlyList> TopologicalSort(IEnumerable commands) + protected virtual IReadOnlyList> TopologicalSort(IEnumerable commands) { - var modificationCommandGraph = new Multigraph(); + var modificationCommandGraph = new Multigraph(); modificationCommandGraph.AddVertices(commands); // The predecessors map allows to populate the graph in linear time @@ -291,7 +284,7 @@ protected virtual IReadOnlyList> TopologicalSort(IEnum return modificationCommandGraph.BatchingTopologicalSort(FormatCycle); } - private string FormatCycle(IReadOnlyList>> data) + private string FormatCycle(IReadOnlyList>> data) { var builder = new StringBuilder(); for (var i = 0; i < data.Count; i++) @@ -323,7 +316,7 @@ private string FormatCycle(IReadOnlyList foreignKey.DeclaringEntityType.IsAssignableFrom(e.EntityType)); if (reverseDependency) @@ -427,7 +420,7 @@ private void Format(IForeignKey foreignKey, ModificationCommand source, Modifica } } - private void Format(IIndex index, ModificationCommand source, ModificationCommand target, StringBuilder builder) + private void Format(IIndex index, IModificationCommand source, IModificationCommand target, StringBuilder builder) { var reverseDependency = source.EntityState != EntityState.Deleted; if (reverseDependency) @@ -472,10 +465,10 @@ private void Format(IIndex index, ModificationCommand source, ModificationComman // Builds a map from foreign key values to list of modification commands, with an entry for every command // that may need to precede some other command involving that foreign key value. - private Dictionary> CreateKeyValuePredecessorMap( - Multigraph commandGraph) + private Dictionary> CreateKeyValuePredecessorMap( + Multigraph commandGraph) { - var predecessorsMap = new Dictionary>(); + var predecessorsMap = new Dictionary>(); foreach (var command in commandGraph.Vertices) { if (command.EntityState == EntityState.Modified @@ -497,7 +490,7 @@ private Dictionary> CreateKeyValuePred continue; } - var principalKeyValue = _keyValueIndexFactorySource + var principalKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(foreignKey.PrincipalKey) .CreatePrincipalKeyValue(entry, foreignKey); @@ -505,7 +498,7 @@ private Dictionary> CreateKeyValuePred { if (!predecessorsMap.TryGetValue(principalKeyValue, out var predecessorCommands)) { - predecessorCommands = new List(); + predecessorCommands = new List(); predecessorsMap.Add(principalKeyValue, predecessorCommands); } @@ -532,7 +525,7 @@ private Dictionary> CreateKeyValuePred continue; } - var dependentKeyValue = _keyValueIndexFactorySource + var dependentKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(foreignKey.PrincipalKey) .CreateDependentKeyValueFromOriginalValues(entry, foreignKey); @@ -540,7 +533,7 @@ private Dictionary> CreateKeyValuePred { if (!predecessorsMap.TryGetValue(dependentKeyValue, out var predecessorCommands)) { - predecessorCommands = new List(); + predecessorCommands = new List(); predecessorsMap.Add(dependentKeyValue, predecessorCommands); } @@ -555,8 +548,8 @@ private Dictionary> CreateKeyValuePred } private void AddForeignKeyEdges( - Multigraph commandGraph, - Dictionary> predecessorsMap) + Multigraph commandGraph, + Dictionary> predecessorsMap) { foreach (var command in commandGraph.Vertices) { @@ -578,7 +571,7 @@ private void AddForeignKeyEdges( continue; } - var dependentKeyValue = _keyValueIndexFactorySource + var dependentKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(foreignKey.PrincipalKey) .CreateDependentKeyValue(entry, foreignKey); if (dependentKeyValue == null) @@ -606,7 +599,7 @@ private void AddForeignKeyEdges( continue; } - var principalKeyValue = _keyValueIndexFactorySource + var principalKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(foreignKey.PrincipalKey) .CreatePrincipalKeyValueFromOriginalValues(entry, foreignKey); if (principalKeyValue != null) @@ -623,10 +616,10 @@ private void AddForeignKeyEdges( } private static void AddMatchingPredecessorEdge( - Dictionary> predecessorsMap, + Dictionary> predecessorsMap, T keyValue, - Multigraph commandGraph, - ModificationCommand command, + Multigraph commandGraph, + IModificationCommand command, IAnnotatable edge) where T: notnull { @@ -642,10 +635,10 @@ private static void AddMatchingPredecessorEdge( } } - private void AddUniqueValueEdges(Multigraph commandGraph) + private void AddUniqueValueEdges(Multigraph commandGraph) { - Dictionary>? indexPredecessorsMap = null; - var keyPredecessorsMap = new Dictionary<(IKey, IKeyValueIndex), List>(); + Dictionary>? indexPredecessorsMap = null; + var keyPredecessorsMap = new Dictionary<(IKey, IKeyValueIndex), List>(); foreach (var command in commandGraph.Vertices) { if (command.EntityState != EntityState.Modified @@ -668,10 +661,10 @@ private void AddUniqueValueEdges(Multigraph c var valueFactory = index.GetNullableValueFactory(); if (valueFactory.TryCreateFromOriginalValues(entry, out var indexValue)) { - indexPredecessorsMap ??= new Dictionary>(); + indexPredecessorsMap ??= new Dictionary>(); if (!indexPredecessorsMap.TryGetValue(index, out var predecessorCommands)) { - predecessorCommands = new Dictionary(valueFactory.EqualityComparer); + predecessorCommands = new Dictionary(valueFactory.EqualityComparer); indexPredecessorsMap.Add(index, predecessorCommands); } @@ -689,7 +682,7 @@ private void AddUniqueValueEdges(Multigraph c foreach (var key in entry.EntityType.GetKeys().Where(k => k.GetMappedConstraints().Any())) { - var principalKeyValue = _keyValueIndexFactorySource + var principalKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(key) .CreatePrincipalKeyValue(entry, null); @@ -697,7 +690,7 @@ private void AddUniqueValueEdges(Multigraph c { if (!keyPredecessorsMap.TryGetValue((key, principalKeyValue), out var predecessorCommands)) { - predecessorCommands = new List(); + predecessorCommands = new List(); keyPredecessorsMap.Add((key, principalKeyValue), predecessorCommands); } @@ -752,7 +745,7 @@ private void AddUniqueValueEdges(Multigraph c { foreach (var key in entry.EntityType.GetKeys().Where(k => k.GetMappedConstraints().Any())) { - var principalKeyValue = _keyValueIndexFactorySource + var principalKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(key) .CreatePrincipalKeyValue(entry, null); diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs index 30698fc5154..17ee8da89e6 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs @@ -63,8 +63,9 @@ public sealed record CommandBatchPreparerDependencies public CommandBatchPreparerDependencies( IModificationCommandBatchFactory modificationCommandBatchFactory, IParameterNameGeneratorFactory parameterNameGeneratorFactory, - IComparer modificationCommandComparer, + IComparer modificationCommandComparer, IKeyValueIndexFactorySource keyValueIndexFactorySource, + IMutableModificationCommandFactory modificationCommandFactory, ILoggingOptions loggingOptions, IDiagnosticsLogger updateLogger, IDbContextOptions options) @@ -73,6 +74,7 @@ public CommandBatchPreparerDependencies( ParameterNameGeneratorFactory = parameterNameGeneratorFactory; ModificationCommandComparer = modificationCommandComparer; KeyValueIndexFactorySource = keyValueIndexFactorySource; + MutableModificationCommandFactory = modificationCommandFactory; LoggingOptions = loggingOptions; UpdateLogger = updateLogger; Options = options; @@ -100,7 +102,7 @@ public CommandBatchPreparerDependencies( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public IComparer ModificationCommandComparer { get; init; } + public IComparer ModificationCommandComparer { get; init; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -110,6 +112,14 @@ public CommandBatchPreparerDependencies( /// public IKeyValueIndexFactorySource KeyValueIndexFactorySource { get; init; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public IMutableModificationCommandFactory MutableModificationCommandFactory { get; init; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Update/Internal/ModificationCommandComparer.cs b/src/EFCore.Relational/Update/Internal/ModificationCommandComparer.cs index ba371ec66af..fa32bb01789 100644 --- a/src/EFCore.Relational/Update/Internal/ModificationCommandComparer.cs +++ b/src/EFCore.Relational/Update/Internal/ModificationCommandComparer.cs @@ -20,7 +20,7 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal /// This service cannot depend on services registered as . /// /// - public class ModificationCommandComparer : IComparer + public class ModificationCommandComparer : IComparer { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -28,7 +28,7 @@ public class ModificationCommandComparer : IComparer /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual int Compare(ModificationCommand? x, ModificationCommand? y) + public virtual int Compare(IModificationCommand? x, IModificationCommand? y) { var result = 0; if (ReferenceEquals(x, y)) diff --git a/src/EFCore.Relational/Update/Internal/MutableModificationCommandFactory.cs b/src/EFCore.Relational/Update/Internal/MutableModificationCommandFactory.cs new file mode 100644 index 00000000000..f789a422d26 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/MutableModificationCommandFactory.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Update.Internal +{ + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + public class MutableModificationCommandFactory : IMutableModificationCommandFactory + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IMutableModificationCommand CreateModificationCommand( + ModificationCommandParameters modificationCommandParameters) + => new ModificationCommand(modificationCommandParameters); + } +} diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 8204a01bd10..9e238beaf97 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -23,13 +22,13 @@ namespace Microsoft.EntityFrameworkCore.Update /// This type is typically used by database providers; it is generally not used in application code. /// /// - public class ModificationCommand + public class ModificationCommand : IMutableModificationCommand { private readonly Func? _generateParameterName; private readonly bool _sensitiveLoggingEnabled; private readonly IComparer? _comparer; private readonly List _entries = new(); - private IReadOnlyList? _columnModifications; + private List? _columnModifications; private bool _requiresResultPropagation; private bool _mainEntryAdded; private readonly IDiagnosticsLogger? _logger; @@ -37,51 +36,15 @@ public class ModificationCommand /// /// Initializes a new instance. /// - /// The name of the table containing the data to be modified. - /// The schema containing the table, or to use the default schema. - /// A delegate to generate parameter names. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. - /// A for s. - /// A for s. - public ModificationCommand( - string name, - string? schema, - Func generateParameterName, - bool sensitiveLoggingEnabled, - IComparer? comparer, - IDiagnosticsLogger? logger) - : this( - Check.NotEmpty(name, nameof(name)), - schema, - null, - sensitiveLoggingEnabled) + /// Creation parameters. + public ModificationCommand(ModificationCommandParameters modificationCommandParameters) { - Check.NotNull(generateParameterName, nameof(generateParameterName)); - - _generateParameterName = generateParameterName; - _comparer = comparer; - _logger = logger; - } - - /// - /// Initializes a new instance. - /// - /// The name of the table containing the data to be modified. - /// The schema containing the table, or to use the default schema. - /// The list of s needed to perform the insert, update, or delete. - /// Indicates whether or not potentially sensitive data (e.g. database values) can be logged. - public ModificationCommand( - string name, - string? schema, - IReadOnlyList? columnModifications, - bool sensitiveLoggingEnabled) - { - Check.NotNull(name, nameof(name)); - - TableName = name; - Schema = schema; - _columnModifications = columnModifications; - _sensitiveLoggingEnabled = sensitiveLoggingEnabled; + TableName = modificationCommandParameters.TableName; + Schema = modificationCommandParameters.Schema; + _generateParameterName = modificationCommandParameters.GenerateParameterName; + _sensitiveLoggingEnabled = modificationCommandParameters.SensitiveLoggingEnabled; + _comparer = modificationCommandParameters.Comparer; + _logger = modificationCommandParameters.Logger; } /// @@ -107,33 +70,12 @@ public virtual IReadOnlyList Entries /// updated (), /// or deleted ((). /// - public virtual EntityState EntityState - { - get - { - if (_mainEntryAdded) - { - var mainEntry = _entries[0]; - if (mainEntry.SharedIdentityEntry == null) - { - return mainEntry.EntityState; - } - - return mainEntry.SharedIdentityEntry.EntityType == mainEntry.EntityType - || mainEntry.SharedIdentityEntry.EntityType.GetTableMappings() - .Any(m => m.Table.Name == TableName && m.Table.Schema == Schema) - ? EntityState.Modified - : mainEntry.EntityState; - } - - return EntityState.Modified; - } - } + public virtual EntityState EntityState { get; private set; } = EntityState.Modified; /// - /// The list of s needed to perform the insert, update, or delete. + /// The list of needed to perform the insert, update, or delete. /// - public virtual IReadOnlyList ColumnModifications + public virtual IReadOnlyList ColumnModifications => NonCapturingLazyInitializer.EnsureInitialized( ref _columnModifications, this, static command => command.GenerateColumnModifications()); @@ -154,7 +96,7 @@ public virtual void AssertColumnsNotInitialized() } /// - /// Indicates whether or not the database will return values for some mapped properties + /// Indicates whether the database will return values for some mapped properties /// that will then need to be propagated back to the tracked entities. /// public virtual bool RequiresResultPropagation @@ -169,14 +111,17 @@ public virtual bool RequiresResultPropagation } /// - /// Adds an to this command representing an entity to be inserted, updated, or deleted. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// The entry representing the entity to add. - /// A value indicating whether this is the main entry for the row. public virtual void AddEntry(IUpdateEntry entry, bool mainEntry) { Check.NotNull(entry, nameof(entry)); + AssertColumnsNotInitialized(); + switch (entry.EntityState) { case EntityState.Deleted: @@ -210,6 +155,14 @@ public virtual void AddEntry(IUpdateEntry entry, bool mainEntry) _mainEntryAdded = true; _entries.Insert(0, entry); + + EntityState = entry.SharedIdentityEntry == null + ? entry.EntityState + : entry.SharedIdentityEntry.EntityType == entry.EntityType + || entry.SharedIdentityEntry.EntityType.GetTableMappings() + .Any(m => m.Table.Name == TableName && m.Table.Schema == Schema) + ? EntityState.Modified + : entry.EntityState; } else { @@ -220,8 +173,6 @@ public virtual void AddEntry(IUpdateEntry entry, bool mainEntry) _entries.Add(entry); } - - _columnModifications = null; } private void ValidateState(IUpdateEntry mainEntry, IUpdateEntry entry) @@ -260,12 +211,39 @@ private void ValidateState(IUpdateEntry mainEntry, IUpdateEntry entry) } } - private IReadOnlyList GenerateColumnModifications() + /// + /// Creates a new and add it to this command. + /// + /// Creation parameters. + /// The new instance. + public virtual IColumnModification AddColumnModification(ColumnModificationParameters columnModificationParameters) + { + var modification = CreateColumnModification(columnModificationParameters); + + if (_columnModifications == null) + { + _columnModifications = new(); + } + + _columnModifications.Add(modification); + + return modification; + } + + /// + /// Creates a new instance that implements interface. + /// + /// Creation parameters. + /// The new instance that implements interface. + protected virtual IColumnModification CreateColumnModification(ColumnModificationParameters columnModificationParameters) + => new ColumnModification(columnModificationParameters); + + private List GenerateColumnModifications() { var state = EntityState; var adding = state == EntityState.Added; var updating = state == EntityState.Modified; - var columnModifications = new List(); + var columnModifications = new List(); Dictionary? sharedTableColumnMap = null; if (_entries.Count > 1 @@ -352,7 +330,7 @@ private IReadOnlyList GenerateColumnModifications() _requiresResultPropagation = true; } - var columnModification = new ColumnModification( + var columnModificationParameters = new ColumnModificationParameters( entry, property, column, @@ -364,6 +342,8 @@ private IReadOnlyList GenerateColumnModifications() isCondition, _sensitiveLoggingEnabled); + var columnModification = CreateColumnModification(columnModificationParameters); + if (columnPropagator != null && column.PropertyMappings.Count() != 1) { @@ -445,7 +425,7 @@ private void InitializeSharedColumns( /// /// Reads values returned from the database in the given and - /// propagates them back to into the appropriate + /// propagates them back to into the appropriate /// from which the values can be propagated on to tracked entities. /// /// The buffer containing the values read from the database. @@ -481,7 +461,7 @@ private sealed class ColumnValuePropagator private object? _originalValue; private object? _currentValue; - public ColumnModification? ColumnModification { get; set; } + public IColumnModification? ColumnModification { get; set; } public void RecordValue(IProperty property, IUpdateEntry entry) { diff --git a/src/EFCore.Relational/Update/ModificationCommandBatch.cs b/src/EFCore.Relational/Update/ModificationCommandBatch.cs index fd06926dce9..3eebf957ec2 100644 --- a/src/EFCore.Relational/Update/ModificationCommandBatch.cs +++ b/src/EFCore.Relational/Update/ModificationCommandBatch.cs @@ -23,7 +23,7 @@ public abstract class ModificationCommandBatch /// /// The list of conceptual insert/update/delete s in the batch. /// - public abstract IReadOnlyList ModificationCommands { get; } + public abstract IReadOnlyList ModificationCommands { get; } /// /// Adds the given insert/update/delete to the batch. @@ -33,7 +33,7 @@ public abstract class ModificationCommandBatch /// if the command was successfully added; if there was no /// room in the current batch to add the command and it must instead be added to a new batch. /// - public abstract bool AddCommand(ModificationCommand modificationCommand); + public abstract bool AddCommand(IModificationCommand modificationCommand); /// /// Sends insert/update/delete commands to the database. diff --git a/src/EFCore.Relational/Update/ModificationCommandParameters.cs b/src/EFCore.Relational/Update/ModificationCommandParameters.cs new file mode 100644 index 00000000000..c1ef03ccd6f --- /dev/null +++ b/src/EFCore.Relational/Update/ModificationCommandParameters.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Microsoft.EntityFrameworkCore.Update +{ + /// + /// + /// Parameters for creating a instance. + /// + /// + /// This type is typically used by database providers; it is generally not used in application code. + /// + /// + public sealed record ModificationCommandParameters + { + /// + /// Creates a new instance. + /// + /// The name of the table containing the data to be modified. + /// The schema containing the table, or to use the default schema. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// An for . + /// A delegate to generate parameter names. + /// An for . + public ModificationCommandParameters( + string tableName, + string? schemaName, + bool sensitiveLoggingEnabled, + IComparer? comparer = null, + Func? generateParameterName = null, + IDiagnosticsLogger? logger = null) + { + TableName = tableName; + Schema = schemaName; + GenerateParameterName = generateParameterName; + SensitiveLoggingEnabled = sensitiveLoggingEnabled; + Comparer = comparer; + Logger = logger; + } + + /// + /// The name of the table containing the data to be modified. + /// + public string TableName { get; init; } + + /// + /// The schema containing the table, or to use the default schema. + /// + public string? Schema { get; init; } + + /// + /// A delegate to generate parameter names. + /// + public Func? GenerateParameterName { get; init; } + + /// + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// + public bool SensitiveLoggingEnabled { get; init; } + + /// + /// An for . + /// + public IComparer? Comparer { get; init; } + + /// + /// A for . + /// + public IDiagnosticsLogger? Logger { get; init; } + } +} diff --git a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs index a4715c04397..046328aabf8 100644 --- a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs +++ b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs @@ -24,7 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Update /// public abstract class ReaderModificationCommandBatch : ModificationCommandBatch { - private readonly List _modificationCommands = new(); + private readonly List _modificationCommands = new(); /// /// Creates a new instance. @@ -62,7 +62,7 @@ protected virtual IUpdateSqlGenerator UpdateSqlGenerator /// /// The list of conceptual insert/update/delete s in the batch. /// - public override IReadOnlyList ModificationCommands + public override IReadOnlyList ModificationCommands => _modificationCommands; /// @@ -78,7 +78,7 @@ public override IReadOnlyList ModificationCommands /// if the command was successfully added; if there was no /// room in the current batch to add the command and it must instead be added to a new batch. /// - public override bool AddCommand(ModificationCommand modificationCommand) + public override bool AddCommand(IModificationCommand modificationCommand) { Check.NotNull(modificationCommand, nameof(modificationCommand)); @@ -121,14 +121,14 @@ protected virtual void ResetCommandText() } /// - /// Checks whether or not a new command can be added to the batch. + /// Checks whether a new command can be added to the batch. /// /// The command to potentially add. /// if the command can be added; otherwise. - protected abstract bool CanAddCommand(ModificationCommand modificationCommand); + protected abstract bool CanAddCommand(IModificationCommand modificationCommand); /// - /// Checks whether or not the command text is valid. + /// Checks whether the command text is valid. /// /// if the command text is valid; otherwise. protected abstract bool IsCommandTextValid(); @@ -328,12 +328,12 @@ protected abstract Task ConsumeAsync( /// to consume the data reader. /// /// - /// The list of s for all the columns + /// The list of s for all the columns /// being modified such that a ValueBuffer with appropriate slots can be created. /// /// The factory. protected virtual IRelationalValueBufferFactory CreateValueBufferFactory( - IReadOnlyList columnModifications) + IReadOnlyList columnModifications) => Dependencies.ValueBufferFactoryFactory .Create( Check.NotNull(columnModifications, nameof(columnModifications)) diff --git a/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs b/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs index 37f45f73928..782e303f844 100644 --- a/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs +++ b/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs @@ -28,7 +28,7 @@ public SingularModificationCommandBatch(ModificationCommandBatchFactoryDependenc /// /// The command to potentially add. /// if no command has already been added. - protected override bool CanAddCommand(ModificationCommand modificationCommand) + protected override bool CanAddCommand(IModificationCommand modificationCommand) => ModificationCommands.Count == 0; /// diff --git a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs index 9e6ea2f5fef..acd7f42cf77 100644 --- a/src/EFCore.Relational/Update/UpdateSqlGenerator.cs +++ b/src/EFCore.Relational/Update/UpdateSqlGenerator.cs @@ -59,7 +59,7 @@ protected virtual ISqlGenerationHelper SqlGenerationHelper /// The for the command. public virtual ResultSetMapping AppendInsertOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); @@ -93,7 +93,7 @@ public virtual ResultSetMapping AppendInsertOperation( /// The for the command. public virtual ResultSetMapping AppendUpdateOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); @@ -128,7 +128,7 @@ public virtual ResultSetMapping AppendUpdateOperation( /// The for the command. public virtual ResultSetMapping AppendDeleteOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); @@ -154,7 +154,7 @@ protected virtual void AppendInsertCommand( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList writeOperations) + IReadOnlyList writeOperations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); @@ -178,8 +178,8 @@ protected virtual void AppendUpdateCommand( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList writeOperations, - IReadOnlyList conditionOperations) + IReadOnlyList writeOperations, + IReadOnlyList conditionOperations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); @@ -202,7 +202,7 @@ protected virtual void AppendDeleteCommand( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList conditionOperations) + IReadOnlyList conditionOperations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); @@ -242,8 +242,8 @@ protected virtual ResultSetMapping AppendSelectAffectedCommand( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList readOperations, - IReadOnlyList conditionOperations, + IReadOnlyList readOperations, + IReadOnlyList conditionOperations, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); @@ -272,7 +272,7 @@ protected virtual void AppendInsertCommandHeader( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); @@ -322,7 +322,7 @@ protected virtual void AppendUpdateCommandHeader( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); @@ -357,7 +357,7 @@ protected virtual void AppendUpdateCommandHeader( /// The operations representing the data to be read. protected virtual void AppendSelectCommandHeader( StringBuilder commandStringBuilder, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(operations, nameof(operations)); @@ -397,7 +397,7 @@ protected virtual void AppendFromClause( /// The operations for which there are values. protected virtual void AppendValuesHeader( StringBuilder commandStringBuilder, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(operations, nameof(operations)); @@ -417,7 +417,7 @@ protected virtual void AppendValues( StringBuilder commandStringBuilder, string name, string? schema, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(operations, nameof(operations)); @@ -459,7 +459,7 @@ protected virtual void AppendValues( /// The operations from which to build the conditions. protected virtual void AppendWhereClause( StringBuilder commandStringBuilder, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(operations, nameof(operations)); @@ -480,7 +480,7 @@ protected virtual void AppendWhereClause( /// The operations from which to build the conditions. protected virtual void AppendWhereAffectedClause( StringBuilder commandStringBuilder, - IReadOnlyList operations) + IReadOnlyList operations) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(operations, nameof(operations)); @@ -532,7 +532,7 @@ protected abstract void AppendRowsAffectedWhereCondition( /// protected virtual void AppendWhereCondition( StringBuilder commandStringBuilder, - ColumnModification columnModification, + IColumnModification columnModification, bool useOriginalValue) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); @@ -551,8 +551,7 @@ protected virtual void AppendWhereCondition( else { commandStringBuilder.Append(" = "); - if (!columnModification.UseCurrentValueParameter - && !columnModification.UseOriginalValueParameter) + if (!columnModification.UseParameter) { AppendSqlLiteral(commandStringBuilder, columnModification, null, null); } @@ -573,7 +572,7 @@ protected virtual void AppendWhereCondition( /// The column for which the condition is being generated. protected abstract void AppendIdentityWhereCondition( StringBuilder commandStringBuilder, - ColumnModification columnModification); + IColumnModification columnModification); /// /// Appends SQL text that defines the start of a batch. @@ -609,7 +608,7 @@ public virtual void AppendNextSequenceValueOperation(StringBuilder commandString SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, Check.NotNull(name, nameof(name)), schema); } - private void AppendSqlLiteral(StringBuilder commandStringBuilder, ColumnModification modification, string? tableName, string? schema) + private void AppendSqlLiteral(StringBuilder commandStringBuilder, IColumnModification modification, string? tableName, string? schema) { if (modification.TypeMapping == null) { diff --git a/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs index ea201aa7ad5..96baca97a66 100644 --- a/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs +++ b/src/EFCore.SqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs @@ -31,7 +31,7 @@ public interface ISqlServerUpdateSqlGenerator : IUpdateSqlGenerator /// ResultSetMapping AppendBulkInsertOperation( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands, + IReadOnlyList modificationCommands, int commandPosition); } } diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs index 889776c0251..53ba1b655cf 100644 --- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs +++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs @@ -24,7 +24,7 @@ public class SqlServerModificationCommandBatch : AffectedCountModificationComman private const int MaxRowCount = 1000; private int _parameterCount = 1; // Implicit parameter for the command text private readonly int _maxBatchSize; - private readonly List _bulkInsertCommands = new(); + private readonly List _bulkInsertCommands = new(); private int _commandsLeftToLengthCheck = 50; /// @@ -62,7 +62,7 @@ public SqlServerModificationCommandBatch( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override bool CanAddCommand(ModificationCommand modificationCommand) + protected override bool CanAddCommand(IModificationCommand modificationCommand) { if (ModificationCommands.Count >= _maxBatchSize) { @@ -113,7 +113,7 @@ protected override bool IsCommandTextValid() protected override int GetParameterCount() => _parameterCount; - private static int CountParameters(ModificationCommand modificationCommand) + private static int CountParameters(IModificationCommand modificationCommand) { var parameterCount = 0; // ReSharper disable once ForCanBeConvertedToForeach @@ -210,7 +210,7 @@ protected override void UpdateCachedCommandText(int commandPosition) } } - private static bool CanBeInsertedInSameStatement(ModificationCommand firstCommand, ModificationCommand secondCommand) + private static bool CanBeInsertedInSameStatement(IModificationCommand firstCommand, IModificationCommand secondCommand) => string.Equals(firstCommand.TableName, secondCommand.TableName, StringComparison.Ordinal) && string.Equals(firstCommand.Schema, secondCommand.Schema, StringComparison.Ordinal) && firstCommand.ColumnModifications.Where(o => o.IsWrite).Select(o => o.ColumnName).SequenceEqual( diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs index 3b4ba9c3649..56fa5677c85 100644 --- a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs +++ b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs @@ -48,7 +48,7 @@ public SqlServerUpdateSqlGenerator( /// public virtual ResultSetMapping AppendBulkInsertOperation( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands, + IReadOnlyList modificationCommands, int commandPosition) { var table = StoreObjectIdentifier.Table(modificationCommands[0].TableName, modificationCommands[0].Schema); @@ -131,8 +131,8 @@ public virtual ResultSetMapping AppendBulkInsertOperation( private ResultSetMapping AppendBulkInsertWithoutServerValues( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands, - List writeOperations) + IReadOnlyList modificationCommands, + List writeOperations) { Check.DebugAssert(writeOperations.Count > 0, $"writeOperations.Count is {writeOperations.Count}"); @@ -162,11 +162,11 @@ private ResultSetMapping AppendBulkInsertWithoutServerValues( private ResultSetMapping AppendBulkInsertWithServerValues( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands, + IReadOnlyList modificationCommands, int commandPosition, - List writeOperations, - List keyOperations, - List readOperations) + List writeOperations, + List keyOperations, + List readOperations) { AppendDeclareTable( commandStringBuilder, @@ -203,11 +203,11 @@ private ResultSetMapping AppendBulkInsertWithServerValues( private ResultSetMapping AppendBulkInsertWithServerValuesOnly( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands, + IReadOnlyList modificationCommands, int commandPosition, - List nonIdentityOperations, - List keyOperations, - List readOperations) + List nonIdentityOperations, + List keyOperations, + List readOperations) { AppendDeclareTable(commandStringBuilder, InsertedTableBaseName, commandPosition, keyOperations); @@ -235,8 +235,8 @@ private void AppendMergeCommandHeader( string name, string? schema, string toInsertTableAlias, - IReadOnlyList modificationCommands, - IReadOnlyList writeOperations, + IReadOnlyList modificationCommands, + IReadOnlyList writeOperations, string? additionalColumns = null) { commandStringBuilder.Append("MERGE "); @@ -301,7 +301,7 @@ private void AppendMergeCommandHeader( private void AppendValues( StringBuilder commandStringBuilder, - IReadOnlyList operations, + IReadOnlyList operations, string additionalLiteral) { if (operations.Count > 0) @@ -332,7 +332,7 @@ private void AppendDeclareTable( StringBuilder commandStringBuilder, string name, int index, - IReadOnlyList operations, + IReadOnlyList operations, string? additionalColumns = null) { commandStringBuilder @@ -375,7 +375,7 @@ private string GetTypeNameForCopy(IProperty property) // ReSharper disable once ParameterTypeCanBeEnumerable.Local private void AppendOutputClause( StringBuilder commandStringBuilder, - IReadOnlyList operations, + IReadOnlyList operations, string tableName, int tableIndex, string? additionalColumns = null) @@ -404,9 +404,9 @@ private void AppendOutputClause( private ResultSetMapping AppendInsertOperationWithServerKeys( StringBuilder commandStringBuilder, - ModificationCommand command, - IReadOnlyList keyOperations, - IReadOnlyList readOperations, + IModificationCommand command, + IReadOnlyList keyOperations, + IReadOnlyList readOperations, int commandPosition) { var name = command.TableName; @@ -429,8 +429,8 @@ private ResultSetMapping AppendInsertOperationWithServerKeys( private ResultSetMapping AppendSelectCommand( StringBuilder commandStringBuilder, - IReadOnlyList readOperations, - IReadOnlyList keyOperations, + IReadOnlyList readOperations, + IReadOnlyList keyOperations, string insertedTableName, int insertedTableIndex, string tableName, @@ -514,7 +514,7 @@ public override void AppendBatchHeader(StringBuilder commandStringBuilder) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, ColumnModification columnModification) + protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, IColumnModification columnModification) { SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, columnModification.ColumnName); commandStringBuilder.Append(" = "); diff --git a/src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs b/src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs index cac439c4e5e..8bcb10b3cb1 100644 --- a/src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs +++ b/src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs @@ -42,7 +42,7 @@ public SqliteUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, ColumnModification columnModification) + protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, IColumnModification columnModification) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(columnModification, nameof(columnModification)); diff --git a/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs index 88518284341..4db01f636a9 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs @@ -4,10 +4,12 @@ using System; using System.Linq; using System.Text; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -647,6 +649,9 @@ public virtual void GenerateNextSequenceValueOperation_correctly_handles_schemas protected abstract string RowsAffected { get; } + protected virtual IMutableModificationCommandFactory CreateMutableModificationCommandFactory() + => new MutableModificationCommandFactory(); + protected virtual string Identity => throw new NotImplementedException(); @@ -665,9 +670,9 @@ protected virtual string SchemaPrefix protected virtual string GetIdentityWhereCondition(string columnName) => OpenDelimiter + columnName + CloseDelimiter + " = " + Identity; - protected ModificationCommand CreateInsertCommand(bool identityKey = true, bool isComputed = true, bool defaultsOnly = false) + protected IMutableModificationCommand CreateInsertCommand(bool identityKey = true, bool isComputed = true, bool defaultsOnly = false) { - var model = GetDuckType().Model.FinalizeModel(); + var model = GetDuckModel(); var stateManager = TestHelpers.CreateContextServices(model).GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new Duck()); var generator = new ParameterNameGenerator(); @@ -678,21 +683,22 @@ protected ModificationCommand CreateInsertCommand(bool identityKey = true, bool var quacksProperty = duckType.FindProperty(nameof(Duck.Quacks)); var computedProperty = duckType.FindProperty(nameof(Duck.Computed)); var concurrencyProperty = duckType.FindProperty(nameof(Duck.ConcurrencyToken)); + var columnModifications = new[] { - new ColumnModification( + new ColumnModificationParameters( entry, idProperty, idProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, idProperty.GetTableColumnMappings().Single().TypeMapping, identityKey, !identityKey, true, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, nameProperty, nameProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, nameProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, quacksProperty, quacksProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, quacksProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, computedProperty, computedProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, computedProperty.GetTableColumnMappings().Single().TypeMapping, isComputed, false, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, concurrencyProperty, concurrencyProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, concurrencyProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, false, true) }; @@ -702,12 +708,12 @@ protected ModificationCommand CreateInsertCommand(bool identityKey = true, bool columnModifications = columnModifications.Where(c => !c.IsWrite).ToArray(); } - return new ModificationCommand("Ducks", Schema, columnModifications, false); + return CreateModificationCommand("Ducks", Schema, columnModifications, false); } - protected ModificationCommand CreateUpdateCommand(bool isComputed = true, bool concurrencyToken = true) + protected IMutableModificationCommand CreateUpdateCommand(bool isComputed = true, bool concurrencyToken = true) { - var model = GetDuckType().Model.FinalizeModel(); + var model = GetDuckModel(); var stateManager = TestHelpers.CreateContextServices(model).GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new Duck()); var generator = new ParameterNameGenerator(); @@ -718,31 +724,32 @@ protected ModificationCommand CreateUpdateCommand(bool isComputed = true, bool c var quacksProperty = duckType.FindProperty(nameof(Duck.Quacks)); var computedProperty = duckType.FindProperty(nameof(Duck.Computed)); var concurrencyProperty = duckType.FindProperty(nameof(Duck.ConcurrencyToken)); + var columnModifications = new[] { - new ColumnModification( + new ColumnModificationParameters( entry, idProperty, idProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, idProperty.GetTableColumnMappings().Single().TypeMapping, false, false, true, true, true), - new ColumnModification( + new ColumnModificationParameters( entry, nameProperty, nameProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, nameProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, quacksProperty, quacksProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, quacksProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, computedProperty, computedProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, computedProperty.GetTableColumnMappings().Single().TypeMapping, isComputed, false, false, false, true), - new ColumnModification( + new ColumnModificationParameters( entry, concurrencyProperty, concurrencyProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, concurrencyProperty.GetTableColumnMappings().Single().TypeMapping, false, true, false, concurrencyToken, true) }; - return new ModificationCommand("Ducks", Schema, columnModifications, false); + return CreateModificationCommand("Ducks", Schema, columnModifications, false); } - protected ModificationCommand CreateDeleteCommand(bool concurrencyToken = true) + protected IMutableModificationCommand CreateDeleteCommand(bool concurrencyToken = true) { - var model = GetDuckType().Model.FinalizeModel(); + var model = GetDuckModel(); var stateManager = TestHelpers.CreateContextServices(model).GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new Duck()); var generator = new ParameterNameGenerator(); @@ -750,26 +757,27 @@ protected ModificationCommand CreateDeleteCommand(bool concurrencyToken = true) var duckType = model.FindEntityType(typeof(Duck)); var idProperty = duckType.FindProperty(nameof(Duck.Id)); var concurrencyProperty = duckType.FindProperty(nameof(Duck.ConcurrencyToken)); + var columnModifications = new[] { - new ColumnModification( + new ColumnModificationParameters( entry, idProperty, idProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, idProperty.GetTableColumnMappings().Single().TypeMapping, false, false, true, true, true), - new ColumnModification( + new ColumnModificationParameters( entry, concurrencyProperty, concurrencyProperty.GetTableColumnMappings().Single().Column, generator.GenerateNext, concurrencyProperty.GetTableColumnMappings().Single().TypeMapping, false, false, false, concurrencyToken, true) }; - return new ModificationCommand("Ducks", Schema, columnModifications, false); + return CreateModificationCommand("Ducks", Schema, columnModifications, false); } protected abstract TestHelpers TestHelpers { get; } - private IMutableEntityType GetDuckType() + private IModel GetDuckModel() { var modelBuilder = TestHelpers.CreateConventionBuilder(); modelBuilder.Entity().ToTable("Ducks", Schema).Property(e => e.Id).ValueGeneratedNever(); - return modelBuilder.Model.FindEntityType(typeof(Duck)); + return modelBuilder.Model.FinalizeModel(); } protected class Duck @@ -780,5 +788,24 @@ protected class Duck public Guid Computed { get; set; } public byte[] ConcurrencyToken { get; set; } } + + private IMutableModificationCommand CreateModificationCommand( + string name, + string schema, + IReadOnlyList columnModifications, + bool sensitiveLoggingEnabled) + { + var modificationCommandParameters = new ModificationCommandParameters( + name, schema, sensitiveLoggingEnabled); + var modificationCommand = CreateMutableModificationCommandFactory().CreateModificationCommand( + modificationCommandParameters); + + foreach (var columnModification in columnModifications) + { + modificationCommand.AddColumnModification(columnModification); + } + + return modificationCommand; + } } } diff --git a/test/EFCore.Relational.Tests/Migrations/MigrationCommandListBuilderTest.cs b/test/EFCore.Relational.Tests/Migrations/MigrationCommandListBuilderTest.cs index bbd50997ae0..842cb8cecbb 100644 --- a/test/EFCore.Relational.Tests/Migrations/MigrationCommandListBuilderTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/MigrationCommandListBuilderTest.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.TestUtilities.FakeProvider; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Update.Internal; using Xunit; namespace Microsoft.EntityFrameworkCore.Migrations @@ -146,6 +147,7 @@ private MigrationCommandListBuilder CreateBuilder() generationHelper, typeMappingSource, new CurrentDbContext(new FakeDbContext()), + new MutableModificationCommandFactory(), new LoggingOptions(), logger, migrationsLogger)); diff --git a/test/EFCore.Relational.Tests/Storage/RelationalCommandTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalCommandTest.cs index ab5081ac9e4..6310664e71a 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalCommandTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalCommandTest.cs @@ -867,7 +867,7 @@ public ReaderThrowingRelationalCommand( } protected override RelationalDataReader CreateRelationalDataReader() - => new ThrowingRelationalReader(this); + => new ThrowingRelationalReader(); public static IRelationalCommand Create(string commandText = "Command Text") => new ReaderThrowingRelationalCommand( @@ -880,8 +880,8 @@ public static IRelationalCommand Create(string commandText = "Command Text") private class ThrowingRelationalReader : RelationalDataReader { - public ThrowingRelationalReader(IRelationalCommand relationalCommand) - : base(relationalCommand) + public ThrowingRelationalReader() + : base() { } diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeModificationCommand.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeModificationCommand.cs deleted file mode 100644 index acb58d3f86f..00000000000 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeModificationCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Update; - -namespace Microsoft.EntityFrameworkCore.TestUtilities -{ - public class FakeModificationCommand : ModificationCommand - { - public FakeModificationCommand( - string name, - string schema, - Func generateParameterName, - bool sensitiveLoggingEnabled, - IReadOnlyList columnModifications) - : base(name, schema, generateParameterName, sensitiveLoggingEnabled, null, null) - { - ColumnModifications = columnModifications; - } - - public override IReadOnlyList ColumnModifications { get; } - } -} diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeSqlGenerator.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeSqlGenerator.cs index bfa2268d3cc..c7eb473a220 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeSqlGenerator.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeSqlGenerator.cs @@ -16,7 +16,7 @@ public FakeSqlGenerator(UpdateSqlGeneratorDependencies dependencies) public override ResultSetMapping AppendInsertOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { AppendInsertOperationCalls++; @@ -25,7 +25,7 @@ public override ResultSetMapping AppendInsertOperation( public override ResultSetMapping AppendUpdateOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { AppendUpdateOperationCalls++; @@ -34,7 +34,7 @@ public override ResultSetMapping AppendUpdateOperation( public override ResultSetMapping AppendDeleteOperation( StringBuilder commandStringBuilder, - ModificationCommand command, + IModificationCommand command, int commandPosition) { AppendDeleteOperationCalls++; @@ -52,7 +52,7 @@ public override void AppendBatchHeader(StringBuilder commandStringBuilder) base.AppendBatchHeader(commandStringBuilder); } - protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, ColumnModification columnModification) + protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, IColumnModification columnModification) => commandStringBuilder .Append(SqlGenerationHelper.DelimitIdentifier(columnModification.ColumnName)) .Append(" = ") diff --git a/test/EFCore.Relational.Tests/TestUtilities/TestModificationCommandBatch.cs b/test/EFCore.Relational.Tests/TestUtilities/TestModificationCommandBatch.cs index d553c99f219..d8b5cc90abf 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/TestModificationCommandBatch.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/TestModificationCommandBatch.cs @@ -17,7 +17,7 @@ public TestModificationCommandBatch( _maxBatchSize = maxBatchSize ?? 1; } - protected override bool CanAddCommand(ModificationCommand modificationCommand) + protected override bool CanAddCommand(IModificationCommand modificationCommand) { return ModificationCommands.Count < _maxBatchSize; } diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index 448fabfcbcf..6fcf531bb5a 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -976,6 +976,7 @@ public ICommandBatchPreparer CreateCommandBatchPreparer( new ParameterNameGeneratorFactory(new ParameterNameGeneratorDependencies()), new ModificationCommandComparer(), new KeyValueIndexFactorySource(), + new MutableModificationCommandFactory(), loggingOptions, new FakeDiagnosticsLogger(), new DbContextOptionsBuilder().Options)); diff --git a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs index 90a77a7ff70..cbbeb1cb48d 100644 --- a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -32,69 +33,73 @@ public void Compare_returns_0_only_for_commands_that_are_equal() var stateManager = new DbContext(optionsBuilder.Options).GetService(); + var modificationCommandSource = CreateModificationCommandSource(); + var entry1 = stateManager.GetOrCreateEntry(new object()); entry1[(IProperty)key] = 1; entry1.SetEntityState(EntityState.Added); - var modificationCommandAdded = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommandAdded = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext)); modificationCommandAdded.AddEntry(entry1, true); var entry2 = stateManager.GetOrCreateEntry(new object()); entry2[(IProperty)key] = 2; entry2.SetEntityState(EntityState.Modified); - var modificationCommandModified = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommandModified = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext)); modificationCommandModified.AddEntry(entry2, true); var entry3 = stateManager.GetOrCreateEntry(new object()); entry3[(IProperty)key] = 3; entry3.SetEntityState(EntityState.Deleted); - var modificationCommandDeleted = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommandDeleted = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext)); modificationCommandDeleted.AddEntry(entry3, true); var mCC = new ModificationCommandComparer(); + Assert.Same(modificationCommandAdded, modificationCommandAdded); + Assert.True(0 == mCC.Compare(modificationCommandAdded, modificationCommandAdded)); Assert.True(0 == mCC.Compare(null, null)); Assert.True( 0 == mCC.Compare( - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", "dbo", false), + CreateModificationCommand("A", "dbo", false))); - Assert.True(0 > mCC.Compare(null, new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null))); - Assert.True(0 < mCC.Compare(new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null), null)); + Assert.True(0 > mCC.Compare(null, CreateModificationCommand("A", null, false))); + Assert.True(0 < mCC.Compare(CreateModificationCommand("A", null, false), null)); Assert.True( 0 > mCC.Compare( - new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", null, false), + CreateModificationCommand("A", "dbo", false))); Assert.True( 0 < mCC.Compare( - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", "dbo", false), + CreateModificationCommand("A", null, false))); Assert.True( 0 > mCC.Compare( - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", "foo", new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", "dbo", false), + CreateModificationCommand("A", "foo", false))); Assert.True( 0 < mCC.Compare( - new ModificationCommand("A", "foo", new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", "dbo", new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", "foo", false), + CreateModificationCommand("A", "dbo", false))); Assert.True( 0 > mCC.Compare( - new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("B", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("A", null, false), + CreateModificationCommand("B", null, false))); Assert.True( 0 < mCC.Compare( - new ModificationCommand("B", null, new ParameterNameGenerator().GenerateNext, false, null, null), - new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("B", null, false), + CreateModificationCommand("A", null, false))); Assert.True(0 > mCC.Compare(modificationCommandModified, modificationCommandAdded)); Assert.True(0 < mCC.Compare(modificationCommandAdded, modificationCommandModified)); @@ -175,19 +180,21 @@ private void Compare_returns_0_only_for_entries_that_have_same_key_values_generi var stateManager = new DbContext(optionsBuilder.Options).GetService(); + var modificationCommandSource = CreateModificationCommandSource(); + var entry1 = stateManager.GetOrCreateEntry(new object()); entry1[(IProperty)keyProperty] = value1; entry1.SetEntityState(EntityState.Modified); - var modificationCommand1 = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommand1 = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext, null)); modificationCommand1.AddEntry(entry1, true); var entry2 = stateManager.GetOrCreateEntry(new object()); entry2[(IProperty)keyProperty] = value2; entry2.SetEntityState(EntityState.Modified); - var modificationCommand2 = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommand2 = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext, null)); modificationCommand2.AddEntry(entry2, true); - var modificationCommand3 = new ModificationCommand("A", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var modificationCommand3 = modificationCommandSource.CreateModificationCommand(new ModificationCommandParameters("A", null, false, null, new ParameterNameGenerator().GenerateNext, null)); modificationCommand3.AddEntry(entry1, true); var mCC = new ModificationCommandComparer(); @@ -204,5 +211,15 @@ private enum FlagsEnum First = 1 << 0, Second = 1 << 2 } + + private static IMutableModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled) + => CreateModificationCommandSource().CreateModificationCommand( + new ModificationCommandParameters(name, schema, sensitiveLoggingEnabled)); + + private static MutableModificationCommandFactory CreateModificationCommandSource() + => new MutableModificationCommandFactory(); } } diff --git a/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs b/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs index 25a47760a3e..24501ebba78 100644 --- a/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs +++ b/test/EFCore.Relational.Tests/Update/ModificationCommandTest.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -22,7 +24,7 @@ public void ModificationCommand_initialized_correctly_for_added_entities_with_te var entry = CreateEntry(EntityState.Added, generateKeyValues: true); entry.SetTemporaryValue(entry.EntityType.FindPrimaryKey().Properties[0], -1); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -66,7 +68,7 @@ public void ModificationCommand_initialized_correctly_for_added_entities_with_no { var entry = CreateEntry(EntityState.Added, generateKeyValues: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -110,7 +112,7 @@ public void ModificationCommand_initialized_correctly_for_added_entities_with_ex { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -154,7 +156,7 @@ public void ModificationCommand_initialized_correctly_for_modified_entities_with { var entry = CreateEntry(EntityState.Modified, generateKeyValues: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -198,7 +200,7 @@ public void ModificationCommand_initialized_correctly_for_modified_entities_with { var entry = CreateEntry(EntityState.Modified); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -242,7 +244,7 @@ public void ModificationCommand_initialized_correctly_for_modified_entities_with { var entry = CreateEntry(EntityState.Modified, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -286,7 +288,7 @@ public void ModificationCommand_initialized_correctly_for_deleted_entities() { var entry = CreateEntry(EntityState.Deleted); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -310,7 +312,7 @@ public void ModificationCommand_initialized_correctly_for_deleted_entities_with_ { var entry = CreateEntry(EntityState.Deleted, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.Equal("T1", command.TableName); @@ -356,7 +358,7 @@ public void ModificationCommand_throws_for_unchanged_entities(bool sensitive) { var entry = CreateEntry(EntityState.Unchanged); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, sensitive, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, sensitive, null); Assert.Equal( sensitive @@ -372,7 +374,7 @@ public void ModificationCommand_throws_for_unknown_entities(bool sensitive) { var entry = CreateEntry(EntityState.Detached); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, sensitive, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, sensitive, null); Assert.Equal( sensitive @@ -387,7 +389,7 @@ public void RequiresResultPropagation_false_for_Delete_operation() var entry = CreateEntry( EntityState.Deleted, generateKeyValues: true, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.False(command.RequiresResultPropagation); @@ -399,7 +401,7 @@ public void RequiresResultPropagation_true_for_Insert_operation_if_store_generat var entry = CreateEntry( EntityState.Added, generateKeyValues: true, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.True(command.RequiresResultPropagation); @@ -410,7 +412,7 @@ public void RequiresResultPropagation_false_for_Insert_operation_if_no_store_gen { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.False(command.RequiresResultPropagation); @@ -422,7 +424,7 @@ public void RequiresResultPropagation_true_for_Update_operation_if_non_key_store var entry = CreateEntry( EntityState.Modified, generateKeyValues: true, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.True(command.RequiresResultPropagation); @@ -433,7 +435,7 @@ public void RequiresResultPropagation_false_for_Update_operation_if_no_non_key_s { var entry = CreateEntry(EntityState.Modified, generateKeyValues: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null); command.AddEntry(entry, true); Assert.False(command.RequiresResultPropagation); @@ -490,5 +492,18 @@ private static InternalEntityEntry CreateEntry( Name2 = computeNonKeyValue ? null : "Test" }); } + + private static IMutableModificationCommand CreateModificationCommand( + string tableName, + string schemaName, + Func generateParameterName, + bool sensitiveLoggingEnabled, + IComparer comparer) + => new MutableModificationCommandFactory().CreateModificationCommand(new ModificationCommandParameters( + tableName, + schemaName, + sensitiveLoggingEnabled, + comparer, + generateParameterName)); } } diff --git a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs index 1af609a071d..6b50bbc4bcb 100644 --- a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs +++ b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; @@ -14,6 +15,7 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.TestUtilities.FakeProvider; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -27,7 +29,7 @@ public class ReaderModificationCommandBatchTest [ConditionalFact] public void AddCommand_adds_command_if_possible() { - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, true, columnModifications: null); var batch = new ModificationCommandBatchFake(); batch.AddCommand(command); @@ -44,7 +46,7 @@ public void AddCommand_adds_command_if_possible() [ConditionalFact] public void AddCommand_does_not_add_command_if_not_possible() { - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, true, columnModifications: null); var batch = new ModificationCommandBatchFake(); batch.AddCommand(command); @@ -60,7 +62,7 @@ public void AddCommand_does_not_add_command_if_not_possible() [ConditionalFact] public void AddCommand_does_not_add_command_if_resulting_sql_is_invalid() { - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, true, columnModifications: null); var batch = new ModificationCommandBatchFake(); batch.AddCommand(command); @@ -78,7 +80,7 @@ public void UpdateCommandText_compiles_inserts() { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var fakeSqlGenerator = new FakeSqlGenerator( @@ -97,7 +99,7 @@ public void UpdateCommandText_compiles_updates() { var entry = CreateEntry(EntityState.Modified, generateKeyValues: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var fakeSqlGenerator = new FakeSqlGenerator( @@ -116,7 +118,7 @@ public void UpdateCommandText_compiles_deletes() { var entry = CreateEntry(EntityState.Deleted); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var fakeSqlGenerator = new FakeSqlGenerator( @@ -135,7 +137,7 @@ public void UpdateCommandText_compiles_multiple_commands() { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var fakeSqlGenerator = new FakeSqlGenerator( @@ -154,7 +156,7 @@ public async Task ExecuteAsync_executes_batch_commands_and_consumes_reader() { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var dbDataReader = CreateFakeDataReader(); @@ -176,7 +178,7 @@ public async Task ExecuteAsync_saves_store_generated_values() var entry = CreateEntry(EntityState.Added, generateKeyValues: true); entry.SetTemporaryValue(entry.EntityType.FindPrimaryKey().Properties[0], -1); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -199,7 +201,7 @@ public async Task ExecuteAsync_saves_store_generated_values_on_non_key_columns() EntityState.Added, generateKeyValues: true, computeNonKeyValue: true); entry.SetTemporaryValue(entry.EntityType.FindPrimaryKey().Properties[0], -1); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -221,7 +223,7 @@ public async Task ExecuteAsync_saves_store_generated_values_when_updating() var entry = CreateEntry( EntityState.Modified, generateKeyValues: true, computeNonKeyValue: true); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -243,7 +245,7 @@ public async Task Exception_not_thrown_for_more_than_one_row_returned_for_single var entry = CreateEntry(EntityState.Added, generateKeyValues: true); entry.SetTemporaryValue(entry.EntityType.FindPrimaryKey().Properties[0], -1); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -264,7 +266,7 @@ public async Task Exception_thrown_if_rows_returned_for_command_without_store_ge { var entry = CreateEntry(EntityState.Added); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -286,7 +288,7 @@ public async Task Exception_thrown_if_no_rows_returned_for_command_with_store_ge var entry = CreateEntry(EntityState.Added, generateKeyValues: true); entry.SetTemporaryValue(entry.EntityType.FindPrimaryKey().Properties[0], -1); - var command = new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null, null); + var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null); command.AddEntry(entry, true); var connection = CreateConnection( @@ -312,14 +314,13 @@ public void CreateStoreCommand_creates_parameters_for_each_ModificationCommand() var parameterNameGenerator = new ParameterNameGenerator(); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( + new ColumnModificationParameters( entry, property, property.GetTableColumnMappings().Single().Column, @@ -329,14 +330,13 @@ public void CreateStoreCommand_creates_parameters_for_each_ModificationCommand() })); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( + new ColumnModificationParameters( entry, property, property.GetTableColumnMappings().Single().Column, @@ -366,21 +366,20 @@ public void PopulateParameters_creates_parameter_for_write_ModificationCommand() var batch = new ModificationCommandBatchFake(); var parameterNameGenerator = new ParameterNameGenerator(); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( - entry, - property, - property.GetTableColumnMappings().Single().Column, - parameterNameGenerator.GenerateNext, - property.GetTableColumnMappings().Single().TypeMapping, - isRead: false, isWrite: true, isKey: false, isCondition: false, - sensitiveLoggingEnabled: true) + new ColumnModificationParameters( + entry, + property, + property.GetTableColumnMappings().Single().Column, + parameterNameGenerator.GenerateNext, + property.GetTableColumnMappings().Single().TypeMapping, + valueIsRead: false, valueIsWrite: true, columnIsKey: false, columnIsCondition: false, + sensitiveLoggingEnabled: true) })); var storeCommand = batch.CreateStoreCommandBase(); @@ -402,20 +401,19 @@ public void PopulateParameters_creates_parameter_for_condition_ModificationComma var batch = new ModificationCommandBatchFake(); var parameterNameGenerator = new ParameterNameGenerator(); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( + new ColumnModificationParameters( entry, property, property.GetTableColumnMappings().Single().Column, parameterNameGenerator.GenerateNext, property.GetTableColumnMappings().Single().TypeMapping, - isRead: false, isWrite: false, isKey: false, isCondition: true, + valueIsRead: false, valueIsWrite: false, columnIsKey: false, columnIsCondition: true, sensitiveLoggingEnabled: true) })); @@ -438,21 +436,20 @@ public void PopulateParameters_creates_parameters_for_write_and_condition_Modifi var batch = new ModificationCommandBatchFake(); var parameterNameGenerator = new ParameterNameGenerator(); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( - entry, - property, - property.GetTableColumnMappings().Single().Column, - parameterNameGenerator.GenerateNext, - property.GetTableColumnMappings().Single().TypeMapping, - isRead: false, isWrite: true, isKey: false, isCondition: true, - sensitiveLoggingEnabled: true) + new ColumnModificationParameters( + entry, + property, + property.GetTableColumnMappings().Single().Column, + parameterNameGenerator.GenerateNext, + property.GetTableColumnMappings().Single().TypeMapping, + valueIsRead: false, valueIsWrite: true, columnIsKey: false, columnIsCondition: true, + sensitiveLoggingEnabled: true) })); var storeCommand = batch.CreateStoreCommandBase(); @@ -476,21 +473,20 @@ public void PopulateParameters_does_not_create_parameter_for_read_ModificationCo var batch = new ModificationCommandBatchFake(); var parameterNameGenerator = new ParameterNameGenerator(); batch.AddCommand( - new FakeModificationCommand( + CreateModificationCommand( "T1", null, - parameterNameGenerator.GenerateNext, true, - new List + new[] { - new( - entry, - property, - property.GetTableColumnMappings().Single().Column, - parameterNameGenerator.GenerateNext, - property.GetTableColumnMappings().Single().TypeMapping, - isRead: true, isWrite: false, isKey: false, isCondition: false, - sensitiveLoggingEnabled: true) + new ColumnModificationParameters( + entry, + property, + property.GetTableColumnMappings().Single().Column, + parameterNameGenerator.GenerateNext, + property.GetTableColumnMappings().Single().TypeMapping, + valueIsRead: true, valueIsWrite: false, columnIsKey: false, columnIsCondition: false, + sensitiveLoggingEnabled: true) })); var storeCommand = batch.CreateStoreCommandBase(); @@ -585,7 +581,7 @@ public string CommandText public bool ShouldAddCommand { get; set; } - protected override bool CanAddCommand(ModificationCommand modificationCommand) + protected override bool CanAddCommand(IModificationCommand modificationCommand) => ShouldAddCommand; public bool ShouldValidateSql { get; set; } @@ -638,5 +634,48 @@ public static IDbContextOptions CreateOptions(RelationalOptionsExtension options return optionsBuilder.Options; } + + private static IMutableModificationCommand CreateModificationCommand( + string table, + string schema, + Func generateParameterName, + bool sensitiveLoggingEnabled, + IComparer comparer) + { + var modificationCommandParameters = new ModificationCommandParameters( + table, + schema, + sensitiveLoggingEnabled, + comparer, + generateParameterName, + logger: null); + return CreateModificationCommandSource().CreateModificationCommand(modificationCommandParameters); + } + + private static IMutableModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled, + IReadOnlyList columnModifications) + { + var modificationCommandParameters = new ModificationCommandParameters( + name, schema, sensitiveLoggingEnabled); + + var modificationCommand = CreateModificationCommandSource().CreateModificationCommand( + modificationCommandParameters); + + if (columnModifications != null) + { + foreach (var columnModification in columnModifications) + { + modificationCommand.AddColumnModification(columnModification); + } + } + + return modificationCommand; + } + + private static MutableModificationCommandFactory CreateModificationCommandSource() + => new MutableModificationCommandFactory(); } } diff --git a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs index 5f4baeb4b17..914d38d513d 100644 --- a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs +++ b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -46,8 +48,8 @@ public void Uses_MaxBatchSize_specified_in_SqlServerOptionsExtension() var batch = factory.Create(); - Assert.True(batch.AddCommand(new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); - Assert.False(batch.AddCommand(new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + Assert.True(batch.AddCommand(CreateModificationCommand("T1", null, false))); + Assert.False(batch.AddCommand(CreateModificationCommand("T1", null, false))); } [ConditionalFact] @@ -83,12 +85,26 @@ public void MaxBatchSize_is_optional() var batch = factory.Create(); - Assert.True(batch.AddCommand(new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); - Assert.True(batch.AddCommand(new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + Assert.True(batch.AddCommand(CreateModificationCommand("T1", null, false))); + Assert.True(batch.AddCommand(CreateModificationCommand("T1", null, false))); } private class FakeDbContext : DbContext { } + + private static IMutableModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled) + { + var modificationCommandParameters = new ModificationCommandParameters( + name, schema, sensitiveLoggingEnabled); + + var modificationCommand = new MutableModificationCommandFactory().CreateModificationCommand( + modificationCommandParameters); + + return modificationCommand; + } } } diff --git a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs index 345410b816a..c7e9f58e5e4 100644 --- a/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs +++ b/test/EFCore.SqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -43,14 +45,28 @@ public void AddCommand_returns_false_when_max_batch_size_is_reached() Assert.True( batch.AddCommand( - new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("T1", null, false))); Assert.False( batch.AddCommand( - new ModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, false, null, null))); + CreateModificationCommand("T1", null, false))); } private class FakeDbContext : DbContext { } + + private static IMutableModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled) + { + var modificationCommandParameters = new ModificationCommandParameters( + name, schema, sensitiveLoggingEnabled); + + var modificationCommand = new MutableModificationCommandFactory().CreateModificationCommand( + modificationCommandParameters); + + return modificationCommand; + } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs index 849c5e17c62..d2c659822db 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs @@ -173,7 +173,7 @@ public override RelationalDataReader ExecuteReader(RelationalCommandParameterObj { var command = parameterObject.Connection.DbConnection.CreateCommand(); command.CommandText = CommandText; - var reader = new BadDataRelationalDataReader(this); + var reader = new BadDataRelationalDataReader(); reader.Initialize( new FakeConnection(), command, @@ -191,8 +191,8 @@ public override void PopulateFrom(IRelationalCommand command) private class BadDataRelationalDataReader : RelationalDataReader { - public BadDataRelationalDataReader(BadDataRelationalCommand relationalCommand) - : base(relationalCommand) + public BadDataRelationalDataReader() + : base() { } }