Skip to content

Commit

Permalink
Allow FK type in database to be different from PK type
Browse files Browse the repository at this point in the history
Approach uses built-in value converters to convert FK value to type of PK.

Fixes #28392
  • Loading branch information
ajcvickers committed Sep 9, 2022
1 parent 9262a34 commit 19eb728
Show file tree
Hide file tree
Showing 15 changed files with 1,059 additions and 89 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,9 @@
<data name="StoredProcedureUnmapped" xml:space="preserve">
<value>The entity type '{entityType}' was configured to use some stored procedures and is not mapped to any table. An entity type that isn't mapped to a table must be mapped to insert, update and delete stored procedures.</value>
</data>
<data name="StoredKeyTypesNotConvertable" xml:space="preserve">
<value>The foreign key column '{fkColumnName}' has '{fkColumnType}' values which cannot be compared to the '{pkColumnType}' values of the associated principal key column '{pkColumnName}'. To use 'SaveChanges` or 'SaveChangesAsync', foreign key column types must be comparable with principal key column types.</value>
</data>
<data name="TableNotMappedEntityType" xml:space="preserve">
<value>The entity type '{entityType}' is not mapped to the store object '{table}'.</value>
</data>
Expand Down Expand Up @@ -1186,4 +1189,4 @@
<data name="VisitChildrenMustBeOverridden" xml:space="preserve">
<value>'VisitChildren' must be overridden in the class deriving from 'SqlExpression'.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,51 @@ public class CompositeRowForeignKeyValueFactory : CompositeRowValueFactory, IRow
{
private readonly IForeignKeyConstraint _foreignKey;
private readonly IRowKeyValueFactory<object?[]> _principalKeyValueFactory;
private readonly List<ValueConverter?> _valueConverters;

/// <summary>
/// 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.
/// </summary>
public CompositeRowForeignKeyValueFactory(IForeignKeyConstraint foreignKey)
public CompositeRowForeignKeyValueFactory(
IForeignKeyConstraint foreignKey,
IValueConverterSelector valueConverterSelector)
: base(foreignKey.Columns)
{
_foreignKey = foreignKey;
_principalKeyValueFactory =
(IRowKeyValueFactory<object?[]>)((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory();
_principalKeyValueFactory = (IRowKeyValueFactory<object?[]>)((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory();

var columns = foreignKey.Columns;
_valueConverters = new(columns.Count);

for (var i = 0; i < columns.Count; i++)
{
var fkColumn = columns[i];
var pkColumn = foreignKey.PrincipalColumns[i];
var fkType = fkColumn.ProviderClrType;
var pkType = pkColumn.ProviderClrType;
if (fkType != pkType)
{
var converterInfos = valueConverterSelector.Select(pkType, fkType).ToList();
if (converterInfos.Count == 0)
{
throw new InvalidOperationException(
RelationalStrings.StoredKeyTypesNotConvertable(
fkColumn.Name, fkColumn.StoreType, pkColumn.StoreType, pkColumn.Name));
}

_valueConverters.Add(converterInfos.First().Create());
}
else
{
_valueConverters.Add(null);
}
}

ValueConverters = _valueConverters;
EqualityComparer = CreateEqualityComparer(columns, _valueConverters);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public CompositeRowIndexValueFactory(ITableIndex index)
: base(index.Columns)
{
_index = index;

EqualityComparer = CreateEqualityComparer(index.Columns, null);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public CompositeRowKeyValueFactory(IUniqueConstraint key)
: base(key.Columns)
{
_constraint = key;

EqualityComparer = CreateEqualityComparer(key.Columns, null);
}

/// <summary>
Expand Down
54 changes: 44 additions & 10 deletions src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@ public abstract class CompositeRowValueFactory
/// 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.
/// </summary>
public CompositeRowValueFactory(IReadOnlyList<IColumn> columns)
protected CompositeRowValueFactory(IReadOnlyList<IColumn> columns)
{
Columns = columns;
EqualityComparer = CreateEqualityComparer(columns);
}

/// <summary>
/// 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.
/// </summary>
protected virtual List<ValueConverter?>? ValueConverters { get; set; }

/// <summary>
/// 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.
/// </summary>
public virtual IEqualityComparer<object?[]> EqualityComparer { get; }
public virtual IEqualityComparer<object?[]> EqualityComparer { get; set; } = null!;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -85,10 +92,11 @@ public virtual bool TryCreateDependentKeyValue(IDictionary<string, object?> keyV
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool TryCreateDependentKeyValue(
IReadOnlyModificationCommand command,
bool fromOriginalValues,
IReadOnlyModificationCommand command,
bool fromOriginalValues,
[NotNullWhen(true)] out object?[]? key)
{
var converters = ValueConverters;
key = new object[Columns.Count];
var index = 0;

Expand All @@ -110,6 +118,13 @@ public virtual bool TryCreateDependentKeyValue(

valueFound = true;
value = fromOriginalValues ? entry.GetOriginalProviderValue(property) : entry.GetCurrentProviderValue(property);

var converter = converters?[i];
if (converter != null)
{
value = converter.ConvertFromProvider(value);
}

if (!fromOriginalValues
&& (entry.EntityState == EntityState.Added
|| entry.EntityState == EntityState.Modified && entry.IsModified(property)))
Expand Down Expand Up @@ -153,18 +168,37 @@ public virtual bool TryCreateDependentKeyValue(
/// 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.
/// </summary>
protected static IEqualityComparer<object?[]> CreateEqualityComparer(IReadOnlyList<IColumn> columns)
=> new CompositeCustomComparer(columns.Select(c => c.ProviderValueComparer).ToList());
protected static IEqualityComparer<object?[]> CreateEqualityComparer(
IReadOnlyList<IColumn> columns,
List<ValueConverter?>? valueConverters)
=> new CompositeCustomComparer(columns.Select(c => c.ProviderValueComparer).ToList(), valueConverters);

private sealed class CompositeCustomComparer : IEqualityComparer<object?[]>
{
private readonly Func<object?, object?, bool>[] _equals;
private readonly Func<object, int>[] _hashCodes;

public CompositeCustomComparer(IList<ValueComparer> comparers)
public CompositeCustomComparer(List<ValueComparer> comparers, List<ValueConverter?>? valueConverters)
{
_equals = comparers.Select(c => (Func<object?, object?, bool>)c.Equals).ToArray();
_hashCodes = comparers.Select(c => (Func<object, int>)c.GetHashCode).ToArray();
var columnCount = comparers.Count;
_equals = new Func<object?, object?, bool>[columnCount];
_hashCodes = new Func<object, int>[columnCount];

for (var i = 0; i < columnCount; i++)
{
var converter = valueConverters?[i];
var comparer = comparers[i];
if (converter != null)
{
_equals[i] = (v1, v2) => comparer.Equals(converter.ConvertToProvider(v1), converter.ConvertToProvider(v2));
_hashCodes[i] = v => comparer.GetHashCode(converter.ConvertToProvider(v)!);
}
else
{
_equals[i] = comparer.Equals;
_hashCodes[i] = comparer.GetHashCode;
}
}
}

public bool Equals(object?[]? x, object?[]? y)
Expand Down
60 changes: 57 additions & 3 deletions src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ namespace Microsoft.EntityFrameworkCore.Update.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.
/// </summary>
public abstract class RowForeignKeyValueFactory<TKey> : IRowForeignKeyValueFactory<TKey>
public abstract class RowForeignKeyValueFactory<TKey, TForeignKey> : IRowForeignKeyValueFactory<TKey>
{
private readonly IForeignKeyConstraint _foreignKey;
private readonly ValueConverter? _valueConverter;
private readonly IRowKeyValueFactory<TKey> _principalKeyValueFactory;

/// <summary>
Expand All @@ -24,13 +25,50 @@ public abstract class RowForeignKeyValueFactory<TKey> : IRowForeignKeyValueFacto
/// 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.
/// </summary>
public RowForeignKeyValueFactory(IForeignKeyConstraint foreignKey)
protected RowForeignKeyValueFactory(
IForeignKeyConstraint foreignKey,
IColumn column,
ColumnAccessors columnAccessors,
IValueConverterSelector valueConverterSelector)
{
_foreignKey = foreignKey;
Column = column;

if (typeof(TKey) == typeof(TForeignKey))
{
ColumnAccessors = columnAccessors;
}
else
{
var converterInfos = valueConverterSelector.Select(typeof(TKey), typeof(TForeignKey)).ToList();
if (converterInfos.Count == 0)
{
var pkColumn = foreignKey.PrincipalColumns[0];
throw new InvalidOperationException(
RelationalStrings.StoredKeyTypesNotConvertable(
column.Name, column.StoreType, pkColumn.StoreType, pkColumn.Name));
}
_valueConverter = converterInfos.First().Create();

ColumnAccessors = new ColumnAccessors(
ConvertAccessor((Func<IReadOnlyModificationCommand, (TForeignKey, bool)>)columnAccessors.CurrentValueGetter),
ConvertAccessor((Func<IReadOnlyModificationCommand, (TForeignKey, bool)>)columnAccessors.OriginalValueGetter));
}

_principalKeyValueFactory =
(IRowKeyValueFactory<TKey>)((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory();
}

private Func<IReadOnlyModificationCommand, (TKey, bool)> ConvertAccessor(
Func<IReadOnlyModificationCommand, (TForeignKey, bool)> columnAccessor)
=> command =>
{
var tuple = columnAccessor(command);
return (tuple.Item1 == null
? (default, tuple.Item2)
: ((TKey)_valueConverter!.ConvertFromProvider(tuple.Item1)!, tuple.Item2))!;
};

/// <summary>
/// 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
Expand All @@ -39,6 +77,22 @@ public RowForeignKeyValueFactory(IForeignKeyConstraint foreignKey)
/// </summary>
public abstract IEqualityComparer<TKey> EqualityComparer { get; }

/// <summary>
/// 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.
/// </summary>
public virtual IColumn Column { get; }

/// <summary>
/// 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.
/// </summary>
public virtual ColumnAccessors ColumnAccessors { get; }

/// <summary>
/// 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
Expand Down Expand Up @@ -101,7 +155,7 @@ public abstract bool TryCreateDependentKeyValue(
/// </summary>
protected virtual IEqualityComparer<TKey> CreateKeyEqualityComparer(IColumn column)
#pragma warning disable EF1001 // Internal EF Core API usage.
=> NullableComparerAdapter<TKey>.Wrap(column.ProviderValueComparer);
=> NullableComparerAdapter<TKey>.Wrap(column.ProviderValueComparer, _valueConverter);
#pragma warning restore EF1001 // Internal EF Core API usage.

/// <summary>
Expand Down
Loading

0 comments on commit 19eb728

Please sign in to comment.