From 3ea48038b19a0dc28826520e5a1ceaac895a0a77 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 5 May 2022 14:41:39 -0700 Subject: [PATCH] Allow to set provider value comparer when configuring value conversion Use client value comparer as provider value comparer if possible The client value comparer will not be used if the model type is a nullable value type since we need the types to match exactly and the provider type is never nullable Fix some API issues Remove some unused code Fixes #27738 Fixes #27850 Fixes #27791 Part of #27588 --- .../CSharpRuntimeModelCodeGenerator.cs | 19 ++ .../RelationalModelValidator.cs | 6 +- src/EFCore.Relational/Metadata/IColumn.cs | 8 + .../Internal/MigrationsModelDiffer.cs | 2 +- .../Storage/RelationalGeometryTypeMapping.cs | 33 ++- .../Storage/RelationalTypeMapping.cs | 33 +-- .../Update/Internal/CommandBatchPreparer.cs | 8 +- .../Internal/CompositeRowValueFactory.cs | 2 +- .../Internal/RowForeignKeyValueFactory.cs | 3 +- .../Internal/SimpleRowIndexValueFactory.cs | 2 +- .../Internal/SimpleRowKeyValueFactory.cs | 10 +- .../Update/ModificationCommand.cs | 4 +- .../SimplePrincipalKeyValueFactory.cs | 6 - .../Internal/ValueComparerExtensions.cs | 62 ----- src/EFCore/Infrastructure/ModelValidator.cs | 18 ++ .../Builders/IConventionPropertyBuilder.cs | 48 ++++ .../PropertiesConfigurationBuilder.cs | 37 ++- .../PropertiesConfigurationBuilder`.cs | 12 +- .../Metadata/Builders/PropertyBuilder.cs | 82 ++++++- .../Metadata/Builders/PropertyBuilder`.cs | 116 ++++++++- .../Conventions/RuntimeModelConvention.cs | 1 + src/EFCore/Metadata/IConventionEntityType.cs | 2 +- src/EFCore/Metadata/IConventionProperty.cs | 28 ++- src/EFCore/Metadata/IEntityType.cs | 2 +- src/EFCore/Metadata/IMutableEntityType.cs | 2 +- src/EFCore/Metadata/IMutableProperty.cs | 14 ++ src/EFCore/Metadata/IProperty.cs | 8 +- src/EFCore/Metadata/IReadOnlyEntityType.cs | 2 +- src/EFCore/Metadata/IReadOnlyProperty.cs | 6 + .../Metadata/Internal/CoreAnnotationNames.cs | 18 ++ .../Internal/InternalPropertyBuilder.cs | 92 ++++++- src/EFCore/Metadata/Internal/Property.cs | 227 +++++++++++++++--- .../Internal/PropertyConfiguration.cs | 27 +++ src/EFCore/Metadata/RuntimeEntityType.cs | 3 + src/EFCore/Metadata/RuntimeProperty.cs | 13 + src/EFCore/Storage/CoreTypeMapping.cs | 46 +++- .../CosmosModelBuilderGenericTest.cs | 2 +- .../CSharpRuntimeModelCodeGeneratorTest.cs | 31 ++- .../Internal/MigrationsModelDifferTest.cs | 38 +++ .../Storage/RelationalTypeMappingTest.cs | 60 +++-- .../CustomConvertersTestBase.cs | 2 +- .../CustomConvertersSqlServerTest.cs | 2 - .../Storage/SqlServerTypeMappingTest.cs | 8 +- .../Internal/ClrPropertyGetterFactoryTest.cs | 3 + .../Internal/ClrPropertySetterFactoryTest.cs | 3 + .../Internal/InternalPropertyBuilderTest.cs | 26 ++ ...ModelBuilderGenericRelationshipTypeTest.cs | 47 +++- .../ModelBuilding/ModelBuilderGenericTest.cs | 123 ++++++---- .../ModelBuilderNonGenericTest.cs | 105 +++++--- .../ModelBuilding/ModelBuilderTestBase.cs | 28 ++- .../ModelBuilding/NonRelationshipTestBase.cs | 137 +++++++++-- .../EFCore.Tests/Storage/ValueComparerTest.cs | 86 ++++++- 52 files changed, 1334 insertions(+), 369 deletions(-) delete mode 100644 src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs 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,