Skip to content

Commit

Permalink
Fix relational mapping hints and add DbType
Browse files Browse the repository at this point in the history
Fixes #29966
Fixes #24771
  • Loading branch information
ajcvickers committed Jan 3, 2023
1 parent 7a7754f commit 7dc248b
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/EFCore.Relational/Storage/RelationalTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public RelationalTypeMappingParameters WithTypeMappingInfo(in RelationalTypeMapp
CoreParameters,
mappingInfo.StoreTypeName ?? StoreType,
StoreTypePostfix,
DbType,
mappingInfo.DbType ?? DbType,
mappingInfo.IsUnicode ?? Unicode,
mappingInfo.Size ?? Size,
mappingInfo.IsFixedLength ?? FixedLength,
Expand Down
42 changes: 38 additions & 4 deletions src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +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.Data;

namespace Microsoft.EntityFrameworkCore.Storage;

/// <summary>
Expand Down Expand Up @@ -33,7 +35,7 @@ public RelationalTypeMappingInfo(IProperty property)
/// <param name="fallbackUnicode">
/// Specifies Unicode or ANSI for the mapping or <see langword="null" /> for default.
/// </param>
/// <param name="fixedLength">Specifies a fixed length mapping, or <see langword="null" /> for default.</param>
/// <param name="fallbackFixedLength">Specifies a fixed length mapping, or <see langword="null" /> for default.</param>
/// <param name="fallbackSize">
/// Specifies a size for the mapping, in case one isn't found at the core level, or <see langword="null" /> for default.
/// </param>
Expand All @@ -48,17 +50,43 @@ public RelationalTypeMappingInfo(
string? storeTypeName = null,
string? storeTypeNameBase = null,
bool? fallbackUnicode = null,
bool? fixedLength = null,
bool? fallbackFixedLength = null,
int? fallbackSize = null,
int? fallbackPrecision = null,
int? fallbackScale = null)
{
_coreTypeMappingInfo = new TypeMappingInfo(principals, fallbackUnicode, fallbackSize, fallbackPrecision, fallbackScale);

IsFixedLength = fixedLength;
ValueConverter? customConverter = null;
for (var i = 0; i < principals.Count; i++)
{
var principal = principals[i];
if (customConverter == null)
{
var converter = principal.GetValueConverter();
if (converter != null)
{
customConverter = converter;
}
}

if (fallbackFixedLength == null)
{
var fixedLength = principal.IsFixedLength();
if (fixedLength != null)
{
fallbackFixedLength = fixedLength;
}
}
}

var mappingHints = customConverter?.MappingHints;

IsFixedLength = fallbackFixedLength ?? (mappingHints as RelationalConverterMappingHints)?.IsFixedLength;
DbType = (mappingHints as RelationalConverterMappingHints)?.DbType;
StoreTypeName = storeTypeName;
StoreTypeNameBase = storeTypeNameBase;
}
}

/// <summary>
/// Creates a new instance of <see cref="RelationalTypeMappingInfo" />.
Expand Down Expand Up @@ -132,6 +160,7 @@ public RelationalTypeMappingInfo(
StoreTypeName = source.StoreTypeName;
StoreTypeNameBase = source.StoreTypeNameBase;
IsFixedLength = source.IsFixedLength ?? (mappingHints as RelationalConverterMappingHints)?.IsFixedLength;
DbType = source.DbType ?? (mappingHints as RelationalConverterMappingHints)?.DbType;
}

/// <summary>
Expand Down Expand Up @@ -208,6 +237,11 @@ public int? Scale
/// </summary>
public bool? IsFixedLength { get; init; }

/// <summary>
/// The <see cref="DbType"/> of the mapping.
/// </summary>
public DbType? DbType { get; init; }

/// <summary>
/// Indicates whether or not the mapping is part of a key or index.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +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.Data;

namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion;

/// <summary>
Expand All @@ -21,24 +23,45 @@ public class RelationalConverterMappingHints : ConverterMappingHints
/// <param name="unicode">Whether or not the mapped data type should support Unicode.</param>
/// <param name="fixedLength">Whether or not the mapped data type is fixed length.</param>
/// <param name="valueGeneratorFactory">An optional factory for creating a specific <see cref="ValueGenerator" />.</param>
/// <param name="dbType">The suggested <see cref="DbType"/>.</param>
public RelationalConverterMappingHints(
int? size = null,
int? precision = null,
int? scale = null,
bool? unicode = null,
bool? fixedLength = null,
Func<IProperty, IEntityType, ValueGenerator>? valueGeneratorFactory = null)
Func<IProperty, IEntityType, ValueGenerator>? valueGeneratorFactory = null,
DbType? dbType = null)
: base(size, precision, scale, unicode, valueGeneratorFactory)
{
IsFixedLength = fixedLength;
DbType = dbType;
}

