diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 557407a0b63..7110d784db8 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -678,6 +678,15 @@ private void Create( property.DeclaringEntityType.ShortName(), property.Name, nameof(PropertyBuilder.HasConversion))); } + var providerValueComparerType = (Type?)property[CoreAnnotationNames.ProviderValueComparerType]; + if (providerValueComparerType == null + && property[CoreAnnotationNames.ProviderValueComparer] != null) + { + throw new InvalidOperationException( + DesignStrings.CompiledModelValueComparer( + property.DeclaringEntityType.ShortName(), property.Name, nameof(PropertyBuilder.HasConversion))); + } + var valueConverterType = (Type?)property[CoreAnnotationNames.ValueConverterType]; if (valueConverterType == null && property.GetValueConverter() != null) @@ -809,6 +818,16 @@ private void Create( .Append("()"); } + if (providerValueComparerType != null) + { + AddNamespace(providerValueComparerType, parameters.Namespaces); + + mainBuilder.AppendLine(",") + .Append("providerValueComparer: new ") + .Append(_code.Reference(providerValueComparerType)) + .Append("()"); + } + mainBuilder .AppendLine(");") .DecrementIndent(); diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index a284accdf5a..8711ffdbf8b 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1318,7 +1318,7 @@ protected override void ValidateInheritanceMapping( var mappingStrategy = (string?)entityType[RelationalAnnotationNames.MappingStrategy]; if (mappingStrategy != null) { - ValidateMappingStrategy(mappingStrategy, entityType); + ValidateMappingStrategy(entityType, mappingStrategy); var storeObject = entityType.GetSchemaQualifiedTableName() ?? entityType.GetSchemaQualifiedViewName() ?? entityType.GetFunctionName(); @@ -1389,9 +1389,9 @@ protected override void ValidateInheritanceMapping( /// /// Validates that the given mapping strategy is supported /// - /// The mapping strategy. /// The entity type. - protected virtual void ValidateMappingStrategy(string? mappingStrategy, IEntityType entityType) + /// The mapping strategy. + protected virtual void ValidateMappingStrategy(IEntityType entityType, string? mappingStrategy) { switch (mappingStrategy) { diff --git a/src/EFCore.Relational/Metadata/IColumn.cs b/src/EFCore.Relational/Metadata/IColumn.cs index 4620f09db9d..99685f72dc3 100644 --- a/src/EFCore.Relational/Metadata/IColumn.cs +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -147,6 +147,14 @@ public virtual string? Collation => PropertyMappings.First().Property .GetCollation(StoreObjectIdentifier.Table(Table.Name, Table.Schema)); + /// + /// Gets the for this column. + /// + /// The comparer. + public virtual ValueComparer ProviderValueComparer + => PropertyMappings.First().Property + .GetProviderValueComparer(); + /// /// Returns the property mapping for the given entity type. /// diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index cf9a4c33d1e..941add0b3ad 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -2062,7 +2062,7 @@ protected virtual void DiffData( var sourceValue = sourceColumnModification.OriginalValue; var targetValue = targetColumnModification.Value; - var comparer = targetMapping.TypeMapping.ProviderValueComparer; + var comparer = targetColumn.ProviderValueComparer; if (sourceColumn.ProviderClrType == targetColumn.ProviderClrType && comparer.Equals(sourceValue, targetValue)) { diff --git a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs index 8bd9dd44c51..f145f044a28 100644 --- a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs @@ -27,7 +27,6 @@ protected RelationalGeometryTypeMapping( : base(CreateRelationalTypeMappingParameters(storeType)) { SpatialConverter = converter; - SetProviderValueComparer(); } /// @@ -38,25 +37,19 @@ protected RelationalGeometryTypeMapping( protected RelationalGeometryTypeMapping( RelationalTypeMappingParameters parameters, ValueConverter? converter) - : base(parameters) + : base(parameters.WithCoreParameters(parameters.CoreParameters with + { + ProviderValueComparer = parameters.CoreParameters.ProviderValueComparer + ?? CreateProviderValueComparer(parameters.CoreParameters.Converter?.ProviderClrType ?? parameters.CoreParameters.ClrType) + })) { SpatialConverter = converter; - SetProviderValueComparer(); } - private void SetProviderValueComparer() - { - var providerType = Converter?.ProviderClrType ?? ClrType; - if (providerType.IsAssignableTo(typeof(TGeometry))) - { - ProviderValueComparer = (ValueComparer)Activator.CreateInstance(typeof(GeometryValueComparer<>).MakeGenericType(providerType))!; - } - } - - /// - /// The underlying Geometry converter. - /// - protected virtual ValueConverter? SpatialConverter { get; } + private static ValueComparer? CreateProviderValueComparer(Type providerType) + => providerType.IsAssignableTo(typeof(TGeometry)) + ? (ValueComparer)Activator.CreateInstance(typeof(GeometryValueComparer<>).MakeGenericType(providerType))! + : null; private static RelationalTypeMappingParameters CreateRelationalTypeMappingParameters(string storeType) { @@ -67,10 +60,16 @@ private static RelationalTypeMappingParameters CreateRelationalTypeMappingParame typeof(TGeometry), null, comparer, - comparer), + comparer, + CreateProviderValueComparer(typeof(TGeometry))), storeType); } + /// + /// The underlying Geometry converter. + /// + protected virtual ValueConverter? SpatialConverter { get; } + /// /// Creates a with the appropriate type information configured. /// diff --git a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs index aa41dff067d..d223502f13d 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs @@ -109,6 +109,24 @@ public RelationalTypeMappingParameters( /// public StoreTypePostfix StoreTypePostfix { get; } + /// + /// Creates a new parameter object with the given + /// core parameters. + /// + /// Parameters for the base class. + /// The new parameter object. + public RelationalTypeMappingParameters WithCoreParameters(in CoreTypeMappingParameters coreParameters) + => new( + coreParameters, + StoreType, + StoreTypePostfix, + DbType, + Unicode, + Size, + FixedLength, + Precision, + Scale); + /// /// Creates a new parameter object with the given /// mapping info. @@ -261,8 +279,6 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p => this; } - private ValueComparer? _providerValueComparer; - /// /// Initializes a new instance of the class. /// @@ -380,19 +396,6 @@ public virtual bool IsFixedLength protected virtual string SqlLiteralFormatString => "{0}"; - /// - /// A for the provider CLR type values. - /// - public virtual ValueComparer ProviderValueComparer - { - get => NonCapturingLazyInitializer.EnsureInitialized( - ref _providerValueComparer, - this, - static c => ValueComparer.CreateDefault(c.Converter?.ProviderClrType ?? c.ClrType, favorStructuralComparisons: true)); - - protected set => _providerValueComparer = value; - } - /// /// Creates a copy of this mapping. /// diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index f062ee0e32f..05e355093b2 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -612,7 +612,7 @@ private static bool IsModified(IReadOnlyList columns, IReadOnlyModifica var column = columns[columnIndex]; object? originalValue = null; object? currentValue = null; - RelationalTypeMapping? typeMapping = null; + ValueComparer? providerValueComparer = null; for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) { var entry = command.Entries[entryIndex]; @@ -641,12 +641,12 @@ private static bool IsModified(IReadOnlyList columns, IReadOnlyModifica break; } - typeMapping = columnMapping!.TypeMapping; + providerValueComparer = property.GetProviderValueComparer(); } } - if (typeMapping != null - && !typeMapping.ProviderValueComparer.Equals(originalValue, currentValue)) + if (providerValueComparer != null + && !providerValueComparer.Equals(originalValue, currentValue)) { return true; } diff --git a/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs index 348495ac8ad..c4ab62efb5f 100644 --- a/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs +++ b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs @@ -150,7 +150,7 @@ public virtual bool TryCreateDependentKeyValue(IReadOnlyModificationCommand comm /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected static IEqualityComparer CreateEqualityComparer(IReadOnlyList columns) - => new CompositeCustomComparer(columns.Select(c => c.PropertyMappings.First().TypeMapping.ProviderValueComparer).ToList()); + => new CompositeCustomComparer(columns.Select(c => c.ProviderValueComparer).ToList()); private sealed class CompositeCustomComparer : IEqualityComparer { diff --git a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs index fdec90c5e4e..866b070f031 100644 --- a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs +++ b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs @@ -96,8 +96,7 @@ public abstract bool TryCreateDependentKeyValue( /// protected virtual IEqualityComparer CreateKeyEqualityComparer(IColumn column) #pragma warning disable EF1001 // Internal EF Core API usage. - => NullableComparerAdapter.Wrap( - column.PropertyMappings.First().TypeMapping.ProviderValueComparer); + => NullableComparerAdapter.Wrap(column.ProviderValueComparer); #pragma warning restore EF1001 // Internal EF Core API usage. /// diff --git a/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs index 8e9d8521218..b208536a03a 100644 --- a/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs +++ b/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs @@ -31,7 +31,7 @@ public SimpleRowIndexValueFactory(ITableIndex index) _column = index.Columns.Single(); _columnAccessors = ((Column)_column).Accessors; #pragma warning disable EF1001 // Internal EF Core API usage. - EqualityComparer = NullableComparerAdapter.Wrap(_column.PropertyMappings.First().TypeMapping.ProviderValueComparer); + EqualityComparer = NullableComparerAdapter.Wrap(_column.ProviderValueComparer); #pragma warning restore EF1001 // Internal EF Core API usage. } diff --git a/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs index 7ab03fb0832..bbff7df81ad 100644 --- a/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs +++ b/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs @@ -31,7 +31,7 @@ public SimpleRowKeyValueFactory(IUniqueConstraint constraint) _constraint = constraint; _column = constraint.Columns.Single(); _columnAccessors = ((Column)_column).Accessors; - EqualityComparer = new NoNullsCustomEqualityComparer(_column.PropertyMappings.First().TypeMapping.ProviderValueComparer); + EqualityComparer = new NoNullsCustomEqualityComparer(_column.ProviderValueComparer); } /// @@ -139,14 +139,6 @@ private sealed class NoNullsCustomEqualityComparer : IEqualityComparer public NoNullsCustomEqualityComparer(ValueComparer comparer) { - if (comparer.Type != typeof(TKey) - && comparer.Type == typeof(TKey).UnwrapNullableType()) - { -#pragma warning disable EF1001 // Internal EF Core API usage. - comparer = comparer.ToNonNullNullableComparer(); -#pragma warning restore EF1001 // Internal EF Core API usage. - } - _equals = (Func)comparer.EqualsExpression.Compile(); _hashCode = (Func)comparer.HashCodeExpression.Compile(); } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 6a472a267eb..5c1ca6bbb27 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -490,7 +490,7 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry) break; case EntityState.Added: _currentValue = entry.GetCurrentProviderValue(property); - _write = !mapping.TypeMapping.ProviderValueComparer.Equals(_originalValue, _currentValue); + _write = !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue); break; case EntityState.Deleted: @@ -513,7 +513,7 @@ public bool TryPropagate(IColumnMapping mapping, IUpdateEntry entry) && (entry.EntityState == EntityState.Unchanged || (entry.EntityState == EntityState.Modified && !entry.IsModified(property)) || (entry.EntityState == EntityState.Added - && mapping.TypeMapping.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentValue(property))))) + && mapping.Column.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentValue(property))))) { if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Save || entry.EntityState == EntityState.Added) diff --git a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs index c890413d708..74851ea9646 100644 --- a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs @@ -122,12 +122,6 @@ private sealed class NoNullsCustomEqualityComparer : IEqualityComparer public NoNullsCustomEqualityComparer(ValueComparer comparer) { - if (comparer.Type != typeof(TKey) - && comparer.Type == typeof(TKey).UnwrapNullableType()) - { - comparer = comparer.ToNonNullNullableComparer(); - } - _equals = (Func)comparer.EqualsExpression.Compile(); _hashCode = (Func)comparer.HashCodeExpression.Compile(); } diff --git a/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs b/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs deleted file mode 100644 index 5b31626f249..00000000000 --- a/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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.ChangeTracking.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 static class ValueComparerExtensions -{ - /// - /// 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 static ValueComparer ToNonNullNullableComparer(this ValueComparer comparer) - { - var type = comparer.EqualsExpression.Parameters[0].Type; - var nullableType = type.MakeNullable(); - - var newEqualsParam1 = Expression.Parameter(nullableType, "v1"); - var newEqualsParam2 = Expression.Parameter(nullableType, "v2"); - var newHashCodeParam = Expression.Parameter(nullableType, "v"); - var newSnapshotParam = Expression.Parameter(nullableType, "v"); - - return (ValueComparer)Activator.CreateInstance( - typeof(NonNullNullableValueComparer<>).MakeGenericType(nullableType), - Expression.Lambda( - comparer.ExtractEqualsBody( - Expression.Convert(newEqualsParam1, type), - Expression.Convert(newEqualsParam2, type)), - newEqualsParam1, newEqualsParam2), - Expression.Lambda( - comparer.ExtractHashCodeBody( - Expression.Convert(newHashCodeParam, type)), - newHashCodeParam), - Expression.Lambda( - Expression.Convert( - comparer.ExtractSnapshotBody( - Expression.Convert(newSnapshotParam, type)), - nullableType), - newSnapshotParam))!; - } - - private sealed class NonNullNullableValueComparer : ValueComparer - { - public NonNullNullableValueComparer( - LambdaExpression equalsExpression, - LambdaExpression hashCodeExpression, - LambdaExpression snapshotExpression) - : base( - (Expression>)equalsExpression, - (Expression>)hashCodeExpression, - (Expression>)snapshotExpression) - { - } - } -} diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index cdc344f2ff8..d53e42b96dc 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -868,6 +868,24 @@ protected virtual void ValidateTypeMappings( { _ = property.GetCurrentValueComparer(); // Will throw if there is no way to compare } + + var providerComparer = property.GetProviderValueComparer(); + if (providerComparer == null) + { + continue; + } + + var typeMapping = property.GetTypeMapping(); + var actualProviderClrType = (typeMapping.Converter?.ProviderClrType ?? typeMapping.ClrType).UnwrapNullableType(); + + if (providerComparer.Type.UnwrapNullableType() != actualProviderClrType) + { + throw new InvalidOperationException(CoreStrings.ComparerPropertyMismatch( + providerComparer.Type.ShortDisplayName(), + property.DeclaringEntityType.DisplayName(), + property.Name, + actualProviderClrType.ShortDisplayName())); + } } } } diff --git a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs index cfa24fb1b5e..beeb081763f 100644 --- a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs @@ -487,4 +487,52 @@ bool CanSetValueGeneratorFactory( /// if the given can be configured for this property. /// bool CanSetValueComparer(Type? comparerType, bool fromDataAnnotation = false); + + /// + /// Configures the to use for the provider values for this property. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, otherwise. + /// + IConventionPropertyBuilder? HasProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured for this property from the current configuration source. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured for this property. + /// + bool CanSetProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Configures the to use for the provider values for this property. + /// + /// + /// A type that derives from , + /// or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, otherwise. + /// + IConventionPropertyBuilder? HasProviderValueComparer(Type? comparerType, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured for this property from the current configuration source. + /// + /// + /// A type that derives from , + /// or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured for this property. + /// + bool CanSetProviderValueComparer(Type? comparerType, bool fromDataAnnotation = false); } diff --git a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs index 1bd36886580..072223dec5b 100644 --- a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs +++ b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs @@ -112,7 +112,7 @@ public virtual PropertiesConfigurationBuilder AreUnicode(bool unicode = true) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertiesConfigurationBuilder HaveConversion() => HaveConversion(typeof(TConversion)); @@ -121,7 +121,7 @@ public virtual PropertiesConfigurationBuilder HaveConversion() /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType) { @@ -143,8 +143,8 @@ public virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertiesConfigurationBuilder HaveConversion() where TComparer : ValueComparer @@ -154,10 +154,33 @@ public virtual PropertiesConfigurationBuilder HaveConversion - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion() + where TComparer : ValueComparer + => HaveConversion(typeof(TConversion), typeof(TComparer), typeof(TProviderComparer)); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType, Type? comparerType) + => HaveConversion(conversionType, comparerType, null); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType, Type? comparerType, Type? providerComparerType) { Check.NotNull(conversionType, nameof(conversionType)); @@ -172,6 +195,8 @@ public virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType Configuration.SetValueComparer(comparerType); + Configuration.SetProviderValueComparer(providerComparerType); + return this; } diff --git a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs index 78fc52f3fd8..fc7c0fb7d69 100644 --- a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs @@ -78,7 +78,7 @@ public PropertiesConfigurationBuilder(PropertyConfiguration property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertiesConfigurationBuilder HaveConversion() => (PropertiesConfigurationBuilder)base.HaveConversion(); @@ -87,7 +87,7 @@ public PropertiesConfigurationBuilder(PropertyConfiguration property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType) => (PropertiesConfigurationBuilder)base.HaveConversion(conversionType); @@ -96,8 +96,8 @@ public PropertiesConfigurationBuilder(PropertyConfiguration property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertiesConfigurationBuilder HaveConversion() where TComparer : ValueComparer @@ -107,8 +107,8 @@ public PropertiesConfigurationBuilder(PropertyConfiguration property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertiesConfigurationBuilder HaveConversion(Type conversionType, Type? comparerType) => (PropertiesConfigurationBuilder)base.HaveConversion(conversionType, comparerType); diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder.cs b/src/EFCore/Metadata/Builders/PropertyBuilder.cs index 7e8e2163397..a3ec58aba26 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder.cs @@ -439,7 +439,7 @@ public virtual PropertyBuilder UsePropertyAccessMode(PropertyAccessMode property /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion() => HasConversion(typeof(TConversion)); @@ -448,7 +448,7 @@ public virtual PropertyBuilder HasConversion() /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(Type? conversionType) { @@ -471,18 +471,14 @@ public virtual PropertyBuilder HasConversion(Type? conversionType) /// The converter to use. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(ValueConverter? converter) - { - Builder.HasConversion(converter, ConfigurationSource.Explicit); - - return this; - } + => HasConversion(converter, null, null); /// /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// /// The comparer to use for values before conversion. - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(ValueComparer? valueComparer) => HasConversion(typeof(TConversion), valueComparer); @@ -491,10 +487,32 @@ public virtual PropertyBuilder HasConversion(ValueComparer? valueCo /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The type to convert to and from or a type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparer) + => HasConversion(typeof(TConversion), valueComparer, providerComparer); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(Type conversionType, ValueComparer? valueComparer) + => HasConversion(conversionType, valueComparer, null); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion(Type conversionType, ValueComparer? valueComparer, ValueComparer? providerComparer) { Check.NotNull(conversionType, nameof(conversionType)); @@ -508,6 +526,7 @@ public virtual PropertyBuilder HasConversion(Type conversionType, ValueComparer? } Builder.HasValueComparer(valueComparer, ConfigurationSource.Explicit); + Builder.HasProviderValueComparer(providerComparer, ConfigurationSource.Explicit); return this; } @@ -520,9 +539,21 @@ public virtual PropertyBuilder HasConversion(Type conversionType, ValueComparer? /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer) + => HasConversion(converter, valueComparer, null); + + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// The converter to use. + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer, ValueComparer? providerComparer) { Builder.HasConversion(converter, ConfigurationSource.Explicit); Builder.HasValueComparer(valueComparer, ConfigurationSource.Explicit); + Builder.HasProviderValueComparer(providerComparer, ConfigurationSource.Explicit); return this; } @@ -531,8 +562,8 @@ public virtual PropertyBuilder HasConversion(ValueConverter? converter, ValueCom /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion() where TComparer : ValueComparer @@ -542,10 +573,34 @@ public virtual PropertyBuilder HasConversion() /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion() + where TComparer : ValueComparer + where TProviderComparer : ValueComparer + => HasConversion(typeof(TConversion), typeof(TComparer), typeof(TProviderComparer)); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion(Type conversionType, Type? comparerType) + => HasConversion(conversionType, comparerType, null); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion(Type conversionType, Type? comparerType, Type? providerComparerType) { Check.NotNull(conversionType, nameof(conversionType)); @@ -559,6 +614,7 @@ public virtual PropertyBuilder HasConversion(Type conversionType, Type? comparer } Builder.HasValueComparer(comparerType, ConfigurationSource.Explicit); + Builder.HasProviderValueComparer(providerComparerType, ConfigurationSource.Explicit); return this; } diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs index 01fff1bf5fb..44485ab0741 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs @@ -337,7 +337,7 @@ public PropertyBuilder(IMutableProperty property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion() => (PropertyBuilder)base.HasConversion(); @@ -346,7 +346,7 @@ public PropertyBuilder(IMutableProperty property) /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion(Type? providerClrType) => (PropertyBuilder)base.HasConversion(providerClrType); @@ -390,7 +390,7 @@ public virtual PropertyBuilder HasConversion(ValueConverte /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion(ValueComparer? valueComparer) @@ -400,7 +400,18 @@ public virtual PropertyBuilder HasConversion(ValueConverte /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . + /// The type to convert to and from or a type that inherits from . + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparer) + => (PropertyBuilder)base.HasConversion(valueComparer, providerComparer); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( @@ -408,6 +419,20 @@ public virtual PropertyBuilder HasConversion(ValueConverte ValueComparer? valueComparer) => (PropertyBuilder)base.HasConversion(conversionType, valueComparer); + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertyBuilder HasConversion( + Type conversionType, + ValueComparer? valueComparer, + ValueComparer? providerComparer) + => (PropertyBuilder)base.HasConversion(conversionType, valueComparer, providerComparer); + /// /// Configures the property so that the property value is converted to and from the database /// using the given conversion expressions. @@ -427,6 +452,28 @@ public virtual PropertyBuilder HasConversion( Check.NotNull(convertFromProviderExpression, nameof(convertFromProviderExpression))), valueComparer); + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given conversion expressions. + /// + /// The store type generated by the conversions. + /// An expression to convert objects when writing data to the store. + /// An expression to convert objects when reading data from the store. + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer, + ValueComparer? providerComparer) + => HasConversion( + new ValueConverter( + Check.NotNull(convertToProviderExpression, nameof(convertToProviderExpression)), + Check.NotNull(convertFromProviderExpression, nameof(convertFromProviderExpression))), + valueComparer, + providerComparer); + /// /// Configures the property so that the property value is converted to and from the database /// using the given . @@ -440,6 +487,21 @@ public virtual PropertyBuilder HasConversion( ValueComparer? valueComparer) => HasConversion((ValueConverter?)converter, valueComparer); + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// The store type generated by the converter. + /// The converter to use. + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertyBuilder HasConversion( + ValueConverter? converter, + ValueComparer? valueComparer, + ValueComparer? providerComparer) + => HasConversion((ValueConverter?)converter, valueComparer, providerComparer); + /// /// Configures the property so that the property value is converted to and from the database /// using the given . @@ -452,12 +514,26 @@ public virtual PropertyBuilder HasConversion( ValueComparer? valueComparer) => (PropertyBuilder)base.HasConversion(converter, valueComparer); + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// The converter to use. + /// The comparer to use for values before conversion. + /// The comparer to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertyBuilder HasConversion( + ValueConverter? converter, + ValueComparer? valueComparer, + ValueComparer? providerComparer) + => (PropertyBuilder)base.HasConversion(converter, valueComparer, providerComparer); + /// /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion() where TComparer : ValueComparer @@ -467,9 +543,33 @@ public virtual PropertyBuilder HasConversion( /// Configures the property so that the property value is converted before /// writing to the database and converted back when reading from the database. /// - /// The type to convert to and from or a type that derives from . - /// A type that derives from . + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertyBuilder HasConversion() + where TComparer : ValueComparer + where TProviderComparer : ValueComparer + => (PropertyBuilder)base.HasConversion(); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion(Type conversionType, Type? comparerType) => (PropertyBuilder)base.HasConversion(conversionType, comparerType); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// A type that inherits from to use for the provider values. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertyBuilder HasConversion(Type conversionType, Type? comparerType, Type? providerComparerType) + => (PropertyBuilder)base.HasConversion(conversionType, comparerType, providerComparerType); } diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 93fdd6ccf8e..4755df168fc 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -348,6 +348,7 @@ private static RuntimeProperty Create(IProperty property, RuntimeEntityType runt property.GetValueConverter(), property.GetValueComparer(), property.GetKeyValueComparer(), + property.GetProviderValueComparer(), property.GetTypeMapping()); /// diff --git a/src/EFCore/Metadata/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index db0d4e0e7ad..60258e72781 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -910,7 +910,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// to find a navigation property. /// /// The property name. - /// The property, or if none is found. + /// The property. new IConventionProperty GetProperty(string name) => (IConventionProperty)((IReadOnlyEntityType)this).GetProperty(name); diff --git a/src/EFCore/Metadata/IConventionProperty.cs b/src/EFCore/Metadata/IConventionProperty.cs index 4e701938efe..90e00d0c2b7 100644 --- a/src/EFCore/Metadata/IConventionProperty.cs +++ b/src/EFCore/Metadata/IConventionProperty.cs @@ -338,7 +338,7 @@ bool IsImplicitlyCreated() /// Sets the custom for this property. /// /// - /// A type that derives from , or to remove any previously set converter. + /// A type that inherits from , or to remove any previously set converter. /// /// Indicates whether the configuration was specified using a data annotation. /// The configured value. @@ -376,7 +376,7 @@ bool IsImplicitlyCreated() /// Sets the custom for this property. /// /// - /// A type that derives from , or to remove any previously set comparer. + /// A type that inherits from , or to remove any previously set comparer. /// /// Indicates whether the configuration was specified using a data annotation. /// The configured value. @@ -387,4 +387,28 @@ bool IsImplicitlyCreated() /// /// The configuration source for . ConfigurationSource? GetValueComparerConfigurationSource(); + + /// + /// Sets the custom to use for the provider values for this property. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + ValueComparer? SetProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Sets the custom to use for the provider values for this property. + /// + /// + /// A type that inherits from , or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + Type? SetProviderValueComparer(Type? comparerType, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetProviderValueComparerConfigurationSource(); } diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 65f21399a57..23a38ca8a8a 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -458,7 +458,7 @@ public interface IEntityType : IReadOnlyEntityType, ITypeBase /// to find a navigation property. /// /// The property name. - /// The property, or if none is found. + /// The property. new IProperty GetProperty(string name) => (IProperty)((IReadOnlyEntityType)this).GetProperty(name); diff --git a/src/EFCore/Metadata/IMutableEntityType.cs b/src/EFCore/Metadata/IMutableEntityType.cs index 8a0a614cb22..3b1b160c5f0 100644 --- a/src/EFCore/Metadata/IMutableEntityType.cs +++ b/src/EFCore/Metadata/IMutableEntityType.cs @@ -731,7 +731,7 @@ IMutableIndex AddIndex(IMutableProperty property, string name) /// to find a navigation property. /// /// The property name. - /// The property, or if none is found. + /// The property. new IMutableProperty GetProperty(string name) => (IMutableProperty)((IReadOnlyEntityType)this).GetProperty(name); diff --git a/src/EFCore/Metadata/IMutableProperty.cs b/src/EFCore/Metadata/IMutableProperty.cs index 6377e2dfc9d..a253dcf74c7 100644 --- a/src/EFCore/Metadata/IMutableProperty.cs +++ b/src/EFCore/Metadata/IMutableProperty.cs @@ -235,4 +235,18 @@ public interface IMutableProperty : IReadOnlyProperty, IMutablePropertyBase /// A type that derives from , or to remove any previously set comparer. /// void SetValueComparer(Type? comparerType); + + /// + /// Sets the custom to use for the provider values for this property. + /// + /// The comparer, or to remove any previously set comparer. + void SetProviderValueComparer(ValueComparer? comparer); + + /// + /// Sets the custom to use for the provider values for this property. + /// + /// + /// A type that derives from , or to remove any previously set comparer. + /// + void SetProviderValueComparer(Type? comparerType); } diff --git a/src/EFCore/Metadata/IProperty.cs b/src/EFCore/Metadata/IProperty.cs index 47aa28b8049..6e6edd84b1e 100644 --- a/src/EFCore/Metadata/IProperty.cs +++ b/src/EFCore/Metadata/IProperty.cs @@ -84,13 +84,17 @@ IEqualityComparer CreateKeyEqualityComparer() /// Gets the for this property. /// /// The comparer. - [DebuggerStepThrough] new ValueComparer GetValueComparer(); /// /// Gets the to use with keys for this property. /// /// The comparer. - [DebuggerStepThrough] new ValueComparer GetKeyValueComparer(); + + /// + /// Gets the to use for the provider values for this property. + /// + /// The comparer. + new ValueComparer GetProviderValueComparer(); } diff --git a/src/EFCore/Metadata/IReadOnlyEntityType.cs b/src/EFCore/Metadata/IReadOnlyEntityType.cs index 607b09a60d7..f3e53bfee95 100644 --- a/src/EFCore/Metadata/IReadOnlyEntityType.cs +++ b/src/EFCore/Metadata/IReadOnlyEntityType.cs @@ -638,7 +638,7 @@ bool IsInOwnershipPath(IReadOnlyEntityType targetType) /// to find a navigation property. /// /// The property name. - /// The property, or if none is found. + /// The property. IReadOnlyProperty GetProperty(string name) { Check.NotEmpty(name, nameof(name)); diff --git a/src/EFCore/Metadata/IReadOnlyProperty.cs b/src/EFCore/Metadata/IReadOnlyProperty.cs index 2ea51499b81..5aa4c42f8fd 100644 --- a/src/EFCore/Metadata/IReadOnlyProperty.cs +++ b/src/EFCore/Metadata/IReadOnlyProperty.cs @@ -155,6 +155,12 @@ CoreTypeMapping GetTypeMapping() /// The comparer, or if none has been set. ValueComparer? GetKeyValueComparer(); + /// + /// Gets the to use for the provider values for this property. + /// + /// The comparer, or if none has been set. + ValueComparer? GetProviderValueComparer(); + /// /// Finds the first principal property that the given property is constrained by /// if the given property is part of a foreign key. diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index ff82e0e45f4..1bf90857a09 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -139,6 +139,22 @@ public static class CoreAnnotationNames /// public const string ValueComparerType = "ValueComparerType"; + /// + /// 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 const string ProviderValueComparer = "ProviderValueComparer"; + + /// + /// 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 const string ProviderValueComparerType = "ProviderValueComparerType"; + /// /// 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 @@ -300,6 +316,8 @@ public static class CoreAnnotationNames ValueConverterType, ValueComparer, ValueComparerType, + ProviderValueComparer, + ProviderValueComparerType, AfterSaveBehavior, BeforeSaveBehavior, QueryFilter, diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 755e20eda87..017b116a1b1 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -647,10 +647,56 @@ public virtual bool CanSetValueComparer(Type? comparerType, ConfigurationSource? /// 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 InternalPropertyBuilder? HasKeyValueComparer( + public virtual InternalPropertyBuilder? HasProviderValueComparer( ValueComparer? comparer, ConfigurationSource configurationSource) - => HasValueComparer(comparer, configurationSource); + { + if (CanSetProviderValueComparer(comparer, configurationSource)) + { + Metadata.SetProviderValueComparer(comparer, configurationSource); + + return this; + } + + return null; + } + + /// + /// 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 bool CanSetProviderValueComparer(ValueComparer? comparer, ConfigurationSource? configurationSource) + { + if (configurationSource.Overrides(Metadata.GetProviderValueComparerConfigurationSource())) + { + return true; + } + + return Metadata[CoreAnnotationNames.ProviderValueComparerType] == null + && Metadata[CoreAnnotationNames.ProviderValueComparer] == comparer; + } + + /// + /// 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 InternalPropertyBuilder? HasProviderValueComparer( + Type? comparerType, + ConfigurationSource configurationSource) + { + if (CanSetProviderValueComparer(comparerType, configurationSource)) + { + Metadata.SetProviderValueComparer(comparerType, configurationSource); + + return this; + } + + return null; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -658,8 +704,10 @@ public virtual bool CanSetValueComparer(Type? comparerType, ConfigurationSource? /// 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 bool CanSetKeyValueComparer(ValueComparer? comparer, ConfigurationSource? configurationSource) - => CanSetValueComparer(comparer, configurationSource); + public virtual bool CanSetProviderValueComparer(Type? comparerType, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetProviderValueComparerConfigurationSource()) + || (Metadata[CoreAnnotationNames.ProviderValueComparer] == null + && (Type?)Metadata[CoreAnnotationNames.ProviderValueComparerType] == comparerType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1198,4 +1246,40 @@ bool IConventionPropertyBuilder.CanSetValueComparer(ValueComparer? comparer, boo /// bool IConventionPropertyBuilder.CanSetValueComparer(Type? comparerType, bool fromDataAnnotation) => CanSetValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionPropertyBuilder? IConventionPropertyBuilder.HasProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => HasProviderValueComparer(comparer, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionPropertyBuilder.CanSetProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => CanSetProviderValueComparer(comparer, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionPropertyBuilder? IConventionPropertyBuilder.HasProviderValueComparer(Type? comparerType, bool fromDataAnnotation) + => HasProviderValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionPropertyBuilder.CanSetProviderValueComparer(Type? comparerType, bool fromDataAnnotation) + => CanSetProviderValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 28396b2d384..3679a32197b 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -710,6 +710,10 @@ public virtual PropertySaveBehavior GetAfterSaveBehavior() public virtual ConfigurationSource? GetProviderClrTypeConfigurationSource() => FindAnnotation(CoreAnnotationNames.ProviderClrType)?.GetConfigurationSource(); + private Type GetEffectiveProviderClrType() + => TypeMapping?.Converter?.ProviderClrType + ?? ClrType.UnwrapNullableType(); + /// /// 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 @@ -814,6 +818,42 @@ public virtual CoreTypeMapping? TypeMapping => ToNullableComparer(GetValueComparer(null) ?? TypeMapping?.Comparer); + private ValueComparer? GetValueComparer(HashSet? checkedProperties) + { + var comparer = (ValueComparer?)this[CoreAnnotationNames.ValueComparer]; + if (comparer != null) + { + return comparer; + } + + var principal = (Property?)FindFirstDifferentPrincipal(); + if (principal == null) + { + return null; + } + + if (checkedProperties == null) + { + checkedProperties = new HashSet(); + } + else if (checkedProperties.Contains(this)) + { + return null; + } + + checkedProperties.Add(this); + return principal.GetValueComparer(checkedProperties); + } + + /// + /// 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 ConfigurationSource? GetValueComparerConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ValueComparer)?.GetConfigurationSource(); + /// /// 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 @@ -824,6 +864,99 @@ public virtual CoreTypeMapping? TypeMapping => ToNullableComparer(GetValueComparer(null) ?? TypeMapping?.KeyComparer); + /// + /// 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 ValueComparer? SetProviderValueComparer(ValueComparer? comparer, ConfigurationSource configurationSource) + { + RemoveAnnotation(CoreAnnotationNames.ProviderValueComparerType); + return (ValueComparer?)SetOrRemoveAnnotation(CoreAnnotationNames.ProviderValueComparer, comparer, configurationSource)?.Value; + } + + /// + /// 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 Type? SetProviderValueComparer(Type? comparerType, ConfigurationSource configurationSource) + { + ValueComparer? comparer = null; + if (comparerType != null) + { + if (!typeof(ValueComparer).IsAssignableFrom(comparerType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueComparerType(comparerType.ShortDisplayName(), typeof(ValueComparer).ShortDisplayName())); + } + + try + { + comparer = (ValueComparer?)Activator.CreateInstance(comparerType); + } + catch (Exception e) + { + throw new InvalidOperationException( + CoreStrings.CannotCreateValueComparer( + comparerType.ShortDisplayName(), nameof(PropertyBuilder.HasConversion)), e); + } + } + + SetProviderValueComparer(comparer, configurationSource); + return (Type?)SetOrRemoveAnnotation(CoreAnnotationNames.ProviderValueComparerType, comparerType, configurationSource)?.Value; + } + + /// + /// 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 ValueComparer? GetProviderValueComparer() + => GetProviderValueComparer(null) ?? (GetEffectiveProviderClrType() == ClrType + ? GetKeyValueComparer() + : TypeMapping?.ProviderValueComparer); + + private ValueComparer? GetProviderValueComparer(HashSet? checkedProperties) + { + var comparer = (ValueComparer?)this[CoreAnnotationNames.ProviderValueComparer]; + if (comparer != null) + { + return comparer; + } + + var principal = (Property?)FindFirstDifferentPrincipal(); + if (principal == null + || principal.GetEffectiveProviderClrType() != GetEffectiveProviderClrType()) + { + return null; + } + + if (checkedProperties == null) + { + checkedProperties = new HashSet(); + } + else if (checkedProperties.Contains(this)) + { + return null; + } + + checkedProperties.Add(this); + return principal.GetProviderValueComparer(checkedProperties); + } + + /// + /// 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 ConfigurationSource? GetProviderValueComparerConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ProviderValueComparer)?.GetConfigurationSource(); + private ValueComparer? ToNullableComparer(ValueComparer? valueComparer) { if (valueComparer == null @@ -877,50 +1010,14 @@ public virtual CoreTypeMapping? TypeMapping Expression.Default(ClrType)), newSnapshotParam))!; } - - private ValueComparer? GetValueComparer(HashSet? checkedProperties) - { - var comparer = (ValueComparer?)this[CoreAnnotationNames.ValueComparer]; - if (comparer != null) - { - return comparer; - } - - var principal = ((Property?)FindFirstDifferentPrincipal()); - if (principal == null) - { - return null; - } - - if (checkedProperties == null) - { - checkedProperties = new HashSet(); - } - else if (checkedProperties.Contains(this)) - { - return null; - } - - checkedProperties.Add(this); - return principal.GetValueComparer(checkedProperties); - } - + private IProperty? FindFirstDifferentPrincipal() { var principal = ((IProperty)this).FindFirstPrincipal(); return principal != this ? principal : null; } - - /// - /// 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 ConfigurationSource? GetValueComparerConfigurationSource() - => FindAnnotation(CoreAnnotationNames.ValueComparer)?.GetConfigurationSource(); - + /// /// 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 @@ -1655,4 +1752,58 @@ ValueComparer IProperty.GetValueComparer() /// ValueComparer IProperty.GetKeyValueComparer() => GetKeyValueComparer()!; + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableProperty.SetProviderValueComparer(ValueComparer? comparer) + => SetProviderValueComparer(comparer, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + ValueComparer? IConventionProperty.SetProviderValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => SetProviderValueComparer( + comparer, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableProperty.SetProviderValueComparer(Type? comparerType) + => SetProviderValueComparer(comparerType, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + Type? IConventionProperty.SetProviderValueComparer(Type? comparerType, bool fromDataAnnotation) + => SetProviderValueComparer( + comparerType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + ValueComparer IProperty.GetProviderValueComparer() + => GetProviderValueComparer()!; } diff --git a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs index 7abe702985a..106bd655913 100644 --- a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs +++ b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs @@ -77,6 +77,13 @@ public virtual void Apply(IMutableProperty property) property.SetValueComparer((Type?)annotation.Value); } + break; + case CoreAnnotationNames.ProviderValueComparerType: + if (ClrType.UnwrapNullableType() == property.ClrType.UnwrapNullableType()) + { + property.SetProviderValueComparer((Type?)annotation.Value); + } + break; default: if (!CoreAnnotationNames.AllNames.Contains(annotation.Name)) @@ -259,4 +266,24 @@ public virtual void SetValueComparer(Type? comparerType) this[CoreAnnotationNames.ValueComparerType] = comparerType; } + + /// + /// 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 void SetProviderValueComparer(Type? comparerType) + { + if (comparerType != null) + { + if (!typeof(ValueComparer).IsAssignableFrom(comparerType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueComparerType(comparerType.ShortDisplayName(), typeof(ValueComparer).ShortDisplayName())); + } + } + + this[CoreAnnotationNames.ProviderValueComparerType] = comparerType; + } } diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 021fdad507a..ada954cb6bd 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -583,6 +583,7 @@ private IEnumerable GetIndexes() /// The custom set for this property. /// The for this property. /// The to use with keys for this property. + /// The to use for the provider values for this property. /// The for this property. /// The newly created property. public virtual RuntimeProperty AddProperty( @@ -605,6 +606,7 @@ public virtual RuntimeProperty AddProperty( ValueConverter? valueConverter = null, ValueComparer? valueComparer = null, ValueComparer? keyValueComparer = null, + ValueComparer? providerValueComparer = null, CoreTypeMapping? typeMapping = null) { var property = new RuntimeProperty( @@ -628,6 +630,7 @@ public virtual RuntimeProperty AddProperty( valueConverter, valueComparer, keyValueComparer, + providerValueComparer, typeMapping); _properties.Add(property.Name, property); diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index 9bcc9281e36..94ffdcd29ed 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -23,6 +23,7 @@ public class RuntimeProperty : RuntimePropertyBase, IProperty private readonly ValueConverter? _valueConverter; private readonly ValueComparer? _valueComparer; private readonly ValueComparer? _keyValueComparer; + private readonly ValueComparer? _providerValueComparer; private CoreTypeMapping? _typeMapping; /// @@ -53,6 +54,7 @@ public RuntimeProperty( ValueConverter? valueConverter, ValueComparer? valueComparer, ValueComparer? keyValueComparer, + ValueComparer? providerValueComparer, CoreTypeMapping? typeMapping) : base(name, propertyInfo, fieldInfo, propertyAccessMode) { @@ -94,6 +96,7 @@ public RuntimeProperty( _typeMapping = typeMapping; _valueComparer = valueComparer; _keyValueComparer = keyValueComparer ?? valueComparer; + _providerValueComparer = providerValueComparer; } /// @@ -288,6 +291,16 @@ ValueComparer IProperty.GetValueComparer() ValueComparer IProperty.GetKeyValueComparer() => _keyValueComparer ?? TypeMapping.KeyComparer; + /// + [DebuggerStepThrough] + ValueComparer? IReadOnlyProperty.GetProviderValueComparer() + => _providerValueComparer ?? TypeMapping.ProviderValueComparer; + + /// + [DebuggerStepThrough] + ValueComparer IProperty.GetProviderValueComparer() + => _providerValueComparer ?? TypeMapping.ProviderValueComparer; + /// [DebuggerStepThrough] bool IReadOnlyProperty.IsForeignKey() diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index 0e9b99ac0c1..d80348b4984 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -23,7 +23,7 @@ public abstract class CoreTypeMapping /// /// Parameter object for use in the hierarchy. /// - protected readonly struct CoreTypeMappingParameters + protected readonly record struct CoreTypeMappingParameters { /// /// Creates a new parameter object. @@ -32,40 +32,48 @@ protected readonly struct CoreTypeMappingParameters /// Converts types to and from the store whenever this mapping is used. /// Supports custom value snapshotting and comparisons. /// Supports custom comparisons between keys--e.g. PK to FK comparison. + /// Supports custom comparisons between converted provider values. /// An optional factory for creating a specific . public CoreTypeMappingParameters( Type clrType, ValueConverter? converter = null, ValueComparer? comparer = null, ValueComparer? keyComparer = null, + ValueComparer? providerValueComparer = null, Func? valueGeneratorFactory = null) { ClrType = clrType; Converter = converter; Comparer = comparer; KeyComparer = keyComparer; + ProviderValueComparer = providerValueComparer; ValueGeneratorFactory = valueGeneratorFactory; } /// /// The mapping CLR type. /// - public Type ClrType { get; } + public Type ClrType { get; init; } /// /// The mapping converter. /// - public ValueConverter? Converter { get; } + public ValueConverter? Converter { get; init; } /// /// The mapping comparer. /// - public ValueComparer? Comparer { get; } + public ValueComparer? Comparer { get; init; } /// /// The mapping key comparer. /// - public ValueComparer? KeyComparer { get; } + public ValueComparer? KeyComparer { get; init; } + + /// + /// The provider comparer. + /// + public ValueComparer? ProviderValueComparer { get; init; } /// /// An optional factory for creating a specific to use with @@ -85,11 +93,13 @@ public CoreTypeMappingParameters WithComposedConverter(ValueConverter? converter converter == null ? Converter : converter.ComposeWith(Converter), Comparer, KeyComparer, + ProviderValueComparer, ValueGeneratorFactory); } private ValueComparer? _comparer; private ValueComparer? _keyComparer; + private ValueComparer? _providerValueComparer; /// /// Initializes a new instance of the class. @@ -106,10 +116,9 @@ protected CoreTypeMapping(CoreTypeMappingParameters parameters) Check.DebugAssert( parameters.Comparer == null - || parameters.ClrType == null || converter != null - || parameters.Comparer.Type == parameters.ClrType, - $"Expected {parameters.ClrType}, got {parameters.Comparer?.Type}"); + || parameters.Comparer.Type == clrType, + $"Expected {clrType}, got {parameters.Comparer?.Type}"); if (parameters.Comparer?.Type == clrType) { _comparer = parameters.Comparer; @@ -117,7 +126,6 @@ protected CoreTypeMapping(CoreTypeMappingParameters parameters) Check.DebugAssert( parameters.KeyComparer == null - || parameters.ClrType == null || converter != null || parameters.KeyComparer.Type == parameters.ClrType, $"Expected {parameters.ClrType}, got {parameters.KeyComparer?.Type}"); @@ -126,6 +134,15 @@ protected CoreTypeMapping(CoreTypeMappingParameters parameters) _keyComparer = parameters.KeyComparer; } + Check.DebugAssert( + parameters.ProviderValueComparer == null + || parameters.ProviderValueComparer.Type == (converter?.ProviderClrType ?? clrType), + $"Expected {converter?.ProviderClrType ?? clrType}, got {parameters.ProviderValueComparer?.Type}"); + if (parameters.ProviderValueComparer?.Type == (converter?.ProviderClrType ?? clrType)) + { + _providerValueComparer = parameters.ProviderValueComparer; + } + ValueGeneratorFactory = parameters.ValueGeneratorFactory ?? converter?.MappingHints?.ValueGeneratorFactory; } @@ -174,6 +191,17 @@ public virtual ValueComparer KeyComparer this, static c => ValueComparer.CreateDefault(c.ClrType, favorStructuralComparisons: true)); + /// + /// A for the provider CLR type values. + /// + public virtual ValueComparer ProviderValueComparer + => NonCapturingLazyInitializer.EnsureInitialized( + ref _providerValueComparer, + this, + static c => (c.Converter?.ProviderClrType ?? c.ClrType) == c.ClrType + ? c.KeyComparer + : ValueComparer.CreateDefault(c.Converter?.ProviderClrType ?? c.ClrType, favorStructuralComparisons: true)); + /// /// Returns a new copy of this type mapping with the given /// added. diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs index c96217b5715..c486739e8cc 100644 --- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs +++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs @@ -48,7 +48,7 @@ public override void Properties_can_have_provider_type_set_for_type() b.Property(e => e.Down); b.Property("Charm"); b.Property("Strange"); - b.Property("__id").HasConversion((Type)null); + b.Property("__id").HasConversion(null); }); var model = modelBuilder.FinalizeModel(); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 03cdc62567c..89b102108f1 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -349,6 +349,29 @@ public override int GetHashCode(object instance) public override object Snapshot(object instance) => throw new NotImplementedException(); } + + [ConditionalFact] + public void Throws_for_provider_value_comparer() + => Test( + new ProviderValueComparerContext(), + new CompiledModelCodeGenerationOptions(), + expectedExceptionMessage: DesignStrings.CompiledModelValueComparer( + "MyEntity", "Id", nameof(PropertyBuilder.HasConversion))); + + public class ProviderValueComparerContext : ContextBase + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + "MyEntity", e => + { + e.Property("Id").HasConversion(typeof(int), null, new FakeValueComparer()); + e.HasKey("Id"); + }); + } + } [ConditionalFact] public void Throws_for_custom_type_mapping() @@ -367,7 +390,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity( "MyEntity", e => { - e.Property("Id").Metadata.SetTypeMapping(new InMemoryTypeMapping(typeof(int[]))); + e.Property("Id").Metadata.SetTypeMapping(new InMemoryTypeMapping(typeof(int))); e.HasKey("Id"); }); } @@ -1083,7 +1106,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw, valueConverter: new CastingConverter(), - valueComparer: new CSharpRuntimeModelCodeGeneratorTest.CustomValueComparer()); + valueComparer: new CSharpRuntimeModelCodeGeneratorTest.CustomValueComparer(), + providerValueComparer: new CSharpRuntimeModelCodeGeneratorTest.CustomValueComparer()); alternateId.AddAnnotation(""Relational:ColumnType"", ""geometry""); alternateId.AddAnnotation(""Relational:DefaultValue"", (NetTopologySuite.Geometries.Point)new NetTopologySuite.IO.WKTReader().Read(""SRID=0;POINT Z(0 0 0)"")); alternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1674,6 +1698,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.IsType>(principalAlternateId.GetValueConverter()); Assert.IsType>(principalAlternateId.GetValueComparer()); Assert.IsType>(principalAlternateId.GetKeyValueComparer()); + Assert.IsType>(principalAlternateId.GetProviderValueComparer()); Assert.Equal(SqlServerValueGenerationStrategy.None, principalAlternateId.GetValueGenerationStrategy()); Assert.Equal(PropertyAccessMode.FieldDuringConstruction, principalAlternateId.GetPropertyAccessMode()); Assert.Null(principalAlternateId[CoreAnnotationNames.PropertyAccessMode]); @@ -2049,7 +2074,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnType("geometry") .HasDefaultValue( NtsGeometryServices.Instance.CreateGeometryFactory(srid: 0).CreatePoint(new CoordinateZM(0, 0, 0, 0))) - .HasConversion, CustomValueComparer>(); + .HasConversion, CustomValueComparer, CustomValueComparer>(); eb.HasIndex(e => e.AlternateId, "AlternateIndex") .IsUnique() diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index d111d6e98e0..d7b88032f36 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -9842,6 +9842,44 @@ public void SeedData_binary_change() v => Assert.Equal(new byte[] { 2, 1 }, v)); })); + [ConditionalFact] + public void SeedData_binary_change_custom_comparer() + => Execute( + source => source.Entity( + "EntityWithTwoProperties", + x => + { + x.Property("Id"); + x.Property("Value1").HasConversion(typeof(byte[]), null, new RightmostValueComparer()); + }), + source => source.Entity( + "EntityWithTwoProperties", + x => + { + x.HasData( + new { Id = 42, Value1 = new byte[] { 0, 1 } }); + }), + target => target.Entity( + "EntityWithTwoProperties", + x => + { + x.HasData( + new { Id = 42, Value1 = new byte[] { 1 } }); + }), + upOps => Assert.Empty(upOps), + downOps => Assert.Empty(downOps)); + + private class RightmostValueComparer : ValueComparer + { + public RightmostValueComparer() + : base(false) + { + } + + public override bool Equals(byte[] left, byte[] right) + => object.Equals(left[^1], right[^1]); + } + [ConditionalFact] public void SeedData_binary_no_change() => Execute( diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs index 6f8d327a52b..dd1888c36ac 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs @@ -8,27 +8,39 @@ namespace Microsoft.EntityFrameworkCore.Storage; public abstract class RelationalTypeMappingTest { - protected class FakeValueConverter : ValueConverter + protected class FakeValueConverter : ValueConverter { public FakeValueConverter() - : base(_ => _, _ => _) + : base(_ => (TProvider)(object)_, _ => (TModel)(object)_) { } - public override Type ModelClrType { get; } = typeof(object); - public override Type ProviderClrType { get; } = typeof(object); + public override Type ModelClrType { get; } = typeof(TModel); + public override Type ProviderClrType { get; } = typeof(TProvider); } - protected class FakeValueComparer : ValueComparer + protected class FakeValueComparer : ValueComparer { public FakeValueComparer() : base(false) { } - public override Type Type { get; } = typeof(object); + public override Type Type { get; } = typeof(T); } + public static ValueConverter CreateConverter(Type modelType) + => (ValueConverter)Activator.CreateInstance( + typeof(FakeValueConverter<,>).MakeGenericType(modelType, typeof(object))); + + public static ValueConverter CreateConverter(Type modelType, Type providerType) + => (ValueConverter)Activator.CreateInstance( + typeof(FakeValueConverter<,>).MakeGenericType(modelType, providerType)); + + public static ValueComparer CreateComparer(Type type) + => (ValueComparer)Activator.CreateInstance( + typeof(FakeValueComparer<>).MakeGenericType(type)); + [ConditionalTheory] [InlineData(typeof(BoolTypeMapping), typeof(bool))] [InlineData(typeof(ByteTypeMapping), typeof(byte))] @@ -68,10 +80,10 @@ public virtual void Create_and_clone_with_converter(Type mappingType, Type type) Assert.Same(mapping.Converter, clone.Converter); Assert.Same(mapping.Comparer, clone.Comparer); Assert.Same(mapping.KeyComparer, clone.KeyComparer); - Assert.Same(typeof(object), clone.ClrType); + Assert.Same(type, clone.ClrType); Assert.Equal(StoreTypePostfix.PrecisionAndScale, clone.StoreTypePostfix); - var newConverter = new FakeValueConverter(); + var newConverter = CreateConverter(typeof(object), type); clone = (RelationalTypeMapping)mapping.Clone(newConverter); Assert.NotSame(mapping, clone); @@ -80,8 +92,9 @@ public virtual void Create_and_clone_with_converter(Type mappingType, Type type) Assert.Equal(DbType.VarNumeric, clone.DbType); Assert.Null(clone.Size); Assert.NotSame(mapping.Converter, clone.Converter); - Assert.Same(mapping.Comparer, clone.Comparer); - Assert.Same(mapping.KeyComparer, clone.KeyComparer); + Assert.NotSame(mapping.Comparer, clone.Comparer); + Assert.NotSame(mapping.KeyComparer, clone.KeyComparer); + Assert.Same(mapping.ProviderValueComparer, clone.ProviderValueComparer); Assert.Same(typeof(object), clone.ClrType); Assert.Equal(StoreTypePostfix.PrecisionAndScale, clone.StoreTypePostfix); } @@ -123,12 +136,13 @@ protected virtual void ConversionCloneTest( Assert.Same(mapping.Converter, clone.Converter); Assert.Same(mapping.Comparer, clone.Comparer); Assert.Same(mapping.KeyComparer, clone.KeyComparer); - Assert.Same(typeof(object), clone.ClrType); + Assert.Same(mapping.ProviderValueComparer, clone.ProviderValueComparer); + Assert.Same(type, clone.ClrType); Assert.True(mapping.IsFixedLength); Assert.True(clone.IsFixedLength); Assert.Equal(StoreTypePostfix.Size, clone.StoreTypePostfix); - var newConverter = new FakeValueConverter(); + var newConverter = CreateConverter(typeof(object), type); clone = (RelationalTypeMapping)mapping.Clone(newConverter); Assert.NotSame(mapping, clone); @@ -139,8 +153,9 @@ protected virtual void ConversionCloneTest( Assert.Equal(33, mapping.Size); Assert.Equal(33, clone.Size); Assert.NotSame(mapping.Converter, clone.Converter); - Assert.Same(mapping.Comparer, clone.Comparer); - Assert.Same(mapping.KeyComparer, clone.KeyComparer); + Assert.NotSame(mapping.Comparer, clone.Comparer); + Assert.NotSame(mapping.KeyComparer, clone.KeyComparer); + Assert.Same(mapping.ProviderValueComparer, clone.ProviderValueComparer); Assert.Same(typeof(object), clone.ClrType); Assert.True(mapping.IsFixedLength); Assert.True(clone.IsFixedLength); @@ -187,12 +202,13 @@ protected virtual void UnicodeConversionCloneTest( Assert.Same(mapping.Converter, clone.Converter); Assert.Same(mapping.Comparer, clone.Comparer); Assert.Same(mapping.KeyComparer, clone.KeyComparer); - Assert.Same(typeof(object), clone.ClrType); + Assert.Same(mapping.ProviderValueComparer, clone.ProviderValueComparer); + Assert.Same(type, clone.ClrType); Assert.True(mapping.IsFixedLength); Assert.True(clone.IsFixedLength); Assert.Equal(StoreTypePostfix.Size, clone.StoreTypePostfix); - var newConverter = new FakeValueConverter(); + var newConverter = CreateConverter(typeof(object), type); clone = (RelationalTypeMapping)mapping.Clone(newConverter); Assert.NotSame(mapping, clone); @@ -205,8 +221,9 @@ protected virtual void UnicodeConversionCloneTest( Assert.False(mapping.IsUnicode); Assert.False(clone.IsUnicode); Assert.NotSame(mapping.Converter, clone.Converter); - Assert.Same(mapping.Comparer, clone.Comparer); - Assert.Same(mapping.KeyComparer, clone.KeyComparer); + Assert.NotSame(mapping.Comparer, clone.Comparer); + Assert.NotSame(mapping.KeyComparer, clone.KeyComparer); + Assert.Same(mapping.ProviderValueComparer, clone.ProviderValueComparer); Assert.Same(typeof(object), clone.ClrType); Assert.True(mapping.IsFixedLength); Assert.True(clone.IsFixedLength); @@ -234,9 +251,10 @@ public static object CreateParameters( => new RelationalTypeMappingParameters( new CoreTypeMappingParameters( type, - new FakeValueConverter(), - new FakeValueComparer(), - new FakeValueComparer()), + CreateConverter(type), + CreateComparer(type), + CreateComparer(type), + CreateComparer(typeof(object))), "", storeTypePostfix, System.Data.DbType.VarNumeric, diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index cd639c580e5..870a0086f1e 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -254,7 +254,7 @@ public Fuel(double volume) public double Volume { get; } } - [ConditionalFact(Skip = "Issue #27738")] + [ConditionalFact] public virtual void Can_insert_and_read_back_with_case_insensitive_string_key() { using (var context = CreateContext()) diff --git a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs index fe0cd97d6bb..4493b30ed04 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - - // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs index 44f0af461ff..8995c2a5a97 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs @@ -111,9 +111,9 @@ public virtual void Create_and_clone_UDT_mapping_with_converter() literalGenerator, StoreTypePostfix.None, "udtType", - new FakeValueConverter(), - new FakeValueComparer(), - new FakeValueComparer(), + CreateConverter(typeof(object)), + CreateComparer(typeof(object)), + CreateComparer(typeof(object)), DbType.VarNumeric, false, 33, @@ -141,7 +141,7 @@ public virtual void Create_and_clone_UDT_mapping_with_converter() Assert.True(clone.IsFixedLength); Assert.Same(literalGenerator, clone.LiteralGenerator); - var newConverter = new FakeValueConverter(); + var newConverter = CreateConverter(typeof(object)); clone = (SqlServerUdtTypeMapping)mapping.Clone(newConverter); Assert.NotSame(mapping, clone); diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 6d90df406fa..85830b7be1c 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -75,6 +75,9 @@ public ValueComparer GetValueComparer() public ValueComparer GetKeyValueComparer() => throw new NotImplementedException(); + public ValueComparer GetProviderValueComparer() + => throw new NotImplementedException(); + public bool IsForeignKey() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs index 582ed68105b..28597c008f2 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -92,6 +92,9 @@ public ValueComparer GetValueComparer() public ValueComparer GetKeyValueComparer() => throw new NotImplementedException(); + public ValueComparer GetProviderValueComparer() + => throw new NotImplementedException(); + public bool IsForeignKey() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Metadata/Internal/InternalPropertyBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalPropertyBuilderTest.cs index a1f1f31cc72..c626791aba2 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalPropertyBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalPropertyBuilderTest.cs @@ -395,6 +395,32 @@ public CustomValueComparer() } } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_ProviderValueComparer() + { + var builder = CreateInternalPropertyBuilder(); + var metadata = builder.Metadata; + + Assert.NotNull(builder.HasProviderValueComparer(new CustomValueComparer(), ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.HasProviderValueComparer(new ValueComparer(false), ConfigurationSource.DataAnnotation)); + + Assert.IsType>(metadata.GetProviderValueComparer()); + + Assert.Null(builder.HasProviderValueComparer(new CustomValueComparer(), ConfigurationSource.Convention)); + Assert.IsType>(metadata.GetProviderValueComparer()); + + Assert.Null(builder.HasProviderValueComparer(typeof(CustomValueComparer), ConfigurationSource.Convention)); + Assert.IsType>(metadata.GetProviderValueComparer()); + + Assert.NotNull(builder.HasProviderValueComparer(typeof(CustomValueComparer), ConfigurationSource.DataAnnotation)); + Assert.IsType>(metadata.GetProviderValueComparer()); + + Assert.NotNull(builder.HasProviderValueComparer((ValueComparer)null, ConfigurationSource.DataAnnotation)); + Assert.Null(metadata.GetProviderValueComparer()); + Assert.Null(metadata[CoreAnnotationNames.ProviderValueComparer]); + Assert.Null(metadata[CoreAnnotationNames.ProviderValueComparerType]); + } + [ConditionalFact] public void Can_only_override_lower_or_equal_source_IsUnicode() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs index 5950a49036a..8f2f402a127 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs @@ -14,6 +14,14 @@ protected override TestModelBuilder CreateTestModelBuilder( Action? configure) => new GenericTypeTestModelBuilder(testHelpers, configure); } + + public class GenericNonRelationshipTest : NonRelationshipTestBase + { + protected override TestModelBuilder CreateTestModelBuilder( + TestHelpers testHelpers, + Action? configure) + => new GenericTypeTestModelBuilder(testHelpers, configure); + } private class GenericTypeTestModelBuilder : TestModelBuilder { @@ -66,6 +74,9 @@ public GenericTypeTestEntityTypeBuilder(EntityTypeBuilder entityTypeBui protected override TestEntityTypeBuilder Wrap(EntityTypeBuilder entityTypeBuilder) => new GenericTypeTestEntityTypeBuilder(entityTypeBuilder); + protected override TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new GenericTypeTestPropertyBuilder(propertyBuilder); + public override TestOwnedNavigationBuilder OwnsOne( Expression> navigationExpression) where TRelatedEntity : class @@ -75,10 +86,9 @@ public override TestEntityTypeBuilder OwnsOne( Expression> navigationExpression, Action> buildAction) where TRelatedEntity : class - => Wrap( - EntityTypeBuilder.OwnsOne( - navigationExpression, - r => buildAction(new GenericTypeTestOwnedNavigationBuilder(r)))); + => Wrap(EntityTypeBuilder.OwnsOne( + navigationExpression, + r => buildAction(new GenericTypeTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( Expression>? navigationExpression = null) @@ -92,8 +102,33 @@ public override TestCollectionNavigationBuilder HasMany => new GenericTypeTestCollectionNavigationBuilder(EntityTypeBuilder.HasMany(navigationExpression)); } - private class GenericTypeTestReferenceNavigationBuilder : GenericTestReferenceNavigationBuilder + private class GenericTypeTestPropertyBuilder : GenericTestPropertyBuilder + { + public GenericTypeTestPropertyBuilder(PropertyBuilder propertyBuilder) + : base(propertyBuilder) + { + } + + protected override TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new GenericTypeTestPropertyBuilder(propertyBuilder); + + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion(typeof(TProvider))); + + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion(typeof(TProvider), valueComparer)); + + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(typeof(TProvider), valueComparer, providerComparerType)); + + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion(typeof(TConverter), typeof(TComparer))); + + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion(typeof(TConverter), typeof(TComparer), typeof(TProviderComparer))); + } + + private class GenericTypeTestReferenceNavigationBuilder : GenericTestReferenceNavigationBuilder where TEntity : class where TRelatedEntity : class { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index c7a2e0af068..4ec6e41d27b 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -169,6 +169,9 @@ public override IMutableEntityType Metadata protected virtual TestEntityTypeBuilder Wrap(EntityTypeBuilder entityTypeBuilder) => new GenericTestEntityTypeBuilder(entityTypeBuilder); + + protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new GenericTestPropertyBuilder(propertyBuilder); public override TestEntityTypeBuilder HasAnnotation(string annotation, object? value) => Wrap(EntityTypeBuilder.HasAnnotation(annotation, value)); @@ -196,13 +199,13 @@ public override TestEntityTypeBuilder HasNoKey() public override TestPropertyBuilder Property(Expression> propertyExpression) where TProperty : default - => new GenericTestPropertyBuilder(EntityTypeBuilder.Property(propertyExpression)); + => Wrap(EntityTypeBuilder.Property(propertyExpression)); public override TestPropertyBuilder Property(string propertyName) - => new GenericTestPropertyBuilder(EntityTypeBuilder.Property(propertyName)); + => Wrap(EntityTypeBuilder.Property(propertyName)); public override TestPropertyBuilder IndexerProperty(string propertyName) - => new GenericTestPropertyBuilder(EntityTypeBuilder.IndexerProperty(propertyName)); + => Wrap(EntityTypeBuilder.IndexerProperty(propertyName)); public override TestNavigationBuilder Navigation( Expression> navigationExpression) @@ -451,94 +454,130 @@ public GenericTestPropertyBuilder(PropertyBuilder propertyBuilder) PropertyBuilder = propertyBuilder; } - private PropertyBuilder PropertyBuilder { get; } + protected PropertyBuilder PropertyBuilder { get; } public override IMutableProperty Metadata => PropertyBuilder.Metadata; + protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new GenericTestPropertyBuilder(propertyBuilder); + public override TestPropertyBuilder HasAnnotation(string annotation, object? value) - => new GenericTestPropertyBuilder(PropertyBuilder.HasAnnotation(annotation, value)); + => Wrap(PropertyBuilder.HasAnnotation(annotation, value)); public override TestPropertyBuilder IsRequired(bool isRequired = true) - => new GenericTestPropertyBuilder(PropertyBuilder.IsRequired(isRequired)); + => Wrap(PropertyBuilder.IsRequired(isRequired)); public override TestPropertyBuilder HasMaxLength(int maxLength) - => new GenericTestPropertyBuilder(PropertyBuilder.HasMaxLength(maxLength)); + => Wrap(PropertyBuilder.HasMaxLength(maxLength)); public override TestPropertyBuilder HasPrecision(int precision, int scale) - => new GenericTestPropertyBuilder(PropertyBuilder.HasPrecision(precision, scale)); + => Wrap(PropertyBuilder.HasPrecision(precision, scale)); public override TestPropertyBuilder IsUnicode(bool unicode = true) - => new GenericTestPropertyBuilder(PropertyBuilder.IsUnicode(unicode)); + => Wrap(PropertyBuilder.IsUnicode(unicode)); public override TestPropertyBuilder IsRowVersion() - => new GenericTestPropertyBuilder(PropertyBuilder.IsRowVersion()); + => Wrap(PropertyBuilder.IsRowVersion()); public override TestPropertyBuilder IsConcurrencyToken(bool isConcurrencyToken = true) - => new GenericTestPropertyBuilder(PropertyBuilder.IsConcurrencyToken(isConcurrencyToken)); + => Wrap(PropertyBuilder.IsConcurrencyToken(isConcurrencyToken)); public override TestPropertyBuilder ValueGeneratedNever() - => new GenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedNever()); + => Wrap(PropertyBuilder.ValueGeneratedNever()); public override TestPropertyBuilder ValueGeneratedOnAdd() - => new GenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnAdd()); + => Wrap(PropertyBuilder.ValueGeneratedOnAdd()); public override TestPropertyBuilder ValueGeneratedOnAddOrUpdate() - => new GenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnAddOrUpdate()); + => Wrap(PropertyBuilder.ValueGeneratedOnAddOrUpdate()); public override TestPropertyBuilder ValueGeneratedOnUpdate() - => new GenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnUpdate()); + => Wrap(PropertyBuilder.ValueGeneratedOnUpdate()); public override TestPropertyBuilder HasValueGenerator() - => new GenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator()); + => Wrap(PropertyBuilder.HasValueGenerator()); public override TestPropertyBuilder HasValueGenerator(Type valueGeneratorType) - => new GenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator(valueGeneratorType)); + => Wrap(PropertyBuilder.HasValueGenerator(valueGeneratorType)); public override TestPropertyBuilder HasValueGenerator( Func factory) - => new GenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator(factory)); + => Wrap(PropertyBuilder.HasValueGenerator(factory)); public override TestPropertyBuilder HasValueGeneratorFactory() - => new GenericTestPropertyBuilder(PropertyBuilder.HasValueGeneratorFactory()); + => Wrap(PropertyBuilder.HasValueGeneratorFactory()); public override TestPropertyBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) - => new GenericTestPropertyBuilder(PropertyBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + => Wrap(PropertyBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); public override TestPropertyBuilder HasField(string fieldName) - => new GenericTestPropertyBuilder(PropertyBuilder.HasField(fieldName)); + => Wrap(PropertyBuilder.HasField(fieldName)); public override TestPropertyBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) - => new GenericTestPropertyBuilder(PropertyBuilder.UsePropertyAccessMode(propertyAccessMode)); + => Wrap(PropertyBuilder.UsePropertyAccessMode(propertyAccessMode)); public override TestPropertyBuilder HasConversion() - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion()); + => Wrap(PropertyBuilder.HasConversion()); + + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion(valueComparer)); - public override TestPropertyBuilder HasConversion(Type? providerClrType) - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion(providerClrType)); + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(valueComparer, providerComparerType)); public override TestPropertyBuilder HasConversion( Expression> convertToProviderExpression, Expression> convertFromProviderExpression) - => new GenericTestPropertyBuilder( - PropertyBuilder.HasConversion( - convertToProviderExpression, - convertFromProviderExpression)); + => Wrap(PropertyBuilder.HasConversion( + convertToProviderExpression, + convertFromProviderExpression)); + + public override TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion( + convertToProviderExpression, + convertFromProviderExpression, + valueComparer)); + + public override TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer, + ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion( + convertToProviderExpression, + convertFromProviderExpression, + valueComparer, + providerComparerType)); public override TestPropertyBuilder HasConversion(ValueConverter converter) - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter)); + => Wrap(PropertyBuilder.HasConversion(converter)); + + public override TestPropertyBuilder HasConversion(ValueConverter converter, + ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer)); + + public override TestPropertyBuilder HasConversion(ValueConverter converter, + ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer, providerComparerType)); public override TestPropertyBuilder HasConversion(ValueConverter? converter) - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter)); + => Wrap(PropertyBuilder.HasConversion(converter)); public override TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer) - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter, valueComparer)); + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer)); - public override TestPropertyBuilder HasConversion() - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion()); + public override TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer, providerComparerType)); - public override TestPropertyBuilder HasConversion(Type converterType, Type? comparerType) - => new GenericTestPropertyBuilder(PropertyBuilder.HasConversion(converterType, comparerType)); + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion()); + + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion()); PropertyBuilder IInfrastructure>.Instance => PropertyBuilder; @@ -1026,6 +1065,9 @@ protected virtual GenericTestOwnedNavigationBuilder new(ownershipBuilder); + protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new GenericTestPropertyBuilder(propertyBuilder); + public override TestOwnedNavigationBuilder HasAnnotation( string annotation, object? value) @@ -1038,14 +1080,14 @@ public override TestKeyBuilder HasKey(params string[] property => new GenericTestKeyBuilder(OwnedNavigationBuilder.HasKey(propertyNames)); public override TestPropertyBuilder Property(string propertyName) - => new GenericTestPropertyBuilder(OwnedNavigationBuilder.Property(propertyName)); + => Wrap(OwnedNavigationBuilder.Property(propertyName)); public override TestPropertyBuilder IndexerProperty(string propertyName) - => new GenericTestPropertyBuilder(OwnedNavigationBuilder.IndexerProperty(propertyName)); + => Wrap(OwnedNavigationBuilder.IndexerProperty(propertyName)); public override TestPropertyBuilder Property( Expression> propertyExpression) - => new GenericTestPropertyBuilder(OwnedNavigationBuilder.Property(propertyExpression)); + => Wrap(OwnedNavigationBuilder.Property(propertyExpression)); public override TestNavigationBuilder Navigation( Expression> navigationExpression) @@ -1151,8 +1193,7 @@ public override DataBuilder HasData(IEnumerable HasData(IEnumerable data) => OwnedNavigationBuilder.HasData(data); - OwnedNavigationBuilder IInfrastructure>. - Instance + OwnedNavigationBuilder IInfrastructure>.Instance => OwnedNavigationBuilder; } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 6904a9b0078..4aee1e9fb34 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -210,6 +210,9 @@ public override IMutableEntityType Metadata protected virtual NonGenericTestEntityTypeBuilder Wrap(EntityTypeBuilder entityTypeBuilder) => new(entityTypeBuilder); + protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new NonGenericTestPropertyBuilder(propertyBuilder); + public override TestEntityTypeBuilder HasAnnotation(string annotation, object? value) => Wrap(EntityTypeBuilder.HasAnnotation(annotation, value)); @@ -240,15 +243,14 @@ public override TestEntityTypeBuilder HasNoKey() public override TestPropertyBuilder Property(Expression> propertyExpression) { var memberInfo = propertyExpression.GetMemberAccess(); - return new NonGenericTestPropertyBuilder( - EntityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName())); + return Wrap(EntityTypeBuilder.Property(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName())); } public override TestPropertyBuilder Property(string propertyName) - => new NonGenericTestPropertyBuilder(EntityTypeBuilder.Property(propertyName)); + => Wrap(EntityTypeBuilder.Property(propertyName)); public override TestPropertyBuilder IndexerProperty(string propertyName) - => new NonGenericTestPropertyBuilder(EntityTypeBuilder.IndexerProperty(propertyName)); + => Wrap(EntityTypeBuilder.IndexerProperty(propertyName)); public override TestNavigationBuilder Navigation(Expression> navigationExpression) where TNavigation : class @@ -537,88 +539,123 @@ public NonGenericTestPropertyBuilder(PropertyBuilder propertyBuilder) public override IMutableProperty Metadata => PropertyBuilder.Metadata; + protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) + => new NonGenericTestPropertyBuilder(propertyBuilder); + public override TestPropertyBuilder HasAnnotation(string annotation, object? value) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasAnnotation(annotation, value)); + => Wrap(PropertyBuilder.HasAnnotation(annotation, value)); public override TestPropertyBuilder IsRequired(bool isRequired = true) - => new NonGenericTestPropertyBuilder(PropertyBuilder.IsRequired(isRequired)); + => Wrap(PropertyBuilder.IsRequired(isRequired)); public override TestPropertyBuilder HasMaxLength(int maxLength) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasMaxLength(maxLength)); + => Wrap(PropertyBuilder.HasMaxLength(maxLength)); public override TestPropertyBuilder HasPrecision(int precision, int scale) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasPrecision(precision, scale)); + => Wrap(PropertyBuilder.HasPrecision(precision, scale)); public override TestPropertyBuilder IsUnicode(bool unicode = true) - => new NonGenericTestPropertyBuilder(PropertyBuilder.IsUnicode(unicode)); + => Wrap(PropertyBuilder.IsUnicode(unicode)); public override TestPropertyBuilder IsRowVersion() - => new NonGenericTestPropertyBuilder(PropertyBuilder.IsRowVersion()); + => Wrap(PropertyBuilder.IsRowVersion()); public override TestPropertyBuilder IsConcurrencyToken(bool isConcurrencyToken = true) - => new NonGenericTestPropertyBuilder(PropertyBuilder.IsConcurrencyToken(isConcurrencyToken)); + => Wrap(PropertyBuilder.IsConcurrencyToken(isConcurrencyToken)); public override TestPropertyBuilder ValueGeneratedNever() - => new NonGenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedNever()); + => Wrap(PropertyBuilder.ValueGeneratedNever()); public override TestPropertyBuilder ValueGeneratedOnAdd() - => new NonGenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnAdd()); + => Wrap(PropertyBuilder.ValueGeneratedOnAdd()); public override TestPropertyBuilder ValueGeneratedOnAddOrUpdate() - => new NonGenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnAddOrUpdate()); + => Wrap(PropertyBuilder.ValueGeneratedOnAddOrUpdate()); public override TestPropertyBuilder ValueGeneratedOnUpdate() - => new NonGenericTestPropertyBuilder(PropertyBuilder.ValueGeneratedOnUpdate()); + => Wrap(PropertyBuilder.ValueGeneratedOnUpdate()); public override TestPropertyBuilder HasValueGenerator() - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator()); + => Wrap(PropertyBuilder.HasValueGenerator()); public override TestPropertyBuilder HasValueGenerator(Type valueGeneratorType) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator(valueGeneratorType)); + => Wrap(PropertyBuilder.HasValueGenerator(valueGeneratorType)); public override TestPropertyBuilder HasValueGenerator( Func factory) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasValueGenerator(factory)); + => Wrap(PropertyBuilder.HasValueGenerator(factory)); public override TestPropertyBuilder HasValueGeneratorFactory() - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasValueGeneratorFactory()); + => Wrap(PropertyBuilder.HasValueGeneratorFactory()); public override TestPropertyBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + => Wrap(PropertyBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); public override TestPropertyBuilder HasField(string fieldName) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasField(fieldName)); + => Wrap(PropertyBuilder.HasField(fieldName)); public override TestPropertyBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) - => new NonGenericTestPropertyBuilder(PropertyBuilder.UsePropertyAccessMode(propertyAccessMode)); + => Wrap(PropertyBuilder.UsePropertyAccessMode(propertyAccessMode)); public override TestPropertyBuilder HasConversion() - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion()); + => Wrap(PropertyBuilder.HasConversion()); - public override TestPropertyBuilder HasConversion(Type? providerClrType) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion(providerClrType)); + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion(valueComparer)); + + public override TestPropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(valueComparer, providerComparerType)); public override TestPropertyBuilder HasConversion( Expression> convertToProviderExpression, Expression> convertFromProviderExpression) - => new NonGenericTestPropertyBuilder( - PropertyBuilder.HasConversion( - new ValueConverter(convertToProviderExpression, convertFromProviderExpression))); + => Wrap(PropertyBuilder.HasConversion( + new ValueConverter(convertToProviderExpression, convertFromProviderExpression))); + + public override TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion( + new ValueConverter(convertToProviderExpression, convertFromProviderExpression), + valueComparer)); + + public override TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer, + ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion( + new ValueConverter(convertToProviderExpression, convertFromProviderExpression), + valueComparer, + providerComparerType)); public override TestPropertyBuilder HasConversion(ValueConverter converter) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter)); + => Wrap(PropertyBuilder.HasConversion(converter)); + + public override TestPropertyBuilder HasConversion(ValueConverter converter, ValueComparer? valueComparer) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer)); + + public override TestPropertyBuilder HasConversion( + ValueConverter converter, + ValueComparer? valueComparer, + ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer, providerComparerType)); public override TestPropertyBuilder HasConversion(ValueConverter? converter) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter)); + => Wrap(PropertyBuilder.HasConversion(converter)); public override TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion(converter, valueComparer)); + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer)); + + public override TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer, ValueComparer? providerComparerType) + => Wrap(PropertyBuilder.HasConversion(converter, valueComparer, providerComparerType)); public override TestPropertyBuilder HasConversion() - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion()); + => Wrap(PropertyBuilder.HasConversion()); - public override TestPropertyBuilder HasConversion(Type converterType, Type? comparerType) - => new NonGenericTestPropertyBuilder(PropertyBuilder.HasConversion(converterType, comparerType)); + public override TestPropertyBuilder HasConversion() + => Wrap(PropertyBuilder.HasConversion()); PropertyBuilder IInfrastructure.Instance => PropertyBuilder; diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 368b3aaa84f..0a49457809f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -398,21 +398,41 @@ public abstract TestPropertyBuilder HasValueGeneratorFactory UsePropertyAccessMode(PropertyAccessMode propertyAccessMode); public abstract TestPropertyBuilder HasConversion(); - public abstract TestPropertyBuilder HasConversion(Type providerClrType); + public abstract TestPropertyBuilder HasConversion(ValueComparer? valueComparer); + public abstract TestPropertyBuilder HasConversion(ValueComparer? valueComparer, ValueComparer? providerComparerType); public abstract TestPropertyBuilder HasConversion( Expression> convertToProviderExpression, Expression> convertFromProviderExpression); + public abstract TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer); + + public abstract TestPropertyBuilder HasConversion( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ValueComparer? valueComparer, + ValueComparer? providerComparerType); + public abstract TestPropertyBuilder HasConversion(ValueConverter converter); + public abstract TestPropertyBuilder HasConversion(ValueConverter converter, ValueComparer? valueComparer); + public abstract TestPropertyBuilder HasConversion( + ValueConverter converter, + ValueComparer? valueComparer, + ValueComparer? providerComparerType); + public abstract TestPropertyBuilder HasConversion(ValueConverter? converter); public abstract TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer); + public abstract TestPropertyBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer, ValueComparer? providerComparerType); public abstract TestPropertyBuilder HasConversion() - where TConverter : ValueConverter where TComparer : ValueComparer; - - public abstract TestPropertyBuilder HasConversion(Type converterType, Type? comparerType); + + public abstract TestPropertyBuilder HasConversion() + where TComparer : ValueComparer + where TProviderComparer : ValueComparer; } public abstract class TestNavigationBuilder diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index a7603a2a5ce..af7bf6582e5 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -750,9 +750,10 @@ public virtual void Properties_can_have_provider_type_set() { b.Property(e => e.Up); b.Property(e => e.Down).HasConversion(); - b.Property("Charm").HasConversion(typeof(long), typeof(CustomValueComparer)); - b.Property("Strange").HasConversion(); - b.Property("Strange").HasConversion((Type)null); + b.Property("Charm").HasConversion>(); + b.Property("Strange").HasConversion(new CustomValueComparer(), new CustomValueComparer()); + b.Property("Strange").HasConversion(null); + b.Property("Top").HasConversion(new CustomValueComparer()); }); var model = modelBuilder.FinalizeModel(); @@ -765,14 +766,22 @@ public virtual void Properties_can_have_provider_type_set() var down = entityType.FindProperty("Down"); Assert.Same(typeof(byte[]), down.GetProviderClrType()); Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetProviderValueComparer()); var charm = entityType.FindProperty("Charm"); Assert.Same(typeof(long), charm.GetProviderClrType()); Assert.IsType>(charm.GetValueComparer()); + Assert.IsType>(charm.GetProviderValueComparer()); var strange = entityType.FindProperty("Strange"); Assert.Null(strange.GetProviderClrType()); Assert.IsType>(strange.GetValueComparer()); + Assert.IsType>(strange.GetProviderValueComparer()); + + var top = entityType.FindProperty("Top"); + Assert.Same(typeof(string), top.GetProviderClrType()); + Assert.IsType>(top.GetValueComparer()); + Assert.IsType>(top.GetProviderValueComparer()); } [ConditionalFact] @@ -799,7 +808,7 @@ public virtual void Properties_can_have_provider_type_set_for_type() } [ConditionalFact] - public virtual void Properties_can_have_value_converter_set_non_generic() + public virtual void Properties_can_have_non_generic_value_converter_set() { var modelBuilder = CreateModelBuilder(); @@ -811,51 +820,67 @@ public virtual void Properties_can_have_value_converter_set_non_generic() { b.Property(e => e.Up); b.Property(e => e.Down).HasConversion(stringConverter); - b.Property("Charm").HasConversion(intConverter); + b.Property("Charm").HasConversion(intConverter, null, new CustomValueComparer()); b.Property("Strange").HasConversion(stringConverter); - b.Property("Strange").HasConversion((ValueConverter)null); + b.Property("Strange").HasConversion(null); }); var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetValueConverter()); - Assert.Same(stringConverter, entityType.FindProperty("Down").GetValueConverter()); - Assert.Same(intConverter, entityType.FindProperty("Charm").GetValueConverter()); + + var down = entityType.FindProperty("Down"); + Assert.Same(stringConverter, down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetProviderValueComparer()); + + var charm = entityType.FindProperty("Charm"); + Assert.Same(intConverter, charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + Assert.IsType>(charm.GetProviderValueComparer()); + Assert.Null(entityType.FindProperty("Strange").GetValueConverter()); } [ConditionalFact] - public virtual void Properties_can_have_value_converter_type_set() + public virtual void Properties_can_have_custom_type_value_converter_type_set() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity( b => { - b.Property(e => e.Up); - b.Property(e => e.Down).HasConversion(typeof(UTF8StringToBytesConverter)); + b.Property(e => e.Up).HasConversion>(); + b.Property(e => e.Down).HasConversion, CustomValueComparer>(); b.Property("Charm").HasConversion, CustomValueComparer>(); - b.Property("Strange").HasConversion( - typeof(UTF8StringToBytesConverter), typeof(CustomValueComparer)); - b.Property("Strange").HasConversion((ValueConverter)null, null); + b.Property("Strange").HasConversion>(); + b.Property("Strange").HasConversion(null, null); }); var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); - Assert.Null(entityType.FindProperty("Up").GetValueConverter()); + var up = entityType.FindProperty("Up"); + Assert.Equal(typeof(int), up.GetProviderClrType()); + Assert.Null(up.GetValueConverter()); + Assert.IsType>(up.GetValueComparer()); + Assert.IsType>(up.GetProviderValueComparer()); var down = entityType.FindProperty("Down"); Assert.IsType(down.GetValueConverter()); - Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetProviderValueComparer()); var charm = entityType.FindProperty("Charm"); Assert.IsType>(charm.GetValueConverter()); Assert.IsType>(charm.GetValueComparer()); + Assert.IsType>(charm.GetProviderValueComparer()); - Assert.Null(entityType.FindProperty("Strange").GetValueConverter()); - Assert.IsAssignableFrom>(entityType.FindProperty("Strange").GetValueComparer()); + var strange = entityType.FindProperty("Strange"); + Assert.Null(strange.GetValueConverter()); + Assert.IsType>(strange.GetValueComparer()); + Assert.IsType>(strange.GetProviderValueComparer()); } private class UTF8StringToBytesConverter : StringToBytesConverter @@ -883,16 +908,76 @@ public virtual void Properties_can_have_value_converter_set_inline() b => { b.Property(e => e.Up); - b.Property(e => e.Down).HasConversion(v => v.ToCharArray(), v => new string(v)); - b.Property("Charm").HasConversion(v => (long)v, v => (int)v); + b.Property(e => e.Down).HasConversion(v => int.Parse(v), v => v.ToString()); + b.Property("Charm").HasConversion(v => (long)v, v => (int)v, new CustomValueComparer()); + b.Property("Strange").HasConversion(v => (double)v, v => (float)v, new CustomValueComparer(), new CustomValueComparer()); }); - var model = (IReadOnlyModel)modelBuilder.Model; + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Quarks)); + + var up = entityType.FindProperty("Up"); + Assert.Null(up.GetProviderClrType()); + Assert.Null(up.GetValueConverter()); + Assert.IsType>(up.GetValueComparer()); + Assert.IsType>(up.GetProviderValueComparer()); - Assert.Null(entityType.FindProperty("Up").GetValueConverter()); - Assert.NotNull(entityType.FindProperty("Down").GetValueConverter()); - Assert.NotNull(entityType.FindProperty("Charm").GetValueConverter()); + var down = entityType.FindProperty("Down"); + Assert.IsType>(down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetProviderValueComparer()); + + var charm = entityType.FindProperty("Charm"); + Assert.IsType>(charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + Assert.IsType>(charm.GetProviderValueComparer()); + + var strange = entityType.FindProperty("Strange"); + Assert.IsType>(strange.GetValueConverter()); + Assert.IsType>(strange.GetValueComparer()); + Assert.IsType>(strange.GetProviderValueComparer()); + } + + [ConditionalFact] + public virtual void Properties_can_have_value_converter_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Up); + b.Property(e => e.Down).HasConversion( + new ValueConverter(v => int.Parse(v), v => v.ToString())); + b.Property("Charm").HasConversion( + new ValueConverter(v => v, v => (int)v), new CustomValueComparer()); + b.Property("Strange").HasConversion( + new ValueConverter(v => (double)v, v => (float)v), new CustomValueComparer(), new CustomValueComparer()); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + var up = entityType.FindProperty("Up"); + Assert.Null(up.GetProviderClrType()); + Assert.Null(up.GetValueConverter()); + Assert.IsType>(up.GetValueComparer()); + Assert.IsType>(up.GetProviderValueComparer()); + + var down = entityType.FindProperty("Down"); + Assert.IsType>(down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + Assert.IsType>(down.GetProviderValueComparer()); + + var charm = entityType.FindProperty("Charm"); + Assert.IsType>(charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + Assert.IsType>(charm.GetProviderValueComparer()); + + var strange = entityType.FindProperty("Strange"); + Assert.IsType>(strange.GetValueConverter()); + Assert.IsType>(strange.GetValueComparer()); + Assert.IsType>(strange.GetProviderValueComparer()); } [ConditionalFact] @@ -1001,7 +1086,7 @@ public virtual void Value_converter_configured_on_nullable_type_overrides_non_nu c => { c.Properties().HaveConversion, CustomValueComparer>(); - c.Properties().HaveConversion, CustomValueComparer>(); + c.Properties().HaveConversion, CustomValueComparer, CustomValueComparer>(); }); modelBuilder.Entity( @@ -1016,10 +1101,12 @@ public virtual void Value_converter_configured_on_nullable_type_overrides_non_nu var id = entityType.FindProperty("Id"); Assert.IsType>(id.GetValueConverter()); Assert.IsType>(id.GetValueComparer()); + Assert.IsType>(id.GetProviderValueComparer()); var wierd = entityType.FindProperty("Wierd"); Assert.IsType>(wierd.GetValueConverter()); Assert.IsType>(wierd.GetValueComparer()); + Assert.IsType>(wierd.GetProviderValueComparer()); } [ConditionalFact] diff --git a/test/EFCore.Tests/Storage/ValueComparerTest.cs b/test/EFCore.Tests/Storage/ValueComparerTest.cs index dce88aabbd9..4547169c4a6 100644 --- a/test/EFCore.Tests/Storage/ValueComparerTest.cs +++ b/test/EFCore.Tests/Storage/ValueComparerTest.cs @@ -7,15 +7,6 @@ namespace Microsoft.EntityFrameworkCore.Storage; public class ValueComparerTest { - private class SomeDbContext : DbContext - { - protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString()); - - protected internal override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity().Property(e => e.Bar).HasConversion(new FakeValueComparer()); - } - protected class FakeValueComparer : ValueComparer { public FakeValueComparer() @@ -33,12 +24,40 @@ private class Foo [ConditionalFact] public void Throws_for_comparer_with_wrong_type() { - using var context = new SomeDbContext(); + using var context = new InvalidDbContext(); Assert.Equal( CoreStrings.ComparerPropertyMismatch("double", nameof(Foo), nameof(Foo.Bar), "int"), Assert.Throws(() => context.Model).Message); } + + private class InvalidDbContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString()); + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Bar).HasConversion(new FakeValueComparer()); + } + + [ConditionalFact] + public void Throws_for_provider_comparer_with_wrong_type() + { + using var context = new InvalidProviderDbContext(); + + Assert.Equal( + CoreStrings.ComparerPropertyMismatch("double", nameof(Foo), nameof(Foo.Bar), "string"), + Assert.Throws(() => context.Model).Message); + } + + private class InvalidProviderDbContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString()); + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Bar).HasConversion((ValueComparer)null, new FakeValueComparer()); + } [ConditionalTheory] [InlineData(typeof(byte), (byte)1, (byte)2, 1)] @@ -68,7 +87,7 @@ private static ValueComparer CompareTest(Type type, object value1, object value2 var comparer = (ValueComparer)Activator.CreateInstance(typeof(ValueComparer<>).MakeGenericType(type), new object[] { false }); if (toNullable) { - comparer = comparer.ToNonNullNullableComparer(); + comparer = ToNonNullNullableComparer(comparer); } Assert.True(comparer.Equals(value1, value1)); @@ -84,7 +103,7 @@ private static ValueComparer CompareTest(Type type, object value1, object value2 var keyComparer = (ValueComparer)Activator.CreateInstance(typeof(ValueComparer<>).MakeGenericType(type), new object[] { true }); if (toNullable) { - keyComparer = keyComparer.ToNonNullNullableComparer(); + keyComparer = ToNonNullNullableComparer(keyComparer); } Assert.True(keyComparer.Equals(value1, value1)); @@ -98,6 +117,49 @@ private static ValueComparer CompareTest(Type type, object value1, object value2 return comparer; } + public static ValueComparer ToNonNullNullableComparer(ValueComparer comparer) + { + var type = comparer.EqualsExpression.Parameters[0].Type; + var nullableType = type.MakeNullable(); + + var newEqualsParam1 = Expression.Parameter(nullableType, "v1"); + var newEqualsParam2 = Expression.Parameter(nullableType, "v2"); + var newHashCodeParam = Expression.Parameter(nullableType, "v"); + var newSnapshotParam = Expression.Parameter(nullableType, "v"); + + return (ValueComparer)Activator.CreateInstance( + typeof(NonNullNullableValueComparer<>).MakeGenericType(nullableType), + Expression.Lambda( + comparer.ExtractEqualsBody( + Expression.Convert(newEqualsParam1, type), + Expression.Convert(newEqualsParam2, type)), + newEqualsParam1, newEqualsParam2), + Expression.Lambda( + comparer.ExtractHashCodeBody( + Expression.Convert(newHashCodeParam, type)), + newHashCodeParam), + Expression.Lambda( + Expression.Convert( + comparer.ExtractSnapshotBody( + Expression.Convert(newSnapshotParam, type)), + nullableType), + newSnapshotParam))!; + } + + private sealed class NonNullNullableValueComparer : ValueComparer + { + public NonNullNullableValueComparer( + LambdaExpression equalsExpression, + LambdaExpression hashCodeExpression, + LambdaExpression snapshotExpression) + : base( + (Expression>)equalsExpression, + (Expression>)hashCodeExpression, + (Expression>)snapshotExpression) + { + } + } + private enum JustAnEnum : ushort { A,