From c2772b82732d1847dff0da77eaedce7f36bc6b26 Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Tue, 10 Aug 2021 14:04:34 -0700 Subject: [PATCH] Allow TypeMappingSource to use the pre-convention configuration Part of #25084 --- .../Internal/CSharpDbContextGenerator.cs | 3 +- .../RelationalPropertyExtensions.cs | 5 +- .../Storage/IRelationalTypeMappingSource.cs | 21 ++- .../Storage/RelationalTypeMappingInfo.cs | 72 ++++----- .../Storage/RelationalTypeMappingSource.cs | 143 +++++++++++++++-- .../Conventions/RuntimeModelConvention.cs | 127 ++++++++++++--- src/EFCore/Metadata/IModel.cs | 25 ++- .../Metadata/IPropertyTypeConfiguration.cs | 58 +++++++ src/EFCore/Metadata/IReadOnlyProperty.cs | 5 +- src/EFCore/Metadata/Internal/Model.cs | 31 +++- .../Metadata/Internal/ModelConfiguration.cs | 69 +++----- .../Internal/PropertyConfiguration.cs | 79 +++++++++- src/EFCore/Metadata/RuntimeModel.cs | 64 +++++++- .../RuntimePropertyTypeConfiguration.cs | 98 ++++++++++++ src/EFCore/Storage/ITypeMappingSource.cs | 24 ++- src/EFCore/Storage/TypeMappingInfo.cs | 49 +----- src/EFCore/Storage/TypeMappingSource.cs | 98 ++++++++++-- src/EFCore/Storage/TypeMappingSourceBase.cs | 21 ++- src/Shared/SharedTypeExtensions.cs | 46 ++++++ .../Storage/RelationalTypeMapperTest.cs | 147 ++++++++++++------ .../Storage/RelationalTypeMapperTestBase.cs | 113 +++++++++++++- .../TestRelationalTypeMappingSource.cs | 5 + .../Storage/SqlServerTypeMappingSourceTest.cs | 140 +++++++---------- .../ModelBuilding/NonRelationshipTestBase.cs | 25 ++- 24 files changed, 1117 insertions(+), 351 deletions(-) create mode 100644 src/EFCore/Metadata/IPropertyTypeConfiguration.cs create mode 100644 src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 71304918d42..0bd9bfd98f9 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -545,8 +545,7 @@ private void GenerateIndex(IIndex index) var lines = new List { - $".{nameof(EntityTypeBuilder.HasIndex)}({_code.Lambda(index.Properties, "e")}, " - + $"{_code.Literal(index.GetDatabaseName())})" + $".{nameof(EntityTypeBuilder.HasIndex)}({_code.Lambda(index.Properties, "e")}, {_code.Literal(index.GetDatabaseName())})" }; annotations.Remove(RelationalAnnotationNames.Name); diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 53821b76831..eea20249000 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -1,11 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -833,7 +830,7 @@ public static void SetDefaultValue(this IMutableProperty property, object? value /// /// The property. /// The identifier of the table-like store object containing the column. - /// The maximum length, or if none if defined. + /// The maximum length, or if none is defined. public static int? GetMaxLength(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) { Check.NotNull(property, nameof(property)); diff --git a/src/EFCore.Relational/Storage/IRelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/IRelationalTypeMappingSource.cs index aa69790eb32..a0183527bf7 100644 --- a/src/EFCore.Relational/Storage/IRelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/IRelationalTypeMappingSource.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -10,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Storage { /// /// - /// The relational type mapping interface for EF Core, starting with version 2.1. Type mappings describe how a + /// The relational type mapping source. Type mappings describe how a /// provider maps CLR types/values to database types/values. /// /// @@ -55,14 +54,28 @@ public interface IRelationalTypeMappingSource : ITypeMappingSource /// /// /// Note: Only call this method if there is no - /// or available, otherwise call - /// or + /// or available, otherwise call + /// or /// /// /// The CLR type. /// The type mapping, or if none was found. new RelationalTypeMapping? FindMapping(Type type); + /// + /// + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// Note: Only call this method if there is no , + /// otherwise call . + /// + /// + /// The CLR type. + /// The model. + /// The type mapping, or if none was found. + new RelationalTypeMapping? FindMapping(Type type, IModel model); + /// /// /// Finds the type mapping for a given database type name. diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs index 5d9deb61ed3..7d14e81bed2 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.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. -using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -14,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Storage /// Describes metadata needed to decide on a relational type mapping for /// a property, type, or provider-specific relational type name. /// - public readonly struct RelationalTypeMappingInfo : IEquatable + public readonly record struct RelationalTypeMappingInfo : IEquatable { private readonly TypeMappingInfo _coreTypeMappingInfo; @@ -181,59 +179,80 @@ public RelationalTypeMappingInfo( /// /// The provider-specific relational type name for which mapping is needed. /// - public string? StoreTypeName { get; } + public string? StoreTypeName { get; init; } /// /// The provider-specific relational type name, with any facets removed. /// - public string? StoreTypeNameBase { get; } + public string? StoreTypeNameBase { get; init; } /// /// Indicates the store-size to use for the mapping, or null if none. /// public int? Size - => _coreTypeMappingInfo.Size; + { + get => _coreTypeMappingInfo.Size; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { Size = value }; + } /// /// The suggested precision of the mapped data type. /// public int? Precision - => _coreTypeMappingInfo.Precision; + { + get => _coreTypeMappingInfo.Precision; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { Precision = value }; + } /// /// The suggested scale of the mapped data type. /// public int? Scale - => _coreTypeMappingInfo.Scale; + { + get => _coreTypeMappingInfo.Scale; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { Scale = value }; + } /// /// Whether or not the mapped data type is fixed length. /// - public bool? IsFixedLength { get; } + public bool? IsFixedLength { get; init; } /// /// Indicates whether or not the mapping is part of a key or index. /// public bool IsKeyOrIndex - => _coreTypeMappingInfo.IsKeyOrIndex; + { + get => _coreTypeMappingInfo.IsKeyOrIndex; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { IsKeyOrIndex = value }; + } /// /// Indicates whether or not the mapping supports Unicode, or null if not defined. /// public bool? IsUnicode - => _coreTypeMappingInfo.IsUnicode; + { + get => _coreTypeMappingInfo.IsUnicode; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { IsUnicode = value }; + } /// /// Indicates whether or not the mapping will be used for a row version, or null if not defined. /// public bool? IsRowVersion - => _coreTypeMappingInfo.IsRowVersion; + { + get => _coreTypeMappingInfo.IsRowVersion; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { IsRowVersion = value }; + } /// /// The CLR type in the model. /// public Type? ClrType - => _coreTypeMappingInfo.ClrType; + { + get => _coreTypeMappingInfo.ClrType; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { ClrType = value }; + } /// /// Returns a new with the given converter applied. @@ -242,32 +261,5 @@ public Type? ClrType /// The new mapping info. public RelationalTypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) => new(this, converterInfo); - - /// - /// Compares this to another to check if they represent the same mapping. - /// - /// The other object. - /// if they represent the same mapping; otherwise. - public bool Equals(RelationalTypeMappingInfo other) - => _coreTypeMappingInfo.Equals(other._coreTypeMappingInfo) - && IsFixedLength == other.IsFixedLength - && StoreTypeName == other.StoreTypeName; - - /// - /// Compares this to another to check if they represent the same mapping. - /// - /// The other object. - /// if they represent the same mapping; otherwise. - public override bool Equals(object? obj) - => obj != null - && obj.GetType() == GetType() - && Equals((RelationalTypeMappingInfo)obj); - - /// - /// Returns a hash code for this object. - /// - /// The hash code. - public override int GetHashCode() - => HashCode.Combine(_coreTypeMappingInfo, StoreTypeName, IsFixedLength); } } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs index 757cace92c5..a53179a581d 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -18,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Storage { /// /// - /// The base class for relational type mapping starting with version 2.1. Relational providers + /// The base class for relational type mapping source. Relational providers /// should derive from this class and override /// /// @@ -122,7 +120,15 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) } } - var resolvedMapping = _explicitMappings.GetOrAdd( + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + + ValidateMapping(resolvedMapping, principals?[0]); + + return resolvedMapping; + } + + private RelationalTypeMapping? FindMappingWithConversion(RelationalTypeMappingInfo mappingInfo, Type? providerClrType, ValueConverter? customConverter) + => _explicitMappings.GetOrAdd( (mappingInfo, providerClrType, customConverter), k => { @@ -180,11 +186,6 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) return mapping; }); - ValidateMapping(resolvedMapping, principals?[0]); - - return resolvedMapping; - } - /// /// /// Finds the type mapping for a given . @@ -233,8 +234,8 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) /// /// /// Note: Only call this method if there is no - /// or available, otherwise call - /// or + /// or available, otherwise call + /// or /// /// /// Note: providers should typically not need to override this method. @@ -245,6 +246,122 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) public override CoreTypeMapping? FindMapping(Type type) => FindMappingWithConversion(new RelationalTypeMappingInfo(type), null); + /// + /// + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// Note: Only call this method if there is no , + /// otherwise call . + /// + /// + /// The CLR type. + /// The model. + /// The type mapping, or if none was found. + public override CoreTypeMapping? FindMapping(Type type, IModel model) + { + var typeConfigurations = model.FindPropertyTypeConfigurations(type); + var mappingInfo = new RelationalTypeMappingInfo(type); + Type? providerClrType = null; + ValueConverter? customConverter = null; + if (typeConfigurations != null) + { + foreach (var typeConfiguration in typeConfigurations) + { + if (providerClrType == null) + { + var providerType = typeConfiguration.GetProviderClrType(); + if (providerType != null) + { + providerClrType = providerType.UnwrapNullableType(); + } + } + + var isUnicode = typeConfiguration.IsUnicode(); + var scale = typeConfiguration.GetScale(); + var precision = typeConfiguration.GetPrecision(); + var size = typeConfiguration.GetMaxLength(); + + if (mappingInfo.StoreTypeName == null) + { + var storeTypeName = (string?)typeConfiguration[RelationalAnnotationNames.ColumnType]; + if (storeTypeName != null) + { + var storeTypeBaseName = ParseStoreTypeName( + storeTypeName, out var parsedUnicode, out var parsedSize, out var parsedPrecision, out var parsedScale); + + mappingInfo = mappingInfo with { StoreTypeName = storeTypeName }; + + if (mappingInfo.StoreTypeNameBase == null) + { + mappingInfo = mappingInfo with { StoreTypeNameBase = storeTypeBaseName }; + } + + if (size == null) + { + size = parsedSize; + } + + if (precision == null) + { + precision = parsedPrecision; + } + + if (scale == null) + { + scale = parsedScale; + } + + if (isUnicode == null) + { + isUnicode = parsedUnicode; + } + } + } + + if (mappingInfo.IsUnicode == null + && isUnicode != null) + { + mappingInfo = mappingInfo with { IsUnicode = isUnicode }; + } + + if (mappingInfo.Scale == null + && scale != null) + { + mappingInfo = mappingInfo with { Scale = scale }; + } + + if (mappingInfo.Precision == null + && precision != null) + { + mappingInfo = mappingInfo with { Precision = precision }; + } + + if (mappingInfo.Size == null + && size != null) + { + mappingInfo = mappingInfo with { Size = size }; + } + + if (mappingInfo.IsFixedLength == null) + { + var isFixedLength = (bool?)typeConfiguration[RelationalAnnotationNames.IsFixedLength]; + if (isFixedLength != null) + { + mappingInfo = mappingInfo with { IsFixedLength = isFixedLength }; + } + } + } + + var firstConfiguration = typeConfigurations.FirstOrDefault(); + customConverter = firstConfiguration?.ClrType == type + ? firstConfiguration.GetValueConverter() + : null; + } + + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + } + /// /// /// Finds the type mapping for a given representing @@ -370,6 +487,10 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) RelationalTypeMapping? IRelationalTypeMappingSource.FindMapping(Type type) => (RelationalTypeMapping?)FindMapping(type); + /// + RelationalTypeMapping? IRelationalTypeMappingSource.FindMapping(Type type, IModel model) + => (RelationalTypeMapping?)FindMapping(type); + /// RelationalTypeMapping? IRelationalTypeMappingSource.FindMapping(MemberInfo member) => (RelationalTypeMapping?)FindMapping(member); diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 13ff2660a61..949128c5fcf 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -150,6 +150,13 @@ protected virtual RuntimeModel Create(IModel model) convention.ProcessEntityTypeAnnotations(annotations, source, target, runtime)); } + foreach (var typeConfiguration in model.GetPropertyTypeConfigurations()) + { + var runtimeTypeConfiguration = Create(typeConfiguration, runtimeModel); + CreateAnnotations(typeConfiguration, runtimeTypeConfiguration, static (convention, annotations, source, target, runtime) => + convention.ProcessPropertyTypeConfigurationAnnotations(annotations, source, target, runtime)); + } + CreateAnnotations(model, runtimeModel, static (convention, annotations, source, target, runtime) => convention.ProcessModelAnnotations(annotations, source, target, runtime)); @@ -185,14 +192,14 @@ protected virtual void ProcessModelAnnotations( RuntimeModel runtimeModel, bool runtime) { - if (runtime) + if (!runtime) { - annotations.Remove(CoreAnnotationNames.ModelDependencies); - annotations[CoreAnnotationNames.ReadOnlyModel] = runtimeModel; + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); } else { - annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + annotations.Remove(CoreAnnotationNames.ModelDependencies); + annotations[CoreAnnotationNames.ReadOnlyModel] = runtimeModel; } } @@ -251,6 +258,41 @@ protected virtual void ProcessEntityTypeAnnotations( } } + private RuntimePropertyTypeConfiguration Create(IPropertyTypeConfiguration typeConfiguration, RuntimeModel model) + => model.AddPropertyTypeConfiguration( + typeConfiguration.ClrType, + typeConfiguration.GetMaxLength(), + typeConfiguration.IsUnicode(), + typeConfiguration.GetPrecision(), + typeConfiguration.GetScale(), + typeConfiguration.GetProviderClrType(), + (Type?)typeConfiguration[CoreAnnotationNames.ValueConverterType]); + + /// + /// Updates the property annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source property. + /// The target property that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessPropertyTypeConfigurationAnnotations( + Dictionary annotations, + IPropertyTypeConfiguration typeConfiguration, + RuntimePropertyTypeConfiguration runtimeTypeConfiguration, + bool runtime) + { + if (!runtime) + { + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } + } + } + private RuntimeProperty Create(IProperty property, RuntimeEntityType runtimeEntityType) => runtimeEntityType.AddProperty( property.Name, @@ -289,20 +331,13 @@ protected virtual void ProcessPropertyAnnotations( { if (!runtime) { - annotations.Remove(CoreAnnotationNames.PropertyAccessMode); - annotations.Remove(CoreAnnotationNames.BeforeSaveBehavior); - annotations.Remove(CoreAnnotationNames.AfterSaveBehavior); - annotations.Remove(CoreAnnotationNames.MaxLength); - annotations.Remove(CoreAnnotationNames.Unicode); - annotations.Remove(CoreAnnotationNames.Precision); - annotations.Remove(CoreAnnotationNames.Scale); - annotations.Remove(CoreAnnotationNames.ProviderClrType); - annotations.Remove(CoreAnnotationNames.ValueGeneratorFactory); - annotations.Remove(CoreAnnotationNames.ValueGeneratorFactoryType); - annotations.Remove(CoreAnnotationNames.ValueConverter); - annotations.Remove(CoreAnnotationNames.ValueConverterType); - annotations.Remove(CoreAnnotationNames.ValueComparer); - annotations.Remove(CoreAnnotationNames.ValueComparerType); + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } } } @@ -328,7 +363,13 @@ protected virtual void ProcessServicePropertyAnnotations( { if (!runtime) { - annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } } } @@ -348,6 +389,16 @@ protected virtual void ProcessKeyAnnotations( RuntimeKey runtimeKey, bool runtime) { + if (!runtime) + { + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } + } } private RuntimeIndex Create(IIndex index, RuntimeEntityType runtimeEntityType) @@ -369,6 +420,16 @@ protected virtual void ProcessIndexAnnotations( RuntimeIndex runtimeIndex, bool runtime) { + if (!runtime) + { + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } + } } private RuntimeForeignKey Create(IForeignKey foreignKey, RuntimeEntityType runtimeEntityType) @@ -398,6 +459,16 @@ protected virtual void ProcessForeignKeyAnnotations( RuntimeForeignKey runtimeForeignKey, bool runtime) { + if (!runtime) + { + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } + } } private RuntimeNavigation Create(INavigation navigation, RuntimeForeignKey runtimeForeigKey) @@ -427,8 +498,13 @@ protected virtual void ProcessNavigationAnnotations( { if (!runtime) { - annotations.Remove(CoreAnnotationNames.PropertyAccessMode); - annotations.Remove(CoreAnnotationNames.EagerLoaded); + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } } } @@ -493,8 +569,13 @@ protected virtual void ProcessSkipNavigationAnnotations( { if (!runtime) { - annotations.Remove(CoreAnnotationNames.PropertyAccessMode); - annotations.Remove(CoreAnnotationNames.EagerLoaded); + foreach (var annotation in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(annotation.Key)) + { + annotations.Remove(annotation.Key); + } + } } } diff --git a/src/EFCore/Metadata/IModel.cs b/src/EFCore/Metadata/IModel.cs index 108bfc0460a..6607ca7fed2 100644 --- a/src/EFCore/Metadata/IModel.cs +++ b/src/EFCore/Metadata/IModel.cs @@ -1,15 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -149,5 +147,26 @@ RuntimeModelDependencies GetModelDependencies() /// /// The to check. bool IsIndexerMethod(MethodInfo methodInfo); + + /// + /// + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// The CLR type. + CoreTypeMapping? FindMapping(Type type); + + /// + /// Gets all the pre-convention configurations. + /// + /// The pre-convention configurations. + IEnumerable GetPropertyTypeConfigurations(); + + /// + /// Finds the pre-convention configurations for a given scalar . + /// + /// The CLR type. + /// The pre-convention configurations in order of most to least specific, or if none is found. + IEnumerable? FindPropertyTypeConfigurations(Type propertyType); } } diff --git a/src/EFCore/Metadata/IPropertyTypeConfiguration.cs b/src/EFCore/Metadata/IPropertyTypeConfiguration.cs new file mode 100644 index 00000000000..1bdb905489e --- /dev/null +++ b/src/EFCore/Metadata/IPropertyTypeConfiguration.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents the configuration for a scalar property type. + /// + public interface IPropertyTypeConfiguration : IAnnotatable + { + /// + /// Gets the type configured by this object. + /// + Type ClrType { get; } + + /// + /// Gets the maximum length of data that is allowed in this property. For example, if the property is a + /// then this is the maximum number of characters. + /// + /// The maximum length, or if none is defined. + int? GetMaxLength(); + + /// + /// Gets the precision of data that is allowed in this property. + /// For example, if the property is a then this is the maximum number of digits. + /// + /// The precision, or if none is defined. + int? GetPrecision(); + + /// + /// Gets the scale of data that is allowed in this property. + /// For example, if the property is a then this is the maximum number of decimal places. + /// + /// The scale, or if none is defined. + int? GetScale(); + + /// + /// Gets a value indicating whether or not the property can persist Unicode characters. + /// + /// The Unicode setting, or if none is defined. + bool? IsUnicode(); + + /// + /// Gets the custom set for this property. + /// + /// The converter, or if none has been set. + ValueConverter? GetValueConverter(); + + /// + /// Gets the type that the property value will be converted to before being sent to the database provider. + /// + /// The provider type, or if none has been set. + Type? GetProviderClrType(); + } +} diff --git a/src/EFCore/Metadata/IReadOnlyProperty.cs b/src/EFCore/Metadata/IReadOnlyProperty.cs index e97b5a8880f..cf3cf6407d3 100644 --- a/src/EFCore/Metadata/IReadOnlyProperty.cs +++ b/src/EFCore/Metadata/IReadOnlyProperty.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -74,7 +71,7 @@ CoreTypeMapping GetTypeMapping() /// Gets the maximum length of data that is allowed in this property. For example, if the property is a /// then this is the maximum number of characters. /// - /// The maximum length, or if none if defined. + /// The maximum length, or if none is defined. int? GetMaxLength(); /// diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index f901a5821c8..40ed614e20a 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -1,12 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -14,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -649,6 +647,33 @@ public virtual bool IsIgnoredType(Type type) return _ignoredTypeNames.Remove(name) ? name : 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 CoreTypeMapping? FindMapping(Type type) + => ((IModel)this).GetModelDependencies().TypeMappingSource.FindMapping(type, this); + + /// + /// 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 IEnumerable GetPropertyTypeConfigurations() + => Configuration?.GetPropertyTypeConfigurations() ?? Enumerable.Empty(); + + /// + /// 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 IEnumerable? FindPropertyTypeConfigurations(Type propertyType) + => Configuration?.FindPropertyTypeConfigurations(propertyType); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/ModelConfiguration.cs b/src/EFCore/Metadata/Internal/ModelConfiguration.cs index 0a349f32266..0e09821e030 100644 --- a/src/EFCore/Metadata/Internal/ModelConfiguration.cs +++ b/src/EFCore/Metadata/Internal/ModelConfiguration.cs @@ -80,7 +80,7 @@ public virtual bool IsEmpty() type.GetGenericTypeDefinition(), configurationType, ref configuredType, getBaseTypes: false); } - foreach (var @interface in GetDeclaredInterfaces(type)) + foreach (var @interface in type.GetDeclaredInterfaces()) { configurationType = GetConfigurationType( @interface, configurationType, ref configuredType, getBaseTypes: false); @@ -111,18 +111,6 @@ public virtual bool IsEmpty() return configurationType ?? previousConfiguration; } - private IEnumerable GetDeclaredInterfaces(Type type) - { - var interfaces = type.GetInterfaces(); - if (type.BaseType == typeof(object) - || type.BaseType == null) - { - return interfaces; - } - - return interfaces.Except(type.BaseType.GetInterfaces()); - } - private static void EnsureCompatible( TypeConfigurationType configurationType, Type type, TypeConfigurationType? previousConfiguration, Type? previousType) @@ -136,39 +124,27 @@ private static void EnsureCompatible( } } - private IList GetBaseTypesAndInterfacesInclusive(Type type) - { - var baseTypes = new List(); - var typesToProcess = new Queue(); - typesToProcess.Enqueue(type); - - while (typesToProcess.Count > 0) - { - type = typesToProcess.Dequeue(); - baseTypes.Add(type); - - if (!type.IsGenericTypeDefinition - && !type.IsInterface) - { - if (type.BaseType != null) - { - typesToProcess.Enqueue(type.BaseType); - } - - if (type.IsConstructedGenericType) - { - typesToProcess.Enqueue(type.GetGenericTypeDefinition()); - } - - foreach (var @interface in GetDeclaredInterfaces(type)) - { - typesToProcess.Enqueue(@interface); - } - } - } + /// + /// 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 IEnumerable GetPropertyTypeConfigurations() + => _properties.Values; - return baseTypes; - } + /// + /// 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 IEnumerable? FindPropertyTypeConfigurations(Type propertyType) + => GetConfigurationType(propertyType) != TypeConfigurationType.Property + ? null + : propertyType.GetBaseTypesAndInterfacesInclusive() + .Select(type => _properties.GetValueOrDefault(type)!) + .Where(type => type != null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -178,7 +154,7 @@ private IList GetBaseTypesAndInterfacesInclusive(Type type) /// public virtual void ConfigureProperty(IMutableProperty property) { - var types = GetBaseTypesAndInterfacesInclusive(property.ClrType); + var types = property.ClrType.GetBaseTypesAndInterfacesInclusive(); for (var i = types.Count - 1; i >= 0; i--) { var type = types[i]; @@ -206,7 +182,6 @@ public virtual PropertyConfiguration GetOrAddProperty(Type type) } var property = FindProperty(type); - if (property == null) { RemoveIgnored(type); diff --git a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs index 2cf65d319bc..e7198b4af19 100644 --- a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs +++ b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -16,8 +15,10 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public class PropertyConfiguration : AnnotatableBase + public class PropertyConfiguration : AnnotatableBase, IPropertyTypeConfiguration { + private ValueConverter? _valueConverter; + /// /// 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 @@ -72,11 +73,17 @@ public virtual void Apply(IMutableProperty property) break; case CoreAnnotationNames.ValueConverterType: - property.SetValueConverter((Type?)annotation.Value); + if (ClrType == property.ClrType) + { + property.SetValueConverter((Type?)annotation.Value); + } break; case CoreAnnotationNames.ValueComparerType: - property.SetValueComparer((Type?)annotation.Value); + if (ClrType == property.ClrType) + { + property.SetValueComparer((Type?)annotation.Value); + } break; default: @@ -89,6 +96,15 @@ public virtual void Apply(IMutableProperty property) } } + /// + /// 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 int? GetMaxLength() + => (int?)this[CoreAnnotationNames.MaxLength]; + /// /// 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 @@ -106,6 +122,15 @@ public virtual void SetMaxLength(int? maxLength) this[CoreAnnotationNames.MaxLength] = maxLength; } + /// + /// 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? IsUnicode() + => (bool?)this[CoreAnnotationNames.Unicode]; + /// /// 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 @@ -115,6 +140,15 @@ public virtual void SetMaxLength(int? maxLength) public virtual void SetIsUnicode(bool? unicode) => this[CoreAnnotationNames.Unicode] = unicode; + /// + /// 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 int? GetPrecision() + => (int?)this[CoreAnnotationNames.Precision]; + /// /// 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 @@ -131,6 +165,15 @@ public virtual void SetPrecision(int? precision) this[CoreAnnotationNames.Precision] = precision; } + /// + /// 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 int? GetScale() + => (int?)this[CoreAnnotationNames.Scale]; + /// /// 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 @@ -147,6 +190,15 @@ public virtual void SetScale(int? scale) this[CoreAnnotationNames.Scale] = scale; } + /// + /// 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? GetProviderClrType() + => (Type?)this[CoreAnnotationNames.ProviderClrType]; + /// /// 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 @@ -156,6 +208,25 @@ public virtual void SetScale(int? scale) public virtual void SetProviderClrType(Type? providerClrType) => this[CoreAnnotationNames.ProviderClrType] = providerClrType; + /// + /// 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 ValueConverter? GetValueConverter() + { + if (_valueConverter != null) + { + return _valueConverter; + } + + var converterType = (Type?)this[CoreAnnotationNames.ValueConverterType]; + return converterType == null + ? null + : _valueConverter = (ValueConverter?)Activator.CreateInstance(converterType); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/RuntimeModel.cs b/src/EFCore/Metadata/RuntimeModel.cs index 9dbebce1b08..5af3cf6b042 100644 --- a/src/EFCore/Metadata/RuntimeModel.cs +++ b/src/EFCore/Metadata/RuntimeModel.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -29,11 +28,14 @@ namespace Microsoft.EntityFrameworkCore.Metadata public class RuntimeModel : AnnotatableBase, IRuntimeModel { private readonly SortedDictionary _entityTypes = new(StringComparer.Ordinal); - private readonly ConcurrentDictionary _indexerPropertyInfoMap = new(); - private readonly ConcurrentDictionary _clrTypeNameMap = new(); private readonly Dictionary> _sharedTypes = new(); + private readonly Dictionary _typeConfigurations = new(); private bool _skipDetectChanges; + private readonly ConcurrentDictionary _indexerPropertyInfoMap = new(); + private readonly ConcurrentDictionary _clrTypeNameMap = new(); + private readonly ConcurrentDictionary _typeMappings = new(); + /// /// Creates a new instance of /// @@ -136,6 +138,42 @@ private IEnumerable FindEntityTypes(Type type) : result; } + /// + /// Adds configuration for a scalar property type. + /// + /// The type of value the property will hold. + /// The maximum length of data that is allowed in this property type. + /// A value indicating whether or not the property can persist Unicode characters. + /// The precision of data that is allowed in this property type. + /// The scale of data that is allowed in this property type. + /// + /// The type that the property value will be converted to before being sent to the database provider. + /// + /// The type of a custom set for this property type. + /// The newly created property. + public virtual RuntimePropertyTypeConfiguration AddPropertyTypeConfiguration( + Type clrType, + int? maxLength = null, + bool? unicode = null, + int? precision = null, + int? scale = null, + Type? providerPropertyType = null, + Type? valueConverterType = null) + { + var typeConfiguration = new RuntimePropertyTypeConfiguration( + clrType, + maxLength, + unicode, + precision, + scale, + providerPropertyType, + valueConverterType); + + _typeConfigurations.Add(clrType, typeConfiguration); + + return typeConfiguration; + } + private string GetDisplayName(Type type) => _clrTypeNameMap.GetOrAdd(type, t => t.DisplayName()); @@ -254,5 +292,21 @@ IEnumerable IModel.FindEntityTypes(Type type) [DebuggerStepThrough] bool IReadOnlyModel.IsShared(Type type) => _sharedTypes.ContainsKey(type); + + /// + IEnumerable IModel.GetPropertyTypeConfigurations() + => _typeConfigurations.Values; + + /// + IEnumerable? IModel.FindPropertyTypeConfigurations(Type propertyType) + => _typeConfigurations.Count == 0 + ? null + : propertyType.GetBaseTypesAndInterfacesInclusive() + .Select(type => _typeConfigurations.GetValueOrDefault(type)!) + .Where(type => type != null); + + /// + CoreTypeMapping? IModel.FindMapping(Type type) + => _typeMappings.GetOrAdd(type, (type, model) => ((IModel)model).GetModelDependencies().TypeMappingSource.FindMapping(type, model), this); } } diff --git a/src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs b/src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs new file mode 100644 index 00000000000..73b23b7f0fd --- /dev/null +++ b/src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a scalar property type. + /// + public sealed class RuntimePropertyTypeConfiguration : AnnotatableBase, IPropertyTypeConfiguration + { + private readonly ValueConverter? _valueConverter; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public RuntimePropertyTypeConfiguration( + Type clrType, + int? maxLength, + bool? unicode, + int? precision, + int? scale, + Type? providerClrType, + Type? valueConverterType) + { + ClrType = clrType; + + if (maxLength != null) + { + SetAnnotation(CoreAnnotationNames.MaxLength, maxLength); + } + + if (unicode != null) + { + SetAnnotation(CoreAnnotationNames.Unicode, unicode); + } + + if (precision != null) + { + SetAnnotation(CoreAnnotationNames.Precision, precision); + } + + if (scale != null) + { + SetAnnotation(CoreAnnotationNames.Scale, scale); + } + + if (providerClrType != null) + { + SetAnnotation(CoreAnnotationNames.ProviderClrType, providerClrType); + } + + if (valueConverterType != null) + { + _valueConverter = (ValueConverter?)Activator.CreateInstance(valueConverterType); + } + } + + /// + /// Gets the type of value that this property-like object holds. + /// + public Type ClrType { get; } + + /// + [DebuggerStepThrough] + int? IPropertyTypeConfiguration.GetMaxLength() => (int?)this[CoreAnnotationNames.MaxLength]; + + /// + [DebuggerStepThrough] + bool? IPropertyTypeConfiguration.IsUnicode() => (bool?)this[CoreAnnotationNames.Unicode]; + + /// + [DebuggerStepThrough] + int? IPropertyTypeConfiguration.GetPrecision() => (int?)this[CoreAnnotationNames.Precision]; + + /// + [DebuggerStepThrough] + int? IPropertyTypeConfiguration.GetScale() => (int?)this[CoreAnnotationNames.Scale]; + + /// + [DebuggerStepThrough] + ValueConverter? IPropertyTypeConfiguration.GetValueConverter() + => _valueConverter; + + /// + [DebuggerStepThrough] + Type? IPropertyTypeConfiguration.GetProviderClrType() + => (Type?)this[CoreAnnotationNames.ProviderClrType]; + } +} diff --git a/src/EFCore/Storage/ITypeMappingSource.cs b/src/EFCore/Storage/ITypeMappingSource.cs index f5e766c5da1..e7cc25b9b4c 100644 --- a/src/EFCore/Storage/ITypeMappingSource.cs +++ b/src/EFCore/Storage/ITypeMappingSource.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -10,12 +9,11 @@ namespace Microsoft.EntityFrameworkCore.Storage { /// /// - /// The core type mapping interface for EF Core, starting with version 2.1. Type mappings describe how a - /// provider maps CLR types/values to database types/values. + /// The core type mapping source. Type mappings describe how a provider maps CLR types/values to database types/values. /// /// /// Warning: do not implement this interface directly. Instead, derive from - /// for non-relational providers, or 'RelationalTypeMappingSourceBase' for relational providers. + /// for non-relational providers, or 'RelationalTypeMappingSource' for relational providers. /// /// /// This type is typically used by database providers (and other extensions). It is generally @@ -56,12 +54,26 @@ public interface ITypeMappingSource /// /// /// Note: Only call this method if there is no - /// or available, otherwise call - /// or + /// or available, otherwise call + /// or /// /// /// The CLR type. /// The type mapping, or if none was found. CoreTypeMapping? FindMapping(Type type); + + /// + /// + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// Note: Only call this method if there is no , + /// otherwise call . + /// + /// + /// The CLR type. + /// The model. + /// The type mapping, or if none was found. + CoreTypeMapping? FindMapping(Type type, IModel model); } } diff --git a/src/EFCore/Storage/TypeMappingInfo.cs b/src/EFCore/Storage/TypeMappingInfo.cs index 20bcfb305ee..99df5fe85cc 100644 --- a/src/EFCore/Storage/TypeMappingInfo.cs +++ b/src/EFCore/Storage/TypeMappingInfo.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. -using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -13,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Storage /// /// Describes metadata needed to decide on a type mapping for a property or type. /// - public readonly struct TypeMappingInfo : IEquatable + public readonly record struct TypeMappingInfo : IEquatable { /// /// Creates a new instance of . @@ -213,68 +211,37 @@ public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) /// /// Indicates whether or not the mapping is part of a key or index. /// - public bool IsKeyOrIndex { get; } + public bool IsKeyOrIndex { get; init; } /// /// Indicates the store-size to use for the mapping, or null if none. /// - public int? Size { get; } + public int? Size { get; init; } /// /// Indicates whether or not the mapping supports Unicode, or null if not defined. /// - public bool? IsUnicode { get; } + public bool? IsUnicode { get; init; } /// /// Indicates whether or not the mapping will be used for a row version, or null if not defined. /// - public bool? IsRowVersion { get; } + public bool? IsRowVersion { get; init; } /// /// The suggested precision of the mapped data type. /// - public int? Precision { get; } + public int? Precision { get; init; } /// /// The suggested scale of the mapped data type. /// - public int? Scale { get; } + public int? Scale { get; init; } /// /// The CLR type in the model. May be null if type information is conveyed via other means /// (e.g. the store name in a relational type mapping info) /// - public Type? ClrType { get; } - - /// - /// Compares this to another to check if they represent the same mapping. - /// - /// The other object. - /// if they represent the same mapping; otherwise. - public bool Equals(TypeMappingInfo other) - => ClrType == other.ClrType - && IsKeyOrIndex == other.IsKeyOrIndex - && Size == other.Size - && IsUnicode == other.IsUnicode - && IsRowVersion == other.IsRowVersion - && Precision == other.Precision - && Scale == other.Scale; - - /// - /// Compares this to another to check if they represent the same mapping. - /// - /// The other object. - /// if they represent the same mapping; otherwise. - public override bool Equals(object? obj) - => obj != null - && obj.GetType() == GetType() - && Equals((TypeMappingInfo)obj); - - /// - /// Returns a hash code for this object. - /// - /// The hash code. - public override int GetHashCode() - => HashCode.Combine(ClrType, IsKeyOrIndex, Size, IsUnicode, IsRowVersion, Scale, Precision); + public Type? ClrType { get; init; } } } diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index 414ddcb2ab2..8ea1c77f792 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Reflection; +using System.Security.Principal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.DependencyInjection; @@ -71,7 +71,15 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) } } - var resolvedMapping = _explicitMappings.GetOrAdd( + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + + ValidateMapping(resolvedMapping, principals?[0]); + + return resolvedMapping; + } + + private CoreTypeMapping? FindMappingWithConversion(TypeMappingInfo mappingInfo, Type? providerClrType, ValueConverter? customConverter) + => _explicitMappings.GetOrAdd( (mappingInfo, providerClrType, customConverter), k => { @@ -128,11 +136,6 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) return mapping; }); - ValidateMapping(resolvedMapping, principals?[0]); - - return resolvedMapping; - } - /// /// /// Finds the type mapping for a given . @@ -155,8 +158,8 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) /// /// /// Note: Only call this method if there is no - /// or available, otherwise call - /// or + /// or available, otherwise call + /// or /// /// /// Note: providers should typically not need to override this method. @@ -167,6 +170,83 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) public override CoreTypeMapping? FindMapping(Type type) => FindMappingWithConversion(new TypeMappingInfo(type), null); + /// + /// + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// Note: Only call this method if there is no , + /// otherwise call . + /// + /// + /// The CLR type. + /// The model. + /// The type mapping, or if none was found. + public override CoreTypeMapping? FindMapping(Type type, IModel model) + { + var typeConfigurations = model.FindPropertyTypeConfigurations(type); + var mappingInfo = new TypeMappingInfo(type); + Type? providerClrType = null; + ValueConverter? customConverter = null; + if (typeConfigurations != null) + { + foreach (var typeConfiguration in typeConfigurations) + { + if (providerClrType == null) + { + var providerType = typeConfiguration.GetProviderClrType(); + if (providerType != null) + { + providerClrType = providerType.UnwrapNullableType(); + } + } + + if (mappingInfo.IsUnicode == null) + { + var isUnicode = typeConfiguration.IsUnicode(); + if (isUnicode != null) + { + mappingInfo = mappingInfo with { IsUnicode = isUnicode }; + } + } + + if (mappingInfo.Scale == null) + { + var scale = typeConfiguration.GetScale(); + if (scale != null) + { + mappingInfo = mappingInfo with { Scale = scale }; + } + } + + if (mappingInfo.Precision == null) + { + var precision = typeConfiguration.GetPrecision(); + if (precision != null) + { + mappingInfo = mappingInfo with { Precision = precision }; + } + } + + if (mappingInfo.Size == null) + { + var size = typeConfiguration.GetMaxLength(); + if (size != null) + { + mappingInfo = mappingInfo with { Size = size }; + } + } + } + + var firstConfiguration = typeConfigurations.FirstOrDefault(); + customConverter = firstConfiguration?.ClrType == type + ? firstConfiguration.GetValueConverter() + : null; + } + + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + } + /// /// /// Finds the type mapping for a given representing diff --git a/src/EFCore/Storage/TypeMappingSourceBase.cs b/src/EFCore/Storage/TypeMappingSourceBase.cs index d8d4ca2d847..0954eb6777d 100644 --- a/src/EFCore/Storage/TypeMappingSourceBase.cs +++ b/src/EFCore/Storage/TypeMappingSourceBase.cs @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Storage { /// /// - /// The base class for non-relational type mapping starting with version 2.1. Non-relational providers + /// The base class for non-relational type mapping source. Non-relational providers /// should derive from this class and override /// /// @@ -98,16 +98,27 @@ protected virtual void ValidateMapping( /// /// /// Note: Only call this method if there is no - /// or available, otherwise call - /// or + /// or available, otherwise call + /// or /// + /// + /// The CLR type. + /// The type mapping, or if none was found. + public abstract CoreTypeMapping? FindMapping(Type type); + + /// /// - /// Note: providers should typically not need to override this method. + /// Finds the type mapping for a given , taking pre-convention configuration into the account. + /// + /// + /// Note: Only call this method if there is no , + /// otherwise call . /// /// /// The CLR type. + /// The model. /// The type mapping, or if none was found. - public abstract CoreTypeMapping? FindMapping(Type type); + public abstract CoreTypeMapping? FindMapping(Type type, IModel model); /// /// diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index ccc73beb4ac..d8264295efb 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -347,6 +347,40 @@ public static IEnumerable GetBaseTypes(this Type type) } } + public static List GetBaseTypesAndInterfacesInclusive(this Type type) + { + var baseTypes = new List(); + var typesToProcess = new Queue(); + typesToProcess.Enqueue(type); + + while (typesToProcess.Count > 0) + { + type = typesToProcess.Dequeue(); + baseTypes.Add(type); + + if (!type.IsGenericTypeDefinition + && !type.IsInterface) + { + if (type.BaseType != null) + { + typesToProcess.Enqueue(type.BaseType); + } + + if (type.IsConstructedGenericType) + { + typesToProcess.Enqueue(type.GetGenericTypeDefinition()); + } + + foreach (var @interface in GetDeclaredInterfaces(type)) + { + typesToProcess.Enqueue(@interface); + } + } + } + + return baseTypes; + } + public static IEnumerable GetTypesInHierarchy(this Type type) { var currentType = type; @@ -359,6 +393,18 @@ public static IEnumerable GetTypesInHierarchy(this Type type) } } + public static IEnumerable GetDeclaredInterfaces(this Type type) + { + var interfaces = type.GetInterfaces(); + if (type.BaseType == typeof(object) + || type.BaseType == null) + { + return interfaces; + } + + return interfaces.Except(type.BaseType.GetInterfaces()); + } + public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[]? types) { types ??= Array.Empty(); diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs index 7aa5611eed5..3d4e0a50cbf 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -34,7 +33,7 @@ public void Does_type_mapping_from_string_with_no_MaxLength() [ConditionalFact] public void Does_type_mapping_from_string_with_MaxLength() { - var mapping = GetTypeMapping(typeof(string), 666); + var mapping = GetTypeMapping(typeof(string), maxLength: 666); Assert.Equal("just_string(666)", mapping.StoreType); Assert.Equal(666, mapping.Size); @@ -43,7 +42,7 @@ public void Does_type_mapping_from_string_with_MaxLength() [ConditionalFact] public void Does_type_mapping_from_string_with_MaxLength_greater_than_unbounded_max() { - var mapping = GetTypeMapping(typeof(string), 2020); + var mapping = GetTypeMapping(typeof(string), maxLength: 2020); Assert.Equal("just_string(2020)", mapping.StoreType); Assert.Equal(2020, mapping.Size); @@ -60,7 +59,7 @@ public void Does_type_mapping_from_btye_array_with_no_MaxLength() [ConditionalFact] public void Does_type_mapping_from_btye_array_with_MaxLength() { - var mapping = GetTypeMapping(typeof(byte[]), 777); + var mapping = GetTypeMapping(typeof(byte[]), maxLength: 777); Assert.Equal("just_binary(777)", mapping.StoreType); Assert.Equal(777, mapping.Size); @@ -69,38 +68,27 @@ public void Does_type_mapping_from_btye_array_with_MaxLength() [ConditionalFact] public void Does_type_mapping_from_btye_array_greater_than_unbounded_max() { - var mapping = GetTypeMapping(typeof(byte[]), 2020); + var mapping = GetTypeMapping(typeof(byte[]), maxLength: 2020); Assert.Equal("just_binary(2020)", mapping.StoreType); } - private RelationalTypeMapping GetTypeMapping(Type propertyType, int? maxLength = null) - { - var property = CreateEntityType().AddProperty("MyProp", propertyType); - if (maxLength.HasValue) - { - property.SetMaxLength(maxLength); - } - - return GetMapping((IProperty)property); - } - [ConditionalFact] public void Does_simple_mapping_from_name() { - Assert.Equal("int", GetNamedMapping(typeof(int), "int").StoreType); + Assert.Equal("int", GetTypeMapping(typeof(int), storeTypeName: "int").StoreType); } [ConditionalFact] public void Does_default_mapping_for_unrecognized_store_type() { - Assert.Equal("int", GetNamedMapping(typeof(int), "int").StoreType); + Assert.Equal("int", GetTypeMapping(typeof(int), storeTypeName: "int").StoreType); } [ConditionalFact] public void Does_type_mapping_from_named_string_with_no_MaxLength() { - var mapping = GetNamedMapping(typeof(string), "some_string(max)"); + var mapping = GetTypeMapping(typeof(string), storeTypeName: "some_string(max)"); Assert.Equal("some_string(max)", mapping.StoreType); } @@ -108,7 +96,7 @@ public void Does_type_mapping_from_named_string_with_no_MaxLength() [ConditionalFact] public void Does_type_mapping_from_named_string_with_MaxLength() { - var mapping = GetNamedMapping(typeof(string), "some_string(666)"); + var mapping = GetTypeMapping(typeof(string), storeTypeName: "some_string(666)"); Assert.Equal("(666)some_string", mapping.StoreType); Assert.Equal(666, mapping.Size); @@ -117,24 +105,16 @@ public void Does_type_mapping_from_named_string_with_MaxLength() [ConditionalFact] public void Does_type_mapping_from_named_binary_with_no_MaxLength() { - var mapping = GetNamedMapping(typeof(byte[]), "some_binary(max)"); + var mapping = GetTypeMapping(typeof(byte[]), storeTypeName: "some_binary(max)"); Assert.Equal("some_binary(max)", mapping.StoreType); } - private RelationalTypeMapping GetNamedMapping(Type propertyType, string typeName) - { - var property = CreateEntityType().AddProperty("MyProp", propertyType); - property.SetColumnType(typeName); - - return GetMapping((IProperty)property); - } - [ConditionalFact] public void Key_with_store_type_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "money", @@ -149,7 +129,7 @@ public void Key_with_store_type_is_picked_up_by_FK() public void Does_default_type_mapping_from_decimal() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "default_decimal_mapping", @@ -160,7 +140,7 @@ public void Does_default_type_mapping_from_decimal() public void Does_type_mapping_from_decimal_with_precision_only() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "decimal_mapping(16)", @@ -171,31 +151,100 @@ public void Does_type_mapping_from_decimal_with_precision_only() public void Does_type_mapping_from_decimal_with_precision_and_scale() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "decimal_mapping(18,7)", GetMapping(mapper, model.FindEntityType(typeof(MyPrecisionType)).FindProperty("PrecisionAndScale")).StoreType); } - private static IRelationalTypeMappingSource CreateTestTypeMapper() + [ConditionalFact] + public void Does_type_mapping_from_string_with_configuration() + { + var mapping = GetTypeMapping(typeof(string), + maxLength: 666, + precision: 66, + scale: 6, + unicode: false, + fixedLength: true, + useConfiguration: true); + + Assert.Equal("ansi_string_fixed(666)", mapping.StoreType); + Assert.Equal("ansi_string_fixed", mapping.StoreTypeNameBase); + Assert.Equal(666, mapping.Size); + Assert.Null(mapping.Precision); + Assert.Null(mapping.Scale); + Assert.False(mapping.IsUnicode); + Assert.True(mapping.IsFixedLength); + } + + [ConditionalFact] + public void Does_type_mapping_from_string_type_with_configuration() + { + var mapping = GetTypeMapping(typeof(string), + storeTypeName: "ansi_string_fixed(666)", + useConfiguration: true); + + Assert.Equal("ansi_string_fixed(666)", mapping.StoreType); + Assert.Equal("ansi_string_fixed", mapping.StoreTypeNameBase); + Assert.Equal(666, mapping.Size); + Assert.Null(mapping.Precision); + Assert.Null(mapping.Scale); + Assert.False(mapping.IsUnicode); + } + + [ConditionalFact] + public void Does_type_mapping_from_decimal_with_configuration() + { + var mapping = GetTypeMapping(typeof(decimal), + maxLength: 666, + precision: 66, + scale: 6, + unicode: false, + fixedLength: true, + useConfiguration: true); + + Assert.Equal("decimal_mapping(66,6)", mapping.StoreType); + Assert.Equal("decimal_mapping", mapping.StoreTypeNameBase); + Assert.Null(mapping.Size); + Assert.Equal(66, mapping.Precision); + Assert.Equal(6, mapping.Scale); + Assert.False(mapping.IsUnicode); + Assert.False(mapping.IsFixedLength); + } + + [ConditionalFact] + public void Does_type_mapping_from_decimal_type_with_configuration() + { + var mapping = GetTypeMapping(typeof(decimal), + storeTypeName: "decimal_mapping(66,6)", + useConfiguration: true); + + Assert.Equal("decimal_mapping(66,6)", mapping.StoreType); + Assert.Equal("decimal_mapping", mapping.StoreTypeNameBase); + Assert.Null(mapping.Size); + Assert.Equal(66, mapping.Precision); + Assert.Equal(6, mapping.Scale); + Assert.False(mapping.IsUnicode); + Assert.False(mapping.IsFixedLength); + } + + protected override IRelationalTypeMappingSource CreateRelationalTypeMappingSource() => new TestRelationalTypeMappingSource( TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()); - public static RelationalTypeMapping GetMapping( - Type type) - => CreateTestTypeMapper().FindMapping(type); + public RelationalTypeMapping GetMapping(Type type) + => CreateRelationalTypeMappingSource().FindMapping(type); - public static RelationalTypeMapping GetMapping( - IProperty property) - => CreateTestTypeMapper().FindMapping(property); + public RelationalTypeMapping GetMapping(IProperty property) + => CreateRelationalTypeMappingSource().FindMapping(property); [ConditionalFact] public void String_key_with_max_fixed_length_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "just_string_fixed(200)", @@ -210,7 +259,7 @@ public void String_key_with_max_fixed_length_is_picked_up_by_FK() public void Binary_key_with_max_fixed_length_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "just_binary_fixed(100)", @@ -225,7 +274,7 @@ public void Binary_key_with_max_fixed_length_is_picked_up_by_FK() public void String_key_with_unicode_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "ansi_string(900)", @@ -240,7 +289,7 @@ public void String_key_with_unicode_is_picked_up_by_FK() public void Key_store_type_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "money", @@ -255,7 +304,7 @@ public void Key_store_type_is_preferred_if_specified() public void String_FK_max_length_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "just_string_fixed(200)", @@ -270,7 +319,7 @@ public void String_FK_max_length_is_preferred_if_specified() public void Binary_FK_max_length_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "just_binary_fixed(100)", @@ -285,7 +334,7 @@ public void Binary_FK_max_length_is_preferred_if_specified() public void String_FK_unicode_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTestTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "ansi_string(900)", @@ -301,7 +350,7 @@ public static RelationalTypeMapping GetMapping( IProperty property) => typeMappingSource.FindMapping(property); - protected override ModelBuilder CreateModelBuilder() - => RelationalTestHelpers.Instance.CreateConventionBuilder(); + protected override ModelBuilder CreateModelBuilder(Action configure = null) + => RelationalTestHelpers.Instance.CreateConventionBuilder(configure: configure); } } diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs index 1887e4d4476..6bc720c65a3 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs @@ -28,7 +28,118 @@ protected IMutableEntityType CreateEntityType() protected IModel CreateModel() => CreateEntityType().Model.FinalizeModel(); - protected abstract ModelBuilder CreateModelBuilder(); + protected RelationalTypeMapping GetTypeMapping( + Type propertyType, + bool? nullable = null, + int? maxLength = null, + int? precision = null, + int? scale = null, + Type providerType = null, + bool? unicode = null, + bool? fixedLength = null, + string storeTypeName = null, + bool useConfiguration = false) + { + if (useConfiguration) + { + var model = CreateModelBuilder(c => + { + var properties = c.Properties(propertyType); + + if (maxLength.HasValue) + { + properties.HaveMaxLength(maxLength.Value); + } + + if (precision.HasValue) + { + if (scale.HasValue) + { + properties.HavePrecision(precision.Value, scale.Value); + } + else + { + properties.HavePrecision(precision.Value); + } + } + + if (providerType != null) + { + properties.HaveConversion(providerType); + } + + if (unicode.HasValue) + { + properties.AreUnicode(unicode.Value); + } + + if (fixedLength.HasValue) + { + properties.AreFixedLength(fixedLength.Value); + } + + if (storeTypeName != null) + { + properties.HaveColumnType(storeTypeName); + } + }).FinalizeModel(); + + return (RelationalTypeMapping)model.FindMapping(propertyType); + } + else + { + var modelBuilder = CreateModelBuilder(); + var entityType = modelBuilder.Entity(); + entityType.Property(e => e.Id); + var property = entityType.Property(propertyType, "MyProp").Metadata; + + if (nullable.HasValue) + { + property.IsNullable = nullable.Value; + } + + if (maxLength.HasValue) + { + property.SetMaxLength(maxLength); + } + + if (precision.HasValue) + { + property.SetPrecision(precision); + } + + if (scale.HasValue) + { + property.SetScale(scale); + } + + if (providerType != null) + { + property.SetProviderClrType(providerType); + } + + if (unicode.HasValue) + { + property.SetIsUnicode(unicode); + } + + if (fixedLength.HasValue) + { + property.SetIsFixedLength(fixedLength); + } + + if (storeTypeName != null) + { + property.SetColumnType(storeTypeName); + } + + var model = modelBuilder.Model.FinalizeModel(); + return CreateRelationalTypeMappingSource().GetMapping(model.FindEntityType(typeof(MyType)).FindProperty(property.Name)); + } + } + + protected abstract ModelBuilder CreateModelBuilder(Action configure = null); + protected abstract IRelationalTypeMappingSource CreateRelationalTypeMappingSource(); protected class MyType { diff --git a/test/EFCore.Relational.Tests/TestUtilities/TestRelationalTypeMappingSource.cs b/test/EFCore.Relational.Tests/TestUtilities/TestRelationalTypeMappingSource.cs index e7716c56f0d..7e0cfbe5c32 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/TestRelationalTypeMappingSource.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/TestRelationalTypeMappingSource.cs @@ -254,6 +254,11 @@ protected override string ParseStoreTypeName( scale = 0; } + if (storeTypeName?.StartsWith("ansi_string", StringComparison.OrdinalIgnoreCase) == true) + { + unicode = false; + } + return parsedName; } } diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs index 5051001af14..a7f30a6a638 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs @@ -149,7 +149,7 @@ public void Does_non_key_SQL_Server_string_mapping(bool? unicode, bool? fixedLen [InlineData(null, null)] public void Does_non_key_SQL_Server_string_mapping_with_value_that_fits_max_length(bool? unicode, bool? fixedLength) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: fixedLength); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(3)", typeMapping.StoreType); @@ -166,7 +166,7 @@ public void Does_non_key_SQL_Server_string_mapping_with_value_that_fits_max_leng [InlineData(null, null)] public void Does_non_key_SQL_Server_string_mapping_with_max_length(bool? unicode, bool? fixedLength) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: fixedLength); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(3)", typeMapping.StoreType); @@ -181,7 +181,7 @@ public void Does_non_key_SQL_Server_string_mapping_with_max_length(bool? unicode [InlineData(null)] public void Does_non_key_SQL_Server_fixed_string_mapping_with_max_length_large_value(bool? unicode) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength: true); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: true); Assert.Equal(DbType.StringFixedLength, typeMapping.DbType); Assert.Equal("nchar(3)", typeMapping.StoreType); @@ -199,7 +199,7 @@ public void Does_non_key_SQL_Server_fixed_string_mapping_with_max_length_large_v [InlineData(null)] public void Does_non_key_SQL_Server_fixed_string_mapping_with_max_length_small_value(bool? unicode) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength: true); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: true); Assert.Equal(DbType.StringFixedLength, typeMapping.DbType); Assert.Equal("nchar(3)", typeMapping.StoreType); @@ -217,7 +217,7 @@ public void Does_non_key_SQL_Server_fixed_string_mapping_with_max_length_small_v [InlineData(null)] public void Does_non_key_SQL_Server_fixed_string_mapping_with_max_length_exact_value(bool? unicode) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength: true); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: true); Assert.Equal(DbType.StringFixedLength, typeMapping.DbType); Assert.Equal("nchar(3)", typeMapping.StoreType); @@ -254,7 +254,7 @@ public void Does_non_key_SQL_Server_string_mapping_with_long_string(bool? unicod [InlineData(null, null)] public void Does_non_key_SQL_Server_string_mapping_with_max_length_with_long_string(bool? unicode, bool? fixedLength) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode, fixedLength); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: unicode, fixedLength: fixedLength); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(3)", typeMapping.StoreType); @@ -294,7 +294,7 @@ public void Does_key_SQL_Server_string_mapping(bool? unicode, bool? fixedLength) property.SetIsFixedLength(fixedLength); property.DeclaringEntityType.SetPrimaryKey(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -319,7 +319,7 @@ public void Does_foreign_key_SQL_Server_string_mapping(bool? unicode, bool? fixe var pk = property.DeclaringEntityType.SetPrimaryKey(property); property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -345,7 +345,7 @@ public void Does_required_foreign_key_SQL_Server_string_mapping(bool? unicode, b property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); fkProperty.IsNullable = false; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -368,7 +368,7 @@ public void Does_indexed_column_SQL_Server_string_mapping(bool? unicode, bool? f property.SetIsFixedLength(fixedLength); entityType.AddIndex(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -391,7 +391,7 @@ public void Does_IndexAttribute_column_SQL_Server_string_mapping(bool? unicode, property.SetIsFixedLength(fixedLength); entityType.Model.FinalizeModel(); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Null(typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -421,7 +421,7 @@ public void Does_non_key_SQL_Server_string_mapping_ansi(bool? fixedLength) [InlineData(null)] public void Does_non_key_SQL_Server_string_mapping_for_value_that_fits_with_max_length_ansi(bool? fixedLength) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: false, fixedLength); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: false, fixedLength: fixedLength); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(3)", typeMapping.StoreType); @@ -514,7 +514,7 @@ public void Does_non_key_SQL_Server_string_mapping_with_long_string_ansi(bool? f [InlineData(null)] public void Does_non_key_SQL_Server_string_mapping_with_max_length_with_long_string_ansi(bool? fixedLength) { - var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: false, fixedLength); + var typeMapping = GetTypeMapping(typeof(string), null, 3, unicode: false, fixedLength: fixedLength); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(3)", typeMapping.StoreType); @@ -550,7 +550,7 @@ public void Does_key_SQL_Server_string_mapping_ansi(bool? fixedLength) property.SetIsFixedLength(fixedLength); property.DeclaringEntityType.SetPrimaryKey(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); @@ -573,7 +573,7 @@ public void Does_foreign_key_SQL_Server_string_mapping_ansi(bool? fixedLength) var pk = property.DeclaringEntityType.SetPrimaryKey(property); property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); @@ -597,7 +597,7 @@ public void Does_required_foreign_key_SQL_Server_string_mapping_ansi(bool? fixed property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); fkProperty.IsNullable = false; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); @@ -618,7 +618,7 @@ public void Does_indexed_column_SQL_Server_string_mapping_ansi(bool? fixedLength property.SetIsFixedLength(fixedLength); entityType.AddIndex(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); @@ -639,7 +639,7 @@ public void Does_IndexAttribute_column_SQL_Server_string_mapping_ansi(bool? fixe property.SetIsFixedLength(fixedLength); entityType.Model.FinalizeModel(); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); @@ -809,7 +809,7 @@ private RelationalTypeMapping CreateBinaryMapping(string typeName, int? maxLengt property.SetMaxLength(maxLength); } - return CreateTypeMapper().GetMapping((IProperty)property); + return CreateRelationalTypeMappingSource().GetMapping((IProperty)property); } [ConditionalTheory] @@ -822,7 +822,7 @@ public void Does_key_SQL_Server_binary_mapping(bool? fixedLength) property.SetIsFixedLength(fixedLength); property.DeclaringEntityType.SetPrimaryKey(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.Binary, typeMapping.DbType); Assert.Equal("varbinary(900)", typeMapping.StoreType); @@ -842,7 +842,7 @@ public void Does_foreign_key_SQL_Server_binary_mapping(bool? fixedLength) var pk = property.DeclaringEntityType.SetPrimaryKey(property); property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.False(typeMapping.IsFixedLength); Assert.Equal(DbType.Binary, typeMapping.DbType); @@ -863,7 +863,7 @@ public void Does_required_foreign_key_SQL_Server_binary_mapping(bool? fixedLengt property.DeclaringEntityType.AddForeignKey(fkProperty, pk, property.DeclaringEntityType); fkProperty.IsNullable = false; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)fkProperty); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)fkProperty); Assert.False(typeMapping.IsFixedLength); Assert.Equal(DbType.Binary, typeMapping.DbType); @@ -881,7 +881,7 @@ public void Does_indexed_column_SQL_Server_binary_mapping(bool? fixedLength) property.SetIsFixedLength(fixedLength); entityType.AddIndex(property); - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.False(typeMapping.IsFixedLength); Assert.Equal(DbType.Binary, typeMapping.DbType); @@ -896,7 +896,7 @@ public void Does_non_key_SQL_Server_rowversion_mapping() property.IsConcurrencyToken = true; property.ValueGenerated = ValueGenerated.OnAddOrUpdate; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.Binary, typeMapping.DbType); Assert.Equal("rowversion", typeMapping.StoreType); @@ -913,7 +913,7 @@ public void Does_non_key_SQL_Server_required_rowversion_mapping() property.ValueGenerated = ValueGenerated.OnAddOrUpdate; property.IsNullable = false; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.Binary, typeMapping.DbType); Assert.Equal("rowversion", typeMapping.StoreType); @@ -928,74 +928,42 @@ public void Does_not_do_rowversion_mapping_for_non_computed_concurrency_tokens() var property = CreateEntityType().AddProperty("MyProp", typeof(byte[])); property.IsConcurrencyToken = true; - var typeMapping = CreateTypeMapper().GetMapping((IProperty)property); + var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); Assert.Equal(DbType.Binary, typeMapping.DbType); Assert.False(typeMapping.IsFixedLength); Assert.Equal("varbinary(max)", typeMapping.StoreType); } - private RelationalTypeMapping GetTypeMapping( - Type propertyType, - bool? nullable = null, - int? maxLength = null, - bool? unicode = null, - bool? fixedLength = null) - { - var property = CreateEntityType().AddProperty("MyProp", propertyType); - - if (nullable.HasValue) - { - property.IsNullable = nullable.Value; - } - - if (maxLength.HasValue) - { - property.SetMaxLength(maxLength); - } - - if (unicode.HasValue) - { - property.SetIsUnicode(unicode); - } - - if (fixedLength.HasValue) - { - property.SetIsFixedLength(fixedLength); - } - - return CreateTypeMapper().GetMapping((IProperty)property); - } - [ConditionalFact] public void Does_default_mappings_for_sequence_types() { - Assert.Equal("int", CreateTypeMapper().GetMapping(typeof(int)).StoreType); - Assert.Equal("smallint", CreateTypeMapper().GetMapping(typeof(short)).StoreType); - Assert.Equal("bigint", CreateTypeMapper().GetMapping(typeof(long)).StoreType); - Assert.Equal("tinyint", CreateTypeMapper().GetMapping(typeof(byte)).StoreType); + Assert.Equal("int", CreateRelationalTypeMappingSource().GetMapping(typeof(int)).StoreType); + Assert.Equal("smallint", CreateRelationalTypeMappingSource().GetMapping(typeof(short)).StoreType); + Assert.Equal("bigint", CreateRelationalTypeMappingSource().GetMapping(typeof(long)).StoreType); + Assert.Equal("tinyint", CreateRelationalTypeMappingSource().GetMapping(typeof(byte)).StoreType); } [ConditionalFact] public void Does_default_mappings_for_strings_and_byte_arrays() { - Assert.Equal("nvarchar(max)", CreateTypeMapper().GetMapping(typeof(string)).StoreType); - Assert.Equal("varbinary(max)", CreateTypeMapper().GetMapping(typeof(byte[])).StoreType); + Assert.Equal("nvarchar(max)", CreateRelationalTypeMappingSource().GetMapping(typeof(string)).StoreType); + Assert.Equal("varbinary(max)", CreateRelationalTypeMappingSource().GetMapping(typeof(byte[])).StoreType); } [ConditionalFact] public void Does_default_mappings_for_values() { - Assert.Equal("nvarchar(max)", CreateTypeMapper().GetMappingForValue("Cheese").StoreType); - Assert.Equal("varbinary(max)", CreateTypeMapper().GetMappingForValue(new byte[1]).StoreType); - Assert.Equal("datetime2", CreateTypeMapper().GetMappingForValue(new DateTime()).StoreType); + Assert.Equal("nvarchar(max)", CreateRelationalTypeMappingSource().GetMappingForValue("Cheese").StoreType); + Assert.Equal("varbinary(max)", CreateRelationalTypeMappingSource().GetMappingForValue(new byte[1]).StoreType); + Assert.Equal("datetime2", CreateRelationalTypeMappingSource().GetMappingForValue(new DateTime()).StoreType); } [ConditionalFact] public void Does_default_mappings_for_null_values() { - Assert.Equal("NULL", CreateTypeMapper().GetMappingForValue(null).StoreType); - Assert.Equal("NULL", CreateTypeMapper().GetMappingForValue(DBNull.Value).StoreType); + Assert.Equal("NULL", CreateRelationalTypeMappingSource().GetMappingForValue(null).StoreType); + Assert.Equal("NULL", CreateRelationalTypeMappingSource().GetMappingForValue(DBNull.Value).StoreType); } [ConditionalFact] @@ -1003,14 +971,14 @@ public void Throws_for_unrecognized_property_types() { var property = ((IMutableModel)new Model()).AddEntityType("Entity1") .AddProperty("Strange", typeof(object)); - var ex = Assert.Throws(() => CreateTypeMapper().GetMapping((IProperty)property)); + var ex = Assert.Throws(() => CreateRelationalTypeMappingSource().GetMapping((IProperty)property)); Assert.Equal(RelationalStrings.UnsupportedPropertyType("Entity1 (Dictionary)", "Strange", "object"), ex.Message); Assert.Equal(RelationalStrings.UnsupportedType("object"), - Assert.Throws(() => CreateTypeMapper().GetMapping(typeof(object))).Message); + Assert.Throws(() => CreateRelationalTypeMappingSource().GetMapping(typeof(object))).Message); Assert.Equal(RelationalStrings.UnsupportedStoreType("object"), - Assert.Throws(() => CreateTypeMapper().GetMapping("object")).Message); + Assert.Throws(() => CreateRelationalTypeMappingSource().GetMapping("object")).Message); } [ConditionalTheory] @@ -1064,7 +1032,7 @@ public void Throws_for_unrecognized_property_types() [InlineData("VARCHAR(max)", typeof(string), null, false, false, "VARCHAR(max)")] public void Can_map_by_type_name(string typeName, Type type, int? size, bool unicode, bool fixedLength, string expectedType = null) { - var mapping = CreateTypeMapper().FindMapping(typeName); + var mapping = CreateRelationalTypeMappingSource().FindMapping(typeName); Assert.Equal(type, mapping.ClrType); Assert.Equal(size, mapping.Size); @@ -1096,7 +1064,7 @@ public void Can_map_string_base_type_name_and_size(string typeName) .HasMaxLength(2018) .Metadata; - var mapping = CreateTypeMapper().FindMapping((IProperty)property); + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); Assert.Same(typeof(string), mapping.ClrType); Assert.Equal(2018, mapping.Size); @@ -1119,7 +1087,7 @@ public void Can_map_binary_base_type_name_and_size(string typeName) .HasMaxLength(2018) .Metadata; - var mapping = CreateTypeMapper().FindMapping((IProperty)property); + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); Assert.Same(typeof(byte[]), mapping.ClrType); Assert.Equal(2018, mapping.Size); @@ -1138,7 +1106,7 @@ private class StringCheese public void Key_with_store_type_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "money", @@ -1153,7 +1121,7 @@ public void Key_with_store_type_is_picked_up_by_FK() public void String_key_with_max_fixed_length_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "nchar(200)", @@ -1168,7 +1136,7 @@ public void String_key_with_max_fixed_length_is_picked_up_by_FK() public void Binary_key_with_max_fixed_length_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "binary(100)", @@ -1183,7 +1151,7 @@ public void Binary_key_with_max_fixed_length_is_picked_up_by_FK() public void String_key_with_unicode_is_picked_up_by_FK() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "varchar(900)", @@ -1198,7 +1166,7 @@ public void String_key_with_unicode_is_picked_up_by_FK() public void Key_store_type_if_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "money", @@ -1213,7 +1181,7 @@ public void Key_store_type_if_preferred_if_specified() public void String_FK_max_length_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "nchar(200)", @@ -1228,7 +1196,7 @@ public void String_FK_max_length_is_preferred_if_specified() public void Binary_FK_max_length_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "binary(100)", @@ -1243,7 +1211,7 @@ public void Binary_FK_max_length_is_preferred_if_specified() public void String_FK_unicode_is_preferred_if_specified() { var model = CreateModel(); - var mapper = CreateTypeMapper(); + var mapper = CreateRelationalTypeMappingSource(); Assert.Equal( "varchar(900)", @@ -1273,7 +1241,7 @@ public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInf => new StringTypeMapping("datetime2"); } - private static IRelationalTypeMappingSource CreateTypeMapper() + protected override IRelationalTypeMappingSource CreateRelationalTypeMappingSource() => new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()); @@ -1294,8 +1262,8 @@ private enum ByteEnum : byte { } - protected override ModelBuilder CreateModelBuilder() - => SqlServerTestHelpers.Instance.CreateConventionBuilder(); + protected override ModelBuilder CreateModelBuilder(Action configure = null) + => SqlServerTestHelpers.Instance.CreateConventionBuilder(configure: configure); private class TestParameter : DbParameter { diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index a4108138432..3eb4e314bca 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1,12 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.Dynamic; -using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -934,7 +931,10 @@ private static ExpandoObject DeserializeExpandoObject(string value) public virtual void IEnumerable_properties_can_have_value_converter_configured_by_type() { var modelBuilder = CreateModelBuilder(c => - c.Properties().HaveConversion(typeof(ExpandoObjectConverter), typeof(ExpandoObjectComparer))); + { + c.Properties>().HaveMaxLength(20); + c.Properties().HaveConversion(typeof(ExpandoObjectConverter), typeof(ExpandoObjectComparer)); + }); modelBuilder.Entity(); @@ -942,9 +942,26 @@ public virtual void IEnumerable_properties_can_have_value_converter_configured_b var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); var expandoProperty = entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)); + Assert.Equal(20, expandoProperty.GetMaxLength()); Assert.IsType(expandoProperty.GetValueConverter()); Assert.IsType(expandoProperty.GetValueComparer()); } + + [ConditionalFact] + public virtual void Value_converter_configured_on_base_type_is_not_applied() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties>().HaveConversion(typeof(ExpandoObjectConverter), typeof(ExpandoObjectComparer)); + }); + + modelBuilder.Entity(); + + Assert.Equal(CoreStrings.PropertyNotMapped( + nameof(DynamicProperty), nameof(DynamicProperty.ExpandoObject), nameof(ExpandoObject)), + Assert.Throws(() => modelBuilder.FinalizeModel()).Message); + } + private class ExpandoObjectConverter : ValueConverter { public ExpandoObjectConverter()