/// <summary>
/// Adds hints from the given object to this one. Hints that are already specified are
/// not overridden.
/// Creates a new <see cref="ConverterMappingHints" /> instance. Any hint contained in the instance
/// can be <see langword="null" /> to indicate it has not been specified.
/// </summary>
/// <param name="hints">The hints to add.</param>
/// <returns>The combined hints.</returns>
/// <param name="size">The suggested size of the mapped data type.</param>
/// <param name="precision">The suggested precision of the mapped data type.</param>
/// <param name="scale">The suggested scale of the mapped data type.</param>
/// <param name="unicode">Whether or not the mapped data type should support Unicode.</param>
/// <param name="fixedLength">Whether or not the mapped data type is fixed length.</param>
/// <param name="valueGeneratorFactory">An optional factory for creating a specific <see cref="ValueGenerator" />.</param>
[Obsolete("Use the overload with more parameters.")]
public RelationalConverterMappingHints(
int? size,
int? precision,
int? scale,
bool? unicode,
bool? fixedLength,
Func<IProperty, IEntityType, ValueGenerator>? valueGeneratorFactory)
: base(size, precision, scale, unicode, valueGeneratorFactory)
{
IsFixedLength = fixedLength;
}

/// <inheritdoc />
public override ConverterMappingHints With(ConverterMappingHints? hints)
=> hints == null
? this
Expand All @@ -48,10 +71,29 @@ public override ConverterMappingHints With(ConverterMappingHints? hints)
hints.Scale ?? Scale,
hints.IsUnicode ?? IsUnicode,
(hints as RelationalConverterMappingHints)?.IsFixedLength ?? IsFixedLength,
hints.ValueGeneratorFactory ?? ValueGeneratorFactory);
hints.ValueGeneratorFactory ?? ValueGeneratorFactory,
(hints as RelationalConverterMappingHints)?.DbType ?? DbType);

/// <inheritdoc />
public override ConverterMappingHints Override(ConverterMappingHints? hints)
=> hints == null
? this
: new RelationalConverterMappingHints(
Size ?? hints.Size,
Precision ?? hints.Precision,
Scale ?? hints.Scale,
IsUnicode ?? hints.IsUnicode,
IsFixedLength ?? (hints as RelationalConverterMappingHints)?.IsFixedLength,
ValueGeneratorFactory ?? hints.ValueGeneratorFactory,
DbType ?? (hints as RelationalConverterMappingHints)?.DbType);

/// <summary>
/// Whether or not the mapped data type is fixed length.
/// </summary>
public virtual bool? IsFixedLength { get; }

/// <summary>
/// The suggested <see cref="DbType"/>
/// </summary>
public virtual DbType? DbType { get; }
}
37 changes: 29 additions & 8 deletions src/EFCore/Storage/ValueConversion/ConverterMappingHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public ConverterMappingHints(
}

/// <summary>
/// Adds hints from the given object to this one. Hints that are already specified are
/// not overridden.
/// Adds hints from the given object to this one. Hints that are already specified are not overridden.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-value-converters">EF Core value converters</see> for more information and examples.
Expand All @@ -49,12 +48,34 @@ public ConverterMappingHints(
public virtual ConverterMappingHints With(ConverterMappingHints? hints)
=> hints == null
? this
: new ConverterMappingHints(
hints.Size ?? Size,
hints.Precision ?? Precision,
hints.Scale ?? Scale,
hints.IsUnicode ?? IsUnicode,
hints.ValueGeneratorFactory ?? ValueGeneratorFactory);
: hints.GetType().IsAssignableFrom(GetType())
? new ConverterMappingHints(
hints.Size ?? Size,
hints.Precision ?? Precision,
hints.Scale ?? Scale,
hints.IsUnicode ?? IsUnicode,
hints.ValueGeneratorFactory ?? ValueGeneratorFactory)
: hints.Override(this);

/// <summary>
/// Adds hints from the given object to this one. Hints that are already specified are overridden.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-value-converters">EF Core value converters</see> for more information and examples.
/// </remarks>
/// <param name="hints">The hints to add.</param>
/// <returns>The combined hints.</returns>
public virtual ConverterMappingHints Override(ConverterMappingHints? hints)
=> hints == null
? this
: GetType().IsAssignableFrom(hints.GetType())
? new ConverterMappingHints(
Size ?? hints.Size,
Precision ?? hints.Precision,
Scale ?? hints.Scale,
IsUnicode ?? hints.IsUnicode,
ValueGeneratorFactory ?? hints.ValueGeneratorFactory)
: hints.With(this);

/// <summary>
/// The suggested size of the mapped data type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#nullable enable

using System.Data;

namespace Microsoft.EntityFrameworkCore;

public class ValueConvertersEndToEndSqlServerTest
Expand Down Expand Up @@ -194,6 +196,78 @@ public WrappedStringToStringConverter()
}
}

