From 927c9da3e21e3ea630ff4dadcdacf8ad0fa968dc Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 31 Aug 2022 20:54:53 -0700 Subject: [PATCH] Use correct value comparer for FKs in compiled model. Fixes #28108 --- .../Internal/ValueComparerExtensions.cs | 73 ++++ src/EFCore/Metadata/Internal/Property.cs | 74 +--- .../Metadata/Internal/PropertyExtensions.cs | 15 + src/EFCore/Metadata/RuntimeProperty.cs | 87 ++++- src/EFCore/Query/ExpressionPrinter.cs | 19 +- .../CSharpRuntimeModelCodeGeneratorTest.cs | 329 +++++++++++------- .../TestUtilities/DatabaseFacadeExtensions.cs | 4 +- 7 files changed, 382 insertions(+), 219 deletions(-) create mode 100644 src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs diff --git a/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs b/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs new file mode 100644 index 00000000000..bcc7cbaa812 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public static class ValueComparerExtensions +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static ValueComparer? ToNullableComparer(this ValueComparer? valueComparer, IReadOnlyProperty property) + { + if (valueComparer == null + || !property.ClrType.IsNullableValueType() + || valueComparer.Type.IsNullableValueType()) + { + return valueComparer; + } + + var newEqualsParam1 = Expression.Parameter(property.ClrType, "v1"); + var newEqualsParam2 = Expression.Parameter(property.ClrType, "v2"); + var newHashCodeParam = Expression.Parameter(property.ClrType, "v"); + var newSnapshotParam = Expression.Parameter(property.ClrType, "v"); + var hasValueMethod = property.ClrType.GetMethod("get_HasValue")!; + var v1HasValue = Expression.Parameter(typeof(bool), "v1HasValue"); + var v2HasValue = Expression.Parameter(typeof(bool), "v2HasValue"); + + return (ValueComparer)Activator.CreateInstance( + typeof(ValueComparer<>).MakeGenericType(property.ClrType), + Expression.Lambda( + Expression.Block( + typeof(bool), + new[] { v1HasValue, v2HasValue }, + Expression.Assign(v1HasValue, Expression.Call(newEqualsParam1, hasValueMethod)), + Expression.Assign(v2HasValue, Expression.Call(newEqualsParam2, hasValueMethod)), + Expression.OrElse( + Expression.AndAlso( + v1HasValue, + Expression.AndAlso( + v2HasValue, + valueComparer.ExtractEqualsBody( + Expression.Convert(newEqualsParam1, valueComparer.Type), + Expression.Convert(newEqualsParam2, valueComparer.Type)))), + Expression.AndAlso( + Expression.Not(v1HasValue), + Expression.Not(v2HasValue)))), + newEqualsParam1, newEqualsParam2), + Expression.Lambda( + Expression.Condition( + Expression.Call(newHashCodeParam, hasValueMethod), + valueComparer.ExtractHashCodeBody( + Expression.Convert(newHashCodeParam, valueComparer.Type)), + Expression.Constant(0, typeof(int))), + newHashCodeParam), + Expression.Lambda( + Expression.Condition( + Expression.Call(newSnapshotParam, hasValueMethod), + Expression.Convert( + valueComparer.ExtractSnapshotBody( + Expression.Convert(newSnapshotParam, valueComparer.Type)), property.ClrType), + Expression.Default(property.ClrType)), + newSnapshotParam))!; + } +} diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 07895688053..326ea5df85b 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -815,8 +816,7 @@ public virtual CoreTypeMapping? TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ValueComparer? GetValueComparer() - => ToNullableComparer(GetValueComparer(null) - ?? TypeMapping?.Comparer); + => (GetValueComparer(null) ?? TypeMapping?.Comparer).ToNullableComparer(this); private ValueComparer? GetValueComparer(HashSet? checkedProperties) { @@ -826,7 +826,7 @@ public virtual CoreTypeMapping? TypeMapping return comparer; } - var principal = (Property?)FindFirstDifferentPrincipal(); + var principal = (Property?)this.FindFirstDifferentPrincipal(); if (principal == null) { return null; @@ -861,8 +861,7 @@ public virtual CoreTypeMapping? TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ValueComparer? GetKeyValueComparer() - => ToNullableComparer(GetValueComparer(null) - ?? TypeMapping?.KeyComparer); + => (GetValueComparer(null) ?? TypeMapping?.KeyComparer).ToNullableComparer(this); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -928,7 +927,7 @@ public virtual CoreTypeMapping? TypeMapping return comparer; } - var principal = (Property?)FindFirstDifferentPrincipal(); + var principal = (Property?)this.FindFirstDifferentPrincipal(); if (principal == null || principal.GetEffectiveProviderClrType() != GetEffectiveProviderClrType()) { @@ -947,7 +946,7 @@ public virtual CoreTypeMapping? TypeMapping checkedProperties.Add(this); return principal.GetProviderValueComparer(checkedProperties); } - + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -957,67 +956,6 @@ public virtual CoreTypeMapping? TypeMapping public virtual ConfigurationSource? GetProviderValueComparerConfigurationSource() => FindAnnotation(CoreAnnotationNames.ProviderValueComparer)?.GetConfigurationSource(); - private ValueComparer? ToNullableComparer(ValueComparer? valueComparer) - { - if (valueComparer == null - || !ClrType.IsNullableValueType() - || valueComparer.Type.IsNullableValueType()) - { - return valueComparer; - } - - var newEqualsParam1 = Expression.Parameter(ClrType, "v1"); - var newEqualsParam2 = Expression.Parameter(ClrType, "v2"); - var newHashCodeParam = Expression.Parameter(ClrType, "v"); - var newSnapshotParam = Expression.Parameter(ClrType, "v"); - var hasValueMethod = ClrType.GetMethod("get_HasValue")!; - var v1HasValue = Expression.Parameter(typeof(bool), "v1HasValue"); - var v2HasValue = Expression.Parameter(typeof(bool), "v2HasValue"); - - return (ValueComparer)Activator.CreateInstance( - typeof(ValueComparer<>).MakeGenericType(ClrType), - Expression.Lambda( - Expression.Block( - typeof(bool), - new[] { v1HasValue, v2HasValue }, - Expression.Assign(v1HasValue, Expression.Call(newEqualsParam1, hasValueMethod)), - Expression.Assign(v2HasValue, Expression.Call(newEqualsParam2, hasValueMethod)), - Expression.OrElse( - Expression.AndAlso( - v1HasValue, - Expression.AndAlso( - v2HasValue, - valueComparer.ExtractEqualsBody( - Expression.Convert(newEqualsParam1, valueComparer.Type), - Expression.Convert(newEqualsParam2, valueComparer.Type)))), - Expression.AndAlso( - Expression.Not(v1HasValue), - Expression.Not(v2HasValue)))), - newEqualsParam1, newEqualsParam2), - Expression.Lambda( - Expression.Condition( - Expression.Call(newHashCodeParam, hasValueMethod), - valueComparer.ExtractHashCodeBody( - Expression.Convert(newHashCodeParam, valueComparer.Type)), - Expression.Constant(0, typeof(int))), - newHashCodeParam), - Expression.Lambda( - Expression.Condition( - Expression.Call(newSnapshotParam, hasValueMethod), - Expression.Convert( - valueComparer.ExtractSnapshotBody( - Expression.Convert(newSnapshotParam, valueComparer.Type)), ClrType), - Expression.Default(ClrType)), - newSnapshotParam))!; - } - - private IProperty? FindFirstDifferentPrincipal() - { - var principal = ((IProperty)this).FindFirstPrincipal(); - - return principal != this ? principal : null; - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/PropertyExtensions.cs b/src/EFCore/Metadata/Internal/PropertyExtensions.cs index c14d32bfad4..8fd00f10c22 100644 --- a/src/EFCore/Metadata/Internal/PropertyExtensions.cs +++ b/src/EFCore/Metadata/Internal/PropertyExtensions.cs @@ -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 Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// @@ -29,6 +31,19 @@ public static bool ForAdd(this ValueGenerated valueGenerated) public static bool ForUpdate(this ValueGenerated valueGenerated) => (valueGenerated & ValueGenerated.OnUpdate) != 0; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyProperty? FindFirstDifferentPrincipal(this IReadOnlyProperty property) + { + var principal = property.FindFirstPrincipal(); + + return principal != property ? principal : null; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index 94ffdcd29ed..941a46cc7b5 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -1,6 +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 Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -21,8 +22,10 @@ public class RuntimeProperty : RuntimePropertyBase, IProperty private readonly PropertySaveBehavior _afterSaveBehavior; private readonly Func? _valueGeneratorFactory; private readonly ValueConverter? _valueConverter; - private readonly ValueComparer? _valueComparer; - private readonly ValueComparer? _keyValueComparer; + private readonly bool _explicitValueComparer; + private ValueComparer? _valueComparer; + private readonly bool _explicitKeyValueComparer; + private ValueComparer? _keyValueComparer; private readonly ValueComparer? _providerValueComparer; private CoreTypeMapping? _typeMapping; @@ -95,7 +98,9 @@ public RuntimeProperty( _typeMapping = typeMapping; _valueComparer = valueComparer; + _explicitValueComparer = _valueComparer != null; _keyValueComparer = keyValueComparer ?? valueComparer; + _explicitKeyValueComparer = keyValueComparer != null; _providerValueComparer = providerValueComparer; } @@ -167,6 +172,68 @@ public virtual CoreTypeMapping TypeMapping set => _typeMapping = value; } + private ValueComparer GetValueComparer() + => (GetValueComparer(null) ?? TypeMapping.Comparer) + .ToNullableComparer(this)!; + + private ValueComparer GetKeyValueComparer() + => (GetKeyValueComparer(null) ?? TypeMapping.KeyComparer) + .ToNullableComparer(this)!; + + private ValueComparer? GetValueComparer(HashSet? checkedProperties) + { + if (_explicitValueComparer // This condition is needed due to #28944 + && _valueComparer != null) + { + return _valueComparer; + } + + var principal = (RuntimeProperty?)this.FindFirstDifferentPrincipal(); + if (principal == null) + { + return null; + } + + if (checkedProperties == null) + { + checkedProperties = new HashSet(); + } + else if (checkedProperties.Contains(this)) + { + return null; + } + + checkedProperties.Add(this); + return principal.GetValueComparer(checkedProperties); + } + + private ValueComparer? GetKeyValueComparer(HashSet? checkedProperties) + { + if (_explicitKeyValueComparer // This condition is needed due to #28944 + && _keyValueComparer != null) + { + return _keyValueComparer; + } + + var principal = (RuntimeProperty?)this.FindFirstDifferentPrincipal(); + if (principal == null) + { + return null; + } + + if (checkedProperties == null) + { + checkedProperties = new HashSet(); + } + else if (checkedProperties.Contains(this)) + { + return null; + } + + checkedProperties.Add(this); + return principal.GetKeyValueComparer(checkedProperties); + } + /// /// Returns a string that represents the current object. /// @@ -274,22 +341,30 @@ IEntityType IProperty.DeclaringEntityType /// [DebuggerStepThrough] ValueComparer? IReadOnlyProperty.GetValueComparer() - => _valueComparer ?? TypeMapping.Comparer; + => NonCapturingLazyInitializer.EnsureInitialized( + ref _valueComparer, this, + static property => property.GetValueComparer()); /// [DebuggerStepThrough] ValueComparer IProperty.GetValueComparer() - => _valueComparer ?? TypeMapping.Comparer; + => NonCapturingLazyInitializer.EnsureInitialized( + ref _valueComparer, this, + static property => property.GetValueComparer()); /// [DebuggerStepThrough] ValueComparer? IReadOnlyProperty.GetKeyValueComparer() - => _keyValueComparer ?? TypeMapping.KeyComparer; + => NonCapturingLazyInitializer.EnsureInitialized( + ref _keyValueComparer, this, + static property => property.GetKeyValueComparer()); /// [DebuggerStepThrough] ValueComparer IProperty.GetKeyValueComparer() - => _keyValueComparer ?? TypeMapping.KeyComparer; + => NonCapturingLazyInitializer.EnsureInitialized( + ref _keyValueComparer, this, + static property => property.GetKeyValueComparer()); /// [DebuggerStepThrough] diff --git a/src/EFCore/Query/ExpressionPrinter.cs b/src/EFCore/Query/ExpressionPrinter.cs index 4c5481ec545..ae786634157 100644 --- a/src/EFCore/Query/ExpressionPrinter.cs +++ b/src/EFCore/Query/ExpressionPrinter.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System; namespace Microsoft.EntityFrameworkCore.Query; @@ -613,15 +612,17 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp { if (methodCallExpression.Object != null) { - if (methodCallExpression.Object is BinaryExpression) + switch (methodCallExpression.Object) { - _stringBuilder.Append("("); - Visit(methodCallExpression.Object); - _stringBuilder.Append(")"); - } - else - { - Visit(methodCallExpression.Object); + case BinaryExpression: + case UnaryExpression: + _stringBuilder.Append("("); + Visit(methodCallExpression.Object); + _stringBuilder.Append(")"); + break; + default: + Visit(methodCallExpression.Object); + break; } _stringBuilder.Append("."); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 257cff9a12a..964a2dd4d59 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -4,6 +4,7 @@ using System.Collections; using System.ComponentModel; using System.Data; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; @@ -15,6 +16,7 @@ using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; using Microsoft.EntityFrameworkCore.TestModels.AspNetIdentity; using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; +using Microsoft.Extensions.Options; using NetTopologySuite; using NetTopologySuite.Geometries; using Newtonsoft.Json.Linq; @@ -988,7 +990,6 @@ partial void Initialize() using Microsoft.EntityFrameworkCore.Migrations.Design; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; using Microsoft.EntityFrameworkCore.ValueGeneration; -using NetTopologySuite.Geometries; #pragma warning disable 219, 612, 618 #nullable enable @@ -1014,7 +1015,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalAlternateId = runtimeEntityType.AddProperty( ""PrincipalAlternateId"", - typeof(Point), + typeof(Guid), afterSaveBehavior: PropertySaveBehavior.Throw); principalAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1148,17 +1149,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var alternateId = runtimeEntityType.AddProperty( ""AlternateId"", - typeof(Point), + typeof(Guid), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField(""AlternateId"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.FieldDuringConstruction, + afterSaveBehavior: PropertySaveBehavior.Throw); + alternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + + var point = runtimeEntityType.AddProperty( + ""Point"", + typeof(Point), + nullable: true, valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw, valueConverter: new CastingConverter(), valueComparer: new CSharpRuntimeModelCodeGeneratorTest.CustomValueComparer(), providerValueComparer: new CSharpRuntimeModelCodeGeneratorTest.CustomValueComparer()); - alternateId.AddAnnotation(""Relational:ColumnType"", ""geometry""); - alternateId.AddAnnotation(""Relational:DefaultValue"", (NetTopologySuite.Geometries.Point)new NetTopologySuite.IO.WKTReader().Read(""SRID=0;POINT Z(0 0 0)"")); - alternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + point.AddAnnotation(""Relational:ColumnType"", ""geometry""); + point.AddAnnotation(""Relational:DefaultValue"", (NetTopologySuite.Geometries.Point)new NetTopologySuite.IO.WKTReader().Read(""SRID=0;POINT Z(0 0 0)"")); + point.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); var key = runtimeEntityType.AddKey( new[] { id }); @@ -1171,12 +1178,6 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var index = runtimeEntityType.AddIndex( new[] { alternateId, id }); - var alternateIndex = runtimeEntityType.AddIndex( - new[] { alternateId }, - name: ""AlternateIndex"", - unique: true); - alternateIndex.AddAnnotation(""Relational:Name"", ""AIX""); - return runtimeEntityType; } @@ -1229,7 +1230,6 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; -using NetTopologySuite.Geometries; #pragma warning disable 219, 612, 618 #nullable enable @@ -1268,9 +1268,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalBaseAlternateId = runtimeEntityType.AddProperty( ""PrincipalBaseAlternateId"", - typeof(Point), + typeof(Guid), propertyAccessMode: PropertyAccessMode.Field, - valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); principalBaseAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1379,7 +1378,6 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; -using NetTopologySuite.Geometries; #pragma warning disable 219, 612, 618 #nullable enable @@ -1404,7 +1402,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalDerivedAlternateId = runtimeEntityType.AddProperty( ""PrincipalDerivedAlternateId"", - typeof(Point), + typeof(Guid), afterSaveBehavior: PropertySaveBehavior.Throw); principalDerivedAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1484,7 +1482,6 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -using NetTopologySuite.Geometries; #pragma warning disable 219, 612, 618 #nullable enable @@ -1512,7 +1509,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var derivedsAlternateId = runtimeEntityType.AddProperty( ""DerivedsAlternateId"", - typeof(Point), + typeof(Guid), propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(), afterSaveBehavior: PropertySaveBehavior.Throw); derivedsAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1526,7 +1523,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalsAlternateId = runtimeEntityType.AddProperty( ""PrincipalsAlternateId"", - typeof(Point), + typeof(Guid), propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(), afterSaveBehavior: PropertySaveBehavior.Throw); principalsAlternateId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); @@ -1747,18 +1744,6 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal( CoreStrings.RuntimeModelMissingData, Assert.Throws(() => model.GetPropertyAccessMode()).Message); - Assert.Null(model[SqlServerAnnotationNames.MaxDatabaseSize]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => model.GetDatabaseMaxSize()).Message); - Assert.Null(model[SqlServerAnnotationNames.PerformanceLevelSql]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => model.GetPerformanceLevelSql()).Message); - Assert.Null(model[SqlServerAnnotationNames.ServiceTierSql]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => model.GetServiceTierSql()).Message); Assert.Null(model[SqlServerAnnotationNames.IdentitySeed]); Assert.Equal( CoreStrings.RuntimeModelMissingData, @@ -1817,62 +1802,33 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal( CoreStrings.RuntimeModelMissingData, Assert.Throws(() => principalId.GetIdentityIncrement()).Message); - Assert.Null(principalId[SqlServerAnnotationNames.Sparse]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => principalId.IsSparse()).Message); - var principalAlternateId = principalBase.FindProperty(nameof(PrincipalBase.AlternateId)); - Assert.Equal(typeof(Point), principalAlternateId.ClrType); - Assert.False(principalAlternateId.IsNullable); - Assert.Equal(ValueGenerated.OnAdd, principalAlternateId.ValueGenerated); - Assert.Equal("AlternateId", principalAlternateId.GetColumnName()); - Assert.Equal("geometry", principalAlternateId.GetColumnType()); - Assert.Equal(0, ((Point)principalAlternateId.GetDefaultValue()).SRID); - Assert.IsType>(principalAlternateId.GetValueConverter()); - Assert.IsType>(principalAlternateId.GetValueComparer()); - Assert.IsType>(principalAlternateId.GetKeyValueComparer()); - Assert.IsType>(principalAlternateId.GetProviderValueComparer()); - Assert.Equal(SqlServerValueGenerationStrategy.None, principalAlternateId.GetValueGenerationStrategy()); - Assert.Equal(PropertyAccessMode.FieldDuringConstruction, principalAlternateId.GetPropertyAccessMode()); - Assert.Null(principalAlternateId[CoreAnnotationNames.PropertyAccessMode]); + var pointProperty = principalBase.FindProperty("Point"); + Assert.Equal(typeof(Point), pointProperty.ClrType); + Assert.True(pointProperty.IsNullable); + Assert.Equal(ValueGenerated.OnAdd, pointProperty.ValueGenerated); + Assert.Equal("Point", pointProperty.GetColumnName()); + Assert.Equal("geometry", pointProperty.GetColumnType()); + Assert.Equal(0, ((Point)pointProperty.GetDefaultValue()).SRID); + Assert.IsType>(pointProperty.GetValueConverter()); + Assert.IsType>(pointProperty.GetValueComparer()); + Assert.IsType>(pointProperty.GetKeyValueComparer()); + Assert.IsType>(pointProperty.GetProviderValueComparer()); + Assert.Equal(SqlServerValueGenerationStrategy.None, pointProperty.GetValueGenerationStrategy()); + Assert.Null(pointProperty[CoreAnnotationNames.PropertyAccessMode]); Assert.Null(principalBase.FindDiscriminatorProperty()); - Assert.Equal(2, principalBase.GetIndexes().Count()); - - var compositeIndex = principalBase.GetIndexes().First(); + var principalAlternateId = principalBase.FindProperty(nameof(PrincipalBase.AlternateId)); + var compositeIndex = principalBase.GetIndexes().Single(); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, principalAlternateId.GetPropertyAccessMode()); Assert.Empty(compositeIndex.GetAnnotations()); Assert.Equal(new[] { principalAlternateId, principalId }, compositeIndex.Properties); Assert.False(compositeIndex.IsUnique); Assert.Null(compositeIndex.Name); Assert.Equal("IX_PrincipalBase_AlternateId_Id", compositeIndex.GetDatabaseName()); - Assert.Null(compositeIndex[SqlServerAnnotationNames.Clustered]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => compositeIndex.IsClustered()).Message); - Assert.Null(compositeIndex[SqlServerAnnotationNames.CreatedOnline]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => compositeIndex.IsCreatedOnline()).Message); - Assert.Null(compositeIndex[SqlServerAnnotationNames.FillFactor]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => compositeIndex.GetFillFactor()).Message); - Assert.Null(compositeIndex[SqlServerAnnotationNames.Include]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => compositeIndex.GetIncludeProperties()).Message); - - var alternateIndex = principalBase.GetIndexes().Last(); - Assert.Same(principalAlternateId, alternateIndex.Properties.Single()); - Assert.True(alternateIndex.IsUnique); - Assert.Equal("AlternateIndex", alternateIndex.Name); - Assert.Equal("AIX", alternateIndex.GetDatabaseName()); - Assert.Null(alternateIndex[RelationalAnnotationNames.Filter]); - Assert.Null(alternateIndex.GetFilter()); - Assert.Equal(new[] { compositeIndex, alternateIndex }, principalAlternateId.GetContainingIndexes()); + Assert.Equal(new[] { compositeIndex }, principalAlternateId.GetContainingIndexes()); Assert.Equal(2, principalBase.GetKeys().Count()); @@ -1952,11 +1908,19 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) CoreStrings.RuntimeModelMissingData, Assert.Throws(() => principalId.GetIdentitySeed(principalTable)).Message); + var detailsProperty = referenceOwnedType.FindProperty(nameof(OwnedType.Details)); + Assert.Null(detailsProperty[SqlServerAnnotationNames.Sparse]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => detailsProperty.IsSparse()).Message); + Assert.Null(detailsProperty[RelationalAnnotationNames.Collation]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => detailsProperty.GetCollation()).Message); + var ownedFragment = referenceOwnedType.GetMappingFragments().Single(); - Assert.Equal(nameof(OwnedType.Details), - referenceOwnedType.FindProperty(nameof(OwnedType.Details)).GetColumnName(ownedFragment.StoreObject)); - Assert.Null(referenceOwnedType.FindProperty(nameof(OwnedType.Details)) - .GetColumnName(principalTable)); + Assert.Equal(nameof(OwnedType.Details), detailsProperty.GetColumnName(ownedFragment.StoreObject)); + Assert.Null(detailsProperty.GetColumnName(principalTable)); var referenceOwnership = referenceOwnedNavigation.ForeignKey; Assert.Empty(referenceOwnership.GetAnnotations()); @@ -2104,10 +2068,6 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal( CoreStrings.RuntimeModelMissingData, Assert.Throws(() => rowid.GetComment()).Message); - Assert.Null(rowid[RelationalAnnotationNames.Collation]); - Assert.Equal( - CoreStrings.RuntimeModelMissingData, - Assert.Throws(() => rowid.GetCollation()).Message); Assert.Null(rowid[RelationalAnnotationNames.ColumnOrder]); Assert.Equal( CoreStrings.RuntimeModelMissingData, @@ -2214,7 +2174,19 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) }, model.GetEntityTypes()); }, - typeof(SqlServerNetTopologySuiteDesignTimeServices)); + typeof(SqlServerNetTopologySuiteDesignTimeServices), + c => + { + c.Set>>().Add( + new PrincipalDerived> + { + AlternateId = new Guid(), + Dependent = new DependentBase(1), + Owned = new OwnedType(c) + }); + + c.SaveChanges(); + }); public class BigContext : SqlServerContextBase { @@ -2222,34 +2194,25 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.HasDatabaseMaxSize("20TB") - .HasPerformanceLevel("High") - .HasServiceTier("AB") - .UseCollation("pi-PI") + modelBuilder + .UseCollation("Latin1_General_CS_AS") .UseIdentityColumns(3, 2); modelBuilder.Entity( eb => { - eb.Property(e => e.Id).UseIdentityColumn(2, 3).IsSparse() + eb.Property(e => e.Id).UseIdentityColumn(2, 3) .Metadata.SetColumnName("DerivedId", StoreObjectIdentifier.Table("PrincipalDerived")); + eb.Property(e => e.AlternateId) - .IsRequired() - .UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction) + .UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + + eb.Property("Point") .HasColumnType("geometry") .HasDefaultValue( NtsGeometryServices.Instance.CreateGeometryFactory(srid: 0).CreatePoint(new CoordinateZM(0, 0, 0, 0))) .HasConversion, CustomValueComparer, CustomValueComparer>(); - eb.HasIndex(e => e.AlternateId, "AlternateIndex") - .IsUnique() - .HasDatabaseName("AIX") - .HasFilter("AlternateId <> NULL") - .IsClustered() - .IsCreatedOnline() - .HasFillFactor(40) - .IncludeProperties(e => e.Id); - eb.HasIndex(e => new { e.AlternateId, e.Id }); eb.HasKey(e => new { e.Id, e.AlternateId }) @@ -2263,17 +2226,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { ob.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues); ob.UsePropertyAccessMode(PropertyAccessMode.Field); + ob.Property(e => e.Details) + .IsSparse() + .UseCollation("Latin1_General_CI_AI"); ob.ToTable("PrincipalBase", "mySchema", t => t.Property("PrincipalBaseId").UseIdentityColumn(2, 3)); ob.SplitToTable("Details", s => s.Property(e => e.Details)); + + ob.HasData(new { Number = 10, PrincipalBaseId = 1L, PrincipalBaseAlternateId = new Guid() }); }); eb.Navigation(e => e.Owned).IsRequired().HasField("_ownedField") .UsePropertyAccessMode(PropertyAccessMode.Field); - eb.HasData(new PrincipalBase { Id = 1, AlternateId = new Point(0, 0) }); + eb.HasData(new PrincipalBase { Id = 1, AlternateId = new Guid() }); eb.ToTable("PrincipalBase", "mySchema"); }); @@ -2301,7 +2269,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) jb.Property("rowid") .IsRowVersion() .HasComment("RowVersion") - .UseCollation("ri") .HasColumnOrder(1); }); @@ -2339,6 +2306,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasPrecision(9, 3); }); } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + SqlServerTestStore.Create("RuntimeModelTest" + GetType().Name).AddProviderOptions(options); + new SqlServerDbContextOptionsBuilder(options).UseNetTopologySuite(); + } } [ConditionalFact] @@ -2584,11 +2557,14 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba runtimeEntityType.SetPrimaryKey(key); var index = runtimeEntityType.AddIndex( - new[] { principalBaseId }); - - var index0 = runtimeEntityType.AddIndex( new[] { principalDerivedId }); + var principalIndex = runtimeEntityType.AddIndex( + new[] { principalBaseId }, + name: ""PrincipalIndex"", + unique: true); + principalIndex.AddAnnotation(""Relational:Name"", ""PIX""); + return runtimeEntityType; } @@ -2767,6 +2743,18 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) model => { Assert.Equal("TPC", model.GetDefaultSchema()); + Assert.Null(model[SqlServerAnnotationNames.MaxDatabaseSize]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => model.GetDatabaseMaxSize()).Message); + Assert.Null(model[SqlServerAnnotationNames.PerformanceLevelSql]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => model.GetPerformanceLevelSql()).Message); + Assert.Null(model[SqlServerAnnotationNames.ServiceTierSql]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => model.GetServiceTierSql()).Message); var principalBase = model.FindEntityType(typeof(PrincipalBase)); var id = principalBase.FindProperty("Id"); @@ -2780,13 +2768,43 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("PrincipalBaseView", principalBase.GetViewName()); Assert.Equal("TPC", principalBase.GetViewSchema()); Assert.Equal("Id", id.GetColumnName(StoreObjectIdentifier.Create(principalBase, StoreObjectType.View).Value)); - Assert.Equal("bar2", + Assert.Equal( + "bar2", id.FindOverrides(StoreObjectIdentifier.Create(principalBase, StoreObjectType.View).Value)["foo"]); + var principalBaseId = principalBase.FindProperty("PrincipalBaseId"); + + var alternateIndex = principalBase.GetIndexes().Last(); + Assert.Same(principalBaseId, alternateIndex.Properties.Single()); + Assert.True(alternateIndex.IsUnique); + Assert.Equal("PrincipalIndex", alternateIndex.Name); + Assert.Equal("PIX", alternateIndex.GetDatabaseName()); + Assert.Null(alternateIndex[RelationalAnnotationNames.Filter]); + Assert.Null(alternateIndex.GetFilter()); + Assert.Null(alternateIndex[SqlServerAnnotationNames.Clustered]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.IsClustered()).Message); + Assert.Null(alternateIndex[SqlServerAnnotationNames.CreatedOnline]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.IsCreatedOnline()).Message); + Assert.Null(alternateIndex[SqlServerAnnotationNames.FillFactor]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.GetFillFactor()).Message); + Assert.Null(alternateIndex[SqlServerAnnotationNames.Include]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.GetIncludeProperties()).Message); + + Assert.Equal(new[] { alternateIndex }, principalBaseId.GetContainingIndexes()); + var insertSproc = principalBase.GetInsertStoredProcedure()!; Assert.Equal("PrincipalBase_Insert", insertSproc.Name); Assert.Equal("TPC", insertSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, insertSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal( + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, insertSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(insertSproc.ResultColumns); Assert.False(insertSproc.IsRowsAffectedReturned); Assert.Equal("bar1", insertSproc["foo"]); @@ -2798,7 +2816,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var updateSproc = principalBase.GetUpdateStoredProcedure()!; Assert.Equal("PrincipalBase_Update", updateSproc.Name); Assert.Equal("TPC", updateSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal( + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.False(updateSproc.IsRowsAffectedReturned); Assert.Empty(updateSproc.GetAnnotations()); @@ -2843,19 +2862,25 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Null(insertSproc["foo"]); Assert.Same(principalDerived, insertSproc.EntityType); Assert.Equal("DerivedId", insertSproc.ResultColumns.Last().Name); - Assert.Equal("DerivedId", id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)); + Assert.Equal( + "DerivedId", + id.GetColumnName(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)); Assert.Equal("bar3", insertSproc.ResultColumns.Last()["foo"]); - Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)["foo"]); + Assert.Null( + id.FindOverrides( + StoreObjectIdentifier.Create(principalDerived, StoreObjectType.InsertStoredProcedure).Value)["foo"]); updateSproc = principalDerived.GetUpdateStoredProcedure()!; Assert.Equal("Derived_Update", updateSproc.Name); Assert.Equal("Derived", updateSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal( + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.Empty(updateSproc.GetAnnotations()); Assert.Same(principalDerived, updateSproc.EntityType); Assert.Equal("Id", updateSproc.Parameters.Last().Name); - Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.UpdateStoredProcedure).Value)); + Assert.Null( + id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.UpdateStoredProcedure).Value)); deleteSproc = principalDerived.GetDeleteStoredProcedure()!; Assert.Equal("Derived_Delete", deleteSproc.Name); @@ -2864,7 +2889,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Empty(deleteSproc.ResultColumns); Assert.Same(principalDerived, deleteSproc.EntityType); Assert.Equal("Id", deleteSproc.Parameters.Last().Name); - Assert.Null(id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.DeleteStoredProcedure).Value)); + Assert.Null( + id.FindOverrides(StoreObjectIdentifier.Create(principalDerived, StoreObjectType.DeleteStoredProcedure).Value)); Assert.Equal("PrincipalDerived>", principalDerived.GetDiscriminatorValue()); Assert.Null(principalDerived.FindDiscriminatorProperty()); @@ -2907,12 +2933,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Same(dependentForeignKey, dependentBase.GetForeignKeys().Single()); Assert.Equal( - new[] - { - dependentBase, - principalBase, - principalDerived - }, + new[] { dependentBase, principalBase, principalDerived }, model.GetEntityTypes()); }, typeof(SqlServerNetTopologySuiteDesignTimeServices)); @@ -2923,7 +2944,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.HasDefaultSchema("TPC"); + modelBuilder.HasDefaultSchema("TPC") + .HasDatabaseMaxSize("20TB") + .HasPerformanceLevel("High") + .HasServiceTier("AB"); modelBuilder.Entity( eb => @@ -2947,6 +2971,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.DeleteUsingStoredProcedure(s => s .HasRowsAffectedReturnValue() .HasParameter(p => p.Id)); + + eb.HasIndex(new[] { "PrincipalBaseId" }, "PrincipalIndex") + .IsUnique() + .HasDatabaseName("PIX") + .IsClustered() + .HasFilter("AlternateId <> NULL") + .IsCreatedOnline() + .HasFillFactor(40) + .IncludeProperties(e => e.Id); }); modelBuilder.Entity>>( @@ -2997,7 +3030,7 @@ public abstract class AbstractBase public class PrincipalBase : AbstractBase { public new long? Id { get; set; } - public Point AlternateId; + public Guid AlternateId; private OwnedType _ownedField; public OwnedType Owned { get => _ownedField; set => _ownedField = value; } @@ -3013,12 +3046,23 @@ public class PrincipalDerived : PrincipalBase public class DependentBase : AbstractBase { - private new TKey Id { get; set; } + public DependentBase(TKey id) + { + Id = id; + } + + private new TKey Id { get; init; } + public PrincipalDerived> Principal { get; set; } } public class DependentDerived : DependentBase { + public DependentDerived(TKey id) + : base(id) + { + } + private string Data { get; set; } } @@ -4750,6 +4794,7 @@ protected void Test( Action> assertScaffold = null, Action assertModel = null, Type additionalDesignTimeServices = null, + Action useContext = null, string expectedExceptionMessage = null) { var model = context.GetService().Model; @@ -4803,20 +4848,36 @@ protected void Test( var assembly = build.BuildInMemory(); - if (assertModel != null) - { - var modelType = assembly.GetType(options.ModelNamespace + "." + options.ContextType.Name + "Model"); - var instancePropertyInfo = modelType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static); - var compiledModel = (IModel)instancePropertyInfo.GetValue(null); + var modelType = assembly.GetType(options.ModelNamespace + "." + options.ContextType.Name + "Model"); + var instancePropertyInfo = modelType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static); + var compiledModel = (IModel)instancePropertyInfo.GetValue(null); - var modelRuntimeInitializer = context.GetService(); - assertModel(modelRuntimeInitializer.Initialize(compiledModel, designTime: false)); - } + var modelRuntimeInitializer = context.GetService(); + compiledModel = modelRuntimeInitializer.Initialize(compiledModel, designTime: false); + assertModel(compiledModel); if (assertScaffold != null) { assertScaffold(scaffoldedFiles); } + + if (useContext != null) + { + using var testStore = SqlServerTestStore.Create("RuntimeModelTest" + context.GetType().Name); + testStore.Clean(context); + + var optionsBuilder = testStore.AddProviderOptions(new DbContextOptionsBuilder().UseModel(compiledModel)); + new SqlServerDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(); + var newContext = new DbContext(optionsBuilder.Options); + + newContext.Database.CreateExecutionStrategy().Execute( + newContext, + c => + { + using var transaction = context.Database.BeginTransaction(); + useContext(c); + }); + } } protected static void AssertFileContents( diff --git a/test/EFCore.Specification.Tests/TestUtilities/DatabaseFacadeExtensions.cs b/test/EFCore.Specification.Tests/TestUtilities/DatabaseFacadeExtensions.cs index 6550f0d4fee..ed8010165ba 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/DatabaseFacadeExtensions.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/DatabaseFacadeExtensions.cs @@ -5,8 +5,8 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; public static class DatabaseFacadeExtensions { - public static bool EnsureCreatedResiliently(this DatabaseFacade facade) - => facade.CreateExecutionStrategy().Execute(facade, f => f.EnsureCreated()); + public static bool EnsureCreatedResiliently(this DatabaseFacade façade) + => façade.CreateExecutionStrategy().Execute(façade, f => f.EnsureCreated()); public static Task EnsureCreatedResilientlyAsync(this DatabaseFacade façade, CancellationToken cancellationToken = default) => façade.CreateExecutionStrategy().ExecuteAsync(façade, (f, ct) => f.EnsureCreatedAsync(ct), cancellationToken);