[ConditionalFact]
public virtual void Fixed_length_hints_are_respected()
{
Fixture.TestSqlLoggerFactory.Clear();

using var context = CreateContext();

var guid = new Guid("d854227f-7076-48c3-997c-4e72c1c713b9");

var mapping = context.Set<SqlServerConvertingEntity>()
.EntityType
.FindProperty(nameof(SqlServerConvertingEntity.GuidToFixedLengthString))!
.FindRelationalTypeMapping()!;

Assert.Equal("nchar(40)", mapping.StoreType);
Assert.Equal(40, mapping.Size);

Assert.Empty(context.Set<SqlServerConvertingEntity>().Where(e => e.GuidToFixedLengthString != guid));

Assert.Equal(
"""
@__guid_0='d854227f-7076-48c3-997c-4e72c1c713b9' (Nullable = false) (Size = 40)
SELECT [s].[Id], [s].[GuidToDbTypeString], [s].[GuidToFixedLengthString]
FROM [SqlServerConvertingEntity] AS [s]
WHERE [s].[GuidToFixedLengthString] <> @__guid_0
""",
Fixture.TestSqlLoggerFactory.SqlStatements[0],
ignoreLineEndingDifferences: true);

var parameter = Fixture.TestSqlLoggerFactory.Parameters.Single();
}

[ConditionalFact]
public virtual void DbType_hints_are_respected()
{
Fixture.TestSqlLoggerFactory.Clear();

using var context = CreateContext();

var mapping = context.Set<SqlServerConvertingEntity>()
.EntityType
.FindProperty(nameof(SqlServerConvertingEntity.GuidToDbTypeString))!
.FindRelationalTypeMapping()!;

Assert.Equal(DbType.AnsiStringFixedLength, mapping.DbType!);
Assert.Equal(40, mapping.Size);

var guid = new Guid("d854227f-7076-48c3-997c-4e72c1c713b9");

Assert.Empty(context.Set<SqlServerConvertingEntity>().Where(e => e.GuidToDbTypeString != guid));

Assert.Equal(
"""
@__guid_0='d854227f-7076-48c3-997c-4e72c1c713b9' (Nullable = false) (Size = 40) (DbType = AnsiStringFixedLength)
SELECT [s].[Id], [s].[GuidToDbTypeString], [s].[GuidToFixedLengthString]
FROM [SqlServerConvertingEntity] AS [s]
WHERE [s].[GuidToDbTypeString] <> @__guid_0
""",
Fixture.TestSqlLoggerFactory.SqlStatements[0],
ignoreLineEndingDifferences: true);
}

protected class SqlServerConvertingEntity
{
public Guid Id { get; set; }

public Guid GuidToFixedLengthString { get; set; }
public Guid GuidToDbTypeString { get; set; }
}

public class ValueConvertersEndToEndSqlServerFixture : ValueConvertersEndToEndFixtureBase
{
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
Expand Down Expand Up @@ -221,6 +295,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b.Property(e => e.NullableEnumerableOfInt).HasDefaultValue(Enumerable.Empty<int>());
b.Property(e => e.EnumerableOfInt).HasDefaultValue(Enumerable.Empty<int>());
});

modelBuilder.Entity<SqlServerConvertingEntity>(
b =>
{
b.Property(e => e.GuidToFixedLengthString).HasConversion(
new GuidToStringConverter(
new RelationalConverterMappingHints(
size: 40, fixedLength: true)));

b.Property(e => e.GuidToDbTypeString).HasConversion(
new GuidToStringConverter(
new RelationalConverterMappingHints(
size: 40, unicode: false, dbType: DbType.AnsiStringFixedLength)));
});
}
}
}
Expand Down

0 comments on commit 7dc248b

Please sign in to comment.