diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 1b6e38fb142..3c4ccfbd94a 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -834,6 +834,14 @@ private void Create( .Append("()"); } + var sentinel = property.Sentinel; + if (sentinel != null) + { + mainBuilder.AppendLine(",") + .Append("sentinel: ") + .Append(_code.UnknownLiteral(sentinel)); + } + mainBuilder .AppendLine(");") .DecrementIndent(); diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 8743c916e88..6a8b8c98b5b 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -475,8 +475,9 @@ private void SetServiceProperties(EntityState oldState, EntityState newState) List? dependentServices = null; foreach (var serviceProperty in EntityType.GetServiceProperties()) { - var service = this[serviceProperty] ?? serviceProperty.ParameterBinding.ServiceDelegate( - new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); + var service = this[serviceProperty] + ?? serviceProperty.ParameterBinding.ServiceDelegate( + new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); if (service == null) { @@ -591,16 +592,22 @@ public void SetPropertyModified( || !changeState) { var index = property.GetOriginalValueIndex(); - if (index != -1 - && !IsConceptualNull(property)) + if (index != -1 && !IsConceptualNull(property)) { SetOriginalValue(property, this[property], index); } - } - if (currentState == EntityState.Added) - { - return; + if (currentState == EntityState.Added) + { + if (FlaggedAsTemporary(propertyIndex) + && !FlaggedAsStoreGenerated(propertyIndex) + && !HasSentinelValue(property)) + { + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, false); + } + + return; + } } if (changeState @@ -713,15 +720,7 @@ public void PropagateValue( var principalValue = principalEntry[principalProperty]; if (principalEntry.HasTemporaryValue(principalProperty)) { - if (principalEntry._stateData.IsPropertyFlagged(principalProperty.GetIndex(), PropertyFlag.IsTemporary)) - { - SetProperty(dependentProperty, principalValue, isMaterialization, setModified); - _stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, true); - } - else - { - SetTemporaryValue(dependentProperty, principalValue); - } + SetTemporaryValue(dependentProperty, principalValue); } else if (principalEntry.GetValueType(principalProperty) == CurrentValueType.StoreGenerated) { @@ -730,51 +729,15 @@ public void PropagateValue( else { SetProperty(dependentProperty, principalValue, isMaterialization, setModified); - _stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, false); } } - private CurrentValueType GetValueType( - IProperty property, - Func? equals = null) - { - if (_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary)) - { - return CurrentValueType.Temporary; - } - - var tempIndex = property.GetStoreGeneratedIndex(); - if (tempIndex == -1) - { - return CurrentValueType.Normal; - } - - if (!PropertyHasDefaultValue(property)) - { - return CurrentValueType.Normal; - } - - var defaultValue = property.ClrType.GetDefaultValue(); - var value = ReadPropertyValue(property); - if (!AreEqual(value, defaultValue, property, equals)) - { - return CurrentValueType.Normal; - } - - if (_storeGeneratedValues.TryGetValue(tempIndex, out value) - && !AreEqual(value, defaultValue, property, equals)) - { - return CurrentValueType.StoreGenerated; - } - - if (_temporaryValues.TryGetValue(tempIndex, out value) - && !AreEqual(value, defaultValue, property, equals)) - { - return CurrentValueType.Temporary; - } - - return CurrentValueType.Normal; - } + private CurrentValueType GetValueType(IProperty property) + => _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated) + ? CurrentValueType.StoreGenerated + : _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary) + ? CurrentValueType.Temporary + : CurrentValueType.Normal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -791,6 +754,7 @@ public void SetTemporaryValue(IProperty property, object? value, bool setModifie } SetProperty(property, value, isMaterialization: false, setModified, isCascadeDelete: false, CurrentValueType.Temporary); + _stateData.FlagProperty(property.GetIndex(), PropertyFlag.IsTemporary, true); } /// @@ -951,7 +915,7 @@ public TProperty GetOriginalValue(IProperty property) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TProperty GetRelationshipSnapshotValue(IPropertyBase propertyBase) - => ((Func)propertyBase.GetPropertyAccessors().RelationshipSnapshotGetter)( + => ((Func)propertyBase.GetPropertyAccessors().RelationshipSnapshotGetter)( this); /// @@ -965,17 +929,6 @@ public TProperty GetRelationshipSnapshotValue(IPropertyBase propertyB ? _shadowValues[propertyBase.GetShadowIndex()] : propertyBase.GetGetter().GetClrValue(Entity); - /// - /// 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. - /// - private bool PropertyHasDefaultValue(IPropertyBase propertyBase) - => propertyBase.IsShadowProperty() - ? propertyBase.ClrType.IsDefaultValue(_shadowValues[propertyBase.GetShadowIndex()]) - : propertyBase.GetGetter().HasDefaultValue(Entity); - /// /// 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 @@ -1282,27 +1235,19 @@ public object? this[IPropertyBase propertyBase] var storeGeneratedIndex = propertyBase.GetStoreGeneratedIndex(); if (storeGeneratedIndex != -1) { - var propertyClrType = propertyBase.ClrType; - var defaultValue = propertyClrType.GetDefaultValue(); var property = (IProperty)propertyBase; + var propertyIndex = property.GetIndex(); - if (_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - && !AreEqual(generatedValue, defaultValue, property)) + if (FlaggedAsStoreGenerated(propertyIndex)) { - return generatedValue; + return _storeGeneratedValues.GetValue(storeGeneratedIndex); } - var value = ReadPropertyValue(propertyBase); - if (AreEqual(value, defaultValue, property)) + if (FlaggedAsTemporary(propertyIndex) + && HasSentinelValue(property)) { - if (_temporaryValues.TryGetValue(storeGeneratedIndex, out generatedValue) - && !AreEqual(generatedValue, defaultValue, property)) - { - return generatedValue; - } + return _temporaryValues.GetValue(storeGeneratedIndex); } - - return value; } return ReadPropertyValue(propertyBase); @@ -1311,6 +1256,26 @@ public object? this[IPropertyBase propertyBase] set => SetProperty(propertyBase, value, isMaterialization: false); } + /// + /// 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 bool FlaggedAsStoreGenerated(int propertyIndex) + => !_storeGeneratedValues.IsEmpty + && _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.IsStoreGenerated); + + /// + /// 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 bool FlaggedAsTemporary(int propertyIndex) + => !_temporaryValues.IsEmpty + && _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.IsTemporary); + /// /// 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 @@ -1338,19 +1303,22 @@ private void SetProperty( var asProperty = propertyBase as IProperty; int propertyIndex; CurrentValueType currentValueType; + int storeGeneratedIndex; + bool valuesEqual; - var valuesEqual = false; if (asProperty != null) { propertyIndex = asProperty.GetIndex(); valuesEqual = AreEqual(currentValue, value, asProperty); currentValueType = GetValueType(asProperty); + storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); } else { propertyIndex = -1; valuesEqual = ReferenceEquals(currentValue, value); currentValueType = CurrentValueType.Normal; + storeGeneratedIndex = -1; } if (!valuesEqual @@ -1385,64 +1353,38 @@ private void SetProperty( { StateManager.InternalEntityEntryNotifier.PropertyChanging(this, propertyBase); - if (valueType == CurrentValueType.Normal) + if (storeGeneratedIndex == -1) { WritePropertyValue(propertyBase, value, isMaterialization); - - switch (currentValueType) + } + else + { + switch (valueType) { + case CurrentValueType.Normal: + WritePropertyValue(propertyBase, value, isMaterialization); + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, isFlagged: false); + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: false); + break; case CurrentValueType.StoreGenerated: - if (!_storeGeneratedValues.IsEmpty) - { - var defaultValue = asProperty!.ClrType.GetDefaultValue(); - var storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); - _storeGeneratedValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); - } - + EnsureStoreGeneratedValues(); + _storeGeneratedValues.SetValue(asProperty!, value, storeGeneratedIndex); + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: true); break; case CurrentValueType.Temporary: - if (!_temporaryValues.IsEmpty) + EnsureTemporaryValues(); + _temporaryValues.SetValue(asProperty!, value, storeGeneratedIndex); + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, isFlagged: true); + _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: false); + if (!HasSentinelValue(asProperty!)) { - var defaultValue = asProperty!.ClrType.GetDefaultValue(); - var storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); - _temporaryValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); + WritePropertyValue(propertyBase, value, isMaterialization); } break; - } - } - else - { - var storeGeneratedIndex = asProperty!.GetStoreGeneratedIndex(); - Check.DebugAssert(storeGeneratedIndex >= 0, $"storeGeneratedIndex is {storeGeneratedIndex}"); - - if (valueType == CurrentValueType.StoreGenerated) - { - var defaultValue = asProperty!.ClrType.GetDefaultValue(); - if (!AreEqual(currentValue, defaultValue, asProperty)) - { - WritePropertyValue(asProperty, defaultValue, isMaterialization); - } - - EnsureStoreGeneratedValues(); - _storeGeneratedValues.SetValue(asProperty, value, storeGeneratedIndex); - } - else - { - var defaultValue = asProperty!.ClrType.GetDefaultValue(); - if (!AreEqual(currentValue, defaultValue, asProperty)) - { - WritePropertyValue(asProperty, defaultValue, isMaterialization); - } - - if (_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - && !AreEqual(generatedValue, defaultValue, asProperty)) - { - _storeGeneratedValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); - } - - EnsureTemporaryValues(); - _temporaryValues.SetValue(asProperty, value, storeGeneratedIndex); + default: + Check.DebugFail($"Bad value type {valueType}"); + break; } } @@ -1509,11 +1451,6 @@ public void HandleNullForeignKey( private static bool AreEqual(object? value, object? otherValue, IProperty property) => property.GetValueComparer().Equals(value, otherValue); - private static bool AreEqual(object? value, object? otherValue, IProperty property, Func? equals) - => equals != null - ? equals(value, otherValue) - : AreEqual(value, otherValue, 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 @@ -1528,13 +1465,10 @@ public void AcceptChanges() { var storeGeneratedIndex = property.GetStoreGeneratedIndex(); if (storeGeneratedIndex != -1 + && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated) && _storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var value)) { - var defaultValue = property.ClrType.GetDefaultValue(); - if (!AreEqual(value, defaultValue, property)) - { - this[property] = value; - } + this[property] = value; } } @@ -1542,6 +1476,7 @@ public void AcceptChanges() _temporaryValues = new SidecarValues(); } + _stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.IsStoreGenerated, false); _stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.IsTemporary, false); _stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.Unknown, false); @@ -1580,7 +1515,7 @@ public InternalEntityEntry PrepareToSave() { if (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Throw && !HasTemporaryValue(property) - && !HasDefaultValue(property)) + && HasExplicitValue(property)) { throw new InvalidOperationException( CoreStrings.PropertyReadOnlyBeforeSave( @@ -1763,6 +1698,7 @@ public void DiscardStoreGeneratedValues() if (!_storeGeneratedValues.IsEmpty) { _storeGeneratedValues = new SidecarValues(); + _stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.IsStoreGenerated, false); } } @@ -1777,7 +1713,7 @@ public bool IsStoreGenerated(IProperty property) && EntityState == EntityState.Added && (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Ignore || HasTemporaryValue(property) - || HasDefaultValue(property))) + || !HasExplicitValue(property))) || (property.ValueGenerated.ForUpdate() && (EntityState is EntityState.Modified or EntityState.Deleted) && (property.GetAfterSaveBehavior() == PropertySaveBehavior.Ignore @@ -1790,26 +1726,15 @@ public bool IsStoreGenerated(IProperty property) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasDefaultValue(IProperty property) - { - if (!PropertyHasDefaultValue(property)) - { - return false; - } - - var storeGeneratedIndex = property.GetStoreGeneratedIndex(); - if (storeGeneratedIndex == -1) - { - return true; - } - - var defaultValue = property.ClrType.GetDefaultValue(); + public bool HasExplicitValue(IProperty property) + => !HasSentinelValue(property) + || _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated) + || _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary); - return (!_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - || AreEqual(defaultValue, generatedValue, property)) - && (!_temporaryValues.TryGetValue(storeGeneratedIndex, out generatedValue) - || AreEqual(defaultValue, generatedValue, property)); - } + private bool HasSentinelValue(IProperty property) + => property.IsShadowProperty() + ? AreEqual(_shadowValues[property.GetShadowIndex()], property.Sentinel, property) + : property.GetGetter().HasSentinelValue(Entity); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1832,7 +1757,7 @@ public bool HasDefaultValue(IProperty property) var keyGenerated = keyProperty.ValueGenerated == ValueGenerated.OnAdd; if ((HasTemporaryValue(keyProperty) - || HasDefaultValue(keyProperty)) + || !HasExplicitValue(keyProperty)) && (keyGenerated || keyProperty.FindGenerationProperty() != null)) { return (true, false); diff --git a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs index 15c27725391..7cedc30df75 100644 --- a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs +++ b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs @@ -149,9 +149,8 @@ private static void SetValue(InternalEntityEntry entry, IProperty property, Valu if (principalProperty != property) { - var principalValue = principalEntry[principalProperty]; if (generationProperty == null - || !principalProperty.ClrType.IsDefaultValue(principalValue)) + || principalEntry.HasExplicitValue(principalProperty)) { entry.PropagateValue(principalEntry, principalProperty, property); diff --git a/src/EFCore/ChangeTracking/Internal/SidecarValues.cs b/src/EFCore/ChangeTracking/Internal/SidecarValues.cs index 4cf5b0df484..6c2cfc47676 100644 --- a/src/EFCore/ChangeTracking/Internal/SidecarValues.cs +++ b/src/EFCore/ChangeTracking/Internal/SidecarValues.cs @@ -26,8 +26,11 @@ public bool TryGetValue(int index, out object? value) return true; } + public object? GetValue(int index) + => _values[index]; + public T GetValue(int index) - => IsEmpty ? default! : _values.GetValue(index); + => _values.GetValue(index); public void SetValue(IProperty property, object? value, int index) { diff --git a/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs index 170169937ad..7ac738c4966 100644 --- a/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs @@ -61,7 +61,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen /// public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.CurrentValueGetter)(entry); + key = ((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry); return key != null; } @@ -73,7 +73,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen /// public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry); + key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry); return key != null; } @@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent /// public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.OriginalValueGetter!)(entry); + key = ((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry); return key != null; } @@ -97,7 +97,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe /// public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry); + key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry); return key != null; } } diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs index fc87a02cd4e..2c9162b6ab5 100644 --- a/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs @@ -68,7 +68,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen /// public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.CurrentValueGetter)(entry)!; + key = ((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!; return true; } @@ -80,7 +80,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen /// public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!; + key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!; return true; } @@ -92,7 +92,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent /// public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.OriginalValueGetter!)(entry)!; + key = ((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!; return true; } @@ -104,7 +104,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe /// public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry)!; + key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry)!; return true; } } diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs index 843fc6dccb9..ce52fb1cd8d 100644 --- a/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs @@ -66,7 +66,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, out TKey key /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key) - => HandleNullableValue(((Func)_propertyAccessors.CurrentValueGetter)(entry), out key); + => HandleNullableValue(((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry), out key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -76,7 +76,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key /// public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, out TKey key) => HandleNullableValue( - ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry), out key); + ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry), out key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey key) - => HandleNullableValue(((Func)_propertyAccessors.OriginalValueGetter!)(entry), out key); + => HandleNullableValue(((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry), out key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -94,7 +94,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey ke /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, out TKey key) - => HandleNullableValue(((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry), out key); + => HandleNullableValue(((Func)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry), out key); private static bool HandleNullableValue(TKey? value, out TKey key) { diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs index 0e6b2b7dbfa..b350bec9434 100644 --- a/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs @@ -72,7 +72,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen /// public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = (TKey)(object)((Func)_propertyAccessors.CurrentValueGetter)(entry)!; + key = (TKey)(object)((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!; return true; } @@ -84,7 +84,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen /// public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = (TKey)(object)((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!; + key = (TKey)(object)((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!; return true; } @@ -96,7 +96,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent /// public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = (TKey)(object)((Func)_propertyAccessors.OriginalValueGetter!)(entry)!; + key = (TKey)(object)((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!; return true; } @@ -108,7 +108,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe /// public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) { - key = (TKey)(object)((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry)!; + key = (TKey)(object)((Func)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry)!; return true; } } diff --git a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs index ec2a68c3325..c3dc0865d70 100644 --- a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs @@ -68,7 +68,7 @@ public virtual IProperty FindNullPropertyInKeyValues(IReadOnlyList keyV /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual TKey CreateFromCurrentValues(IUpdateEntry entry) - => ((Func)_propertyAccessors.CurrentValueGetter)(entry); + => ((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -86,7 +86,7 @@ public virtual IProperty FindNullPropertyInCurrentValues(IUpdateEntry entry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual TKey CreateFromOriginalValues(IUpdateEntry entry) - => ((Func)_propertyAccessors.OriginalValueGetter!)(entry); + => ((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -95,7 +95,7 @@ public virtual TKey CreateFromOriginalValues(IUpdateEntry entry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual TKey CreateFromRelationshipSnapshot(IUpdateEntry entry) - => ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry); + => ((Func)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -119,18 +119,6 @@ public virtual object CreateEquatableKey(IUpdateEntry entry, bool fromOriginalVa : CreateFromCurrentValues(entry), EqualityComparer); - private sealed class NoNullsStructuralEqualityComparer : IEqualityComparer - { - private readonly IEqualityComparer _comparer - = StructuralComparisons.StructuralEqualityComparer; - - public bool Equals(TKey? x, TKey? y) - => _comparer.Equals(x, y); - - public int GetHashCode([DisallowNull] TKey obj) - => _comparer.GetHashCode(obj); - } - private sealed class NoNullsCustomEqualityComparer : IEqualityComparer { private readonly Func _equals; diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index 1b88ef9f093..65d37aa9b5e 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -130,7 +130,7 @@ protected virtual Expression CreateSnapshotExpression( if (memberAccess.Type != propertyBase.ClrType) { - var hasDefaultValueExpression = memberAccess.MakeHasDefaultValue(propertyBase); + var hasDefaultValueExpression = memberAccess.MakeHasSentinelValue(propertyBase); memberAccess = Expression.Condition( hasDefaultValueExpression, diff --git a/src/EFCore/ChangeTracking/Internal/StateData.cs b/src/EFCore/ChangeTracking/Internal/StateData.cs index e1c5c83b144..9f40a90f01a 100644 --- a/src/EFCore/ChangeTracking/Internal/StateData.cs +++ b/src/EFCore/ChangeTracking/Internal/StateData.cs @@ -11,7 +11,8 @@ internal enum PropertyFlag Null = 1, Unknown = 2, IsLoaded = 3, - IsTemporary = 4 + IsTemporary = 4, + IsStoreGenerated = 5 } internal readonly struct StateData diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 834fb6f4fc3..f857b68bd28 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -281,13 +281,13 @@ public virtual InternalEntityEntry CreateEntry(IDictionary valu { valuesArray[i++] = values.TryGetValue(property.Name, out var value) ? value - : property.ClrType.GetDefaultValue(); + : property.Sentinel; if (property.IsShadowProperty()) { shadowPropertyValuesArray[property.GetShadowIndex()] = values.TryGetValue(property.Name, out var shadowValue) ? shadowValue - : property.ClrType.GetDefaultValue(); + : property.Sentinel; } } diff --git a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs index 91d60fbbac4..2cb4d4a577e 100644 --- a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs +++ b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs @@ -45,7 +45,8 @@ public ValueGenerationManager( InternalEntityEntry? chosenPrincipal = null; foreach (var property in entry.EntityType.GetForeignKeyProperties()) { - if (!entry.HasDefaultValue(property)) + if (!entry.IsUnknown(property) + && entry.HasExplicitValue(property)) { continue; } @@ -68,7 +69,7 @@ public ValueGenerationManager( InternalEntityEntry? chosenPrincipal = null; foreach (var property in entry.EntityType.GetForeignKeyProperties()) { - if (!entry.HasDefaultValue(property)) + if (entry.HasExplicitValue(property)) { continue; } @@ -94,7 +95,7 @@ public virtual bool Generate(InternalEntityEntry entry, bool includePrimaryKey = foreach (var property in entry.EntityType.GetValueGeneratingProperties()) { - if (!entry.HasDefaultValue(property) + if (entry.HasExplicitValue(property) || (!includePrimaryKey && property.IsPrimaryKey())) { @@ -153,7 +154,7 @@ public virtual async Task GenerateAsync( var hasNonStableValues = false; foreach (var property in entry.EntityType.GetValueGeneratingProperties()) { - if (!entry.HasDefaultValue(property) + if (entry.HasExplicitValue(property) || (!includePrimaryKey && property.IsPrimaryKey())) { diff --git a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs index c986f25059d..e21a30e6884 100644 --- a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs +++ b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs @@ -20,37 +20,55 @@ public static class ExpressionExtensions /// 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 Expression MakeHasDefaultValue( + public static Expression MakeHasSentinelValue( this Expression currentValueExpression, IReadOnlyPropertyBase? propertyBase) { - if (!currentValueExpression.Type.IsValueType) - { - return Expression.ReferenceEqual( - currentValueExpression, - Expression.Constant(null, currentValueExpression.Type)); - } + var sentinel = propertyBase?.Sentinel; - if (currentValueExpression.Type.IsGenericType - && currentValueExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) + var isReferenceType = !currentValueExpression.Type.IsValueType; + var isNullableValueType = currentValueExpression.Type.IsGenericType + && currentValueExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>); + + if (sentinel == null) { - return Expression.Not( - Expression.Call( + return isReferenceType + ? Expression.ReferenceEqual( currentValueExpression, - Check.NotNull( - currentValueExpression.Type.GetMethod("get_HasValue"), $"get_HasValue on {currentValueExpression.Type.Name}"))); + Expression.Constant(null, currentValueExpression.Type)) + : isNullableValueType + ? Expression.Not( + Expression.Call( + currentValueExpression, + currentValueExpression.Type.GetMethod("get_HasValue")!)) + : Expression.Constant(false); } - var property = propertyBase as IReadOnlyProperty; - var comparer = property?.GetValueComparer() + var comparer = (propertyBase as IProperty)?.GetValueComparer() ?? ValueComparer.CreateDefault( propertyBase?.ClrType ?? currentValueExpression.Type, favorStructuralComparisons: false); - return comparer.ExtractEqualsBody( + var equalsExpression = comparer.ExtractEqualsBody( comparer.Type != currentValueExpression.Type ? Expression.Convert(currentValueExpression, comparer.Type) : currentValueExpression, - Expression.Default(comparer.Type)); + Expression.Constant(sentinel, comparer.Type)); + + if (isReferenceType || isNullableValueType) + { + return Expression.AndAlso( + isReferenceType + ? Expression.Not( + Expression.ReferenceEqual( + currentValueExpression, + Expression.Constant(null, currentValueExpression.Type))) + : Expression.Call( + currentValueExpression, + currentValueExpression.Type.GetMethod("get_HasValue")!), + equalsExpression); + } + + return equalsExpression; } /// diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index bd850c42d41..6ceb5e42a67 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -337,6 +337,7 @@ private static RuntimeProperty Create(IProperty property, RuntimeEntityType runt => runtimeEntityType.AddProperty( property.Name, property.ClrType, + property.Sentinel, property.PropertyInfo, property.FieldInfo, property.GetPropertyAccessMode(), diff --git a/src/EFCore/Metadata/IClrPropertyGetter.cs b/src/EFCore/Metadata/IClrPropertyGetter.cs index cc76d80baa4..cfb268e0789 100644 --- a/src/EFCore/Metadata/IClrPropertyGetter.cs +++ b/src/EFCore/Metadata/IClrPropertyGetter.cs @@ -31,5 +31,5 @@ public interface IClrPropertyGetter /// /// The entity instance. /// if the property value is the CLR default; it is any other value. - bool HasDefaultValue(object entity); + bool HasSentinelValue(object entity); } diff --git a/src/EFCore/Metadata/IConventionProperty.cs b/src/EFCore/Metadata/IConventionProperty.cs index 72358454f63..5d2e9dd0e79 100644 --- a/src/EFCore/Metadata/IConventionProperty.cs +++ b/src/EFCore/Metadata/IConventionProperty.cs @@ -95,6 +95,20 @@ public interface IConventionProperty : IReadOnlyProperty, IConventionPropertyBas /// The configuration source for . ConfigurationSource? GetIsConcurrencyTokenConfigurationSource(); + /// + /// Sets the sentinel value that indicates that this property is not set. + /// + /// The sentinel value. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + object? SetSentinel(object? sentinel, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetSentinelConfigurationSource(); + /// /// Returns a value indicating whether the property was created implicitly and isn't based on the CLR model. /// diff --git a/src/EFCore/Metadata/IMutableProperty.cs b/src/EFCore/Metadata/IMutableProperty.cs index d816bb6b5cc..1b042624a34 100644 --- a/src/EFCore/Metadata/IMutableProperty.cs +++ b/src/EFCore/Metadata/IMutableProperty.cs @@ -48,6 +48,11 @@ public interface IMutableProperty : IReadOnlyProperty, IMutablePropertyBase /// new bool IsConcurrencyToken { get; set; } + /// + /// Gets or sets the sentinel value that indicates that this property is not set. + /// + new object? Sentinel { get; set; } + /// /// Finds the first principal property that the given property is constrained by /// if the given property is part of a foreign key. diff --git a/src/EFCore/Metadata/IReadOnlyProperty.cs b/src/EFCore/Metadata/IReadOnlyProperty.cs index 68fe17152fb..ff6117c03be 100644 --- a/src/EFCore/Metadata/IReadOnlyProperty.cs +++ b/src/EFCore/Metadata/IReadOnlyProperty.cs @@ -390,6 +390,11 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.Append(" PropertyAccessMode.").Append(GetPropertyAccessMode()); } + if (Sentinel != null && !Equals(Sentinel, ClrType.GetDefaultValue())) + { + builder.Append(" Sentinel:").Append(Sentinel); + } + if ((options & MetadataDebugStringOptions.IncludePropertyIndexes) != 0 && ((AnnotatableBase)this).IsReadOnly) { diff --git a/src/EFCore/Metadata/IReadOnlyPropertyBase.cs b/src/EFCore/Metadata/IReadOnlyPropertyBase.cs index ce49c16bd77..a1dfe83495d 100644 --- a/src/EFCore/Metadata/IReadOnlyPropertyBase.cs +++ b/src/EFCore/Metadata/IReadOnlyPropertyBase.cs @@ -29,6 +29,11 @@ public interface IReadOnlyPropertyBase : IReadOnlyAnnotatable [DynamicallyAccessedMembers(IEntityType.DynamicallyAccessedMemberTypes | IProperty.DynamicallyAccessedMemberTypes)] Type ClrType { get; } + /// + /// Gets the sentinel value that indicates that this property is not set. + /// + object? Sentinel { get; } + /// /// Gets the for the underlying CLR property for this property-like object. /// This may be for shadow properties or if mapped directly to a field. diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs index b067f4aacec..64709c766f7 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs @@ -16,7 +16,7 @@ public sealed class ClrPropertyGetter : IClrPropertyGetter where TEntity : class { private readonly Func _getter; - private readonly Func _hasDefaultValue; + private readonly Func _hasSentinelValue; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,10 +24,10 @@ public sealed class ClrPropertyGetter : IClrPropertyGetter /// 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 ClrPropertyGetter(Func getter, Func hasDefaultValue) + public ClrPropertyGetter(Func getter, Func hasSentinelValue) { _getter = getter; - _hasDefaultValue = hasDefaultValue; + _hasSentinelValue = hasSentinelValue; } /// @@ -47,6 +47,6 @@ public ClrPropertyGetter(Func getter, Func hasDe /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasDefaultValue(object entity) - => _hasDefaultValue((TEntity)entity); + public bool HasSentinelValue(object entity) + => _hasSentinelValue((TEntity)entity); } diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index c2c1c5089a1..b1c1e83f580 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -58,18 +58,18 @@ protected override IClrPropertyGetter CreateGeneric( Expression.Lambda>(readExpression, entityParameter).Compile(), - Expression.Lambda>(hasDefaultValueExpression, entityParameter).Compile()); + Expression.Lambda>(hasSentinelValueExpression, entityParameter).Compile()); } } diff --git a/src/EFCore/Metadata/Internal/Navigation.cs b/src/EFCore/Metadata/Internal/Navigation.cs index 34a24e4af97..48d4c8a1f32 100644 --- a/src/EFCore/Metadata/Internal/Navigation.cs +++ b/src/EFCore/Metadata/Internal/Navigation.cs @@ -51,6 +51,16 @@ public override Type ClrType ? typeof(IEnumerable<>).MakeGenericType(TargetEntityType.ClrType) : TargetEntityType.ClrType); + + /// + /// 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 object? Sentinel + => 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/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index ec661bce538..00032e5b913 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class Property : PropertyBase, IMutableProperty, IConventionProperty, IProperty { private bool? _isConcurrencyToken; + private object? _sentinel; private bool? _isNullable; private ValueGenerated? _valueGenerated; private CoreTypeMapping? _typeMapping; @@ -24,6 +25,7 @@ public class Property : PropertyBase, IMutableProperty, IConventionProperty, IPr private ConfigurationSource? _typeConfigurationSource; private ConfigurationSource? _isNullableConfigurationSource; private ConfigurationSource? _isConcurrencyTokenConfigurationSource; + private ConfigurationSource? _sentinelConfigurationSource; private ConfigurationSource? _valueGeneratedConfigurationSource; private ConfigurationSource? _typeMappingConfigurationSource; @@ -296,6 +298,50 @@ private static bool DefaultIsConcurrencyToken public virtual ConfigurationSource? GetIsConcurrencyTokenConfigurationSource() => _isConcurrencyTokenConfigurationSource; + /// + /// 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 object? Sentinel + { + get => _sentinel ?? DefaultSentinel; + set => SetSentinel(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual object? SetSentinel(object? sentinel, ConfigurationSource configurationSource) + { + EnsureMutable(); + + _sentinel = sentinel; + + _sentinelConfigurationSource = configurationSource.Max(_sentinelConfigurationSource); + + return sentinel; + } + + private object? DefaultSentinel + => (this is IProperty property + && property.TryGetMemberInfo(forMaterialization: false, forSet: false, out var member, out _) + ? member!.GetMemberType() + : ClrType).GetDefaultValue(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetSentinelConfigurationSource() + => _sentinelConfigurationSource; + /// /// 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 @@ -1490,6 +1536,17 @@ IEnumerable IProperty.GetContainingKeys() => SetIsConcurrencyToken( concurrencyToken, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + object? IConventionProperty.SetSentinel(object? sentinel, bool fromDataAnnotation) + => SetSentinel( + sentinel, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -1858,4 +1915,10 @@ void IMutableProperty.SetProviderValueComparer( [DebuggerStepThrough] ValueComparer IProperty.GetProviderValueComparer() => GetProviderValueComparer()!; + + /// + /// Gets the sentinel value that indicates that this property is not set. + /// + object? IReadOnlyPropertyBase.Sentinel + => Sentinel; } diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 1f2c61988f1..bc821a12371 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -42,17 +42,18 @@ private static PropertyAccessors CreateGeneric(IPropertyBase property property == null ? null : CreateValueBufferGetter(property)); } - private static Func CreateCurrentValueGetter( + private static Func CreateCurrentValueGetter( IPropertyBase propertyBase, bool useStoreGeneratedValues) { var entityClrType = propertyBase.DeclaringType.ClrType; - var updateParameter = Expression.Parameter(typeof(IUpdateEntry), "entry"); - var entryParameter = Expression.Convert(updateParameter, typeof(InternalEntityEntry)); - + var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); + var property = propertyBase as IProperty; + var propertyIndex = propertyBase.GetIndex(); var shadowIndex = propertyBase.GetShadowIndex(); + var storeGeneratedIndex = propertyBase.GetStoreGeneratedIndex(); Expression currentValueExpression; - var propertyDefault = Expression.Constant(default(TProperty), typeof(TProperty)); + Expression hasSentinelValueExpression; if (shadowIndex >= 0) { @@ -60,6 +61,8 @@ private static Func CreateCurrentValueGetter entryParameter, InternalEntityEntry.MakeReadShadowValueMethod(typeof(TProperty)), Expression.Constant(shadowIndex)); + + hasSentinelValueExpression = currentValueExpression.MakeHasSentinelValue(propertyBase); } else { @@ -68,68 +71,81 @@ private static Func CreateCurrentValueGetter entityClrType); var memberInfo = propertyBase.GetMemberInfo(forMaterialization: false, forSet: false); + currentValueExpression = PropertyBase.CreateMemberAccess(propertyBase, convertedExpression, memberInfo); + hasSentinelValueExpression = currentValueExpression.MakeHasSentinelValue(propertyBase); if (currentValueExpression.Type != typeof(TProperty)) { - currentValueExpression = Expression.Condition( - currentValueExpression.MakeHasDefaultValue(propertyBase), - propertyDefault, - Expression.Convert(currentValueExpression, typeof(TProperty))); + if (currentValueExpression.Type.IsNullableType() + && !typeof(TProperty).IsNullableType()) + { + var nullableValue = Expression.Variable(currentValueExpression.Type, "nullableValue"); + + currentValueExpression = Expression.Block( + new[] { nullableValue }, + new List + { + Expression.Assign( + nullableValue, + currentValueExpression), + currentValueExpression.Type.IsValueType + ? Expression.Condition( + Expression.Call( + nullableValue, + nullableValue.Type.GetMethod("get_HasValue")!), + Expression.Convert(nullableValue, typeof(TProperty)), + Expression.Default(typeof(TProperty))) + : Expression.Condition( + Expression.ReferenceEqual(nullableValue, Expression.Constant(null)), + Expression.Default(typeof(TProperty)), + Expression.Convert(nullableValue, typeof(TProperty))) + }); + } + else + { + currentValueExpression = Expression.Convert(currentValueExpression, typeof(TProperty)); + } } } - var storeGeneratedIndex = propertyBase.GetStoreGeneratedIndex(); - if (storeGeneratedIndex >= 0) + if (useStoreGeneratedValues && storeGeneratedIndex >= 0) { - var comparer = (propertyBase as IProperty)?.GetValueComparer() - ?? ValueComparer.CreateDefault(propertyBase.ClrType, favorStructuralComparisons: true); - - var comparerDefault = comparer.Type != typeof(TProperty) - ? (Expression)Expression.Convert(propertyDefault, comparer.Type) - : propertyDefault; - - if (useStoreGeneratedValues) - { - currentValueExpression = Expression.Condition( - comparer.ExtractEqualsBody( - comparer.Type != currentValueExpression.Type - ? Expression.Convert(currentValueExpression, comparer.Type) - : currentValueExpression, - comparerDefault), - Expression.Call( - entryParameter, - InternalEntityEntry.MakeReadStoreGeneratedValueMethod(typeof(TProperty)), - Expression.Constant(storeGeneratedIndex)), - currentValueExpression); - } - currentValueExpression = Expression.Condition( - comparer.ExtractEqualsBody( - comparer.Type != currentValueExpression.Type - ? Expression.Convert(currentValueExpression, comparer.Type) - : currentValueExpression, - comparerDefault), Expression.Call( entryParameter, - InternalEntityEntry.MakeReadTemporaryValueMethod(typeof(TProperty)), + typeof(InternalEntityEntry).GetMethod(nameof(InternalEntityEntry.FlaggedAsStoreGenerated))!, + Expression.Constant(propertyIndex)), + Expression.Call( + entryParameter, + InternalEntityEntry.MakeReadStoreGeneratedValueMethod(typeof(TProperty)), Expression.Constant(storeGeneratedIndex)), - currentValueExpression); + Expression.Condition( + Expression.AndAlso( + Expression.Call( + entryParameter, + typeof(InternalEntityEntry).GetMethod(nameof(InternalEntityEntry.FlaggedAsTemporary))!, + Expression.Constant(propertyIndex)), + hasSentinelValueExpression), + Expression.Call( + entryParameter, + InternalEntityEntry.MakeReadTemporaryValueMethod(typeof(TProperty)), + Expression.Constant(storeGeneratedIndex)), + currentValueExpression)); } - return Expression.Lambda>( + return Expression.Lambda>( currentValueExpression, - updateParameter) + entryParameter) .Compile(); } - private static Func CreateOriginalValueGetter(IProperty property) + private static Func CreateOriginalValueGetter(IProperty property) { - var updateParameter = Expression.Parameter(typeof(IUpdateEntry), "entry"); - var entryParameter = Expression.Convert(updateParameter, typeof(InternalEntityEntry)); + var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); var originalValuesIndex = property.GetOriginalValueIndex(); - return Expression.Lambda>( + return Expression.Lambda>( originalValuesIndex >= 0 ? Expression.Call( entryParameter, @@ -141,20 +157,17 @@ private static Func CreateOriginalValueGetter CreateRelationshipSnapshotGetter(IPropertyBase propertyBase) + private static Func CreateRelationshipSnapshotGetter(IPropertyBase propertyBase) { - var updateParameter = Expression.Parameter(typeof(IUpdateEntry), "entry"); - var entryParameter = Expression.Convert(updateParameter, typeof(InternalEntityEntry)); + var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); var relationshipIndex = (propertyBase as IProperty)?.GetRelationshipIndex() ?? -1; - return Expression.Lambda>( + return Expression.Lambda>( relationshipIndex >= 0 ? Expression.Call( entryParameter, @@ -165,7 +178,7 @@ private static Func CreateRelationshipSnapshotGetter IPropertyBase.GetCurrentValueComparer() => CurrentValueComparer; + + /// + /// Gets the sentinel value that indicates that this property is not set. + /// + object? IReadOnlyPropertyBase.Sentinel + => null; } diff --git a/src/EFCore/Metadata/Internal/ServiceProperty.cs b/src/EFCore/Metadata/Internal/ServiceProperty.cs index 35adf4c85f6..b133d9c2d26 100644 --- a/src/EFCore/Metadata/Internal/ServiceProperty.cs +++ b/src/EFCore/Metadata/Internal/ServiceProperty.cs @@ -166,6 +166,12 @@ public virtual ServiceParameterBinding? ParameterBinding private void UpdateParameterBindingConfigurationSource(ConfigurationSource configurationSource) => _parameterBindingConfigurationSource = configurationSource.Max(_parameterBindingConfigurationSource); + /// + /// Gets the sentinel value that indicates that this property is not set. + /// + public virtual object? Sentinel + => 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/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs index 1c3fb7136dc..e80127fa9a4 100644 --- a/src/EFCore/Metadata/Internal/SkipNavigation.cs +++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs @@ -322,6 +322,12 @@ public override PropertyAccessMode GetPropertyAccessMode() => (PropertyAccessMode)(this[CoreAnnotationNames.PropertyAccessMode] ?? ((IReadOnlyTypeBase)DeclaringType).GetNavigationAccessMode()); + /// + /// Gets the sentinel value that indicates that this property is not set. + /// + public virtual object? Sentinel + => null; + /// /// Runs the conventions when an annotation was set or removed. /// diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 8c0462ac8bb..863a8e20c03 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -580,6 +580,7 @@ private IEnumerable GetIndexes() /// /// The name of the property to add. /// The type of value the property will hold. + /// The property value to use to consider the property not set. /// The corresponding CLR property or for a shadow property. /// The corresponding CLR field or for a shadow property. /// The used for this property. @@ -609,6 +610,7 @@ private IEnumerable GetIndexes() public virtual RuntimeProperty AddProperty( string name, Type clrType, + object? sentinel = null, PropertyInfo? propertyInfo = null, FieldInfo? fieldInfo = null, PropertyAccessMode propertyAccessMode = Internal.Model.DefaultPropertyAccessMode, @@ -632,6 +634,7 @@ public virtual RuntimeProperty AddProperty( var property = new RuntimeProperty( name, clrType, + sentinel, propertyInfo, fieldInfo, this, diff --git a/src/EFCore/Metadata/RuntimeNavigation.cs b/src/EFCore/Metadata/RuntimeNavigation.cs index 3e6fb2f38b4..b6850fa1705 100644 --- a/src/EFCore/Metadata/RuntimeNavigation.cs +++ b/src/EFCore/Metadata/RuntimeNavigation.cs @@ -69,6 +69,10 @@ public override RuntimeEntityType DeclaringEntityType get => ((IReadOnlyNavigation)this).IsOnDependent ? ForeignKey.DeclaringEntityType : ForeignKey.PrincipalEntityType; } + /// + public override object? Sentinel + => null; + /// /// Returns a string that represents the current object. /// diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index bb47c2e7014..7ecc327ab3e 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -19,6 +19,7 @@ public class RuntimeProperty : RuntimePropertyBase, IProperty private readonly bool _isNullable; private readonly ValueGenerated _valueGenerated; private readonly bool _isConcurrencyToken; + private readonly object? _sentinel; private readonly PropertySaveBehavior _beforeSaveBehavior; private readonly PropertySaveBehavior _afterSaveBehavior; private readonly Func? _valueGeneratorFactory; @@ -38,6 +39,7 @@ public class RuntimeProperty : RuntimePropertyBase, IProperty public RuntimeProperty( string name, Type clrType, + object? sentinel, PropertyInfo? propertyInfo, FieldInfo? fieldInfo, RuntimeEntityType declaringEntityType, @@ -62,6 +64,7 @@ public RuntimeProperty( { DeclaringEntityType = declaringEntityType; ClrType = clrType; + _sentinel = sentinel; _isNullable = nullable; _isConcurrencyToken = concurrencyToken; _valueGenerated = valueGenerated; @@ -230,6 +233,10 @@ private ValueComparer GetKeyValueComparer() return principal.GetKeyValueComparer(checkedProperties); } + /// + public override object? Sentinel + => _sentinel; + /// /// Returns a string that represents the current object. /// diff --git a/src/EFCore/Metadata/RuntimePropertyBase.cs b/src/EFCore/Metadata/RuntimePropertyBase.cs index 1073786388f..6f8b598d3b7 100644 --- a/src/EFCore/Metadata/RuntimePropertyBase.cs +++ b/src/EFCore/Metadata/RuntimePropertyBase.cs @@ -82,6 +82,9 @@ protected RuntimePropertyBase( PropertyAccessMode IReadOnlyPropertyBase.GetPropertyAccessMode() => _propertyAccessMode; + /// + public abstract object? Sentinel { get; } + /// IReadOnlyTypeBase IReadOnlyPropertyBase.DeclaringType { diff --git a/src/EFCore/Metadata/RuntimeServiceProperty.cs b/src/EFCore/Metadata/RuntimeServiceProperty.cs index 480d63a5334..307d050026f 100644 --- a/src/EFCore/Metadata/RuntimeServiceProperty.cs +++ b/src/EFCore/Metadata/RuntimeServiceProperty.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -68,6 +69,10 @@ public virtual ServiceParameterBinding ParameterBinding set => _parameterBinding = value; } + /// + public override object? Sentinel + => null; + /// /// Returns a string that represents the current object. /// diff --git a/src/EFCore/Metadata/RuntimeSkipNavigation.cs b/src/EFCore/Metadata/RuntimeSkipNavigation.cs index 515d7819fb1..e57aacea091 100644 --- a/src/EFCore/Metadata/RuntimeSkipNavigation.cs +++ b/src/EFCore/Metadata/RuntimeSkipNavigation.cs @@ -94,6 +94,10 @@ public RuntimeSkipNavigation( [DisallowNull] public virtual RuntimeSkipNavigation? Inverse { get; set; } + /// + public override object? Sentinel + => null; + /// /// Returns a string that represents the current object. /// diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 01d7dd474ec..4ef80d7d68f 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -639,7 +639,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas propertyInfo: typeof(Microsoft.EntityFrameworkCore.Scaffolding.Internal.Index).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(Microsoft.EntityFrameworkCore.Scaffolding.Internal.Index).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); var key = runtimeEntityType.AddKey( new[] { id }); @@ -689,7 +690,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas propertyInfo: typeof(Microsoft.EntityFrameworkCore.Scaffolding.Internal.Internal).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(Microsoft.EntityFrameworkCore.Scaffolding.Internal.Internal).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); var key = runtimeEntityType.AddKey( new[] { id }); @@ -746,7 +748,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "AccessFailedCount", typeof(int), propertyInfo: typeof(IdentityUser).GetProperty("AccessFailedCount", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: 0); var concurrencyStamp = runtimeEntityType.AddProperty( "ConcurrencyStamp", @@ -772,13 +775,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "EmailConfirmed", typeof(bool), propertyInfo: typeof(IdentityUser).GetProperty("EmailConfirmed", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: false); var lockoutEnabled = runtimeEntityType.AddProperty( "LockoutEnabled", typeof(bool), propertyInfo: typeof(IdentityUser).GetProperty("LockoutEnabled", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: false); var lockoutEnd = runtimeEntityType.AddProperty( "LockoutEnd", @@ -819,7 +824,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "PhoneNumberConfirmed", typeof(bool), propertyInfo: typeof(IdentityUser).GetProperty("PhoneNumberConfirmed", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: false); var securityStamp = runtimeEntityType.AddProperty( "SecurityStamp", @@ -832,7 +838,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "TwoFactorEnabled", typeof(bool), propertyInfo: typeof(IdentityUser).GetProperty("TwoFactorEnabled", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(IdentityUser).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: false); var userName = runtimeEntityType.AddProperty( "UserName", @@ -1617,20 +1624,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalId = runtimeEntityType.AddProperty( "PrincipalId", typeof(long), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); principalId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var principalAlternateId = runtimeEntityType.AddProperty( "PrincipalAlternateId", typeof(Guid), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var enumDiscriminator = runtimeEntityType.AddProperty( "EnumDiscriminator", typeof(CSharpMigrationsGeneratorTest.Enum1), afterSaveBehavior: PropertySaveBehavior.Throw, - valueGeneratorFactory: new DiscriminatorValueGeneratorFactory().Create); + valueGeneratorFactory: new DiscriminatorValueGeneratorFactory().Create, + sentinel: CSharpMigrationsGeneratorTest.Enum1.Default); enumDiscriminator.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var id = runtimeEntityType.AddProperty( @@ -1762,7 +1772,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba typeof(Guid), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField("AlternateId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.FieldDuringConstruction, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); alternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var point = runtimeEntityType.AddProperty( @@ -1864,7 +1875,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba typeof(long), propertyAccessMode: PropertyAccessMode.Field, valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); var overrides = new StoreObjectDictionary(); var principalBaseIdPrincipalBase = new RuntimeRelationalPropertyOverrides( @@ -1882,7 +1894,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "PrincipalBaseAlternateId", typeof(Guid), propertyAccessMode: PropertyAccessMode.Field, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalBaseAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var details = runtimeEntityType.AddProperty( @@ -1909,7 +1922,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba typeof(int), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetProperty("Number", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), - propertyAccessMode: PropertyAccessMode.Field); + propertyAccessMode: PropertyAccessMode.Field, + sentinel: 0); number.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var context = runtimeEntityType.AddServiceProperty( @@ -2012,20 +2026,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalDerivedId = runtimeEntityType.AddProperty( "PrincipalDerivedId", typeof(long), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); principalDerivedId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var principalDerivedAlternateId = runtimeEntityType.AddProperty( "PrincipalDerivedAlternateId", typeof(Guid), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalDerivedAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var id = runtimeEntityType.AddProperty( "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); var details = runtimeEntityType.AddProperty( @@ -2040,7 +2057,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "Number", typeof(int), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetProperty("Number", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: 0); number.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var context = runtimeEntityType.AddServiceProperty( @@ -2246,7 +2264,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "Money", typeof(decimal), precision: 9, - scale: 3); + scale: 3, + sentinel: 0m); money.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); return runtimeEntityType; @@ -3335,20 +3354,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalId = runtimeEntityType.AddProperty( "PrincipalId", typeof(long), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); principalId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var principalAlternateId = runtimeEntityType.AddProperty( "PrincipalAlternateId", typeof(Guid), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var enumDiscriminator = runtimeEntityType.AddProperty( "EnumDiscriminator", typeof(CSharpMigrationsGeneratorTest.Enum1), afterSaveBehavior: PropertySaveBehavior.Throw, - valueGeneratorFactory: new DiscriminatorValueGeneratorFactory().Create); + valueGeneratorFactory: new DiscriminatorValueGeneratorFactory().Create, + sentinel: CSharpMigrationsGeneratorTest.Enum1.Default); enumDiscriminator.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var id = runtimeEntityType.AddProperty( @@ -3471,7 +3493,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba typeof(Guid), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField("AlternateId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.FieldDuringConstruction, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); alternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var discriminator = runtimeEntityType.AddProperty( @@ -3580,14 +3603,16 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "PrincipalBaseId", typeof(long), propertyAccessMode: PropertyAccessMode.Field, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); principalBaseId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var principalBaseAlternateId = runtimeEntityType.AddProperty( "PrincipalBaseAlternateId", typeof(Guid), propertyAccessMode: PropertyAccessMode.Field, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalBaseAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var details = runtimeEntityType.AddProperty( @@ -3604,7 +3629,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba typeof(int), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetProperty("Number", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), - propertyAccessMode: PropertyAccessMode.Field); + propertyAccessMode: PropertyAccessMode.Field, + sentinel: 0); number.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var context = runtimeEntityType.AddServiceProperty( @@ -3688,20 +3714,23 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba var principalDerivedId = runtimeEntityType.AddProperty( "PrincipalDerivedId", typeof(long), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0L); principalDerivedId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var principalDerivedAlternateId = runtimeEntityType.AddProperty( "PrincipalDerivedAlternateId", typeof(Guid), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); principalDerivedAlternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var id = runtimeEntityType.AddProperty( "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); var details = runtimeEntityType.AddProperty( @@ -3716,7 +3745,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "Number", typeof(int), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetProperty("Number", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), - fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: 0); number.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); var context = runtimeEntityType.AddServiceProperty( @@ -3922,7 +3952,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba "Money", typeof(decimal), precision: 9, - scale: 3); + scale: 3, + sentinel: 0m); money.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); return runtimeEntityType; @@ -6605,7 +6636,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("SqlServer:HiLoSequenceName", "HL"); id.AddAnnotation("SqlServer:HiLoSequenceSchema", "S"); id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); @@ -6854,7 +6886,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("Relational:DefaultValueSql", "NEXT VALUE FOR [KeySeqSchema].[KeySeq]"); id.AddAnnotation("SqlServer:SequenceName", "KeySeq"); id.AddAnnotation("SqlServer:SequenceSchema", "KeySeqSchema"); @@ -7077,7 +7110,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); var blob = runtimeEntityType.AddProperty( @@ -7291,7 +7325,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); var blob = runtimeEntityType.AddProperty( @@ -7520,7 +7555,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "Id", typeof(int), valueGenerated: ValueGenerated.OnAdd, - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); var blob = runtimeEntityType.AddProperty( "Blob", @@ -7705,7 +7741,8 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas var id = runtimeEntityType.AddProperty( "Id", typeof(int), - afterSaveBehavior: PropertySaveBehavior.Throw); + afterSaveBehavior: PropertySaveBehavior.Throw, + sentinel: 0); var partitionId = runtimeEntityType.AddProperty( "PartitionId", diff --git a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs index 20d3d07362c..de9a3b50806 100644 --- a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs +++ b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs @@ -19,6 +19,10 @@ public override Task Can_insert_when_composite_FK_has_default_value_for_one_part public override Task Can_insert_when_FK_has_default_value(bool async) => Task.CompletedTask; + // In-memory database does not have database default values + public override Task Can_insert_when_FK_has_sentinel_value(bool async) + => Task.CompletedTask; + public override void Required_many_to_one_dependents_are_cascade_deleted_in_store( CascadeTiming? cascadeDeleteTiming, CascadeTiming? deleteOrphansTiming) diff --git a/test/EFCore.InMemory.FunctionalTests/StoreGeneratedFixupInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/StoreGeneratedFixupInMemoryTest.cs index 1f7f667de0e..eb2e76eaf17 100644 --- a/test/EFCore.InMemory.FunctionalTests/StoreGeneratedFixupInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/StoreGeneratedFixupInMemoryTest.cs @@ -154,20 +154,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.Id2).ValueGeneratedNever(); }); - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedNever(); - b.Property(e => e.Id2).ValueGeneratedNever(); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedNever(); - b.Property(e => e.Id2).ValueGeneratedNever(); - }); - modelBuilder.Entity(b => b.Property(e => e.Id).ValueGeneratedNever()); modelBuilder.Entity(b => b.Property(e => e.Id).ValueGeneratedNever()); diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs index a36c8d7627f..0b199e212d0 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs @@ -3576,6 +3576,49 @@ public virtual ICollection Users } } + protected class CruiserWithSentinel : NotifyingEntity + { + private int _cruiserWithSentinelId; + private int _idUserState; + private AccessStateWithSentinel _userState; + + public int CruiserWithSentinelId + { + get => _cruiserWithSentinelId; + set => SetWithNotify(value, ref _cruiserWithSentinelId); + } + + public int IdUserState + { + get => _idUserState; + set => SetWithNotify(value, ref _idUserState); + } + + public virtual AccessStateWithSentinel UserState + { + get => _userState; + set => SetWithNotify(value, ref _userState); + } + } + + protected class AccessStateWithSentinel : NotifyingEntity + { + private int _accessStateWithSentinelId; + private ICollection _users = new ObservableHashSet(); + + public int AccessStateWithSentinelId + { + get => _accessStateWithSentinelId; + set => SetWithNotify(value, ref _accessStateWithSentinelId); + } + + public virtual ICollection Users + { + get => _users; + set => SetWithNotify(value, ref _users); + } + } + protected class SomethingCategory : NotifyingEntity { private int _id; diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs index fab0f7d85bd..2e5f0564dd4 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs @@ -78,6 +78,31 @@ public virtual async Task Can_insert_when_FK_has_default_value(bool async) Assert.Equal(cruiser.IdUserState, cruiser.UserState.AccessStateId); }); + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Can_insert_when_FK_has_sentinel_value(bool async) + => await ExecuteWithStrategyInTransactionAsync( + async context => + { + if (async) + { + await context.AddAsync(new CruiserWithSentinel { IdUserState = 667 }); + await context.SaveChangesAsync(); + } + else + { + context.Add(new CruiserWithSentinel { IdUserState = 667 }); + context.SaveChanges(); + } + }, + async context => + { + var queryable = context.Set().Include(e => e.UserState); + var cruiser = async ? (await queryable.SingleAsync()) : queryable.Single(); + Assert.Equal(cruiser.IdUserState, cruiser.UserState.AccessStateWithSentinelId); + }); + [ConditionalTheory] // Issue #23043 [InlineData(false)] [InlineData(true)] diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs index 5e03a7f96ec..fa967c3a428 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs @@ -1590,19 +1590,19 @@ public virtual void Optional_many_to_one_dependents_are_orphaned_with_Added_grap context.ChangeTracker.CascadeChanges(); } - foreach (var orphanEntry in orphaned.Select(context.Entry)) - { - Assert.Equal(EntityState.Added, orphanEntry.State); - Assert.Null(orphanEntry.Entity.ParentId); - Assert.Equal(Fixture.ForceClientNoAction ? removedId : null, orphanEntry.Property(e => e.ParentId).CurrentValue); - } - if (Fixture.ForceClientNoAction) { Assert.Throws(() => context.SaveChanges()); } else { + foreach (var orphanEntry in orphaned.Select(context.Entry)) + { + Assert.Equal(EntityState.Added, orphanEntry.State); + Assert.Null(orphanEntry.Entity.ParentId); + Assert.Null(orphanEntry.Property(e => e.ParentId).CurrentValue); + } + context.SaveChanges(); Assert.False(context.ChangeTracker.HasChanges()); diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToOne.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToOne.cs index 8eb8f4cbcce..75d85ca5701 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToOne.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToOne.cs @@ -888,6 +888,8 @@ public virtual void Save_required_one_to_one_changed_by_reference( { context.Add(new1); new1.Id = root.Id; + context.Entry(new1).Property(e => e.Id).IsTemporary = false; + context.Entry(new2).Property(e => e.Id).IsTemporary = false; } Assert.True(context.ChangeTracker.HasChanges()); diff --git a/test/EFCore.Specification.Tests/GraphUpdates/ProxyGraphUpdatesTestBaseOneToOne.cs b/test/EFCore.Specification.Tests/GraphUpdates/ProxyGraphUpdatesTestBaseOneToOne.cs index 25d7672c1b3..bf0744944fa 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/ProxyGraphUpdatesTestBaseOneToOne.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/ProxyGraphUpdatesTestBaseOneToOne.cs @@ -288,6 +288,8 @@ public virtual void Save_required_one_to_one_changed_by_reference(ChangeMechanis { context.Add(new1); new1.Id = root.Id; + context.Entry(new1).Property(e => e.Id).IsTemporary = false; + context.Entry(new2).Property(e => e.Id).IsTemporary = false; } Assert.True(context.ChangeTracker.HasChanges()); diff --git a/test/EFCore.Specification.Tests/StoreGeneratedFixupTestBase.cs b/test/EFCore.Specification.Tests/StoreGeneratedFixupTestBase.cs index 263808ebbff..5545e345b43 100644 --- a/test/EFCore.Specification.Tests/StoreGeneratedFixupTestBase.cs +++ b/test/EFCore.Specification.Tests/StoreGeneratedFixupTestBase.cs @@ -49,16 +49,13 @@ public virtual void Add_dependent_then_principal_one_to_many_FK_not_set_both_nav => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; + var principal = new Category(); var dependent = new Product { - Id1 = -78, - Id2 = Guid78, Category = principal }; - principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); + principal.Products.Add(dependent); context.Add(dependent); context.Add(principal); @@ -139,12 +136,10 @@ public virtual void Add_dependent_then_principal_one_to_many_FK_not_set_principa => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; + var principal = new Category(); + var dependent = new Product(); principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -156,16 +151,12 @@ public virtual void Add_dependent_then_principal_one_to_many_FK_not_set_dependen => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; + var principal = new Category(); var dependent = new Product { - Id1 = -78, - Id2 = Guid78, Category = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -201,17 +192,13 @@ public virtual void Add_principal_then_dependent_one_to_many_FK_not_set_both_nav => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; + var principal = new Category(); var dependent = new Product { - Id1 = -78, - Id2 = Guid78, Category = principal }; principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -291,12 +278,10 @@ public virtual void Add_principal_then_dependent_one_to_many_FK_not_set_principa => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; + var principal = new Category(); + var dependent = new Product(); principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -308,16 +293,12 @@ public virtual void Add_principal_then_dependent_one_to_many_FK_not_set_dependen => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; + var principal = new Category(); var dependent = new Product { - Id1 = -78, - Id2 = Guid78, Category = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -330,8 +311,14 @@ private void AssertFixupAndSave(DbContext context, Category principal, Product d context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -425,12 +412,10 @@ public virtual void Add_dependent_then_principal_one_to_many_prin_uni_FK_not_set => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductPN { Id1 = -78, Id2 = Guid78 }; + var principal = new CategoryPN(); + var dependent = new ProductPN(); principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -465,12 +450,10 @@ public virtual void Add_principal_then_dependent_one_to_many_prin_uni_FK_not_set => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductPN { Id1 = -78, Id2 = Guid78 }; + var principal = new CategoryPN(); + var dependent = new ProductPN(); principal.Products.Add(dependent); - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -483,8 +466,14 @@ private void AssertFixupAndSave(DbContext context, CategoryPN principal, Product context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -554,16 +543,12 @@ public virtual void Add_dependent_then_principal_one_to_many_dep_uni_FK_not_set_ => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryDN { Id1 = -77, Id2 = Guid77 }; + var principal = new CategoryDN(); var dependent = new ProductDN { - Id1 = -78, - Id2 = Guid78, Category = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -620,16 +605,12 @@ public virtual void Add_principal_then_dependent_one_to_many_dep_uni_FK_not_set_ => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryDN { Id1 = -77, Id2 = Guid77 }; + var principal = new CategoryDN(); var dependent = new ProductDN { - Id1 = -78, - Id2 = Guid78, Category = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -642,8 +623,14 @@ private void AssertFixupAndSave(DbContext context, CategoryDN principal, Product context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -761,17 +748,13 @@ public virtual void Add_dependent_then_principal_one_to_one_FK_not_set_both_navs => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; + var principal = new Parent(); var dependent = new Child { - Id1 = -78, - Id2 = Guid78, Parent = principal }; principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -851,12 +834,10 @@ public virtual void Add_dependent_then_principal_one_to_one_FK_not_set_principal => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; + var principal = new Parent(); + var dependent = new Child(); principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -868,16 +849,12 @@ public virtual void Add_dependent_then_principal_one_to_one_FK_not_set_dependent => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; + var principal = new Parent(); var dependent = new Child { - Id1 = -78, - Id2 = Guid78, Parent = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -913,17 +890,13 @@ public virtual void Add_principal_then_dependent_one_to_one_FK_not_set_both_navs => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; + var principal = new Parent(); var dependent = new Child { - Id1 = -78, - Id2 = Guid78, Parent = principal }; principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -1003,12 +976,10 @@ public virtual void Add_principal_then_dependent_one_to_one_FK_not_set_principal => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; + var principal = new Parent(); + var dependent = new Child(); principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -1020,16 +991,12 @@ public virtual void Add_principal_then_dependent_one_to_one_FK_not_set_dependent => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; + var principal = new Parent(); var dependent = new Child { - Id1 = -78, - Id2 = Guid78, Parent = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -1042,8 +1009,14 @@ private void AssertFixupAndSave(DbContext context, Parent principal, Child depen context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -1137,12 +1110,10 @@ public virtual void Add_dependent_then_principal_one_to_one_prin_uni_FK_not_set_ => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildPN { Id1 = -78, Id2 = Guid78 }; + var principal = new ParentPN(); + var dependent = new ChildPN(); principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -1177,12 +1148,10 @@ public virtual void Add_principal_then_dependent_one_to_one_prin_uni_FK_not_set_ => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildPN { Id1 = -78, Id2 = Guid78 }; + var principal = new ParentPN(); + var dependent = new ChildPN(); principal.Child = dependent; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); @@ -1195,8 +1164,14 @@ private void AssertFixupAndSave(DbContext context, ParentPN principal, ChildPN d context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -1266,16 +1241,12 @@ public virtual void Add_dependent_then_principal_one_to_one_dep_uni_FK_not_set_d => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentDN { Id1 = -77, Id2 = Guid77 }; + var principal = new ParentDN(); var dependent = new ChildDN { - Id1 = -78, - Id2 = Guid78, Parent = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(dependent); context.Add(principal); @@ -1332,19 +1303,17 @@ public virtual void Add_principal_then_dependent_one_to_one_dep_uni_FK_not_set_d => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentDN { Id1 = -77, Id2 = Guid77 }; + var principal = new ParentDN(); var dependent = new ChildDN { - Id1 = -78, - Id2 = Guid78, Parent = principal }; - MarkIdsTemporary(context, dependent, principal); - context.Add(principal); context.Add(dependent); + MarkIdsTemporary(context, dependent, principal); + AssertFixupAndSave(context, principal, dependent); }); @@ -1354,8 +1323,14 @@ private void AssertFixupAndSave(DbContext context, ParentDN principal, ChildDN d context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -1495,10 +1470,8 @@ public virtual void Add_dependent_but_not_principal_one_to_many_FK_not_set_both_ => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(dependent); @@ -1511,8 +1484,14 @@ public virtual void Add_dependent_but_not_principal_one_to_many_FK_not_set_both_ context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -1687,10 +1666,8 @@ public virtual void Add_dependent_but_not_principal_one_to_many_FK_not_set_princ => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(dependent); @@ -1735,10 +1712,8 @@ public virtual void Add_dependent_but_not_principal_one_to_many_FK_not_set_depen => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(dependent); @@ -1750,8 +1725,14 @@ public virtual void Add_dependent_but_not_principal_one_to_many_FK_not_set_depen context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -1824,10 +1805,8 @@ public virtual void Add_principal_but_not_dependent_one_to_many_FK_not_set_both_ => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(principal); @@ -1840,8 +1819,14 @@ public virtual void Add_principal_but_not_dependent_one_to_many_FK_not_set_both_ context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -1998,10 +1983,8 @@ public virtual void Add_principal_but_not_dependent_one_to_many_FK_not_set_princ => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(principal); @@ -2043,10 +2026,8 @@ public virtual void Add_principal_but_not_dependent_one_to_many_FK_not_set_depen => ExecuteWithStrategyInTransaction( context => { - var principal = new Category { Id1 = -77, Id2 = Guid77 }; - var dependent = new Product { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Category(); + var dependent = new Product(); context.Add(principal); @@ -2223,10 +2204,8 @@ public virtual void Add_dependent_but_not_principal_one_to_many_prin_uni_FK_not_ => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductPN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new CategoryPN(); + var dependent = new ProductPN(); context.Add(dependent); @@ -2312,10 +2291,8 @@ public virtual void Add_principal_but_not_dependent_one_to_many_prin_uni_FK_not_ => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductPN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new CategoryPN(); + var dependent = new ProductPN(); context.Add(principal); @@ -2327,8 +2304,14 @@ public virtual void Add_principal_but_not_dependent_one_to_many_prin_uni_FK_not_ context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Equal(new[] { dependent }.ToList(), principal.Products); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -2445,10 +2428,8 @@ public virtual void Add_dependent_but_not_principal_one_to_many_dep_uni_FK_not_s => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryDN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductDN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new CategoryDN(); + var dependent = new ProductDN(); context.Add(dependent); @@ -2460,8 +2441,14 @@ public virtual void Add_dependent_but_not_principal_one_to_many_dep_uni_FK_not_s context, () => { - Assert.Equal(principal.Id1, dependent.CategoryId1); - Assert.Equal(principal.Id2, dependent.CategoryId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.CategoryId2).CurrentValue); + Assert.Same(principal, dependent.Category); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -2567,10 +2554,8 @@ public virtual void Add_principal_but_not_dependent_one_to_many_dep_uni_FK_not_s => ExecuteWithStrategyInTransaction( context => { - var principal = new CategoryDN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ProductDN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new CategoryDN(); + var dependent = new ProductDN(); context.Add(principal); @@ -2737,10 +2722,8 @@ public virtual void Add_dependent_but_not_principal_one_to_one_FK_not_set_both_n => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(dependent); @@ -2753,8 +2736,14 @@ public virtual void Add_dependent_but_not_principal_one_to_one_FK_not_set_both_n context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -2929,10 +2918,8 @@ public virtual void Add_dependent_but_not_principal_one_to_one_FK_not_set_princi => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(dependent); @@ -2977,10 +2964,8 @@ public virtual void Add_dependent_but_not_principal_one_to_one_FK_not_set_depend => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(dependent); @@ -2992,8 +2977,14 @@ public virtual void Add_dependent_but_not_principal_one_to_one_FK_not_set_depend context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -3066,10 +3057,8 @@ public virtual void Add_principal_but_not_dependent_one_to_one_FK_not_set_both_n => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(principal); @@ -3082,8 +3071,14 @@ public virtual void Add_principal_but_not_dependent_one_to_one_FK_not_set_both_n context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -3240,10 +3235,8 @@ public virtual void Add_principal_but_not_dependent_one_to_one_FK_not_set_princi => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(principal); @@ -3255,8 +3248,14 @@ public virtual void Add_principal_but_not_dependent_one_to_one_FK_not_set_princi context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); @@ -3283,10 +3282,8 @@ public virtual void Add_principal_but_not_dependent_one_to_one_FK_not_set_depend => ExecuteWithStrategyInTransaction( context => { - var principal = new Parent { Id1 = -77, Id2 = Guid77 }; - var dependent = new Child { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new Parent(); + var dependent = new Child(); context.Add(principal); @@ -3463,10 +3460,8 @@ public virtual void Add_dependent_but_not_principal_one_to_one_prin_uni_FK_not_s => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildPN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new ParentPN(); + var dependent = new ChildPN(); context.Add(dependent); @@ -3552,10 +3547,8 @@ public virtual void Add_principal_but_not_dependent_one_to_one_prin_uni_FK_not_s => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentPN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildPN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new ParentPN(); + var dependent = new ChildPN(); context.Add(principal); @@ -3567,8 +3560,14 @@ public virtual void Add_principal_but_not_dependent_one_to_one_prin_uni_FK_not_s context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(dependent, principal.Child); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -3685,10 +3684,8 @@ public virtual void Add_dependent_but_not_principal_one_to_one_dep_uni_FK_not_se => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentDN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildDN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new ParentDN(); + var dependent = new ChildDN(); context.Add(dependent); @@ -3700,8 +3697,14 @@ public virtual void Add_dependent_but_not_principal_one_to_one_dep_uni_FK_not_se context, () => { - Assert.Equal(principal.Id1, dependent.ParentId1); - Assert.Equal(principal.Id2, dependent.ParentId2); + Assert.Equal( + context.Entry(principal).Property(e => e.Id1).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId1).CurrentValue); + + Assert.Equal( + context.Entry(principal).Property(e => e.Id2).CurrentValue, + context.Entry(dependent).Property(e => e.ParentId2).CurrentValue); + Assert.Same(principal, dependent.Parent); Assert.Equal(EntityState.Added, context.Entry(principal).State); Assert.Equal(EntityState.Added, context.Entry(dependent).State); @@ -3807,10 +3810,8 @@ public virtual void Add_principal_but_not_dependent_one_to_one_dep_uni_FK_not_se => ExecuteWithStrategyInTransaction( context => { - var principal = new ParentDN { Id1 = -77, Id2 = Guid77 }; - var dependent = new ChildDN { Id1 = -78, Id2 = Guid78 }; - - MarkIdsTemporary(context, dependent, principal); + var principal = new ParentDN(); + var dependent = new ChildDN(); context.Add(principal); @@ -3997,9 +3998,9 @@ private void AssertFixupAndSave(DbContext context, Game game, Level level, Item context, () => { - Assert.Equal(game.Id, level.GameId); - Assert.Equal(game.Id, item.GameId); - Assert.Equal(level.Id, item.LevelId); + Assert.Equal(game.Id, context.Entry(level).Property(e => e.GameId).CurrentValue); + Assert.Equal(game.Id, context.Entry(item).Property(e => e.GameId).CurrentValue); + Assert.Equal(level.Id, context.Entry(item).Property(e => e.LevelId).CurrentValue); Assert.Same(game, level.Game); Assert.Same(game, item.Game); diff --git a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs index 249eca7f6fb..f784d9d387b 100644 --- a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs +++ b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs @@ -22,18 +22,18 @@ protected StoreGeneratedTestBase(TFixture fixture) [ConditionalFact] public virtual void Value_generation_works_for_common_GUID_conversions() { - ValueGenerationPositive(); - ValueGenerationPositive(); + ValueGenerationPositive(Fixture.GuidSentinel); + ValueGenerationPositive(Fixture.GuidSentinel); } - private void ValueGenerationPositive() + private void ValueGenerationPositive(TKey? sentinel) where TEntity : WithConverter, new() { TKey? id; using (var context = CreateContext()) { - var entity = context.Add(new TEntity()).Entity; + var entity = context.Add(new TEntity { Id = sentinel }).Entity; context.SaveChanges(); @@ -63,7 +63,7 @@ public virtual void Before_save_throw_always_throws_if_value_set(string property => ExecuteWithStrategyInTransaction( context => { - context.Add(WithValue(propertyName)); + context.Add(WithValue(propertyName, Fixture.IntSentinel, Fixture.StringSentinel)); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave(propertyName, "Anais"), @@ -89,7 +89,7 @@ public virtual void Before_save_throw_ignores_value_if_not_set(string propertyNa ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -120,7 +120,7 @@ public virtual void Before_save_use_always_uses_value_if_set(string propertyName ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(WithValue(propertyName)).Entity; + var entity = context.Add(WithValue(propertyName, Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -130,28 +130,33 @@ public virtual void Before_save_use_always_uses_value_if_set(string propertyName } [ConditionalTheory] - [InlineData(nameof(Anais.Never), null)] + [InlineData(nameof(Anais.Never), "S")] [InlineData(nameof(Anais.OnAdd), "Rabbit")] - [InlineData(nameof(Anais.OnUpdate), null)] - [InlineData(nameof(Anais.NeverUseBeforeUseAfter), null)] - [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), null)] - [InlineData(nameof(Anais.NeverUseBeforeThrowAfter), null)] + [InlineData(nameof(Anais.OnUpdate), "S")] + [InlineData(nameof(Anais.NeverUseBeforeUseAfter), "S")] + [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), "S")] + [InlineData(nameof(Anais.NeverUseBeforeThrowAfter), "S")] [InlineData(nameof(Anais.OnAddUseBeforeUseAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddUseBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddUseBeforeThrowAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeUseAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeThrowAfter), "Rabbit")] - [InlineData(nameof(Anais.OnUpdateUseBeforeUseAfter), null)] - [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), null)] - [InlineData(nameof(Anais.OnUpdateUseBeforeThrowAfter), null)] - public virtual void Before_save_use_ignores_value_if_not_set(string propertyName, string expectedValue) + [InlineData(nameof(Anais.OnUpdateUseBeforeUseAfter), "S")] + [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), "S")] + [InlineData(nameof(Anais.OnUpdateUseBeforeThrowAfter), "S")] + public virtual void Before_save_use_ignores_value_if_not_set(string propertyName, string? expectedValue) { + if (expectedValue == "S") + { + expectedValue = Fixture.StringSentinel; + } + var id = 0; ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -180,7 +185,7 @@ public virtual void Before_save_ignore_ignores_value_if_not_set(string propertyN ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -209,7 +214,7 @@ public virtual void Before_save_ignore_ignores_value_even_if_set(string property ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(WithValue(propertyName)).Entity; + var entity = context.Add(WithValue(propertyName, Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -235,7 +240,7 @@ public virtual void After_save_throw_always_throws_if_value_modified(string prop => ExecuteWithStrategyInTransaction( context => { - context.Attach(WithValue(propertyName, 1)).Property(propertyName).IsModified = true; + context.Attach(WithValue(propertyName, 1, Fixture.StringSentinel)).Property(propertyName).IsModified = true; Assert.Equal( CoreStrings.PropertyReadOnlyAfterSave(propertyName, "Anais"), @@ -243,7 +248,7 @@ public virtual void After_save_throw_always_throws_if_value_modified(string prop }); [ConditionalTheory] - [InlineData(nameof(Anais.NeverUseBeforeThrowAfter), null)] + [InlineData(nameof(Anais.NeverUseBeforeThrowAfter), "S")] [InlineData(nameof(Anais.NeverIgnoreBeforeThrowAfter), null)] [InlineData(nameof(Anais.NeverThrowBeforeThrowAfter), null)] [InlineData(nameof(Anais.OnAddUseBeforeThrowAfter), "Rabbit")] @@ -252,16 +257,21 @@ public virtual void After_save_throw_always_throws_if_value_modified(string prop [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeThrowAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateIgnoreBeforeThrowAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateThrowBeforeThrowAfter), "Rabbit")] - [InlineData(nameof(Anais.OnUpdateUseBeforeThrowAfter), null)] + [InlineData(nameof(Anais.OnUpdateUseBeforeThrowAfter), "S")] [InlineData(nameof(Anais.OnUpdateIgnoreBeforeThrowAfter), "Rabbit")] [InlineData(nameof(Anais.OnUpdateThrowBeforeThrowAfter), "Rabbit")] - public virtual void After_save_throw_ignores_value_if_not_modified(string propertyName, string expectedValue) + public virtual void After_save_throw_ignores_value_if_not_modified(string propertyName, string? expectedValue) { + if (expectedValue == "S") + { + expectedValue = Fixture.StringSentinel; + } + var id = 0; ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -281,8 +291,8 @@ public virtual void After_save_throw_ignores_value_if_not_modified(string proper [ConditionalTheory] [InlineData(nameof(Anais.OnAddOrUpdate), "Rabbit")] - [InlineData(nameof(Anais.OnUpdate), null)] - [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), null)] + [InlineData(nameof(Anais.OnUpdate), "S")] + [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), "S")] [InlineData(nameof(Anais.NeverIgnoreBeforeIgnoreAfter), null)] [InlineData(nameof(Anais.NeverThrowBeforeIgnoreAfter), null)] [InlineData(nameof(Anais.OnAddUseBeforeIgnoreAfter), "Rabbit")] @@ -291,16 +301,21 @@ public virtual void After_save_throw_ignores_value_if_not_modified(string proper [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateIgnoreBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateThrowBeforeIgnoreAfter), "Rabbit")] - [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), null)] + [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), "S")] [InlineData(nameof(Anais.OnUpdateIgnoreBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnUpdateThrowBeforeIgnoreAfter), "Rabbit")] - public virtual void After_save_ignore_ignores_value_if_not_modified(string propertyName, string expectedValue) + public virtual void After_save_ignore_ignores_value_if_not_modified(string propertyName, string? expectedValue) { + if (expectedValue == "S") + { + expectedValue = Fixture.StringSentinel; + } + var id = 0; ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -320,8 +335,8 @@ public virtual void After_save_ignore_ignores_value_if_not_modified(string prope [ConditionalTheory] [InlineData(nameof(Anais.OnAddOrUpdate), "Rabbit")] - [InlineData(nameof(Anais.OnUpdate), null)] - [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), null)] + [InlineData(nameof(Anais.OnUpdate), "S")] + [InlineData(nameof(Anais.NeverUseBeforeIgnoreAfter), "S")] [InlineData(nameof(Anais.NeverIgnoreBeforeIgnoreAfter), null)] [InlineData(nameof(Anais.NeverThrowBeforeIgnoreAfter), null)] [InlineData(nameof(Anais.OnAddUseBeforeIgnoreAfter), "Rabbit")] @@ -330,16 +345,21 @@ public virtual void After_save_ignore_ignores_value_if_not_modified(string prope [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateIgnoreBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateThrowBeforeIgnoreAfter), "Rabbit")] - [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), null)] + [InlineData(nameof(Anais.OnUpdateUseBeforeIgnoreAfter), "S")] [InlineData(nameof(Anais.OnUpdateIgnoreBeforeIgnoreAfter), "Rabbit")] [InlineData(nameof(Anais.OnUpdateThrowBeforeIgnoreAfter), "Rabbit")] - public virtual void After_save_ignore_ignores_value_even_if_modified(string propertyName, string expectedValue) + public virtual void After_save_ignore_ignores_value_even_if_modified(string propertyName, string? expectedValue) { + if (expectedValue == "S") + { + expectedValue = Fixture.StringSentinel; + } + var id = 0; ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -358,11 +378,11 @@ public virtual void After_save_ignore_ignores_value_even_if_modified(string prop } [ConditionalTheory] - [InlineData(nameof(Anais.Never), null)] + [InlineData(nameof(Anais.Never), "S")] [InlineData(nameof(Anais.OnAdd), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdate), "Rabbit")] - [InlineData(nameof(Anais.OnUpdate), null)] - [InlineData(nameof(Anais.NeverUseBeforeUseAfter), null)] + [InlineData(nameof(Anais.OnUpdate), "S")] + [InlineData(nameof(Anais.NeverUseBeforeUseAfter), "S")] [InlineData(nameof(Anais.NeverIgnoreBeforeUseAfter), null)] [InlineData(nameof(Anais.NeverThrowBeforeUseAfter), null)] [InlineData(nameof(Anais.OnAddUseBeforeUseAfter), "Rabbit")] @@ -371,16 +391,21 @@ public virtual void After_save_ignore_ignores_value_even_if_modified(string prop [InlineData(nameof(Anais.OnAddOrUpdateUseBeforeUseAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateIgnoreBeforeUseAfter), "Rabbit")] [InlineData(nameof(Anais.OnAddOrUpdateThrowBeforeUseAfter), "Rabbit")] - [InlineData(nameof(Anais.OnUpdateUseBeforeUseAfter), null)] + [InlineData(nameof(Anais.OnUpdateUseBeforeUseAfter), "S")] [InlineData(nameof(Anais.OnUpdateIgnoreBeforeUseAfter), "Rabbit")] [InlineData(nameof(Anais.OnUpdateThrowBeforeUseAfter), "Rabbit")] - public virtual void After_save_use_ignores_value_if_not_modified(string propertyName, string expectedValue) + public virtual void After_save_use_ignores_value_if_not_modified(string propertyName, string? expectedValue) { + if (expectedValue == "S") + { + expectedValue = Fixture.StringSentinel; + } + var id = 0; ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -419,7 +444,7 @@ public virtual void After_save_use_uses_value_if_modified(string propertyName, s ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Anais()).Entity; + var entity = context.Add(Anais.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); @@ -436,8 +461,8 @@ public virtual void After_save_use_uses_value_if_modified(string propertyName, s context => Assert.Equal(expectedValue, GetValue(context.Set().Find(id)!, propertyName))); } - private static Anais WithValue(string propertyName, int id = 0) - => SetValue(new Anais { Id = id }, propertyName); + private static Anais WithValue(string propertyName, int id, string? sentinel) + => SetValue(Anais.Create(id, sentinel), propertyName); private static Anais SetValue(Anais entity, string propertyName) { @@ -453,7 +478,7 @@ public virtual void Identity_key_with_read_only_before_save_throws_if_explicit_v => ExecuteWithStrategyInTransaction( context => { - context.Add(new Gumball { Id = 1 }); + context.Add(Gumball.Create(Fixture.IntSentinel + 1, Fixture.StringSentinel)); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("Id", "Gumball"), @@ -468,7 +493,9 @@ public virtual void Identity_property_on_Added_entity_with_temporary_value_gets_ ExecuteWithStrategyInTransaction( context => { - var entry = context.Add(new Gumball { Identity = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.Identity = "Masami"; + var entry = context.Add(gumball); entry.Property(e => e.Identity).IsTemporary = true; context.SaveChanges(); @@ -503,7 +530,12 @@ public virtual void Store_generated_values_are_propagated_with_composite_key_cyc ExecuteWithStrategyInTransaction( context => { - var period = new CompositeDependent { Number = 1, Principal = new CompositePrincipal() }; + var period = new CompositeDependent + { + PrincipalId = Fixture.IntSentinel, + Number = 1, + Principal = new CompositePrincipal { Id = Fixture.IntSentinel } + }; context.Add(period); context.SaveChanges(); @@ -535,7 +567,7 @@ public void Change_state_of_entity_with_temp_non_key_does_not_throw(EntityState => ExecuteWithStrategyInTransaction( context => { - var dependent = new NonStoreGenDependent { Id = 89, }; + var dependent = new NonStoreGenDependent { Id = 89 }; context.Add(dependent); @@ -548,7 +580,7 @@ public void Change_state_of_entity_with_temp_non_key_does_not_throw(EntityState }, context => { - var principal = new StoreGenPrincipal(); + var principal = new StoreGenPrincipal { Id = Fixture.IntSentinel }; var dependent = new NonStoreGenDependent { Id = 89, StoreGenPrincipal = principal }; context.Add(dependent); @@ -578,7 +610,7 @@ public void Clearing_optional_FK_does_not_leave_temporary_value() => ExecuteWithStrategyInTransaction( context => { - var product = new OptionalProduct(); + var product = new OptionalProduct { Id = Fixture.IntSentinel }; context.Add(product); Assert.True(context.ChangeTracker.HasChanges()); @@ -586,7 +618,7 @@ public void Clearing_optional_FK_does_not_leave_temporary_value() var productEntry = context.Entry(product); Assert.Equal(EntityState.Added, productEntry.State); - Assert.Equal(0, product.Id); + Assert.Equal(Fixture.IntSentinel, product.Id); Assert.True(productEntry.Property(e => e.Id).CurrentValue < 0); Assert.True(productEntry.Property(e => e.Id).IsTemporary); @@ -714,7 +746,9 @@ public virtual void Identity_property_on_Added_entity_with_temporary_value_gets_ ExecuteWithStrategyInTransaction( context => { - var entry = context.Add(new Gumball { Identity = "Banana Joe" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.Identity = "Banana Joe"; + var entry = context.Add(gumball); entry.Property(e => e.Identity).IsTemporary = true; context.SaveChanges(); @@ -734,7 +768,7 @@ public virtual void Identity_property_on_Added_entity_with_default_value_gets_va ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -749,7 +783,9 @@ public virtual void Identity_property_on_Added_entity_with_read_only_before_save => ExecuteWithStrategyInTransaction( context => { - context.Add(new Gumball { IdentityReadOnlyBeforeSave = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.IdentityReadOnlyBeforeSave = "Masami"; + context.Add(gumball); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("IdentityReadOnlyBeforeSave", "Gumball"), @@ -764,7 +800,9 @@ public virtual void Identity_property_on_Added_entity_can_have_value_set_explici ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball { Identity = "Masami" }).Entity; + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.Identity = "Masami"; + var entity = context.Add(gumball).Entity; context.SaveChanges(); id = entity.Id; @@ -782,7 +820,7 @@ public virtual void Identity_property_on_Modified_entity_with_read_only_after_sa ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -810,7 +848,7 @@ public virtual void Identity_property_on_Modified_entity_is_included_in_update_w ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -839,7 +877,7 @@ public virtual void Identity_property_on_Modified_entity_is_not_included_in_upda ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -871,7 +909,9 @@ public virtual void Always_identity_property_on_Added_entity_with_temporary_valu ExecuteWithStrategyInTransaction( context => { - var entry = context.Add(new Gumball { AlwaysIdentity = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.AlwaysIdentity = "Masami"; + var entry = context.Add(gumball); entry.Property(e => e.AlwaysIdentity).IsTemporary = true; context.SaveChanges(); @@ -890,7 +930,7 @@ public virtual void Always_identity_property_on_Added_entity_with_default_value_ ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -905,7 +945,9 @@ public virtual void Always_identity_property_on_Added_entity_with_read_only_befo => ExecuteWithStrategyInTransaction( context => { - context.Add(new Gumball { AlwaysIdentityReadOnlyBeforeSave = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.AlwaysIdentityReadOnlyBeforeSave = "Masami"; + context.Add(gumball); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("AlwaysIdentityReadOnlyBeforeSave", "Gumball"), @@ -920,7 +962,7 @@ public virtual void Always_identity_property_on_Modified_entity_with_read_only_a ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -948,7 +990,7 @@ public virtual void Always_identity_property_on_Modified_entity_is_not_included_ ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -979,7 +1021,9 @@ public virtual void Computed_property_on_Added_entity_with_temporary_value_gets_ ExecuteWithStrategyInTransaction( context => { - var entry = context.Add(new Gumball { Computed = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.Computed = "Masami"; + var entry = context.Add(gumball); entry.Property(e => e.Computed).IsTemporary = true; context.SaveChanges(); @@ -998,7 +1042,7 @@ public virtual void Computed_property_on_Added_entity_with_default_value_gets_va ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1013,7 +1057,9 @@ public virtual void Computed_property_on_Added_entity_with_read_only_before_save => ExecuteWithStrategyInTransaction( context => { - context.Add(new Gumball { ComputedReadOnlyBeforeSave = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.ComputedReadOnlyBeforeSave = "Masami"; + context.Add(gumball); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("ComputedReadOnlyBeforeSave", "Gumball"), @@ -1028,7 +1074,9 @@ public virtual void Computed_property_on_Added_entity_can_have_value_set_explici ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball { Computed = "Masami" }).Entity; + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.Computed = "Masami"; + var entity = context.Add(gumball).Entity; context.SaveChanges(); id = entity.Id; @@ -1046,7 +1094,7 @@ public virtual void Computed_property_on_Modified_entity_with_read_only_after_sa ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1074,7 +1122,7 @@ public virtual void Computed_property_on_Modified_entity_is_included_in_update_w ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1103,7 +1151,7 @@ public virtual void Computed_property_on_Modified_entity_is_read_from_store_when ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1135,7 +1183,9 @@ public virtual void Always_computed_property_on_Added_entity_with_temporary_valu ExecuteWithStrategyInTransaction( context => { - var entry = context.Add(new Gumball { AlwaysComputed = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.AlwaysComputed = "Masami"; + var entry = context.Add(gumball); entry.Property(e => e.AlwaysComputed).IsTemporary = true; context.SaveChanges(); @@ -1154,7 +1204,7 @@ public virtual void Always_computed_property_on_Added_entity_with_default_value_ ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1169,7 +1219,9 @@ public virtual void Always_computed_property_on_Added_entity_with_read_only_befo => ExecuteWithStrategyInTransaction( context => { - context.Add(new Gumball { AlwaysComputedReadOnlyBeforeSave = "Masami" }); + var gumball = Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel); + gumball.AlwaysComputedReadOnlyBeforeSave = "Masami"; + context.Add(gumball); Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("AlwaysComputedReadOnlyBeforeSave", "Gumball"), @@ -1184,7 +1236,7 @@ public virtual void Always_computed_property_on_Modified_entity_with_read_only_a ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1212,7 +1264,7 @@ public virtual void Always_computed_property_on_Modified_entity_is_read_from_sto ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new Gumball()).Entity; + var entity = context.Add(Gumball.Create(Fixture.IntSentinel, Fixture.StringSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1243,7 +1295,7 @@ public virtual void Fields_used_correctly_for_store_generated_values() ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new WithBackingFields()).Entity; + var entity = context.Add(WithBackingFields.Create(Fixture.IntSentinel, Fixture.NullableIntSentinel)).Entity; context.SaveChanges(); id = entity.Id; @@ -1261,7 +1313,8 @@ public virtual void Nullable_fields_get_defaults_when_not_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new WithNullableBackingFields()).Entity; + var entity = context.Add(WithNullableBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel)) + .Entity; context.SaveChanges(); @@ -1285,30 +1338,29 @@ public virtual void Nullable_fields_store_non_defaults_when_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add( - new WithNullableBackingFields - { - NullableBackedBoolTrueDefault = false, - NullableBackedIntNonZeroDefault = 0, - NullableBackedBoolFalseDefault = true, - NullableBackedIntZeroDefault = -1 - }).Entity; + var entity = WithNullableBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel); + entity.NullableBackedBoolTrueDefault = Fixture.BoolSentinel; + entity.NullableBackedIntNonZeroDefault = Fixture.IntSentinel; + entity.NullableBackedBoolFalseDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntZeroDefault = Fixture.IntSentinel + 1; + + context.Add(entity); context.SaveChanges(); Assert.NotEqual(0, entity.Id); - Assert.False(entity.NullableBackedBoolTrueDefault); - Assert.Equal(0, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); - Assert.Equal(-1, entity.NullableBackedIntZeroDefault); + Assert.Equal(Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); + Assert.Equal(Fixture.IntSentinel, entity.NullableBackedIntNonZeroDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); + Assert.Equal(Fixture.IntSentinel + 1, entity.NullableBackedIntZeroDefault); }, context => { var entity = context.Set().Single(); - Assert.False(entity.NullableBackedBoolTrueDefault); - Assert.Equal(0, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); - Assert.Equal(-1, entity.NullableBackedIntZeroDefault); + Assert.Equal(Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); + Assert.Equal(Fixture.IntSentinel, entity.NullableBackedIntNonZeroDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); + Assert.Equal(Fixture.IntSentinel + 1, entity.NullableBackedIntZeroDefault); }); [ConditionalFact] @@ -1316,29 +1368,28 @@ public virtual void Nullable_fields_store_any_value_when_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add( - new WithNullableBackingFields - { - NullableBackedBoolTrueDefault = true, - NullableBackedIntNonZeroDefault = 3, - NullableBackedBoolFalseDefault = true, - NullableBackedIntZeroDefault = 5 - }).Entity; + var entity = WithNullableBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel); + entity.NullableBackedBoolTrueDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntNonZeroDefault = 3; + entity.NullableBackedBoolFalseDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntZeroDefault = 5; + + context.Add(entity); context.SaveChanges(); Assert.NotEqual(0, entity.Id); - Assert.True(entity.NullableBackedBoolTrueDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); Assert.Equal(3, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); Assert.Equal(5, entity.NullableBackedIntZeroDefault); }, context => { var entity = context.Set().Single(); - Assert.True(entity.NullableBackedBoolTrueDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); Assert.Equal(3, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); Assert.Equal(5, entity.NullableBackedIntZeroDefault); }); @@ -1347,7 +1398,8 @@ public virtual void Object_fields_get_defaults_when_not_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add(new WithObjectBackingFields()).Entity; + var entity = WithObjectBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel); + context.Add(entity); context.SaveChanges(); @@ -1371,30 +1423,29 @@ public virtual void Object_fields_store_non_defaults_when_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add( - new WithObjectBackingFields - { - NullableBackedBoolTrueDefault = false, - NullableBackedIntNonZeroDefault = 0, - NullableBackedBoolFalseDefault = true, - NullableBackedIntZeroDefault = -1 - }).Entity; + var entity = WithObjectBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel); + entity.NullableBackedBoolTrueDefault = Fixture.BoolSentinel; + entity.NullableBackedIntNonZeroDefault = Fixture.IntSentinel; + entity.NullableBackedBoolFalseDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntZeroDefault = Fixture.IntSentinel + 1; + + context.Add(entity); context.SaveChanges(); Assert.NotEqual(0, entity.Id); - Assert.False(entity.NullableBackedBoolTrueDefault); - Assert.Equal(0, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); - Assert.Equal(-1, entity.NullableBackedIntZeroDefault); + Assert.Equal(Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); + Assert.Equal(Fixture.IntSentinel, entity.NullableBackedIntNonZeroDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); + Assert.Equal(Fixture.IntSentinel + 1, entity.NullableBackedIntZeroDefault); }, context => { var entity = context.Set().Single(); - Assert.False(entity.NullableBackedBoolTrueDefault); - Assert.Equal(0, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); - Assert.Equal(-1, entity.NullableBackedIntZeroDefault); + Assert.Equal(Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); + Assert.Equal(Fixture.IntSentinel, entity.NullableBackedIntNonZeroDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); + Assert.Equal(Fixture.IntSentinel + 1, entity.NullableBackedIntZeroDefault); }); [ConditionalFact] @@ -1402,29 +1453,28 @@ public virtual void Object_fields_store_any_value_when_set() => ExecuteWithStrategyInTransaction( context => { - var entity = context.Add( - new WithObjectBackingFields - { - NullableBackedBoolTrueDefault = true, - NullableBackedIntNonZeroDefault = 3, - NullableBackedBoolFalseDefault = true, - NullableBackedIntZeroDefault = 5 - }).Entity; + var entity = WithObjectBackingFields.Create(Fixture.NullableIntSentinel, Fixture.NullableBoolSentinel); + entity.NullableBackedBoolTrueDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntNonZeroDefault = 3; + entity.NullableBackedBoolFalseDefault = !Fixture.BoolSentinel; + entity.NullableBackedIntZeroDefault = 5; + + context.Add(entity); context.SaveChanges(); Assert.NotEqual(0, entity.Id); - Assert.True(entity.NullableBackedBoolTrueDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); Assert.Equal(3, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); Assert.Equal(5, entity.NullableBackedIntZeroDefault); }, context => { var entity = context.Set().Single(); - Assert.True(entity.NullableBackedBoolTrueDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolTrueDefault); Assert.Equal(3, entity.NullableBackedIntNonZeroDefault); - Assert.True(entity.NullableBackedBoolFalseDefault); + Assert.Equal(!Fixture.BoolSentinel, entity.NullableBackedBoolFalseDefault); Assert.Equal(5, entity.NullableBackedIntZeroDefault); }); @@ -1455,6 +1505,29 @@ protected class Species protected class Gumball { + public static Gumball Create(int intSentinel, string? stringSentinel) + => new() + { + Id = intSentinel, + NotStoreGenerated = stringSentinel, + Identity = stringSentinel, + IdentityReadOnlyBeforeSave = stringSentinel, + IdentityReadOnlyAfterSave = stringSentinel, + AlwaysIdentity = stringSentinel, + AlwaysIdentityReadOnlyBeforeSave = stringSentinel, + AlwaysIdentityReadOnlyAfterSave = stringSentinel, + Computed = stringSentinel, + ComputedReadOnlyBeforeSave = stringSentinel, + ComputedReadOnlyAfterSave = stringSentinel, + AlwaysComputed = stringSentinel, + AlwaysComputedReadOnlyBeforeSave = stringSentinel, + AlwaysComputedReadOnlyAfterSave = stringSentinel + }; + + private Gumball() + { + } + public int Id { get; set; } public string? NotStoreGenerated { get; set; } @@ -1477,6 +1550,56 @@ protected class Gumball protected class Anais { + public static Anais Create(int intSentinel, string? stringSentinel) + => new() + { + Id = intSentinel, + Never = stringSentinel, + NeverUseBeforeUseAfter = stringSentinel, + NeverIgnoreBeforeUseAfter = stringSentinel, + NeverThrowBeforeUseAfter = stringSentinel, + NeverUseBeforeIgnoreAfter = stringSentinel, + NeverIgnoreBeforeIgnoreAfter = stringSentinel, + NeverThrowBeforeIgnoreAfter = stringSentinel, + NeverUseBeforeThrowAfter = stringSentinel, + NeverIgnoreBeforeThrowAfter = stringSentinel, + NeverThrowBeforeThrowAfter = stringSentinel, + OnAdd = stringSentinel, + OnAddUseBeforeUseAfter = stringSentinel, + OnAddIgnoreBeforeUseAfter = stringSentinel, + OnAddThrowBeforeUseAfter = stringSentinel, + OnAddUseBeforeIgnoreAfter = stringSentinel, + OnAddIgnoreBeforeIgnoreAfter = stringSentinel, + OnAddThrowBeforeIgnoreAfter = stringSentinel, + OnAddUseBeforeThrowAfter = stringSentinel, + OnAddIgnoreBeforeThrowAfter = stringSentinel, + OnAddThrowBeforeThrowAfter = stringSentinel, + OnAddOrUpdate = stringSentinel, + OnAddOrUpdateUseBeforeUseAfter = stringSentinel, + OnAddOrUpdateIgnoreBeforeUseAfter = stringSentinel, + OnAddOrUpdateThrowBeforeUseAfter = stringSentinel, + OnAddOrUpdateUseBeforeIgnoreAfter = stringSentinel, + OnAddOrUpdateIgnoreBeforeIgnoreAfter = stringSentinel, + OnAddOrUpdateThrowBeforeIgnoreAfter = stringSentinel, + OnAddOrUpdateUseBeforeThrowAfter = stringSentinel, + OnAddOrUpdateIgnoreBeforeThrowAfter = stringSentinel, + OnAddOrUpdateThrowBeforeThrowAfter = stringSentinel, + OnUpdate = stringSentinel, + OnUpdateUseBeforeUseAfter = stringSentinel, + OnUpdateIgnoreBeforeUseAfter = stringSentinel, + OnUpdateThrowBeforeUseAfter = stringSentinel, + OnUpdateUseBeforeIgnoreAfter = stringSentinel, + OnUpdateIgnoreBeforeIgnoreAfter = stringSentinel, + OnUpdateThrowBeforeIgnoreAfter = stringSentinel, + OnUpdateUseBeforeThrowAfter = stringSentinel, + OnUpdateIgnoreBeforeThrowAfter = stringSentinel, + OnUpdateThrowBeforeThrowAfter = stringSentinel, + }; + + private Anais() + { + } + public int Id { get; set; } public string? Never { get; set; } public string? NeverUseBeforeUseAfter { get; set; } @@ -1525,6 +1648,19 @@ protected class Anais protected class WithBackingFields { + public static WithBackingFields Create(int intSentinel, int? nullableIntSentinel) + { + var entity = new WithBackingFields(); + entity._id = intSentinel; + entity._nullableAsNonNullable = nullableIntSentinel; + entity._nonNullableAsNullable = intSentinel; + return entity; + } + + private WithBackingFields() + { + } + #pragma warning disable RCS1085 // Use auto-implemented property. // ReSharper disable ConvertToAutoProperty private int _id; @@ -1556,6 +1692,21 @@ public int? NonNullableAsNullable protected class WithNullableBackingFields { + public static WithNullableBackingFields Create(int? intSentinel, bool? boolSentinel) + { + var entity = new WithNullableBackingFields(); + entity._id = intSentinel; + entity._nullableBackedBoolTrueDefault = boolSentinel; + entity._nullableBackedIntNonZeroDefault = intSentinel; + entity._nullableBackedBoolFalseDefault = boolSentinel; + entity._nullableBackedIntZeroDefault = intSentinel; + return entity; + } + + private WithNullableBackingFields() + { + } + private int? _id; public int Id @@ -1599,6 +1750,21 @@ public int NullableBackedIntZeroDefault protected class WithObjectBackingFields { + public static WithObjectBackingFields Create(int? intSentinel, bool? boolSentinel) + { + var entity = new WithObjectBackingFields(); + entity._id = intSentinel; + entity._nullableBackedBoolTrueDefault = boolSentinel; + entity._nullableBackedIntNonZeroDefault = intSentinel; + entity._nullableBackedBoolFalseDefault = boolSentinel; + entity._nullableBackedIntZeroDefault = intSentinel; + return entity; + } + + private WithObjectBackingFields() + { + } + private object? _id; public int Id @@ -1661,7 +1827,7 @@ protected class ShortToBytes : WithConverter { } - protected class WrappedIntClass + public class WrappedIntClass { public int Value { get; set; } } @@ -1696,7 +1862,7 @@ public override bool GeneratesTemporaryValues => false; } - protected struct WrappedIntStruct + public struct WrappedIntStruct { public int Value { get; set; } } @@ -1720,7 +1886,7 @@ public override bool GeneratesTemporaryValues => false; } - protected record WrappedIntRecord + public record WrappedIntRecord { public int Value { get; set; } } @@ -1744,7 +1910,7 @@ public override bool GeneratesTemporaryValues => false; } - protected class WrappedIntKeyClass + public class WrappedIntKeyClass { public int Value { get; set; } } @@ -1770,7 +1936,7 @@ public WrappedIntKeyClassComparer() } } - protected struct WrappedIntKeyStruct + public struct WrappedIntKeyStruct { public int Value { get; set; } @@ -1797,7 +1963,7 @@ public WrappedIntKeyStructConverter() } } - protected record WrappedIntKeyRecord + public record WrappedIntKeyRecord { public int Value { get; set; } } @@ -1935,6 +2101,8 @@ public virtual void Insert_update_and_delete_with_wrapped_int_key() var principal1 = context.Add( new WrappedIntClassPrincipal { + Id = Fixture.WrappedIntKeyClassSentinel!, + NonKey = Fixture.WrappedIntClassSentinel, Dependents = { new WrappedIntClassDependentShadow(), new WrappedIntClassDependentShadow() }, OptionalDependents = { new WrappedIntClassDependentOptional(), new WrappedIntClassDependentOptional() }, RequiredDependents = { new WrappedIntClassDependentRequired(), new WrappedIntClassDependentRequired() } @@ -1943,6 +2111,8 @@ public virtual void Insert_update_and_delete_with_wrapped_int_key() var principal2 = context.Add( new WrappedIntStructPrincipal { + Id = Fixture.WrappedIntKeyStructSentinel, + NonKey = Fixture.WrappedIntStructSentinel, Dependents = { new WrappedIntStructDependentShadow(), new WrappedIntStructDependentShadow() }, OptionalDependents = { new WrappedIntStructDependentOptional(), new WrappedIntStructDependentOptional() }, RequiredDependents = { new WrappedIntStructDependentRequired(), new WrappedIntStructDependentRequired() } @@ -1951,6 +2121,8 @@ public virtual void Insert_update_and_delete_with_wrapped_int_key() var principal3 = context.Add( new WrappedIntRecordPrincipal { + Id = Fixture.WrappedIntKeyRecordSentinel!, + NonKey = Fixture.WrappedIntRecordSentinel, Dependents = { new WrappedIntRecordDependentShadow(), new WrappedIntRecordDependentShadow() }, OptionalDependents = { new WrappedIntRecordDependentOptional(), new WrappedIntRecordDependentOptional() }, RequiredDependents = { new WrappedIntRecordDependentRequired(), new WrappedIntRecordDependentRequired() } @@ -2244,6 +2416,7 @@ public virtual void Insert_update_and_delete_with_long_to_int_conversion() var principal1 = context.Add( new LongToIntPrincipal { + Id = Fixture.LongSentinel, Dependents = { new LongToIntDependentShadow(), new LongToIntDependentShadow() }, OptionalDependents = { new LongToIntDependentOptional(), new LongToIntDependentOptional() }, RequiredDependents = { new LongToIntDependentRequired(), new LongToIntDependentRequired() } @@ -2337,7 +2510,7 @@ public virtual void Insert_update_and_delete_with_long_to_int_conversion() }); } - protected class WrappedStringClass + public class WrappedStringClass { public string? Value { get; set; } } @@ -2372,7 +2545,7 @@ public override bool GeneratesTemporaryValues => false; } - protected struct WrappedStringStruct + public struct WrappedStringStruct { public string? Value { get; set; } } @@ -2396,7 +2569,7 @@ public override bool GeneratesTemporaryValues => false; } - protected record WrappedStringRecord + public record WrappedStringRecord { public string? Value { get; set; } } @@ -2420,7 +2593,7 @@ public override bool GeneratesTemporaryValues => false; } - protected class WrappedStringKeyClass + public class WrappedStringKeyClass { public string? Value { get; set; } } @@ -2446,7 +2619,7 @@ public WrappedStringKeyClassComparer() } } - protected struct WrappedStringKeyStruct + public struct WrappedStringKeyStruct { public string Value { get; set; } @@ -2473,7 +2646,7 @@ public WrappedStringKeyStructConverter() } } - protected record WrappedStringKeyRecord + public record WrappedStringKeyRecord { public string? Value { get; set; } } @@ -2623,6 +2796,8 @@ public virtual void Insert_update_and_delete_with_wrapped_string_key() var principal1 = context.Add( new WrappedStringClassPrincipal { + Id = Fixture.WrappedStringKeyClassSentinel!, + NonKey = Fixture.WrappedStringClassSentinel, Dependents = { new WrappedStringClassDependentShadow(), new WrappedStringClassDependentShadow() }, OptionalDependents = { new WrappedStringClassDependentOptional(), new WrappedStringClassDependentOptional() }, RequiredDependents = { new WrappedStringClassDependentRequired(), new WrappedStringClassDependentRequired() } @@ -2631,6 +2806,8 @@ public virtual void Insert_update_and_delete_with_wrapped_string_key() var principal2 = context.Add( new WrappedStringStructPrincipal { + Id = Fixture.WrappedStringKeyStructSentinel, + NonKey = Fixture.WrappedStringStructSentinel, Dependents = { new WrappedStringStructDependentShadow(), new WrappedStringStructDependentShadow() }, OptionalDependents = { new WrappedStringStructDependentOptional(), new WrappedStringStructDependentOptional() }, RequiredDependents = { new WrappedStringStructDependentRequired(), new WrappedStringStructDependentRequired() } @@ -2639,6 +2816,8 @@ public virtual void Insert_update_and_delete_with_wrapped_string_key() var principal3 = context.Add( new WrappedStringRecordPrincipal { + Id = Fixture.WrappedStringKeyRecordSentinel!, + NonKey = Fixture.WrappedStringRecordSentinel, Dependents = { new WrappedStringRecordDependentShadow(), new WrappedStringRecordDependentShadow() }, OptionalDependents = { new WrappedStringRecordDependentOptional(), new WrappedStringRecordDependentOptional() }, RequiredDependents = { new WrappedStringRecordDependentRequired(), new WrappedStringRecordDependentRequired() } @@ -2889,7 +3068,7 @@ public virtual void Insert_update_and_delete_with_wrapped_string_key() // ReSharper disable once StaticMemberInGenericType protected static readonly Guid KnownGuid = Guid.Parse("E871CEA4-8DBE-4269-99F4-87F7128AF399"); - protected class WrappedGuidClass + public class WrappedGuidClass { public Guid Value { get; set; } } @@ -2924,7 +3103,7 @@ public override bool GeneratesTemporaryValues => false; } - protected struct WrappedGuidStruct + public struct WrappedGuidStruct { public Guid Value { get; set; } } @@ -2948,7 +3127,7 @@ public override bool GeneratesTemporaryValues => false; } - protected record WrappedGuidRecord + public record WrappedGuidRecord { public Guid Value { get; set; } } @@ -2972,7 +3151,7 @@ public override bool GeneratesTemporaryValues => false; } - protected class WrappedGuidKeyClass + public class WrappedGuidKeyClass { public Guid Value { get; set; } } @@ -2998,7 +3177,7 @@ public WrappedGuidKeyClassComparer() } } - protected struct WrappedGuidKeyStruct + public struct WrappedGuidKeyStruct { public Guid Value { get; set; } @@ -3025,7 +3204,7 @@ public WrappedGuidKeyStructConverter() } } - protected record WrappedGuidKeyRecord + public record WrappedGuidKeyRecord { public Guid Value { get; set; } } @@ -3163,6 +3342,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Guid_key() var principal1 = context.Add( new WrappedGuidClassPrincipal { + Id = Fixture.WrappedGuidKeyClassSentinel!, + NonKey = Fixture.WrappedGuidClassSentinel, Dependents = { new WrappedGuidClassDependentShadow(), new WrappedGuidClassDependentShadow() }, OptionalDependents = { new WrappedGuidClassDependentOptional(), new WrappedGuidClassDependentOptional() }, RequiredDependents = { new WrappedGuidClassDependentRequired(), new WrappedGuidClassDependentRequired() } @@ -3171,6 +3352,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Guid_key() var principal2 = context.Add( new WrappedGuidStructPrincipal { + Id = Fixture.WrappedGuidKeyStructSentinel, + NonKey = Fixture.WrappedGuidStructSentinel, Dependents = { new WrappedGuidStructDependentShadow(), new WrappedGuidStructDependentShadow() }, OptionalDependents = { new WrappedGuidStructDependentOptional(), new WrappedGuidStructDependentOptional() }, RequiredDependents = { new WrappedGuidStructDependentRequired(), new WrappedGuidStructDependentRequired() } @@ -3179,6 +3362,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Guid_key() var principal3 = context.Add( new WrappedGuidRecordPrincipal { + Id = Fixture.WrappedGuidKeyRecordSentinel!, + NonKey = Fixture.WrappedGuidRecordSentinel, Dependents = { new WrappedGuidRecordDependentShadow(), new WrappedGuidRecordDependentShadow() }, OptionalDependents = { new WrappedGuidRecordDependentOptional(), new WrappedGuidRecordDependentOptional() }, RequiredDependents = { new WrappedGuidRecordDependentRequired(), new WrappedGuidRecordDependentRequired() } @@ -3426,7 +3611,7 @@ public virtual void Insert_update_and_delete_with_wrapped_Guid_key() }); } - protected class WrappedUriClass + public class WrappedUriClass { public Uri? Value { get; set; } } @@ -3461,7 +3646,7 @@ public override bool GeneratesTemporaryValues => false; } - protected struct WrappedUriStruct + public struct WrappedUriStruct { public Uri Value { get; set; } } @@ -3485,7 +3670,7 @@ public override bool GeneratesTemporaryValues => false; } - protected record WrappedUriRecord + public record WrappedUriRecord { public Uri? Value { get; set; } } @@ -3509,7 +3694,7 @@ public override bool GeneratesTemporaryValues => false; } - protected class WrappedUriKeyClass + public class WrappedUriKeyClass { public Uri? Value { get; set; } } @@ -3535,7 +3720,7 @@ public WrappedUriKeyClassComparer() } } - protected struct WrappedUriKeyStruct + public struct WrappedUriKeyStruct { public Uri? Value { get; set; } @@ -3565,7 +3750,7 @@ public WrappedUriKeyStructConverter() } } - protected record WrappedUriKeyRecord + public record WrappedUriKeyRecord { public Uri? Value { get; set; } } @@ -3703,6 +3888,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Uri_key() var principal1 = context.Add( new WrappedUriClassPrincipal { + Id = Fixture.WrappedUriKeyClassSentinel!, + NonKey = Fixture.WrappedUriClassSentinel, Dependents = { new WrappedUriClassDependentShadow(), new WrappedUriClassDependentShadow() }, OptionalDependents = { new WrappedUriClassDependentOptional(), new WrappedUriClassDependentOptional() }, RequiredDependents = { new WrappedUriClassDependentRequired(), new WrappedUriClassDependentRequired() } @@ -3711,6 +3898,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Uri_key() var principal2 = context.Add( new WrappedUriStructPrincipal { + Id = Fixture.WrappedUriKeyStructSentinel, + NonKey = Fixture.WrappedUriStructSentinel, Dependents = { new WrappedUriStructDependentShadow(), new WrappedUriStructDependentShadow() }, OptionalDependents = { new WrappedUriStructDependentOptional(), new WrappedUriStructDependentOptional() }, RequiredDependents = { new WrappedUriStructDependentRequired(), new WrappedUriStructDependentRequired() } @@ -3719,6 +3908,8 @@ public virtual void Insert_update_and_delete_with_wrapped_Uri_key() var principal3 = context.Add( new WrappedUriRecordPrincipal { + Id = Fixture.WrappedUriKeyRecordSentinel!, + NonKey = Fixture.WrappedUriRecordSentinel, Dependents = { new WrappedUriRecordDependentShadow(), new WrappedUriRecordDependentShadow() }, OptionalDependents = { new WrappedUriRecordDependentOptional(), new WrappedUriRecordDependentOptional() }, RequiredDependents = { new WrappedUriRecordDependentRequired(), new WrappedUriRecordDependentRequired() } @@ -4012,6 +4203,7 @@ public virtual void Insert_update_and_delete_with_Uri_key() var principal1 = context.Add( new UriPrincipal { + Id = Fixture.UriSentinel!, Dependents = { new UriDependentShadow(), new UriDependentShadow() }, OptionalDependents = { new UriDependentOptional(), new UriDependentOptional() }, RequiredDependents = { new UriDependentRequired(), new UriDependentRequired() } @@ -4076,7 +4268,7 @@ public virtual void Insert_update_and_delete_with_Uri_key() }); } - protected enum KeyEnum + public enum KeyEnum { A, B, @@ -4131,6 +4323,7 @@ public virtual void Insert_update_and_delete_with_enum_key() var principal1 = context.Add( new EnumPrincipal { + Id = Fixture.KeyEnumSentinel, Dependents = { new EnumDependentShadow(), new EnumDependentShadow() }, OptionalDependents = { new EnumDependentOptional(), new EnumDependentOptional() }, RequiredDependents = { new EnumDependentRequired(), new EnumDependentRequired() } @@ -4240,6 +4433,7 @@ public virtual void Insert_update_and_delete_with_GuidAsString_key() var principal1 = context.Add( new GuidAsStringPrincipal { + Id = Fixture.GuidAsStringSentinel, Dependents = { new GuidAsStringDependentShadow(), new GuidAsStringDependentShadow() }, OptionalDependents = { new GuidAsStringDependentOptional(), new GuidAsStringDependentOptional() }, RequiredDependents = { new GuidAsStringDependentRequired(), new GuidAsStringDependentRequired() } @@ -4349,6 +4543,7 @@ public virtual void Insert_update_and_delete_with_StringAsGuid_key() var principal1 = context.Add( new StringAsGuidPrincipal { + Id = Fixture.StringAsGuidSentinel!, Dependents = { new StringAsGuidDependentShadow(), new StringAsGuidDependentShadow() }, OptionalDependents = { new StringAsGuidDependentOptional(), new StringAsGuidDependentOptional() }, RequiredDependents = { new StringAsGuidDependentRequired(), new StringAsGuidDependentRequired() } @@ -4431,8 +4626,113 @@ protected DbContext CreateContext() public abstract class StoreGeneratedFixtureBase : SharedStoreFixtureBase { - protected override string StoreName - => "StoreGeneratedTest"; + public virtual Guid GuidSentinel + => default; + + public virtual int IntSentinel + => default; + + public virtual short ShortSentinel + => default; + + public virtual long LongSentinel + => default; + + public virtual int? NullableIntSentinel + => default; + + public virtual bool BoolSentinel + => default; + + public virtual bool? NullableBoolSentinel + => default; + + public virtual string? StringSentinel + => default; + + public virtual Uri? UriSentinel + => default; + + public virtual KeyEnum KeyEnumSentinel + => default; + + public virtual string? StringAsGuidSentinel + => default; + + public virtual Guid GuidAsStringSentinel + => default; + + public virtual WrappedIntKeyClass? WrappedIntKeyClassSentinel + => default; + + public virtual WrappedIntClass? WrappedIntClassSentinel + => default; + + public virtual WrappedIntKeyStruct WrappedIntKeyStructSentinel + => default; + + public virtual WrappedIntStruct WrappedIntStructSentinel + => default; + + public virtual WrappedIntKeyRecord? WrappedIntKeyRecordSentinel + => default; + + public virtual WrappedIntRecord? WrappedIntRecordSentinel + => default; + + public virtual WrappedStringKeyClass? WrappedStringKeyClassSentinel + => default; + + public virtual WrappedStringClass? WrappedStringClassSentinel + => default; + + public virtual WrappedStringKeyStruct WrappedStringKeyStructSentinel + => default; + + public virtual WrappedStringStruct WrappedStringStructSentinel + => default; + + public virtual WrappedStringKeyRecord? WrappedStringKeyRecordSentinel + => default; + + public virtual WrappedStringRecord? WrappedStringRecordSentinel + => default; + + public virtual WrappedGuidKeyClass? WrappedGuidKeyClassSentinel + => default; + + public virtual WrappedGuidClass? WrappedGuidClassSentinel + => default; + + public virtual WrappedGuidKeyStruct WrappedGuidKeyStructSentinel + => default; + + public virtual WrappedGuidStruct WrappedGuidStructSentinel + => default; + + public virtual WrappedGuidKeyRecord? WrappedGuidKeyRecordSentinel + => default; + + public virtual WrappedGuidRecord? WrappedGuidRecordSentinel + => default; + + public virtual WrappedUriKeyClass? WrappedUriKeyClassSentinel + => default; + + public virtual WrappedUriClass? WrappedUriClassSentinel + => default; + + public virtual WrappedUriKeyStruct WrappedUriKeyStructSentinel + => default; + + public virtual WrappedUriStruct WrappedUriStructSentinel + => default; + + public virtual WrappedUriKeyRecord? WrappedUriKeyRecordSentinel + => default; + + public virtual WrappedUriRecord? WrappedUriRecordSentinel + => default; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { @@ -4723,7 +5023,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { var keyConverter = new ValueConverter( v => (int)v, - v => (long)v); + v => v); entity.Property(e => e.Id).HasConversion(keyConverter); }); diff --git a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs index 3fcebae65e7..859014b1a05 100644 --- a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs @@ -554,6 +554,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.IdUserState).HasDefaultValue(1); b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.AccessStateWithSentinelId).ValueGeneratedNever(); + b.HasData(new AccessStateWithSentinel { AccessStateWithSentinelId = 1 }); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.IdUserState).HasDefaultValue(1).Metadata.Sentinel = 667; + b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); + }); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerTestBase.cs index 53c34f00557..46bbe702578 100644 --- a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerTestBase.cs @@ -43,6 +43,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); }); + modelBuilder.Entity( + b => + { + b.Property(e => e.AccessStateWithSentinelId).ValueGeneratedNever(); + b.HasData(new AccessStateWithSentinel { AccessStateWithSentinelId = 1 }); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.IdUserState).HasDefaultValue(1).Metadata.Sentinel = 667; + b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); + }); + modelBuilder.Entity().Property("CategoryId").HasDefaultValue(1); modelBuilder.Entity().Property(e => e.CategoryId).HasDefaultValue(2);; } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerSentinelValueGenerationScenariosTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerSentinelValueGenerationScenariosTest.cs new file mode 100644 index 00000000000..7eaeadff57b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerSentinelValueGenerationScenariosTest.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NetTopologySuite.Geometries; + +namespace Microsoft.EntityFrameworkCore; + +public class SqlServerSentinelValueGenerationScenariosTest : SqlServerValueGenerationScenariosTestBase +{ + protected override string DatabaseName + => "SqlServerSentinelValueGenerationScenariosTest"; + + protected override Guid GuidSentinel + => new("56D3784D-6F7F-4935-B7F6-E77DC6E1D91E"); + + protected override int IntSentinel + => 667; + + protected override uint UIntSentinel + => 667; + + protected override IntKey IntKeySentinel + => IntKey.SixSixSeven; + + protected override ULongKey ULongKeySentinel + => ULongKey.Sentinel; + + protected override int? NullableIntSentinel + => 667; + + protected override string StringSentinel + => "667"; + + protected override DateTime DateTimeSentinel + => new(1973, 9, 3, 0, 3, 0); + + protected override NeedsConverter NeedsConverterSentinel + => new(668); + + protected override GeometryCollection GeometryCollectionSentinel + => GeometryFactory.CreateGeometryCollection( + new Geometry[] { GeometryFactory.CreatePoint(new Coordinate(6, 7)) }); + + protected override byte[] TimestampSentinel + => new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = IntSentinel; + b.Property(e => e.Name).Metadata.Sentinel = StringSentinel; + b.Property(e => e.CreatedOn).Metadata.Sentinel = DateTimeSentinel; + b.Property(e => e.GeometryCollection).Metadata.Sentinel = GeometryCollectionSentinel; + b.Property(e => e.OtherId).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NeedsConverter).Metadata.Sentinel = new NeedsConverter(IntSentinel); + }); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTest.cs index 8bebc3bb653..38c07fce62e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTest.cs @@ -1,1605 +1,46 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore.SqlServer.Internal; -using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; -using NetTopologySuite; using NetTopologySuite.Geometries; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; -public class SqlServerValueGenerationScenariosTest +public class SqlServerValueGenerationScenariosTest : SqlServerValueGenerationScenariosTestBase { - private const string DatabaseName = "SqlServerValueGenerationScenariosTest"; + protected override string DatabaseName + => "SqlServerValueGenerationScenariosTest"; - // Positive cases + protected override Guid GuidSentinel + => new(); - [ConditionalFact] - public void Insert_with_Identity_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextIdentity(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); + protected override int IntSentinel + => 0; - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); + protected override uint UIntSentinel + => 0; - context.SaveChanges(); - } + protected override IntKey IntKeySentinel + => IntKey.Zero; - using (var context = new BlogContextIdentity(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + protected override ULongKey ULongKeySentinel + => ULongKey.Zero; - Assert.Equal(1, blogs[0].Id); - Assert.Equal(2, blogs[1].Id); - } - } + protected override int? NullableIntSentinel + => null; - public class BlogContextIdentity : ContextBase - { - public BlogContextIdentity(string databaseName) - : base(databaseName) - { - } - } + protected override string StringSentinel + => null; - [ConditionalFact] - public void Insert_with_sequence_HiLo() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextHiLo(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); + protected override DateTime DateTimeSentinel + => new(); - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); + protected override NeedsConverter NeedsConverterSentinel + => new(0); - context.SaveChanges(); - } + protected override GeometryCollection GeometryCollectionSentinel + => null; - using (var context = new BlogContextHiLo(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, blogs[0].Id); - Assert.Equal(2, blogs[0].OtherId); - Assert.Equal(3, blogs[1].Id); - Assert.Equal(4, blogs[1].OtherId); - } - } - - public class BlogContextHiLo : ContextBase - { - public BlogContextHiLo(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.UseHiLo(); - - modelBuilder.Entity( - eb => - { - eb.HasAlternateKey( - b => new { b.OtherId }); - eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); - }); - } - } - - [ConditionalFact] - public void Insert_with_key_sequence() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextKeySequence(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextKeySequence(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, blogs[0].Id); - Assert.Equal(1, blogs[0].OtherId); - Assert.Equal(2, blogs[1].Id); - Assert.Equal(2, blogs[1].OtherId); - } - } - - public class BlogContextKeySequence : ContextBase - { - public BlogContextKeySequence(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.UseKeySequences(); - - modelBuilder.Entity( - eb => - { - eb.HasAlternateKey( - b => new { b.OtherId }); - eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); - }); - } - } - - [ConditionalFact] - public void Insert_with_non_key_sequence() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextNonKeySequence(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextNonKeySequence(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, blogs[0].Id); - Assert.Equal(1, blogs[0].OtherId); - Assert.Equal(2, blogs[1].Id); - Assert.Equal(2, blogs[1].OtherId); - } - } - - public class BlogContextNonKeySequence : ContextBase - { - public BlogContextNonKeySequence(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity( - eb => - { - eb.Property(b => b.OtherId).UseSequence(); - eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); - }); - } - } - - [ConditionalFact] - public void Insert_with_default_value_from_sequence() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextDefaultValue(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - } - - using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) - { - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - Assert.Equal(2, blogs[2].Id); - Assert.Equal(3, blogs[3].Id); - } - } - - public class BlogContextDefaultValue : ContextBase - { - public BlogContextDefaultValue(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .HasSequence("MySequence") - .StartsAt(0); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("next value for MySequence"); - } - } - - public class BlogContextDefaultValueNoMigrations : ContextBase - { - public BlogContextDefaultValueNoMigrations(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValue(); - } - } - - [ConditionalFact] - public void Insert_with_default_string_value_from_sequence() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextStringDefaultValue(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithStringKey { Name = "One Unicorn" }, new BlogWithStringKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextStringDefaultValue(testStore.Name)) - { - var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal("i77", blogs[0].Id); - Assert.Equal("i78", blogs[1].Id); - } - } - - public class BlogContextStringDefaultValue : ContextBase - { - public BlogContextStringDefaultValue(string databaseName) - : base(databaseName) - { - } - - public DbSet StringyBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .HasSequence("MyStringSequence") - .StartsAt(77); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("'i' + CAST((NEXT VALUE FOR MyStringSequence) AS VARCHAR(20))"); - } - } - - public class BlogWithStringKey - { - public string Id { get; set; } - public string Name { get; set; } - } - - [ConditionalFact] - public void Insert_with_key_default_value_from_sequence() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(77, blogs[0].Id); - Assert.Equal(78, blogs[1].Id); - } - } - - public class BlogContextKeyColumnWithDefaultValue : ContextBase - { - public BlogContextKeyColumnWithDefaultValue(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .HasSequence("MySequence") - .StartsAt(77); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("next value for MySequence") - .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - } - } - - [ConditionalFact] - public void Insert_uint_to_Identity_column_using_value_converter() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithUIntKey { Name = "One Unicorn" }, new BlogWithUIntKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) - { - var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal((uint)1, blogs[0].Id); - Assert.Equal((uint)2, blogs[1].Id); - } - } - - public class BlogContextUIntToIdentityUsingValueConverter : ContextBase - { - public BlogContextUIntToIdentityUsingValueConverter(string databaseName) - : base(databaseName) - { - } - - public DbSet UnsignedBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasConversion(); - } - } - - public class BlogWithUIntKey - { - public uint Id { get; set; } - public string Name { get; set; } - } - - [ConditionalFact] - public void Insert_int_enum_to_Identity_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextIntEnumToIdentity(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithIntEnumKey { Name = "One Unicorn" }, new BlogWithIntEnumKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextIntEnumToIdentity(testStore.Name)) - { - var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, (int)blogs[0].Id); - Assert.Equal(2, (int)blogs[1].Id); - } - } - - public class BlogContextIntEnumToIdentity : ContextBase - { - public BlogContextIntEnumToIdentity(string databaseName) - : base(databaseName) - { - } - - public DbSet EnumBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd(); - } - } - - public class BlogWithIntEnumKey - { - public IntKey Id { get; set; } - public string Name { get; set; } - } - - public enum IntKey - { - } - - [ConditionalFact] - public void Insert_ulong_enum_to_Identity_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextULongEnumToIdentity(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithULongEnumKey { Name = "One Unicorn" }, new BlogWithULongEnumKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextULongEnumToIdentity(testStore.Name)) - { - var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, (int)blogs[0].Id); - Assert.Equal(2, (int)blogs[1].Id); - } - } - - public class BlogContextULongEnumToIdentity : ContextBase - { - public BlogContextULongEnumToIdentity(string databaseName) - : base(databaseName) - { - } - - public DbSet EnumBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd(); - } - } - - public class BlogWithULongEnumKey - { - public ULongKey Id { get; set; } - public string Name { get; set; } - } - - public enum ULongKey : ulong - { - } - - [ConditionalFact] - public void Insert_string_to_Identity_column_using_value_converter() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithStringKey { Name = "One Unicorn" }, new BlogWithStringKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) - { - var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal("1", blogs[0].Id); - Assert.Equal("2", blogs[1].Id); - } - } - - public class BlogContextStringToIdentityUsingValueConverter : ContextBase - { - public BlogContextStringToIdentityUsingValueConverter(string databaseName) - : base(databaseName) - { - } - - public DbSet StringyBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - Guid guid; - modelBuilder - .Entity() - .Property(e => e.Id) - .HasValueGenerator() - .HasConversion( - v => Guid.TryParse(v, out guid) - ? default - : int.Parse(v), - v => v.ToString()) - .ValueGeneratedOnAdd(); - } - } - - [ConditionalFact] - public void Insert_with_explicit_non_default_keys() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextNoKeyGeneration(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Id = 66, Name = "One Unicorn" }, new Blog { Id = 67, Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextNoKeyGeneration(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(66, blogs[0].Id); - Assert.Equal(67, blogs[1].Id); - } - } - - public class BlogContextNoKeyGeneration : ContextBase - { - public BlogContextNoKeyGeneration(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - } - } - - [ConditionalFact] - public void Insert_with_explicit_with_default_keys() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, - new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) - { - var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - } - } - - public class BlogContextNoKeyGenerationNullableKey : ContextBase - { - public BlogContextNoKeyGenerationNullableKey(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - } - } - - [ConditionalFact] - public void Insert_with_non_key_default_value() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - var blogs = new List - { - new() { Name = "One Unicorn" }, - new() - { - Name = "Two Unicorns", - CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0), - NeedsConverter = new NeedsConverter(111), - GeometryCollection = GeometryFactory.CreateGeometryCollection( - new Geometry[] { GeometryFactory.CreatePoint(new Coordinate(1, 3)) }) - } - }; - - context.AddRange(blogs); - - context.SaveChanges(); - - Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); - Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); - Assert.Equal(111, blogs[1].NeedsConverter.Value); - - var point = ((Point)blogs[1].GeometryCollection.Geometries[0]); - Assert.Equal(1, point.X); - Assert.Equal(3, point.Y); - } - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); - Assert.Equal(3, blogs.Count); - - Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); - Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); - Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); - - var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); - Assert.Equal(1, point1.X); - Assert.Equal(3, point1.Y); - - var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); - Assert.Equal(1, point2.X); - Assert.Equal(2, point2.Y); - - blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); - - blogs[1].Name = "X Unicorns"; - blogs[1].NeedsConverter = new NeedsConverter(222); - blogs[1].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 11)); - - blogs[2].Name = "Y Unicorns"; - blogs[2].NeedsConverter = new NeedsConverter(333); - blogs[2].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 22)); - - context.SaveChanges(); - } - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); - Assert.Equal(3, blogs.Count); - - Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn); - Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); - Assert.Equal(222, blogs[1].NeedsConverter.Value); - Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); - Assert.Equal(333, blogs[2].NeedsConverter.Value); - - var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); - Assert.Equal(1, point1.X); - Assert.Equal(11, point1.Y); - - var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); - Assert.Equal(1, point2.X); - Assert.Equal(22, point2.Y); - } - } - - private static readonly GeometryFactory GeometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - - public class BlogContextNonKeyDefaultValue : ContextBase - { - public BlogContextNonKeyDefaultValue(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity( - b => - { - b.Property(e => e.CreatedOn).HasDefaultValueSql("getdate()"); - b.Property(e => e.GeometryCollection).HasDefaultValue(GeometryFactory.CreateGeometryCollection()); - - b.HasData( - new Blog - { - Id = 9979, - Name = "W Unicorns", - CreatedOn = new DateTime(1974, 8, 3, 0, 10, 0), - NeedsConverter = new NeedsConverter(111), - GeometryCollection = GeometryFactory.CreateGeometryCollection( - new Geometry[] { GeometryFactory.CreatePoint(new Coordinate(1, 2)) }) - }); - }); - } - } - - [ConditionalFact] - public void Insert_with_non_key_default_value_readonly() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - - context.SaveChanges(); - - Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); - } - - DateTime dateTime0; - - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - dateTime0 = blogs[0].CreatedOn; - - Assert.NotEqual(new DateTime(), dateTime0); - Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); - - blogs[0].Name = "One Pegasus"; - blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); - - context.SaveChanges(); - } - - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(dateTime0, blogs[0].CreatedOn); - Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn); - } - } - - public class BlogContextNonKeyReadOnlyDefaultValue : ContextBase - { - public BlogContextNonKeyReadOnlyDefaultValue(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity() - .Property(e => e.CreatedOn) - .HasDefaultValueSql("getdate()") - .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - } - } - - [ConditionalFact] - public void Insert_and_update_with_computed_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - var blog = context.Add( - new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; - - context.SaveChanges(); - - Assert.Equal("One Unicorn", blog.FullName); - } - - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - var blog = context.FullNameBlogs.Single(); - - Assert.Equal("One Unicorn", blog.FullName); - - blog.LastName = "Pegasus"; - - context.SaveChanges(); - - Assert.Equal("One Pegasus", blog.FullName); - } - } - - public class BlogContextComputedColumn : ContextBase - { - public BlogContextComputedColumn(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - var property = modelBuilder.Entity() - .Property(e => e.FullName) - .HasComputedColumnSql("FirstName + ' ' + LastName") - .Metadata; - - property.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); - } - } - - public class BlogContextComputedColumnWithTriggerMetadata : BlogContextComputedColumn - { - public BlogContextComputedColumnWithTriggerMetadata(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity().ToTable(tb => tb.HasTrigger("SomeTrigger")); - } - } - - // #6044 - [ConditionalFact] - public void Insert_and_update_with_computed_column_with_function() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) - { - context.Database.ExecuteSqlRaw - ( - @"CREATE FUNCTION -[dbo].[GetFullName](@First NVARCHAR(MAX), @Second NVARCHAR(MAX)) -RETURNS NVARCHAR(MAX) WITH SCHEMABINDING AS BEGIN RETURN @First + @Second END"); - - context.GetService().CreateTables(); - } - - using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) - { - var blog = context.Add( - new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; - - context.SaveChanges(); - - Assert.Equal("OneUnicorn", blog.FullName); - } - - using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) - { - var blog = context.FullNameBlogs.Single(); - - Assert.Equal("OneUnicorn", blog.FullName); - - blog.LastName = "Pegasus"; - - context.SaveChanges(); - - Assert.Equal("OnePegasus", blog.FullName); - } - } - - public class BlogContextComputedColumnWithFunction : ContextBase - { - public BlogContextComputedColumnWithFunction(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity() - .Property(e => e.FullName) - .HasComputedColumnSql("[dbo].[GetFullName]([FirstName], [LastName])") - .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw); - } - } - - // #6044 - [ConditionalFact] - public void Insert_and_update_with_computed_column_with_querying_function() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name)) - { - context.GetService().CreateTables(); - - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); - - context.Database.ExecuteSqlRaw( - @"CREATE FUNCTION [dbo].[GetFullName](@Id int) -RETURNS nvarchar(max) WITH SCHEMABINDING AS -BEGIN - DECLARE @FullName nvarchar(max); - SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id; - RETURN @FullName -END"); - - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); "); - } - - try - { - using (var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name)) - { - var blog = context.Add( - new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; - - context.SaveChanges(); - - Assert.Equal("OneUnicorn", blog.FullName); - } - - using (var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name)) - { - var blog = context.FullNameBlogs.Single(); - - Assert.Equal("OneUnicorn", blog.FullName); - - blog.LastName = "Pegasus"; - - context.SaveChanges(); - - Assert.Equal("OnePegasus", blog.FullName); - } - - using (var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name)) - { - var blog1 = context.Add( - new FullNameBlog { FirstName = "Hank", LastName = "Unicorn" }).Entity; - var blog2 = context.Add( - new FullNameBlog { FirstName = "Jeff", LastName = "Unicorn" }).Entity; - - context.SaveChanges(); - - Assert.Equal("HankUnicorn", blog1.FullName); - Assert.Equal("JeffUnicorn", blog2.FullName); - } - } - finally - { - using var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name); - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); - context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];"); - } - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Insert_with_computed_column_with_function_without_metadata_configuration(bool async) - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - context.GetService().CreateTables(); - - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); - - context.Database.ExecuteSqlRaw( - @"CREATE FUNCTION [dbo].[GetFullName](@Id int) -RETURNS nvarchar(max) WITH SCHEMABINDING AS -BEGIN - DECLARE @FullName nvarchar(max); - SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id; - RETURN @FullName -END"); - - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); "); - } - - try - { - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - await context.AddAsync(new FullNameBlog()); - - var exception = async - ? await Assert.ThrowsAsync(() => context.SaveChangesAsync()) - : Assert.Throws(() => context.SaveChanges()); - - Assert.Equal(SqlServerStrings.SaveChangesFailedBecauseOfComputedColumnWithFunction, exception.Message); - - var sqlException = Assert.IsType(exception.InnerException); - Assert.Equal(4186, sqlException.Number); - } - } - finally - { - using var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name); - context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); - context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];"); - } - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Insert_with_trigger_without_metadata_configuration(bool async) - { - // Execute an insert against a table which has a trigger, but which haven't identified as such in our metadata. - // This causes a specialized exception to be thrown, directing users to the relevant docs. - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - context.GetService().CreateTables(); - - context.Database.ExecuteSqlRaw( - @"CREATE OR ALTER TRIGGER [FullNameBlogs_Trigger] -ON [FullNameBlogs] -FOR INSERT, UPDATE, DELETE AS -BEGIN - IF @@ROWCOUNT = 0 - return -END"); - } - - try - { - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - await context.AddAsync(new FullNameBlog()); - - var exception = async - ? await Assert.ThrowsAsync(() => context.SaveChangesAsync()) - : Assert.Throws(() => context.SaveChanges()); - - Assert.Equal(SqlServerStrings.SaveChangesFailedBecauseOfTriggers, exception.Message); - - var sqlException = Assert.IsType(exception.InnerException); - Assert.Equal(334, sqlException.Number); - } - } - finally - { - using var context = new BlogContextComputedColumn(testStore.Name); - context.Database.ExecuteSqlRaw("DROP TRIGGER [FullNameBlogs_Trigger]"); - } - } - - [ConditionalFact] - public void Insert_with_client_generated_GUID_key() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - Guid afterSave; - using (var context = new BlogContextClientGuidKey(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - var blog = context.Add( - new GuidBlog { Name = "One Unicorn" }).Entity; - - var beforeSave = blog.Id; - var beforeSaveNotId = blog.NotId; - - Assert.NotEqual(default, beforeSave); - Assert.NotEqual(default, beforeSaveNotId); - - context.SaveChanges(); - - afterSave = blog.Id; - var afterSaveNotId = blog.NotId; - - Assert.Equal(beforeSave, afterSave); - Assert.Equal(beforeSaveNotId, afterSaveNotId); - } - - using (var context = new BlogContextClientGuidKey(testStore.Name)) - { - Assert.Equal(afterSave, context.GuidBlogs.Single().Id); - } - } - - public class BlogContextClientGuidKey : ContextBase - { - public BlogContextClientGuidKey(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity( - eb => - { - eb.HasAlternateKey(e => e.NotId); - eb.Property(e => e.NotId).ValueGeneratedOnAdd(); - }); - } - } - - [ConditionalFact] - [SqlServerCondition(SqlServerCondition.IsNotSqlAzure)] - public void Insert_with_ValueGeneratedOnAdd_GUID_nonkey_property_throws() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContextClientGuidNonKey(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - var blog = context.Add( - new GuidBlog { Name = "One Unicorn" }).Entity; - - Assert.Equal(default, blog.NotId); - - // No value set on a required column - var updateException = Assert.Throws(() => context.SaveChanges()); - Assert.Single(updateException.Entries); - } - - public class BlogContextClientGuidNonKey : ContextBase - { - public BlogContextClientGuidNonKey(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity().Property(e => e.NotId).ValueGeneratedOnAdd(); - } - } - - [ConditionalFact] - public void Insert_with_server_generated_GUID_key() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - Guid afterSave; - using (var context = new BlogContextServerGuidKey(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - var blog = context.Add( - new GuidBlog { Name = "One Unicorn" }).Entity; - - var beforeSave = blog.Id; - var beforeSaveNotId = blog.NotId; - - Assert.Equal(default, beforeSave); - Assert.Equal(default, beforeSaveNotId); - - context.SaveChanges(); - - afterSave = blog.Id; - var afterSaveNotId = blog.NotId; - - Assert.NotEqual(default, afterSave); - Assert.NotEqual(default, afterSaveNotId); - Assert.NotEqual(beforeSave, afterSave); - Assert.NotEqual(beforeSaveNotId, afterSaveNotId); - } - - using (var context = new BlogContextServerGuidKey(testStore.Name)) - { - Assert.Equal(afterSave, context.GuidBlogs.Single().Id); - } - } - - public class BlogContextServerGuidKey : ContextBase - { - public BlogContextServerGuidKey(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity( - eb => - { - eb.Property(e => e.Id) - .HasDefaultValueSql("newsequentialid()"); - eb.Property(e => e.NotId) - .HasDefaultValueSql("newsequentialid()"); - }); - } - } - - // Negative cases - [ConditionalFact] - public void Insert_with_explicit_non_default_keys_by_default() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContext(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = 2, Name = "Two Unicorns" }); - - // DbUpdateException : An error occurred while updating the entries. See the - // inner exception for details. - // SqlException : Cannot insert explicit value for identity column in table - // 'Blog' when IDENTITY_INSERT is set to OFF. - context.Database.CreateExecutionStrategy().Execute(context, c => Assert.Throws(() => c.SaveChanges())); - } - - [ConditionalFact] - public void Insert_with_explicit_default_keys() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContext(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" }); - - // DbUpdateException : An error occurred while updating the entries. See the - // inner exception for details. - // SqlException : Cannot insert explicit value for identity column in table - // 'Blog' when IDENTITY_INSERT is set to OFF. - var updateException = Assert.Throws(() => context.SaveChanges()); - Assert.Single(updateException.Entries); - } - - public class BlogContext : ContextBase - { - public BlogContext(string databaseName) - : base(databaseName) - { - } - } - - [ConditionalFact] - public void Insert_with_implicit_default_keys() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - } - } - - public class BlogContextSpecifyKeysUsingDefault : ContextBase - { - public BlogContextSpecifyKeysUsingDefault(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - } - } - - [ConditionalFact] - public void Insert_explicit_value_throws_when_readonly_sequence_before_save() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContextReadOnlySequenceKeyColumnWithDefaultValue(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); - - // The property 'Id' on entity type 'Blog' is defined to be read-only before it is - // saved, but its value has been set to something other than a temporary or default value. - Assert.Equal( - CoreStrings.PropertyReadOnlyBeforeSave("Id", "Blog"), - Assert.Throws(() => context.SaveChanges()).Message); - } - - public class BlogContextReadOnlySequenceKeyColumnWithDefaultValue : ContextBase - { - public BlogContextReadOnlySequenceKeyColumnWithDefaultValue(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.HasSequence("MySequence"); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("next value for MySequence") - .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - } - } - - [ConditionalFact] - public void Insert_explicit_value_throws_when_readonly_before_save() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) }); - - // The property 'CreatedOn' on entity type 'Blog' is defined to be read-only before it is - // saved, but its value has been set to something other than a temporary or default value. - Assert.Equal( - CoreStrings.PropertyReadOnlyBeforeSave("CreatedOn", "Blog"), - Assert.Throws(() => context.SaveChanges()).Message); - } - - [ConditionalFact] - public void Insert_explicit_value_into_computed_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContextComputedColumn(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - context.Add( - new FullNameBlog - { - FirstName = "One", - LastName = "Unicorn", - FullName = "Gerald" - }); - - // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only before it is - // saved, but its value has been set to something other than a temporary or default value. - Assert.Equal( - CoreStrings.PropertyReadOnlyBeforeSave("FullName", "FullNameBlog"), - Assert.Throws(() => context.SaveChanges()).Message); - } - - [ConditionalFact] - public void Update_explicit_value_in_computed_column() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.Add( - new FullNameBlog { FirstName = "One", LastName = "Unicorn" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextComputedColumn(testStore.Name)) - { - var blog = context.FullNameBlogs.Single(); - - blog.FullName = "The Gorilla"; - - // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only after it has been saved, - // but its value has been modified or marked as modified. - Assert.Equal( - CoreStrings.PropertyReadOnlyAfterSave("FullName", "FullNameBlog"), - Assert.Throws(() => context.SaveChanges()).Message); - } - } - - // Concurrency - [ConditionalFact] - public void Resolve_concurrency() - { - using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); - using var context = new BlogContextConcurrencyWithRowversion(testStore.Name); - context.Database.EnsureCreatedResiliently(); - - var blog = context.Add( - new ConcurrentBlog { Name = "One Unicorn" }).Entity; - - context.SaveChanges(); - - using var innerContext = new BlogContextConcurrencyWithRowversion(testStore.Name); - var updatedBlog = innerContext.ConcurrentBlogs.Single(); - updatedBlog.Name = "One Pegasus"; - innerContext.SaveChanges(); - var currentTimestamp = updatedBlog.Timestamp.ToArray(); - - try - { - blog.Name = "One Earth Pony"; - context.SaveChanges(); - } - catch (DbUpdateConcurrencyException) - { - // Update original values (and optionally any current values) - // Would normally do this with just one method call - context.Entry(blog).Property(e => e.Id).OriginalValue = updatedBlog.Id; - context.Entry(blog).Property(e => e.Name).OriginalValue = updatedBlog.Name; - context.Entry(blog).Property(e => e.Timestamp).OriginalValue = updatedBlog.Timestamp; - - context.SaveChanges(); - - Assert.NotEqual(blog.Timestamp, currentTimestamp); - } - } - - public class BlogContextConcurrencyWithRowversion : ContextBase - { - public BlogContextConcurrencyWithRowversion(string databaseName) - : base(databaseName) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity() - .Property(e => e.Timestamp) - .ValueGeneratedOnAddOrUpdate() - .IsConcurrencyToken(); - } - } - - public class Blog - { - public int Id { get; set; } - public string Name { get; set; } - public DateTime CreatedOn { get; set; } - public NeedsConverter NeedsConverter { get; set; } - public GeometryCollection GeometryCollection { get; set; } - public int? OtherId { get; set; } - } - - public class NeedsConverter - { - public NeedsConverter(int value) - { - Value = value; - } - - public int Value { get; } - - public override bool Equals(object obj) - => throw new InvalidOperationException(); - - public override int GetHashCode() - => throw new InvalidOperationException(); - } - - public class NullableKeyBlog - { - public int? Id { get; set; } - public string Name { get; set; } - public DateTime CreatedOn { get; set; } - } - - public class FullNameBlog - { - public int Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string FullName { get; set; } - } - - public class GuidBlog - { - public Guid Id { get; set; } - public string Name { get; set; } - public Guid NotId { get; set; } - } - - public class ConcurrentBlog - { - public int Id { get; set; } - public string Name { get; set; } - public byte[] Timestamp { get; set; } - } - - public abstract class ContextBase : DbContext - { - private readonly string _databaseName; - - protected ContextBase(string databaseName) - { - _databaseName = databaseName; - } - - public DbSet Blogs { get; set; } - public DbSet NullableKeyBlogs { get; set; } - public DbSet FullNameBlogs { get; set; } - public DbSet GuidBlogs { get; set; } - public DbSet ConcurrentBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity() - .Property(e => e.NeedsConverter) - .HasConversion( - v => v.Value, - v => new NeedsConverter(v), - new ValueComparer( - (l, r) => (l == null && r == null) || (l != null && r != null && l.Value == r.Value), - v => v.Value.GetHashCode(), - v => new NeedsConverter(v.Value))) - .HasDefaultValue(new NeedsConverter(999)); - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .EnableServiceProviderCaching(false) - .UseSqlServer( - SqlServerTestStore.CreateConnectionString(_databaseName), - b => b.UseNetTopologySuite().ApplyConfiguration()); - } - - public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; + protected override byte[] TimestampSentinel + => null; } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTestBase.cs new file mode 100644 index 00000000000..242dd36ba6c --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerValueGenerationScenariosTestBase.cs @@ -0,0 +1,1834 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; +using NetTopologySuite; +using NetTopologySuite.Geometries; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class SqlServerValueGenerationScenariosTestBase +{ + protected static readonly GeometryFactory GeometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + protected abstract string DatabaseName { get; } + + protected abstract Guid GuidSentinel { get; } + protected abstract int IntSentinel { get; } + protected abstract uint UIntSentinel { get; } + protected abstract IntKey IntKeySentinel { get; } + protected abstract ULongKey ULongKeySentinel { get; } + protected abstract int? NullableIntSentinel { get; } + protected abstract string StringSentinel { get; } + protected abstract DateTime DateTimeSentinel { get; } + protected abstract NeedsConverter NeedsConverterSentinel { get; } + protected abstract GeometryCollection GeometryCollectionSentinel { get; } + protected abstract byte[] TimestampSentinel { get; } + + // Positive cases + + [ConditionalFact] + public void Insert_with_Identity_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(2, blogs[1].Id); + } + } + + public class BlogContextIdentity : ContextBase + { + public BlogContextIdentity(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + } + + [ConditionalFact] + public void Insert_with_sequence_HiLo() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextHiLo(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextHiLo(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(2, blogs[0].OtherId); + Assert.Equal(3, blogs[1].Id); + Assert.Equal(4, blogs[1].OtherId); + } + } + + public class BlogContextHiLo : ContextBase + { + public BlogContextHiLo(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.UseHiLo(); + + modelBuilder.Entity( + eb => + { + eb.HasAlternateKey( + b => new { b.OtherId }); + eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); + }); + } + } + + [ConditionalFact] + public void Insert_with_key_sequence() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextKeySequence(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextKeySequence(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(1, blogs[0].OtherId); + Assert.Equal(2, blogs[1].Id); + Assert.Equal(2, blogs[1].OtherId); + } + } + + public class BlogContextKeySequence : ContextBase + { + public BlogContextKeySequence(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.UseKeySequences(); + + modelBuilder.Entity( + eb => + { + eb.HasAlternateKey( + b => new { b.OtherId }); + eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); + }); + } + } + + [ConditionalFact] + public void Insert_with_non_key_sequence() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextNonKeySequence(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextNonKeySequence(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(1, blogs[0].OtherId); + Assert.Equal(2, blogs[1].Id); + Assert.Equal(2, blogs[1].OtherId); + } + } + + public class BlogContextNonKeySequence : ContextBase + { + public BlogContextNonKeySequence(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + eb => + { + eb.Property(b => b.OtherId).UseSequence(); + eb.Property(b => b.OtherId).ValueGeneratedOnAdd(); + }); + } + } + + [ConditionalFact] + public void Insert_with_default_value_from_sequence() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + } + + using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating)) + { + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + Assert.Equal(2, blogs[2].Id); + Assert.Equal(3, blogs[3].Id); + } + } + + public class BlogContextDefaultValue : ContextBase + { + public BlogContextDefaultValue(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .HasSequence("MySequence") + .StartsAt(0); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("next value for MySequence"); + } + } + + public class BlogContextDefaultValueNoMigrations : ContextBase + { + public BlogContextDefaultValueNoMigrations(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValue(); + } + } + + [ConditionalFact] + public void Insert_with_default_string_value_from_sequence() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" }, + new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel)) + { + var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal("i77", blogs[0].Id); + Assert.Equal("i78", blogs[1].Id); + } + } + + public class BlogContextStringDefaultValue : ContextBase + { + private readonly string _stringSentinel; + + public BlogContextStringDefaultValue(string databaseName, Action modelBuilder, string stringSentinel) + : base(databaseName, modelBuilder) + { + _stringSentinel = stringSentinel; + } + + public DbSet StringyBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .HasSequence("MyStringSequence") + .StartsAt(77); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("'i' + CAST((NEXT VALUE FOR MyStringSequence) AS VARCHAR(20))") + .Metadata + .Sentinel = _stringSentinel; + } + } + + public class BlogWithStringKey + { + public string Id { get; set; } + public string Name { get; set; } + } + + [ConditionalFact] + public void Insert_with_key_default_value_from_sequence() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); + + context.SaveChanges(); + } + + using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(77, blogs[0].Id); + Assert.Equal(78, blogs[1].Id); + } + } + + public class BlogContextKeyColumnWithDefaultValue : ContextBase + { + public BlogContextKeyColumnWithDefaultValue(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .HasSequence("MySequence") + .StartsAt(77); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("next value for MySequence") + .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + } + } + + [ConditionalFact] + public void Insert_uint_to_Identity_column_using_value_converter() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithUIntKey { Id = UIntSentinel, Name = "One Unicorn" }, + new BlogWithUIntKey { Id = UIntSentinel, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel)) + { + var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal((uint)1, blogs[0].Id); + Assert.Equal((uint)2, blogs[1].Id); + } + } + + public class BlogContextUIntToIdentityUsingValueConverter : ContextBase + { + private readonly uint _uintSentinel; + + public BlogContextUIntToIdentityUsingValueConverter(string databaseName, Action modelBuilder, uint uintSentinel) + : base(databaseName, modelBuilder) + { + _uintSentinel = uintSentinel; + } + + public DbSet UnsignedBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasConversion() + .Metadata.Sentinel = _uintSentinel; + } + } + + public class BlogWithUIntKey + { + public uint Id { get; set; } + public string Name { get; set; } + } + + [ConditionalFact] + public void Insert_int_enum_to_Identity_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "One Unicorn" }, + new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel)) + { + var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, (int)blogs[0].Id); + Assert.Equal(2, (int)blogs[1].Id); + } + } + + public class BlogContextIntEnumToIdentity : ContextBase + { + private readonly IntKey _sentinel; + + public BlogContextIntEnumToIdentity(string databaseName, Action modelBuilder, IntKey sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + public DbSet EnumBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata.Sentinel = _sentinel; + } + } + + public class BlogWithIntEnumKey + { + public IntKey Id { get; set; } + public string Name { get; set; } + } + + public enum IntKey + { + Zero, + One, + SixSixSeven, + } + + [ConditionalFact] + public void Insert_ulong_enum_to_Identity_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "One Unicorn" }, + new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel)) + { + var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, (int)blogs[0].Id); + Assert.Equal(2, (int)blogs[1].Id); + } + } + + public class BlogContextULongEnumToIdentity : ContextBase + { + private readonly ULongKey _sentinel; + + public BlogContextULongEnumToIdentity(string databaseName, Action modelBuilder, ULongKey sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + public DbSet EnumBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata.Sentinel = _sentinel; + } + } + + public class BlogWithULongEnumKey + { + public ULongKey Id { get; set; } + public string Name { get; set; } + } + + public enum ULongKey : ulong + { + Zero, + Sentinel + } + + [ConditionalFact] + public void Insert_string_to_Identity_column_using_value_converter() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" }, + new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel)) + { + var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal("1", blogs[0].Id); + Assert.Equal("2", blogs[1].Id); + } + } + + public class BlogContextStringToIdentityUsingValueConverter : ContextBase + { + private readonly string _sentinel; + + public BlogContextStringToIdentityUsingValueConverter(string databaseName, Action modelBuilder, string sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + public DbSet StringyBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + Guid guid; + modelBuilder + .Entity() + .Property(e => e.Id) + .HasValueGenerator() + .HasConversion( + v => Guid.TryParse(v, out guid) + ? default + : int.Parse(v), + v => v.ToString()) + .ValueGeneratedOnAdd() + .Metadata.Sentinel = _sentinel; + } + } + + [ConditionalFact] + public void Insert_with_explicit_non_default_keys() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog { Id = 66, Name = "One Unicorn" }, new Blog { Id = 67, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(66, blogs[0].Id); + Assert.Equal(67, blogs[1].Id); + } + } + + public class BlogContextNoKeyGeneration : ContextBase + { + public BlogContextNoKeyGeneration(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + } + } + + [ConditionalFact] + public void Insert_with_explicit_with_default_keys() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, + new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel)) + { + var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + } + } + + public class BlogContextNoKeyGenerationNullableKey : ContextBase + { + private readonly int? _sentinel; + + public BlogContextNoKeyGenerationNullableKey(string databaseName, Action modelBuilder, int? sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedNever() + .Metadata.Sentinel = _sentinel; + } + } + + [ConditionalFact] + public void Insert_with_non_key_default_value() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) + { + context.Database.EnsureCreatedResiliently(); + + var blogs = new List + { + new() + { + Id = IntSentinel, + Name = "One Unicorn", + CreatedOn = DateTimeSentinel, + NeedsConverter = NeedsConverterSentinel, + GeometryCollection = GeometryCollectionSentinel + }, + new() + { + Id = IntSentinel, + Name = "Two Unicorns", + CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0), + NeedsConverter = new NeedsConverter(111), + GeometryCollection = GeometryFactory.CreateGeometryCollection( + new Geometry[] { GeometryFactory.CreatePoint(new Coordinate(1, 3)) }) + } + }; + + context.AddRange(blogs); + + context.SaveChanges(); + + Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); + Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); + Assert.Equal(111, blogs[1].NeedsConverter.Value); + + var point = ((Point)blogs[1].GeometryCollection.Geometries[0]); + Assert.Equal(1, point.X); + Assert.Equal(3, point.Y); + } + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); + Assert.Equal(3, blogs.Count); + + Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); + Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); + Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); + + var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); + Assert.Equal(1, point1.X); + Assert.Equal(3, point1.Y); + + var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); + Assert.Equal(1, point2.X); + Assert.Equal(2, point2.Y); + + blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); + + blogs[1].Name = "X Unicorns"; + blogs[1].NeedsConverter = new NeedsConverter(222); + blogs[1].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 11)); + + blogs[2].Name = "Y Unicorns"; + blogs[2].NeedsConverter = new NeedsConverter(333); + blogs[2].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 22)); + + context.SaveChanges(); + } + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) + { + var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); + Assert.Equal(3, blogs.Count); + + Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn); + Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); + Assert.Equal(222, blogs[1].NeedsConverter.Value); + Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); + Assert.Equal(333, blogs[2].NeedsConverter.Value); + + var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); + Assert.Equal(1, point1.X); + Assert.Equal(11, point1.Y); + + var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); + Assert.Equal(1, point2.X); + Assert.Equal(22, point2.Y); + } + } + + public class BlogContextNonKeyDefaultValue : ContextBase + { + public BlogContextNonKeyDefaultValue(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.CreatedOn).HasDefaultValueSql("getdate()"); + b.Property(e => e.GeometryCollection).HasDefaultValue(GeometryFactory.CreateGeometryCollection()); + + b.HasData( + new Blog + { + Id = 9979, + Name = "W Unicorns", + CreatedOn = new DateTime(1974, 8, 3, 0, 10, 0), + NeedsConverter = new NeedsConverter(111), + GeometryCollection = GeometryFactory.CreateGeometryCollection( + new Geometry[] { GeometryFactory.CreatePoint(new Coordinate(1, 2)) }) + }); + }); + } + } + + [ConditionalFact] + public void Insert_with_non_key_default_value_readonly() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog + { + Id = IntSentinel, + Name = "One Unicorn", + CreatedOn = DateTimeSentinel + }, + new Blog + { + Id = IntSentinel, + Name = "Two Unicorns", + CreatedOn = DateTimeSentinel + }); + + context.SaveChanges(); + + Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); + } + + DateTime dateTime0; + + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + dateTime0 = blogs[0].CreatedOn; + + Assert.NotEqual(new DateTime(), dateTime0); + Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); + + blogs[0].Name = "One Pegasus"; + blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); + + context.SaveChanges(); + } + + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(dateTime0, blogs[0].CreatedOn); + Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn); + } + } + + public class BlogContextNonKeyReadOnlyDefaultValue : ContextBase + { + private readonly int _intSentinel; + private readonly DateTime _dateTimeSentinel; + + public BlogContextNonKeyReadOnlyDefaultValue( + string databaseName, + Action modelBuilder, + int intSentinel, + DateTime dateTimeSentinel) + : base(databaseName, modelBuilder) + { + _intSentinel = intSentinel; + _dateTimeSentinel = dateTimeSentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = _intSentinel; + + var property = b.Property(e => e.CreatedOn).HasDefaultValueSql("getdate()"); + property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + property.Metadata.Sentinel = _dateTimeSentinel; + }); + } + } + + [ConditionalFact] + public void Insert_and_update_with_computed_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + var blog = context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "One", + LastName = "Unicorn", + FullName = StringSentinel + }).Entity; + + context.SaveChanges(); + + Assert.Equal("One Unicorn", blog.FullName); + } + + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.FullNameBlogs.Single(); + + Assert.Equal("One Unicorn", blog.FullName); + + blog.LastName = "Pegasus"; + + context.SaveChanges(); + + Assert.Equal("One Pegasus", blog.FullName); + } + } + + public class BlogContextComputedColumn : ContextBase + { + private readonly int _intSentinel; + private readonly string _stringSentinel; + + public BlogContextComputedColumn(string databaseName, Action modelBuilder, int intSentinel, string stringSentinel) + : base(databaseName, modelBuilder) + { + _intSentinel = intSentinel; + _stringSentinel = stringSentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = _intSentinel; + + var property = b.Property(e => e.FullName) + .HasComputedColumnSql("FirstName + ' ' + LastName") + .Metadata; + + property.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); + property.Sentinel = _stringSentinel; + }); + } + } + + public class BlogContextComputedColumnWithTriggerMetadata : BlogContextComputedColumn + { + public BlogContextComputedColumnWithTriggerMetadata( + string databaseName, + Action modelBuilder, + int intSentinel, + string stringSentinel) + : base(databaseName, modelBuilder, intSentinel, stringSentinel) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().ToTable(tb => tb.HasTrigger("SomeTrigger")); + } + } + + // #6044 + [ConditionalFact] + public void Insert_and_update_with_computed_column_with_function() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.Database.ExecuteSqlRaw + ( + @"CREATE FUNCTION +[dbo].[GetFullName](@First NVARCHAR(MAX), @Second NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) WITH SCHEMABINDING AS BEGIN RETURN @First + @Second END"); + + context.GetService().CreateTables(); + } + + using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "One", + LastName = "Unicorn", + FullName = StringSentinel + }).Entity; + + context.SaveChanges(); + + Assert.Equal("OneUnicorn", blog.FullName); + } + + using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.FullNameBlogs.Single(); + + Assert.Equal("OneUnicorn", blog.FullName); + + blog.LastName = "Pegasus"; + + context.SaveChanges(); + + Assert.Equal("OnePegasus", blog.FullName); + } + } + + public class BlogContextComputedColumnWithFunction : ContextBase + { + private readonly int _intSentinel; + private readonly string _stringSentinel; + + public BlogContextComputedColumnWithFunction( + string databaseName, + Action modelBuilder, + int intSentinel, + string stringSentinel) + : base(databaseName, modelBuilder) + { + _intSentinel = intSentinel; + _stringSentinel = stringSentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = _intSentinel; + + var property = modelBuilder.Entity() + .Property(e => e.FullName) + .HasComputedColumnSql("[dbo].[GetFullName]([FirstName], [LastName])") + .Metadata; + + property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); + property.Sentinel = _stringSentinel; + }); + } + } + + // #6044 + [ConditionalFact] + public void Insert_and_update_with_computed_column_with_querying_function() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumnWithTriggerMetadata(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.GetService().CreateTables(); + + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); + + context.Database.ExecuteSqlRaw( + @"CREATE FUNCTION [dbo].[GetFullName](@Id int) +RETURNS nvarchar(max) WITH SCHEMABINDING AS +BEGIN + DECLARE @FullName nvarchar(max); + SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id; + RETURN @FullName +END"); + + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); "); + } + + try + { + using (var context = new BlogContextComputedColumnWithTriggerMetadata( + testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "One", + LastName = "Unicorn", + FullName = StringSentinel + }).Entity; + + context.SaveChanges(); + + Assert.Equal("OneUnicorn", blog.FullName); + } + + using (var context = new BlogContextComputedColumnWithTriggerMetadata( + testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.FullNameBlogs.Single(); + + Assert.Equal("OneUnicorn", blog.FullName); + + blog.LastName = "Pegasus"; + + context.SaveChanges(); + + Assert.Equal("OnePegasus", blog.FullName); + } + + using (var context = new BlogContextComputedColumnWithTriggerMetadata( + testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog1 = context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "Hank", + LastName = "Unicorn", + FullName = StringSentinel + }).Entity; + + var blog2 = context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "Jeff", + LastName = "Unicorn", + FullName = StringSentinel + }).Entity; + + context.SaveChanges(); + + Assert.Equal("HankUnicorn", blog1.FullName); + Assert.Equal("JeffUnicorn", blog2.FullName); + } + } + finally + { + using var context = new BlogContextComputedColumnWithTriggerMetadata( + testStore.Name, OnModelCreating, IntSentinel, StringSentinel); + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); + context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];"); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Insert_with_computed_column_with_function_without_metadata_configuration(bool async) + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.GetService().CreateTables(); + + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); + + context.Database.ExecuteSqlRaw( + @"CREATE FUNCTION [dbo].[GetFullName](@Id int) +RETURNS nvarchar(max) WITH SCHEMABINDING AS +BEGIN + DECLARE @FullName nvarchar(max); + SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id; + RETURN @FullName +END"); + + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); "); + } + + try + { + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + await context.AddAsync(new FullNameBlog { Id = IntSentinel, FullName = StringSentinel }); + + var exception = async + ? await Assert.ThrowsAsync(() => context.SaveChangesAsync()) + : Assert.Throws(() => context.SaveChanges()); + + Assert.Equal(SqlServerStrings.SaveChangesFailedBecauseOfComputedColumnWithFunction, exception.Message); + + var sqlException = Assert.IsType(exception.InnerException); + Assert.Equal(4186, sqlException.Number); + } + } + finally + { + using var context = new BlogContextComputedColumnWithTriggerMetadata( + testStore.Name, OnModelCreating, IntSentinel, StringSentinel); + context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); + context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];"); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Insert_with_trigger_without_metadata_configuration(bool async) + { + // Execute an insert against a table which has a trigger, but which haven't identified as such in our metadata. + // This causes a specialized exception to be thrown, directing users to the relevant docs. + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.GetService().CreateTables(); + + context.Database.ExecuteSqlRaw( + @"CREATE OR ALTER TRIGGER [FullNameBlogs_Trigger] +ON [FullNameBlogs] +FOR INSERT, UPDATE, DELETE AS +BEGIN + IF @@ROWCOUNT = 0 + return +END"); + } + + try + { + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + await context.AddAsync(new FullNameBlog { Id = IntSentinel, FullName = StringSentinel }); + + var exception = async + ? await Assert.ThrowsAsync(() => context.SaveChangesAsync()) + : Assert.Throws(() => context.SaveChanges()); + + Assert.Equal(SqlServerStrings.SaveChangesFailedBecauseOfTriggers, exception.Message); + + var sqlException = Assert.IsType(exception.InnerException); + Assert.Equal(334, sqlException.Number); + } + } + finally + { + using var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel); + context.Database.ExecuteSqlRaw("DROP TRIGGER [FullNameBlogs_Trigger]"); + } + } + + [ConditionalFact] + public void Insert_with_client_generated_GUID_key() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + Guid afterSave; + using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + var blog = context.Add( + new GuidBlog + { + Id = GuidSentinel, + Name = "One Unicorn", + NotId = GuidSentinel + }).Entity; + + var beforeSave = blog.Id; + var beforeSaveNotId = blog.NotId; + + Assert.NotEqual(default, beforeSave); + Assert.NotEqual(default, beforeSaveNotId); + + context.SaveChanges(); + + afterSave = blog.Id; + var afterSaveNotId = blog.NotId; + + Assert.Equal(beforeSave, afterSave); + Assert.Equal(beforeSaveNotId, afterSaveNotId); + } + + using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) + { + Assert.Equal(afterSave, context.GuidBlogs.Single().Id); + } + } + + public class BlogContextClientGuidKey : ContextBase + { + private readonly Guid _sentinel; + + public BlogContextClientGuidKey(string databaseName, Action modelBuilder, Guid sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + eb => + { + eb.HasAlternateKey(e => e.NotId); + eb.Property(e => e.NotId).ValueGeneratedOnAdd().Metadata.Sentinel = _sentinel; + eb.Property(e => e.Id).Metadata.Sentinel = _sentinel; + }); + } + } + + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.IsNotSqlAzure)] + public void Insert_with_ValueGeneratedOnAdd_GUID_nonkey_property_throws() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContextClientGuidNonKey(testStore.Name, OnModelCreating, GuidSentinel); + context.Database.EnsureCreatedResiliently(); + + var blog = context.Add( + new GuidBlog + { + Id = GuidSentinel, + Name = "One Unicorn", + NotId = GuidSentinel + }).Entity; + + Assert.Equal(GuidSentinel, blog.NotId); + + // No value set on a required column + var updateException = Assert.Throws(() => context.SaveChanges()); + Assert.Single(updateException.Entries); + } + + public class BlogContextClientGuidNonKey : ContextBase + { + private readonly Guid _sentinel; + + public BlogContextClientGuidNonKey(string databaseName, Action modelBuilder, Guid sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = _sentinel; + b.Property(e => e.NotId).ValueGeneratedOnAdd().Metadata.Sentinel = _sentinel; + }); + } + } + + [ConditionalFact] + public void Insert_with_server_generated_GUID_key() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + Guid afterSave; + using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + var blog = context.Add( + new GuidBlog + { + Id = GuidSentinel, + Name = "One Unicorn", + NotId = GuidSentinel + }).Entity; + + var beforeSave = blog.Id; + var beforeSaveNotId = blog.NotId; + + Assert.Equal(GuidSentinel, beforeSave); + Assert.Equal(GuidSentinel, beforeSaveNotId); + + context.SaveChanges(); + + afterSave = blog.Id; + var afterSaveNotId = blog.NotId; + + Assert.NotEqual(GuidSentinel, afterSave); + Assert.NotEqual(GuidSentinel, afterSaveNotId); + Assert.NotEqual(beforeSave, afterSave); + Assert.NotEqual(beforeSaveNotId, afterSaveNotId); + } + + using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) + { + Assert.Equal(afterSave, context.GuidBlogs.Single().Id); + } + } + + public class BlogContextServerGuidKey : ContextBase + { + private readonly Guid _sentinel; + + public BlogContextServerGuidKey(string databaseName, Action modelBuilder, Guid sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity( + eb => + { + eb.Property(e => e.Id).HasDefaultValueSql("newsequentialid()").Metadata.Sentinel = _sentinel; + eb.Property(e => e.NotId).HasDefaultValueSql("newsequentialid()").Metadata.Sentinel = _sentinel; + }); + } + } + + // Negative cases + [ConditionalFact] + public void Insert_with_explicit_non_default_keys_by_default() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContext(testStore.Name, OnModelCreating); + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = 2, Name = "Two Unicorns" }); + + // DbUpdateException : An error occurred while updating the entries. See the + // inner exception for details. + // SqlException : Cannot insert explicit value for identity column in table + // 'Blog' when IDENTITY_INSERT is set to OFF. + context.Database.CreateExecutionStrategy().Execute(context, c => Assert.Throws(() => c.SaveChanges())); + } + + [ConditionalFact] + public void Insert_with_explicit_default_keys() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContext(testStore.Name, OnModelCreating); + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog { Id = IntSentinel, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" }); + + // DbUpdateException : An error occurred while updating the entries. See the + // inner exception for details. + // SqlException : Cannot insert explicit value for identity column in table + // 'Blog' when IDENTITY_INSERT is set to OFF. + var updateException = Assert.Throws(() => context.SaveChanges()); + Assert.Single(updateException.Entries); + } + + public class BlogContext : ContextBase + { + public BlogContext(string databaseName, Action modelBuilder) + : base(databaseName, modelBuilder) + { + } + } + + [ConditionalFact] + public void Insert_with_implicit_default_keys() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 667, Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(667, blogs[1].Id); + } + } + + public class BlogContextSpecifyKeysUsingDefault : ContextBase + { + private readonly int _sentinel; + + public BlogContextSpecifyKeysUsingDefault(string databaseName, Action modelBuilder, int sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedNever() + .Metadata.Sentinel = _sentinel; + } + } + + [ConditionalFact] + public void Insert_explicit_value_throws_when_readonly_sequence_before_save() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContextReadOnlySequenceKeyColumnWithDefaultValue(testStore.Name, OnModelCreating, IntSentinel); + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = IntSentinel, Name = "Two Unicorns" }); + + // The property 'Id' on entity type 'Blog' is defined to be read-only before it is + // saved, but its value has been set to something other than a temporary or default value. + Assert.Equal( + CoreStrings.PropertyReadOnlyBeforeSave("Id", "Blog"), + Assert.Throws(() => context.SaveChanges()).Message); + } + + public class BlogContextReadOnlySequenceKeyColumnWithDefaultValue : ContextBase + { + private readonly int _sentinel; + + public BlogContextReadOnlySequenceKeyColumnWithDefaultValue(string databaseName, Action modelBuilder, int sentinel) + : base(databaseName, modelBuilder) + { + _sentinel = sentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasSequence("MySequence"); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("next value for MySequence"); + + property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + property.Metadata.Sentinel = _sentinel; + } + } + + [ConditionalFact] + public void Insert_explicit_value_throws_when_readonly_before_save() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel); + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new Blog + { + Id = IntSentinel, + Name = "One Unicorn", + CreatedOn = DateTimeSentinel + }, + new Blog + { + Id = IntSentinel, + Name = "Two Unicorns", + CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) + }); + + // The property 'CreatedOn' on entity type 'Blog' is defined to be read-only before it is + // saved, but its value has been set to something other than a temporary or default value. + Assert.Equal( + CoreStrings.PropertyReadOnlyBeforeSave("CreatedOn", "Blog"), + Assert.Throws(() => context.SaveChanges()).Message); + } + + [ConditionalFact] + public void Insert_explicit_value_into_computed_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel); + context.Database.EnsureCreatedResiliently(); + + context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "One", + LastName = "Unicorn", + FullName = "Gerald" + }); + + // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only before it is + // saved, but its value has been set to something other than a temporary or default value. + Assert.Equal( + CoreStrings.PropertyReadOnlyBeforeSave("FullName", "FullNameBlog"), + Assert.Throws(() => context.SaveChanges()).Message); + } + + [ConditionalFact] + public void Update_explicit_value_in_computed_column() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + context.Database.EnsureCreatedResiliently(); + + context.Add( + new FullNameBlog + { + Id = IntSentinel, + FirstName = "One", + LastName = "Unicorn", + FullName = StringSentinel + }); + + context.SaveChanges(); + } + + using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) + { + var blog = context.FullNameBlogs.Single(); + + blog.FullName = "The Gorilla"; + + // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only after it has been saved, + // but its value has been modified or marked as modified. + Assert.Equal( + CoreStrings.PropertyReadOnlyAfterSave("FullName", "FullNameBlog"), + Assert.Throws(() => context.SaveChanges()).Message); + } + } + + // Concurrency + [ConditionalFact] + public void Resolve_concurrency() + { + using var testStore = SqlServerTestStore.CreateInitialized(DatabaseName); + using var context = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel); + context.Database.EnsureCreatedResiliently(); + + var blog = context.Add( + new ConcurrentBlog + { + Id = IntSentinel, + Name = "One Unicorn", + Timestamp = TimestampSentinel + }).Entity; + + context.SaveChanges(); + + using var innerContext = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel); + var updatedBlog = innerContext.ConcurrentBlogs.Single(); + updatedBlog.Name = "One Pegasus"; + innerContext.SaveChanges(); + var currentTimestamp = updatedBlog.Timestamp.ToArray(); + + try + { + blog.Name = "One Earth Pony"; + context.SaveChanges(); + } + catch (DbUpdateConcurrencyException) + { + // Update original values (and optionally any current values) + // Would normally do this with just one method call + context.Entry(blog).Property(e => e.Id).OriginalValue = updatedBlog.Id; + context.Entry(blog).Property(e => e.Name).OriginalValue = updatedBlog.Name; + context.Entry(blog).Property(e => e.Timestamp).OriginalValue = updatedBlog.Timestamp; + + context.SaveChanges(); + + Assert.NotEqual(blog.Timestamp, currentTimestamp); + } + } + + public class BlogContextConcurrencyWithRowversion : ContextBase + { + private readonly int _intSentinel; + private readonly byte[] _timestampSentinel; + + public BlogContextConcurrencyWithRowversion( + string databaseName, + Action modelBuilder, + int intSentinel, + byte[] timestampSentinel) + : base(databaseName, modelBuilder) + { + _intSentinel = intSentinel; + _timestampSentinel = timestampSentinel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = _intSentinel; + b.Property(e => e.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken() + .Metadata.Sentinel = _timestampSentinel; + }); + } + } + + protected Blog CreateBlog(string name) + => new() + { + Id = IntSentinel, + Name = name, + CreatedOn = DateTimeSentinel, + GeometryCollection = GeometryCollectionSentinel, + NeedsConverter = NeedsConverterSentinel, + OtherId = NullableIntSentinel + }; + + public class Blog + { + public int Id { get; set; } + public string Name { get; set; } + public DateTime CreatedOn { get; set; } + public NeedsConverter NeedsConverter { get; set; } + public GeometryCollection GeometryCollection { get; set; } + public int? OtherId { get; set; } + } + + public class NeedsConverter + { + public NeedsConverter(int value) + { + Value = value; + } + + public int Value { get; } + + public override bool Equals(object obj) + => throw new InvalidOperationException(); + + public override int GetHashCode() + => throw new InvalidOperationException(); + } + + public class NullableKeyBlog + { + public int? Id { get; set; } + public string Name { get; set; } + public DateTime CreatedOn { get; set; } + } + + public class FullNameBlog + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string FullName { get; set; } + } + + public class GuidBlog + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid NotId { get; set; } + } + + public class ConcurrentBlog + { + public int Id { get; set; } + public string Name { get; set; } + public byte[] Timestamp { get; set; } + } + + protected virtual void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .Property(e => e.NeedsConverter) + .HasConversion( + v => v.Value, + v => new NeedsConverter(v), + new ValueComparer( + (l, r) => (l == null && r == null) || (l != null && r != null && l.Value == r.Value), + v => v.Value.GetHashCode(), + v => new NeedsConverter(v.Value))) + .HasDefaultValue(new NeedsConverter(999)); + + public abstract class ContextBase : DbContext + { + private readonly string _databaseName; + private readonly Action _modelBuilder; + + protected ContextBase(string databaseName, Action modelBuilder) + { + _databaseName = databaseName; + _modelBuilder = modelBuilder; + } + + public DbSet Blogs { get; set; } + public DbSet NullableKeyBlogs { get; set; } + public DbSet FullNameBlogs { get; set; } + public DbSet GuidBlogs { get; set; } + public DbSet ConcurrentBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => _modelBuilder(modelBuilder); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .EnableServiceProviderCaching(false) + .UseSqlServer( + SqlServerTestStore.CreateConnectionString(_databaseName), + b => b.UseNetTopologySuite().ApplyConfiguration()); + } + + public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; +} diff --git a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSentinelSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSentinelSqlServerTest.cs new file mode 100644 index 00000000000..3070c8c5837 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSentinelSqlServerTest.cs @@ -0,0 +1,324 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.EntityFrameworkCore; + +public class StoreGeneratedSentinelSqlServerTest : StoreGeneratedSqlServerTestBase< + StoreGeneratedSentinelSqlServerTest.StoreGeneratedSentinelSqlServerFixture> +{ + public StoreGeneratedSentinelSqlServerTest(StoreGeneratedSentinelSqlServerFixture fixture) + : base(fixture) + { + } + + public class StoreGeneratedSentinelSqlServerFixture : StoreGeneratedSqlServerFixtureBase + { + public override Guid GuidSentinel { get; } = new("91B22A0D-99F4-4AD4-930F-6590AFD30FDD"); + + public override int IntSentinel + => -1; + + public override short ShortSentinel + => -1; + + public override long LongSentinel + => -1; + + public override int? NullableIntSentinel + => -1; + + public override bool BoolSentinel + => true; + + public override bool? NullableBoolSentinel + => true; + + public override string? StringSentinel + => "Sentinel"; + + public override Uri? UriSentinel { get; } = new(@"http://localhost/"); + + public override KeyEnum KeyEnumSentinel + => (KeyEnum)(-1); + + public override string? StringAsGuidSentinel + => "91B22A0D-99F4-4AD4-930F-6590AFD30FDD"; + + public override Guid GuidAsStringSentinel { get; } = new("91B22A0D-99F4-4AD4-930F-6590AFD30FDD"); + public override WrappedIntKeyClass? WrappedIntKeyClassSentinel { get; } = new() { Value = -1 }; + public override WrappedIntClass? WrappedIntClassSentinel { get; } = new() { Value = -1 }; + public override WrappedIntKeyStruct WrappedIntKeyStructSentinel { get; } = new() { Value = -1 }; + public override WrappedIntStruct WrappedIntStructSentinel { get; } = new() { Value = -1 }; + public override WrappedIntKeyRecord? WrappedIntKeyRecordSentinel { get; } = new() { Value = -1 }; + public override WrappedIntRecord? WrappedIntRecordSentinel { get; } = new() { Value = -1 }; + public override WrappedStringKeyClass? WrappedStringKeyClassSentinel { get; } = new() { Value = "Sentinel" }; + public override WrappedStringClass? WrappedStringClassSentinel { get; } = new() { Value = "Sentinel" }; + public override WrappedStringKeyStruct WrappedStringKeyStructSentinel { get; } = new() { Value = "Sentinel" }; + public override WrappedStringStruct WrappedStringStructSentinel { get; } = new() { Value = "Sentinel" }; + public override WrappedStringKeyRecord? WrappedStringKeyRecordSentinel { get; } = new() { Value = "Sentinel" }; + public override WrappedStringRecord? WrappedStringRecordSentinel { get; } = new() { Value = "Sentinel" }; + + public override WrappedGuidKeyClass? WrappedGuidKeyClassSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedGuidClass? WrappedGuidClassSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedGuidKeyStruct WrappedGuidKeyStructSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedGuidStruct WrappedGuidStructSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedGuidKeyRecord? WrappedGuidKeyRecordSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedGuidRecord? WrappedGuidRecordSentinel { get; } = + new() { Value = new Guid("2567F02C-CBA8-4105-9000-387D12B505FF") }; + + public override WrappedUriKeyClass? WrappedUriKeyClassSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + public override WrappedUriClass? WrappedUriClassSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + public override WrappedUriKeyStruct WrappedUriKeyStructSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + public override WrappedUriStruct WrappedUriStructSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + public override WrappedUriKeyRecord? WrappedUriKeyRecordSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + public override WrappedUriRecord? WrappedUriRecordSentinel { get; } = new() { Value = new Uri(@"http://localhost/") }; + + public override long LongToDecimalPrincipalSentinel + => -1; + + public override WrappedIntHiLoKeyClass? WrappedIntHiLoKeyClassSentinel { get; } = new() { Value = -1 }; + public override WrappedIntHiLoKeyStruct WrappedIntHiLoKeyStructSentinel { get; } = new() { Value = -1 }; + public override WrappedIntHiLoKeyRecord? WrappedIntHiLoKeyRecordSentinel { get; } = new() { Value = -1 }; + + protected override string StoreName + => "StoreGeneratedTestSentinel"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = IntSentinel; + b.Property(e => e.NotStoreGenerated).Metadata.Sentinel = StringSentinel; + b.Property(e => e.Identity).Metadata.Sentinel = StringSentinel; + b.Property(e => e.IdentityReadOnlyBeforeSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.IdentityReadOnlyAfterSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysIdentity).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysIdentityReadOnlyBeforeSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysIdentityReadOnlyAfterSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.Computed).Metadata.Sentinel = StringSentinel; + b.Property(e => e.ComputedReadOnlyBeforeSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.ComputedReadOnlyAfterSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysComputed).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysComputedReadOnlyBeforeSave).Metadata.Sentinel = StringSentinel; + b.Property(e => e.AlwaysComputedReadOnlyAfterSave).Metadata.Sentinel = StringSentinel; + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = IntSentinel; + b.Property(e => e.Never).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverUseBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverIgnoreBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverThrowBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverUseBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverIgnoreBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverThrowBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverUseBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverIgnoreBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.NeverThrowBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + + b.Property(e => e.OnAdd).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddUseBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddIgnoreBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddThrowBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddUseBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddIgnoreBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddThrowBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddUseBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddIgnoreBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddThrowBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + + b.Property(e => e.OnAddOrUpdate).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateUseBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateIgnoreBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateThrowBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateUseBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateIgnoreBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateThrowBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateUseBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateIgnoreBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnAddOrUpdateThrowBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + + b.Property(e => e.OnUpdate).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateUseBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateIgnoreBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateThrowBeforeUseAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateUseBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateIgnoreBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateThrowBeforeIgnoreAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateUseBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateIgnoreBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + b.Property(e => e.OnUpdateThrowBeforeThrowAfter).Metadata.Sentinel = StringSentinel; + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NullableAsNonNullable).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NonNullableAsNullable).Metadata.Sentinel = IntSentinel; + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NullableBackedBoolTrueDefault).Metadata.Sentinel = NullableBoolSentinel; + b.Property(e => e.NullableBackedIntNonZeroDefault).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NullableBackedBoolFalseDefault).Metadata.Sentinel = NullableBoolSentinel; + b.Property(e => e.NullableBackedIntZeroDefault).Metadata.Sentinel = NullableIntSentinel; + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NullableBackedBoolTrueDefault).Metadata.Sentinel = NullableBoolSentinel; + b.Property(e => e.NullableBackedIntNonZeroDefault).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.NullableBackedBoolFalseDefault).Metadata.Sentinel = NullableBoolSentinel; + b.Property(e => e.NullableBackedIntZeroDefault).Metadata.Sentinel = NullableIntSentinel; + }); + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + modelBuilder.Entity().Property(e => e.PrincipalId).Metadata.Sentinel = IntSentinel; + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = WrappedIntHiLoKeyClassSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = WrappedIntHiLoKeyStructSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = WrappedIntHiLoKeyRecordSentinel; + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = GuidSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = GuidSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = ShortSentinel; + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = NullableIntSentinel; + b.Property(e => e.Name).Metadata.Sentinel = StringSentinel; + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = IntSentinel; + b.Property(e => e.Name).Metadata.Sentinel = StringSentinel; + }); + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedIntKeyClassSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedIntClassSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedIntKeyStructSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedIntStructSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedIntKeyRecordSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedIntRecordSentinel; + }); + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = LongSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = LongToDecimalPrincipalSentinel; + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedGuidKeyClassSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedGuidClassSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedGuidKeyStructSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedGuidStructSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedGuidKeyRecordSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedGuidRecordSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedStringKeyClassSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedStringClassSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedStringKeyStructSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedStringStructSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedStringKeyRecordSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedStringRecordSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedUriKeyClassSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedUriClassSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedUriKeyStructSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedUriStructSentinel; + }); + + modelBuilder.Entity( + entity => + { + entity.Property(e => e.Id).Metadata.Sentinel = WrappedUriKeyRecordSentinel; + entity.Property(e => e.NonKey).Metadata.Sentinel = WrappedUriRecordSentinel; + }); + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = UriSentinel; + ; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = KeyEnumSentinel; + ; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = GuidAsStringSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = StringAsGuidSentinel; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs index 356aeec7832..6b023627942 100644 --- a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs @@ -1,939 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +#nullable enable -// ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; -#nullable enable - -public class StoreGeneratedSqlServerTest : StoreGeneratedTestBase +public class StoreGeneratedSqlServerTest : StoreGeneratedSqlServerTestBase { public StoreGeneratedSqlServerTest(StoreGeneratedSqlServerFixture fixture) : base(fixture) { } - protected class WrappedIntHiLoClass - { - public int Value { get; set; } - } - - protected class WrappedIntHiLoClassConverter : ValueConverter - { - public WrappedIntHiLoClassConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoClass { Value = v }) - { - } - } - - protected class WrappedIntHiLoClassComparer : ValueComparer - { - public WrappedIntHiLoClassComparer() - : base( - (v1, v2) => (v1 == null && v2 == null) || (v1 != null && v2 != null && v1.Value.Equals(v2.Value)), - v => v != null ? v.Value : 0, - v => v == null ? null : new WrappedIntHiLoClass { Value = v.Value }) - { - } - } - - protected class WrappedIntHiLoClassValueGenerator : ValueGenerator - { - public override WrappedIntHiLoClass Next(EntityEntry entry) - => new() { Value = 66 }; - - public override bool GeneratesTemporaryValues - => false; - } - - protected struct WrappedIntHiLoStruct - { - public int Value { get; set; } - } - - protected class WrappedIntHiLoStructConverter : ValueConverter - { - public WrappedIntHiLoStructConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoStruct { Value = v }) - { - } - } - - protected class WrappedIntHiLoStructValueGenerator : ValueGenerator - { - public override WrappedIntHiLoStruct Next(EntityEntry entry) - => new() { Value = 66 }; - - public override bool GeneratesTemporaryValues - => false; - } - - protected record WrappedIntHiLoRecord - { - public int Value { get; set; } - } - - protected class WrappedIntHiLoRecordConverter : ValueConverter - { - public WrappedIntHiLoRecordConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoRecord { Value = v }) - { - } - } - - protected class WrappedIntHiLoRecordValueGenerator : ValueGenerator - { - public override WrappedIntHiLoRecord Next(EntityEntry entry) - => new() { Value = 66 }; - - public override bool GeneratesTemporaryValues - => false; - } - - protected class WrappedIntHiLoKeyClass - { - public int Value { get; set; } - } - - protected class WrappedIntHiLoKeyClassConverter : ValueConverter - { - public WrappedIntHiLoKeyClassConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoKeyClass { Value = v }) - { - } - } - - protected class WrappedIntHiLoKeyClassComparer : ValueComparer - { - public WrappedIntHiLoKeyClassComparer() - : base( - (v1, v2) => (v1 == null && v2 == null) || (v1 != null && v2 != null && v1.Value.Equals(v2.Value)), - v => v != null ? v.Value : 0, - v => v == null ? null : new WrappedIntHiLoKeyClass { Value = v.Value }) - { - } - } - - protected struct WrappedIntHiLoKeyStruct - { - public int Value { get; set; } - - public override bool Equals(object? obj) - => obj is WrappedIntHiLoKeyStruct other && Value == other.Value; - - public override int GetHashCode() - => Value; - - public static bool operator ==(WrappedIntHiLoKeyStruct left, WrappedIntHiLoKeyStruct right) - => left.Equals(right); - - public static bool operator !=(WrappedIntHiLoKeyStruct left, WrappedIntHiLoKeyStruct right) - => !left.Equals(right); - } - - protected class WrappedIntHiLoKeyStructConverter : ValueConverter - { - public WrappedIntHiLoKeyStructConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoKeyStruct { Value = v }) - { - } - } - - protected record WrappedIntHiLoKeyRecord - { - public int Value { get; set; } - } - - protected class WrappedIntHiLoKeyRecordConverter : ValueConverter - { - public WrappedIntHiLoKeyRecordConverter() - : base( - v => v.Value, - v => new WrappedIntHiLoKeyRecord { Value = v }) - { - } - } - - protected class WrappedIntHiLoClassPrincipal - { - public WrappedIntHiLoKeyClass Id { get; set; } = null!; - public ICollection Dependents { get; } = new List(); - - public ICollection RequiredDependents { get; } = - new List(); - - public ICollection OptionalDependents { get; } = - new List(); - } - - protected class WrappedIntHiLoClassDependentShadow - { - public WrappedIntHiLoClass Id { get; set; } = null!; - public WrappedIntHiLoClassPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoClassDependentRequired - { - public WrappedIntHiLoClass Id { get; set; } = null!; - public WrappedIntHiLoKeyClass PrincipalId { get; set; } = null!; - public WrappedIntHiLoClassPrincipal Principal { get; set; } = null!; - } - - protected class WrappedIntHiLoClassDependentOptional - { - public WrappedIntHiLoClass Id { get; set; } = null!; - public WrappedIntHiLoKeyClass? PrincipalId { get; set; } - public WrappedIntHiLoClassPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoStructPrincipal - { - public WrappedIntHiLoKeyStruct Id { get; set; } - public ICollection Dependents { get; } = new List(); - - public ICollection OptionalDependents { get; } = - new List(); - - public ICollection RequiredDependents { get; } = - new List(); - } - - protected class WrappedIntHiLoStructDependentShadow + public class StoreGeneratedSqlServerFixture : StoreGeneratedSqlServerFixtureBase { - public WrappedIntHiLoStruct Id { get; set; } - public WrappedIntHiLoStructPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoStructDependentOptional - { - public WrappedIntHiLoStruct Id { get; set; } - public WrappedIntHiLoKeyStruct? PrincipalId { get; set; } - public WrappedIntHiLoStructPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoStructDependentRequired - { - public WrappedIntHiLoStruct Id { get; set; } - public WrappedIntHiLoKeyStruct PrincipalId { get; set; } - public WrappedIntHiLoStructPrincipal Principal { get; set; } = null!; - } - - protected class WrappedIntHiLoRecordPrincipal - { - public WrappedIntHiLoKeyRecord Id { get; set; } = null!; - public ICollection Dependents { get; } = new List(); - - public ICollection OptionalDependents { get; } = - new List(); - - public ICollection RequiredDependents { get; } = - new List(); - } - - protected class WrappedIntHiLoRecordDependentShadow - { - public WrappedIntHiLoRecord Id { get; set; } = null!; - public WrappedIntHiLoRecordPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoRecordDependentOptional - { - public WrappedIntHiLoRecord Id { get; set; } = null!; - public WrappedIntHiLoKeyRecord? PrincipalId { get; set; } - public WrappedIntHiLoRecordPrincipal? Principal { get; set; } - } - - protected class WrappedIntHiLoRecordDependentRequired - { - public WrappedIntHiLoRecord Id { get; set; } = null!; - public WrappedIntHiLoKeyRecord PrincipalId { get; set; } = null!; - public WrappedIntHiLoRecordPrincipal Principal { get; set; } = null!; - } - - protected class LongToDecimalPrincipal - { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - public ICollection Dependents { get; } = new List(); - public ICollection RequiredDependents { get; } = new List(); - public ICollection OptionalDependents { get; } = new List(); - } - - protected class LongToDecimalDependentShadow - { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - public LongToDecimalPrincipal? Principal { get; set; } - } - - protected class LongToDecimalDependentRequired - { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - public long PrincipalId { get; set; } - public LongToDecimalPrincipal Principal { get; set; } = null!; - } - - protected class LongToDecimalDependentOptional - { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - public long? PrincipalId { get; set; } - public LongToDecimalPrincipal? Principal { get; set; } - } - - [ConditionalFact] - public virtual void Insert_update_and_delete_with_long_to_decimal_conversion() - { - var id1 = 0L; - ExecuteWithStrategyInTransaction( - context => - { - var principal1 = context.Add( - new LongToDecimalPrincipal - { - Dependents = { new LongToDecimalDependentShadow(), new LongToDecimalDependentShadow() }, - OptionalDependents = { new LongToDecimalDependentOptional(), new LongToDecimalDependentOptional() }, - RequiredDependents = { new LongToDecimalDependentRequired(), new LongToDecimalDependentRequired() } - }).Entity; - - context.SaveChanges(); - - id1 = principal1.Id; - Assert.NotEqual(0L, id1); - foreach (var dependent in principal1.Dependents) - { - Assert.NotEqual(0L, dependent.Id); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal1.OptionalDependents) - { - Assert.NotEqual(0L, dependent.Id); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId); - } - - foreach (var dependent in principal1.RequiredDependents) - { - Assert.NotEqual(0L, dependent.Id); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId); - } - }, - context => - { - var principal1 = context.Set() - .Include(e => e.Dependents) - .Include(e => e.OptionalDependents) - .Include(e => e.RequiredDependents) - .Single(); - - Assert.Equal(principal1.Id, id1); - foreach (var dependent in principal1.Dependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal1.OptionalDependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId!.Value); - } - - foreach (var dependent in principal1.RequiredDependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId); - } - - principal1.Dependents.Remove(principal1.Dependents.First()); - principal1.OptionalDependents.Remove(principal1.OptionalDependents.First()); - principal1.RequiredDependents.Remove(principal1.RequiredDependents.First()); - - context.SaveChanges(); - }, - context => - { - var dependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, dependents1.Count); - Assert.Null( - context.Entry(dependents1.Single(e => e.Principal == null)) - .Property("PrincipalId").CurrentValue); - - var optionalDependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, optionalDependents1.Count); - Assert.Null(optionalDependents1.Single(e => e.Principal == null).PrincipalId); - - var requiredDependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Single(requiredDependents1); - - context.Remove(dependents1.Single(e => e.Principal != null)); - context.Remove(optionalDependents1.Single(e => e.Principal != null)); - context.Remove(requiredDependents1.Single()); - context.Remove(requiredDependents1.Single().Principal); - - context.SaveChanges(); - }, - context => - { - Assert.Equal(1, context.Set().Count()); - Assert.Equal(1, context.Set().Count()); - Assert.Equal(0, context.Set().Count()); - }); - } - - [ConditionalFact] - public virtual void Insert_update_and_delete_with_wrapped_int_key_using_hi_lo() - { - var id1 = 0; - var id2 = 0; - var id3 = 0; - ExecuteWithStrategyInTransaction( - context => - { - var principal1 = context.Add( - new WrappedIntHiLoClassPrincipal - { - Dependents = { new WrappedIntHiLoClassDependentShadow(), new WrappedIntHiLoClassDependentShadow() }, - OptionalDependents = { new WrappedIntHiLoClassDependentOptional(), new WrappedIntHiLoClassDependentOptional() }, - RequiredDependents = { new WrappedIntHiLoClassDependentRequired(), new WrappedIntHiLoClassDependentRequired() } - }).Entity; - - var principal2 = context.Add( - new WrappedIntHiLoStructPrincipal - { - Dependents = { new WrappedIntHiLoStructDependentShadow(), new WrappedIntHiLoStructDependentShadow() }, - OptionalDependents = - { - new WrappedIntHiLoStructDependentOptional(), new WrappedIntHiLoStructDependentOptional() - }, - RequiredDependents = - { - new WrappedIntHiLoStructDependentRequired(), new WrappedIntHiLoStructDependentRequired() - } - }).Entity; - - var principal3 = context.Add( - new WrappedIntHiLoRecordPrincipal - { - Dependents = { new WrappedIntHiLoRecordDependentShadow(), new WrappedIntHiLoRecordDependentShadow() }, - OptionalDependents = - { - new WrappedIntHiLoRecordDependentOptional(), new WrappedIntHiLoRecordDependentOptional() - }, - RequiredDependents = - { - new WrappedIntHiLoRecordDependentRequired(), new WrappedIntHiLoRecordDependentRequired() - } - }).Entity; - - context.SaveChanges(); - - id1 = principal1.Id.Value; - Assert.NotEqual(0, id1); - foreach (var dependent in principal1.Dependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal1.OptionalDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId!.Value); - } - - foreach (var dependent in principal1.RequiredDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId.Value); - } - - id2 = principal2.Id.Value; - Assert.NotEqual(0, id2); - foreach (var dependent in principal2.Dependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value.Value); - } - - foreach (var dependent in principal2.OptionalDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, dependent.PrincipalId!.Value.Value); - } - - foreach (var dependent in principal2.RequiredDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, dependent.PrincipalId.Value); - } - - id3 = principal3.Id.Value; - Assert.NotEqual(0, id3); - foreach (var dependent in principal3.Dependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal3.OptionalDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, dependent.PrincipalId!.Value); - } - - foreach (var dependent in principal3.RequiredDependents) - { - Assert.NotEqual(0, dependent.Id.Value); - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, dependent.PrincipalId.Value); - } - }, - context => - { - var principal1 = context.Set() - .Include(e => e.Dependents) - .Include(e => e.OptionalDependents) - .Include(e => e.RequiredDependents) - .Single(); - - Assert.Equal(principal1.Id.Value, id1); - foreach (var dependent in principal1.Dependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal1.OptionalDependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId!.Value); - } - - foreach (var dependent in principal1.RequiredDependents) - { - Assert.Same(principal1, dependent.Principal); - Assert.Equal(id1, dependent.PrincipalId.Value); - } - - var principal2 = context.Set() - .Include(e => e.Dependents) - .Include(e => e.OptionalDependents) - .Include(e => e.RequiredDependents) - .Single(); - - Assert.Equal(principal2.Id.Value, id2); - foreach (var dependent in principal2.Dependents) - { - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value.Value); - } - - foreach (var dependent in principal2.OptionalDependents) - { - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, dependent.PrincipalId!.Value.Value); - } - - foreach (var dependent in principal2.RequiredDependents) - { - Assert.Same(principal2, dependent.Principal); - Assert.Equal(id2, dependent.PrincipalId.Value); - } - - var principal3 = context.Set() - .Include(e => e.Dependents) - .Include(e => e.OptionalDependents) - .Include(e => e.RequiredDependents) - .Single(); - - Assert.Equal(principal3.Id.Value, id3); - foreach (var dependent in principal3.Dependents) - { - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); - } - - foreach (var dependent in principal3.OptionalDependents) - { - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, dependent.PrincipalId!.Value); - } - - foreach (var dependent in principal3.RequiredDependents) - { - Assert.Same(principal3, dependent.Principal); - Assert.Equal(id3, dependent.PrincipalId.Value); - } - - principal1.Dependents.Remove(principal1.Dependents.First()); - principal2.Dependents.Remove(principal2.Dependents.First()); - principal3.Dependents.Remove(principal3.Dependents.First()); - - principal1.OptionalDependents.Remove(principal1.OptionalDependents.First()); - principal2.OptionalDependents.Remove(principal2.OptionalDependents.First()); - principal3.OptionalDependents.Remove(principal3.OptionalDependents.First()); - - principal1.RequiredDependents.Remove(principal1.RequiredDependents.First()); - principal2.RequiredDependents.Remove(principal2.RequiredDependents.First()); - principal3.RequiredDependents.Remove(principal3.RequiredDependents.First()); - - context.SaveChanges(); - }, - context => - { - var dependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, dependents1.Count); - Assert.Null( - context.Entry(dependents1.Single(e => e.Principal == null)) - .Property("PrincipalId").CurrentValue); - - var optionalDependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, optionalDependents1.Count); - Assert.Null(optionalDependents1.Single(e => e.Principal == null).PrincipalId); - - var requiredDependents1 = context.Set().Include(e => e.Principal).ToList(); - Assert.Single(requiredDependents1); - - var dependents2 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, dependents2.Count); - Assert.Null( - context.Entry(dependents2.Single(e => e.Principal == null)) - .Property("PrincipalId").CurrentValue); - - var optionalDependents2 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, optionalDependents2.Count); - Assert.Null(optionalDependents2.Single(e => e.Principal == null).PrincipalId); - - var requiredDependents2 = context.Set().Include(e => e.Principal).ToList(); - Assert.Single(requiredDependents2); - - var dependents3 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, dependents3.Count); - Assert.Null( - context.Entry(dependents3.Single(e => e.Principal == null)) - .Property("PrincipalId").CurrentValue); - - var optionalDependents3 = context.Set().Include(e => e.Principal).ToList(); - Assert.Equal(2, optionalDependents3.Count); - Assert.Null(optionalDependents3.Single(e => e.Principal == null).PrincipalId); - - var requiredDependents3 = context.Set().Include(e => e.Principal).ToList(); - Assert.Single(requiredDependents3); - - context.Remove(dependents1.Single(e => e.Principal != null)); - context.Remove(optionalDependents1.Single(e => e.Principal != null)); - context.Remove(requiredDependents1.Single()); - context.Remove(requiredDependents1.Single().Principal); - - context.Remove(dependents2.Single(e => e.Principal != null)); - context.Remove(optionalDependents2.Single(e => e.Principal != null)); - context.Remove(requiredDependents2.Single()); - context.Remove(requiredDependents2.Single().Principal); - - context.Remove(dependents3.Single(e => e.Principal != null)); - context.Remove(optionalDependents3.Single(e => e.Principal != null)); - context.Remove(requiredDependents3.Single()); - context.Remove(requiredDependents3.Single().Principal); - - context.SaveChanges(); - }, - context => - { - Assert.Equal(1, context.Set().Count()); - Assert.Equal(1, context.Set().Count()); - Assert.Equal(1, context.Set().Count()); - - Assert.Equal(1, context.Set().Count()); - Assert.Equal(1, context.Set().Count()); - Assert.Equal(1, context.Set().Count()); - - Assert.Equal(0, context.Set().Count()); - Assert.Equal(0, context.Set().Count()); - Assert.Equal(0, context.Set().Count()); - }); - } - - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - [ConditionalFact] - public virtual void Exception_in_SaveChanges_causes_store_values_to_be_reverted() - { - var entities = new List(); - for (var i = 0; i < 100; i++) - { - entities.Add( - new Darwin - { - Species = new Species { Name = "Goldfish (with legs)" }, - MixedMetaphors = new List - { - new() { Name = "Large ground finch" }, - new() { Name = "Medium ground finch" }, - new() { Name = "Small tree finch" }, - new() { Name = "Green warbler-finch" } - } - }); - } - - entities.Add( - new Darwin - { - Id = 1777, - Species = new Species { Name = "Goldfish (with legs)" }, - MixedMetaphors = new List - { - new() { Name = "Large ground finch" }, - new() { Name = "Medium ground finch" }, - new() { Name = "Small tree finch" }, - new() { Name = "Green warbler-finch" } - } - }); - - for (var i = 0; i < 2; i++) - { - ExecuteWithStrategyInTransaction( - context => - { - context.AddRange(entities); - - foreach (var entity in entities.Take(100)) - { - Assert.Equal(0, entity.Id); - Assert.Null(entity._id); - } - - Assert.Equal(1777, entities[100].Id); - - var tempValueIdentityMap = entities.ToDictionary( - e => context.Entry(e).Property(p => p.Id).CurrentValue, - e => e); - - var stateManager = context.GetService(); - var key = context.Model.FindEntityType(typeof(Darwin))!.FindPrimaryKey()!; - - foreach (var entity in entities) - { - Assert.Same( - entity, - stateManager.TryGetEntryTyped( - key, - context.Entry(entity).Property(p => p.Id).CurrentValue)!.Entity); - } - - // DbUpdateException : An error occurred while updating the entries. See the - // inner exception for details. - // SqlException : Cannot insert explicit value for identity column in table - // 'Blog' when IDENTITY_INSERT is set to OFF. - var updateException = Assert.Throws(() => context.SaveChanges()); - Assert.Single(updateException.Entries); - - foreach (var entity in entities.Take(100)) - { - Assert.Equal(0, entity.Id); - Assert.Null(entity._id); - Assert.Null(entity.Species!.DarwinId); - foreach (var species in entity.MixedMetaphors) - { - Assert.Null(species.MetaphoricId); - } - } - - Assert.Equal(1777, entities[100].Id); - Assert.Equal(1777, entities[100].Species!.DarwinId); - foreach (var species in entities[100].MixedMetaphors) - { - Assert.Equal(1777, species.MetaphoricId); - } - - foreach (var entity in entities) - { - Assert.Same( - entity, - tempValueIdentityMap[context.Entry(entity).Property(p => p.Id).CurrentValue]); - } - - foreach (var entity in entities) - { - Assert.Same( - entity, - stateManager.TryGetEntryTyped( - key, - context.Entry(entity).Property(p => p.Id).CurrentValue)!.Entity); - } - }); - } - } - - public class StoreGeneratedSqlServerFixture : StoreGeneratedFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => builder - .EnableSensitiveDataLogging() - .ConfigureWarnings( - b => b.Default(WarningBehavior.Throw) - .Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning) - .Ignore(RelationalEventId.BoolWithDefaultWarning)); - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.Entity( - b => - { - b.Property(e => e.Id).UseIdentityColumn(); - b.Property(e => e.Identity).HasDefaultValue("Banana Joe"); - b.Property(e => e.IdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); - b.Property(e => e.IdentityReadOnlyAfterSave).HasDefaultValue("Anton"); - b.Property(e => e.AlwaysIdentity).HasDefaultValue("Banana Joe"); - b.Property(e => e.AlwaysIdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); - b.Property(e => e.AlwaysIdentityReadOnlyAfterSave).HasDefaultValue("Anton"); - b.Property(e => e.Computed).HasDefaultValue("Alan"); - b.Property(e => e.ComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); - b.Property(e => e.ComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); - b.Property(e => e.AlwaysComputed).HasDefaultValue("Alan"); - b.Property(e => e.AlwaysComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); - b.Property(e => e.AlwaysComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.OnAdd).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - - b.Property(e => e.OnAddOrUpdate).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - - b.Property(e => e.OnUpdate).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableAsNonNullable).HasComputedColumnSql("1"); - b.Property(e => e.NonNullableAsNullable).HasComputedColumnSql("1"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); - b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); - b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); - b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); - b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); - b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); - b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); - }); - - modelBuilder.Entity().Property(e => e.HasTemp).HasDefaultValue(777); - - modelBuilder.Entity().Property(e => e.Id).UseIdentityColumn(); - - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - modelBuilder.Entity().Property(e => e.Id).UseHiLo(); - - modelBuilder.Entity( - entity => - { - var keyConverter = new ValueConverter( - v => new decimal(v), - v => decimal.ToInt64(v)); - - entity.Property(e => e.Id) - .HasPrecision(18, 0) - .HasConversion(keyConverter); - }); - - base.OnModelCreating(modelBuilder, context); - } - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - { - base.ConfigureConventions(configurationBuilder); - - configurationBuilder.Properties() - .HaveConversion(); - configurationBuilder.Properties() - .HaveConversion(); - configurationBuilder.Properties().HaveConversion(); - configurationBuilder.Properties().HaveConversion(); - configurationBuilder.Properties().HaveConversion(); - configurationBuilder.Properties().HaveConversion(); - } + protected override string StoreName + => "StoreGeneratedTest"; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTestBase.cs new file mode 100644 index 00000000000..e9d2663e068 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTestBase.cs @@ -0,0 +1,955 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class StoreGeneratedSqlServerTestBase : StoreGeneratedTestBase + where TFixture : StoreGeneratedSqlServerTestBase.StoreGeneratedSqlServerFixtureBase, new() +{ + protected StoreGeneratedSqlServerTestBase(TFixture fixture) + : base(fixture) + { + } + + public class WrappedIntHiLoClass + { + public int Value { get; set; } + } + + protected class WrappedIntHiLoClassConverter : ValueConverter + { + public WrappedIntHiLoClassConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoClass { Value = v }) + { + } + } + + protected class WrappedIntHiLoClassComparer : ValueComparer + { + public WrappedIntHiLoClassComparer() + : base( + (v1, v2) => (v1 == null && v2 == null) || (v1 != null && v2 != null && v1.Value.Equals(v2.Value)), + v => v != null ? v.Value : 0, + v => v == null ? null : new WrappedIntHiLoClass { Value = v.Value }) + { + } + } + + protected class WrappedIntHiLoClassValueGenerator : ValueGenerator + { + public override WrappedIntHiLoClass Next(EntityEntry entry) + => new() { Value = 66 }; + + public override bool GeneratesTemporaryValues + => false; + } + + public struct WrappedIntHiLoStruct + { + public int Value { get; set; } + } + + protected class WrappedIntHiLoStructConverter : ValueConverter + { + public WrappedIntHiLoStructConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoStruct { Value = v }) + { + } + } + + protected class WrappedIntHiLoStructValueGenerator : ValueGenerator + { + public override WrappedIntHiLoStruct Next(EntityEntry entry) + => new() { Value = 66 }; + + public override bool GeneratesTemporaryValues + => false; + } + + public record WrappedIntHiLoRecord + { + public int Value { get; set; } + } + + protected class WrappedIntHiLoRecordConverter : ValueConverter + { + public WrappedIntHiLoRecordConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoRecord { Value = v }) + { + } + } + + protected class WrappedIntHiLoRecordValueGenerator : ValueGenerator + { + public override WrappedIntHiLoRecord Next(EntityEntry entry) + => new() { Value = 66 }; + + public override bool GeneratesTemporaryValues + => false; + } + + public class WrappedIntHiLoKeyClass + { + public int Value { get; set; } + } + + protected class WrappedIntHiLoKeyClassConverter : ValueConverter + { + public WrappedIntHiLoKeyClassConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoKeyClass { Value = v }) + { + } + } + + protected class WrappedIntHiLoKeyClassComparer : ValueComparer + { + public WrappedIntHiLoKeyClassComparer() + : base( + (v1, v2) => (v1 == null && v2 == null) || (v1 != null && v2 != null && v1.Value.Equals(v2.Value)), + v => v != null ? v.Value : 0, + v => v == null ? null : new WrappedIntHiLoKeyClass { Value = v.Value }) + { + } + } + + public struct WrappedIntHiLoKeyStruct + { + public int Value { get; set; } + + public override bool Equals(object? obj) + => obj is WrappedIntHiLoKeyStruct other && Value == other.Value; + + public override int GetHashCode() + => Value; + + public static bool operator ==(WrappedIntHiLoKeyStruct left, WrappedIntHiLoKeyStruct right) + => left.Equals(right); + + public static bool operator !=(WrappedIntHiLoKeyStruct left, WrappedIntHiLoKeyStruct right) + => !left.Equals(right); + } + + protected class WrappedIntHiLoKeyStructConverter : ValueConverter + { + public WrappedIntHiLoKeyStructConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoKeyStruct { Value = v }) + { + } + } + + public record WrappedIntHiLoKeyRecord + { + public int Value { get; set; } + } + + protected class WrappedIntHiLoKeyRecordConverter : ValueConverter + { + public WrappedIntHiLoKeyRecordConverter() + : base( + v => v.Value, + v => new WrappedIntHiLoKeyRecord { Value = v }) + { + } + } + + protected class WrappedIntHiLoClassPrincipal + { + public WrappedIntHiLoKeyClass Id { get; set; } = null!; + public ICollection Dependents { get; } = new List(); + + public ICollection RequiredDependents { get; } = + new List(); + + public ICollection OptionalDependents { get; } = + new List(); + } + + protected class WrappedIntHiLoClassDependentShadow + { + public WrappedIntHiLoClass Id { get; set; } = null!; + public WrappedIntHiLoClassPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoClassDependentRequired + { + public WrappedIntHiLoClass Id { get; set; } = null!; + public WrappedIntHiLoKeyClass PrincipalId { get; set; } = null!; + public WrappedIntHiLoClassPrincipal Principal { get; set; } = null!; + } + + protected class WrappedIntHiLoClassDependentOptional + { + public WrappedIntHiLoClass Id { get; set; } = null!; + public WrappedIntHiLoKeyClass? PrincipalId { get; set; } + public WrappedIntHiLoClassPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoStructPrincipal + { + public WrappedIntHiLoKeyStruct Id { get; set; } + public ICollection Dependents { get; } = new List(); + + public ICollection OptionalDependents { get; } = + new List(); + + public ICollection RequiredDependents { get; } = + new List(); + } + + protected class WrappedIntHiLoStructDependentShadow + { + public WrappedIntHiLoStruct Id { get; set; } + public WrappedIntHiLoStructPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoStructDependentOptional + { + public WrappedIntHiLoStruct Id { get; set; } + public WrappedIntHiLoKeyStruct? PrincipalId { get; set; } + public WrappedIntHiLoStructPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoStructDependentRequired + { + public WrappedIntHiLoStruct Id { get; set; } + public WrappedIntHiLoKeyStruct PrincipalId { get; set; } + public WrappedIntHiLoStructPrincipal Principal { get; set; } = null!; + } + + protected class WrappedIntHiLoRecordPrincipal + { + public WrappedIntHiLoKeyRecord Id { get; set; } = null!; + public ICollection Dependents { get; } = new List(); + + public ICollection OptionalDependents { get; } = + new List(); + + public ICollection RequiredDependents { get; } = + new List(); + } + + protected class WrappedIntHiLoRecordDependentShadow + { + public WrappedIntHiLoRecord Id { get; set; } = null!; + public WrappedIntHiLoRecordPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoRecordDependentOptional + { + public WrappedIntHiLoRecord Id { get; set; } = null!; + public WrappedIntHiLoKeyRecord? PrincipalId { get; set; } + public WrappedIntHiLoRecordPrincipal? Principal { get; set; } + } + + protected class WrappedIntHiLoRecordDependentRequired + { + public WrappedIntHiLoRecord Id { get; set; } = null!; + public WrappedIntHiLoKeyRecord PrincipalId { get; set; } = null!; + public WrappedIntHiLoRecordPrincipal Principal { get; set; } = null!; + } + + protected class LongToDecimalPrincipal + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public ICollection Dependents { get; } = new List(); + public ICollection RequiredDependents { get; } = new List(); + public ICollection OptionalDependents { get; } = new List(); + } + + protected class LongToDecimalDependentShadow + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public LongToDecimalPrincipal? Principal { get; set; } + } + + protected class LongToDecimalDependentRequired + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long PrincipalId { get; set; } + public LongToDecimalPrincipal Principal { get; set; } = null!; + } + + protected class LongToDecimalDependentOptional + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + public long? PrincipalId { get; set; } + public LongToDecimalPrincipal? Principal { get; set; } + } + + [ConditionalFact] + public virtual void Insert_update_and_delete_with_long_to_decimal_conversion() + { + var id1 = 0L; + ExecuteWithStrategyInTransaction( + context => + { + var principal1 = context.Add( + new LongToDecimalPrincipal + { + Id = Fixture.LongToDecimalPrincipalSentinel, + Dependents = { new LongToDecimalDependentShadow(), new LongToDecimalDependentShadow() }, + OptionalDependents = { new LongToDecimalDependentOptional(), new LongToDecimalDependentOptional() }, + RequiredDependents = { new LongToDecimalDependentRequired(), new LongToDecimalDependentRequired() } + }).Entity; + + context.SaveChanges(); + + id1 = principal1.Id; + Assert.NotEqual(0L, id1); + foreach (var dependent in principal1.Dependents) + { + Assert.NotEqual(0L, dependent.Id); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal1.OptionalDependents) + { + Assert.NotEqual(0L, dependent.Id); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId); + } + + foreach (var dependent in principal1.RequiredDependents) + { + Assert.NotEqual(0L, dependent.Id); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId); + } + }, + context => + { + var principal1 = context.Set() + .Include(e => e.Dependents) + .Include(e => e.OptionalDependents) + .Include(e => e.RequiredDependents) + .Single(); + + Assert.Equal(principal1.Id, id1); + foreach (var dependent in principal1.Dependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal1.OptionalDependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId!.Value); + } + + foreach (var dependent in principal1.RequiredDependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId); + } + + principal1.Dependents.Remove(principal1.Dependents.First()); + principal1.OptionalDependents.Remove(principal1.OptionalDependents.First()); + principal1.RequiredDependents.Remove(principal1.RequiredDependents.First()); + + context.SaveChanges(); + }, + context => + { + var dependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, dependents1.Count); + Assert.Null( + context.Entry(dependents1.Single(e => e.Principal == null)) + .Property("PrincipalId").CurrentValue); + + var optionalDependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, optionalDependents1.Count); + Assert.Null(optionalDependents1.Single(e => e.Principal == null).PrincipalId); + + var requiredDependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Single(requiredDependents1); + + context.Remove(dependents1.Single(e => e.Principal != null)); + context.Remove(optionalDependents1.Single(e => e.Principal != null)); + context.Remove(requiredDependents1.Single()); + context.Remove(requiredDependents1.Single().Principal); + + context.SaveChanges(); + }, + context => + { + Assert.Equal(1, context.Set().Count()); + Assert.Equal(1, context.Set().Count()); + Assert.Equal(0, context.Set().Count()); + }); + } + + [ConditionalFact] + public virtual void Insert_update_and_delete_with_wrapped_int_key_using_hi_lo() + { + var id1 = 0; + var id2 = 0; + var id3 = 0; + ExecuteWithStrategyInTransaction( + context => + { + var principal1 = context.Add( + new WrappedIntHiLoClassPrincipal + { + Id = Fixture.WrappedIntHiLoKeyClassSentinel!, + Dependents = { new WrappedIntHiLoClassDependentShadow(), new WrappedIntHiLoClassDependentShadow() }, + OptionalDependents = { new WrappedIntHiLoClassDependentOptional(), new WrappedIntHiLoClassDependentOptional() }, + RequiredDependents = { new WrappedIntHiLoClassDependentRequired(), new WrappedIntHiLoClassDependentRequired() } + }).Entity; + + var principal2 = context.Add( + new WrappedIntHiLoStructPrincipal + { + Id = Fixture.WrappedIntHiLoKeyStructSentinel!, + Dependents = { new WrappedIntHiLoStructDependentShadow(), new WrappedIntHiLoStructDependentShadow() }, + OptionalDependents = + { + new WrappedIntHiLoStructDependentOptional(), new WrappedIntHiLoStructDependentOptional() + }, + RequiredDependents = + { + new WrappedIntHiLoStructDependentRequired(), new WrappedIntHiLoStructDependentRequired() + } + }).Entity; + + var principal3 = context.Add( + new WrappedIntHiLoRecordPrincipal + { + Id = Fixture.WrappedIntHiLoKeyRecordSentinel!, + Dependents = { new WrappedIntHiLoRecordDependentShadow(), new WrappedIntHiLoRecordDependentShadow() }, + OptionalDependents = + { + new WrappedIntHiLoRecordDependentOptional(), new WrappedIntHiLoRecordDependentOptional() + }, + RequiredDependents = + { + new WrappedIntHiLoRecordDependentRequired(), new WrappedIntHiLoRecordDependentRequired() + } + }).Entity; + + context.SaveChanges(); + + id1 = principal1.Id.Value; + Assert.NotEqual(0, id1); + foreach (var dependent in principal1.Dependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal1.OptionalDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId!.Value); + } + + foreach (var dependent in principal1.RequiredDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId.Value); + } + + id2 = principal2.Id.Value; + Assert.NotEqual(0, id2); + foreach (var dependent in principal2.Dependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value.Value); + } + + foreach (var dependent in principal2.OptionalDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, dependent.PrincipalId!.Value.Value); + } + + foreach (var dependent in principal2.RequiredDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, dependent.PrincipalId.Value); + } + + id3 = principal3.Id.Value; + Assert.NotEqual(0, id3); + foreach (var dependent in principal3.Dependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal3.OptionalDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, dependent.PrincipalId!.Value); + } + + foreach (var dependent in principal3.RequiredDependents) + { + Assert.NotEqual(0, dependent.Id.Value); + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, dependent.PrincipalId.Value); + } + }, + context => + { + var principal1 = context.Set() + .Include(e => e.Dependents) + .Include(e => e.OptionalDependents) + .Include(e => e.RequiredDependents) + .Single(); + + Assert.Equal(principal1.Id.Value, id1); + foreach (var dependent in principal1.Dependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal1.OptionalDependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId!.Value); + } + + foreach (var dependent in principal1.RequiredDependents) + { + Assert.Same(principal1, dependent.Principal); + Assert.Equal(id1, dependent.PrincipalId.Value); + } + + var principal2 = context.Set() + .Include(e => e.Dependents) + .Include(e => e.OptionalDependents) + .Include(e => e.RequiredDependents) + .Single(); + + Assert.Equal(principal2.Id.Value, id2); + foreach (var dependent in principal2.Dependents) + { + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value.Value); + } + + foreach (var dependent in principal2.OptionalDependents) + { + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, dependent.PrincipalId!.Value.Value); + } + + foreach (var dependent in principal2.RequiredDependents) + { + Assert.Same(principal2, dependent.Principal); + Assert.Equal(id2, dependent.PrincipalId.Value); + } + + var principal3 = context.Set() + .Include(e => e.Dependents) + .Include(e => e.OptionalDependents) + .Include(e => e.RequiredDependents) + .Single(); + + Assert.Equal(principal3.Id.Value, id3); + foreach (var dependent in principal3.Dependents) + { + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, context.Entry(dependent).Property("PrincipalId").CurrentValue!.Value); + } + + foreach (var dependent in principal3.OptionalDependents) + { + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, dependent.PrincipalId!.Value); + } + + foreach (var dependent in principal3.RequiredDependents) + { + Assert.Same(principal3, dependent.Principal); + Assert.Equal(id3, dependent.PrincipalId.Value); + } + + principal1.Dependents.Remove(principal1.Dependents.First()); + principal2.Dependents.Remove(principal2.Dependents.First()); + principal3.Dependents.Remove(principal3.Dependents.First()); + + principal1.OptionalDependents.Remove(principal1.OptionalDependents.First()); + principal2.OptionalDependents.Remove(principal2.OptionalDependents.First()); + principal3.OptionalDependents.Remove(principal3.OptionalDependents.First()); + + principal1.RequiredDependents.Remove(principal1.RequiredDependents.First()); + principal2.RequiredDependents.Remove(principal2.RequiredDependents.First()); + principal3.RequiredDependents.Remove(principal3.RequiredDependents.First()); + + context.SaveChanges(); + }, + context => + { + var dependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, dependents1.Count); + Assert.Null( + context.Entry(dependents1.Single(e => e.Principal == null)) + .Property("PrincipalId").CurrentValue); + + var optionalDependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, optionalDependents1.Count); + Assert.Null(optionalDependents1.Single(e => e.Principal == null).PrincipalId); + + var requiredDependents1 = context.Set().Include(e => e.Principal).ToList(); + Assert.Single(requiredDependents1); + + var dependents2 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, dependents2.Count); + Assert.Null( + context.Entry(dependents2.Single(e => e.Principal == null)) + .Property("PrincipalId").CurrentValue); + + var optionalDependents2 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, optionalDependents2.Count); + Assert.Null(optionalDependents2.Single(e => e.Principal == null).PrincipalId); + + var requiredDependents2 = context.Set().Include(e => e.Principal).ToList(); + Assert.Single(requiredDependents2); + + var dependents3 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, dependents3.Count); + Assert.Null( + context.Entry(dependents3.Single(e => e.Principal == null)) + .Property("PrincipalId").CurrentValue); + + var optionalDependents3 = context.Set().Include(e => e.Principal).ToList(); + Assert.Equal(2, optionalDependents3.Count); + Assert.Null(optionalDependents3.Single(e => e.Principal == null).PrincipalId); + + var requiredDependents3 = context.Set().Include(e => e.Principal).ToList(); + Assert.Single(requiredDependents3); + + context.Remove(dependents1.Single(e => e.Principal != null)); + context.Remove(optionalDependents1.Single(e => e.Principal != null)); + context.Remove(requiredDependents1.Single()); + context.Remove(requiredDependents1.Single().Principal); + + context.Remove(dependents2.Single(e => e.Principal != null)); + context.Remove(optionalDependents2.Single(e => e.Principal != null)); + context.Remove(requiredDependents2.Single()); + context.Remove(requiredDependents2.Single().Principal); + + context.Remove(dependents3.Single(e => e.Principal != null)); + context.Remove(optionalDependents3.Single(e => e.Principal != null)); + context.Remove(requiredDependents3.Single()); + context.Remove(requiredDependents3.Single().Principal); + + context.SaveChanges(); + }, + context => + { + Assert.Equal(1, context.Set().Count()); + Assert.Equal(1, context.Set().Count()); + Assert.Equal(1, context.Set().Count()); + + Assert.Equal(1, context.Set().Count()); + Assert.Equal(1, context.Set().Count()); + Assert.Equal(1, context.Set().Count()); + + Assert.Equal(0, context.Set().Count()); + Assert.Equal(0, context.Set().Count()); + Assert.Equal(0, context.Set().Count()); + }); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + [ConditionalFact] + public virtual void Exception_in_SaveChanges_causes_store_values_to_be_reverted() + { + var entities = new List(); + for (var i = 0; i < 100; i++) + { + entities.Add( + new Darwin + { + _id = Fixture.NullableIntSentinel, + Species = new Species { Id = Fixture.IntSentinel, Name = "Goldfish (with legs)" }, + MixedMetaphors = new List + { + new() { Id = Fixture.IntSentinel, Name = "Large ground finch" }, + new() { Id = Fixture.IntSentinel, Name = "Medium ground finch" }, + new() { Id = Fixture.IntSentinel, Name = "Small tree finch" }, + new() { Id = Fixture.IntSentinel, Name = "Green warbler-finch" } + } + }); + } + + entities.Add( + new Darwin + { + Id = 1777, + Species = new Species { Id = Fixture.IntSentinel, Name = "Goldfish (with legs)" }, + MixedMetaphors = new List + { + new() { Id = Fixture.IntSentinel, Name = "Large ground finch" }, + new() { Id = Fixture.IntSentinel, Name = "Medium ground finch" }, + new() { Id = Fixture.IntSentinel, Name = "Small tree finch" }, + new() { Id = Fixture.IntSentinel, Name = "Green warbler-finch" } + } + }); + + for (var i = 0; i < 2; i++) + { + ExecuteWithStrategyInTransaction( + context => + { + context.AddRange(entities); + + foreach (var entity in entities.Take(100)) + { + Assert.Equal(Fixture.NullableIntSentinel ?? 0, entity.Id); + Assert.Equal(Fixture.NullableIntSentinel, entity._id); + } + + Assert.Equal(1777, entities[100].Id); + + var tempValueIdentityMap = entities.ToDictionary( + e => context.Entry(e).Property(p => p.Id).CurrentValue, + e => e); + + var stateManager = context.GetService(); + var key = context.Model.FindEntityType(typeof(Darwin))!.FindPrimaryKey()!; + + foreach (var entity in entities) + { + Assert.Same( + entity, + stateManager.TryGetEntryTyped( + key, + context.Entry(entity).Property(p => p.Id).CurrentValue)!.Entity); + } + + // DbUpdateException : An error occurred while updating the entries. See the + // inner exception for details. + // SqlException : Cannot insert explicit value for identity column in table + // 'Blog' when IDENTITY_INSERT is set to OFF. + var updateException = Assert.Throws(() => context.SaveChanges()); + Assert.Single(updateException.Entries); + + foreach (var entity in entities.Take(100)) + { + Assert.Equal(Fixture.NullableIntSentinel ?? 0, entity.Id); + Assert.Equal(Fixture.NullableIntSentinel, entity._id); + Assert.Null(entity.Species!.DarwinId); + foreach (var species in entity.MixedMetaphors) + { + Assert.Null(species.MetaphoricId); + } + } + + Assert.Equal(1777, entities[100].Id); + Assert.Equal(1777, entities[100].Species!.DarwinId); + foreach (var species in entities[100].MixedMetaphors) + { + Assert.Equal(1777, species.MetaphoricId); + } + + foreach (var entity in entities) + { + Assert.Same( + entity, + tempValueIdentityMap[context.Entry(entity).Property(p => p.Id).CurrentValue]); + } + + foreach (var entity in entities) + { + Assert.Same( + entity, + stateManager.TryGetEntryTyped( + key, + context.Entry(entity).Property(p => p.Id).CurrentValue)!.Entity); + } + }); + } + } + + public abstract class StoreGeneratedSqlServerFixtureBase : StoreGeneratedFixtureBase + { + public virtual long LongToDecimalPrincipalSentinel + => default; + + public virtual WrappedIntHiLoKeyClass? WrappedIntHiLoKeyClassSentinel + => default; + + public virtual WrappedIntHiLoKeyStruct WrappedIntHiLoKeyStructSentinel + => default; + + public virtual WrappedIntHiLoKeyRecord? WrappedIntHiLoKeyRecordSentinel + => default; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder + .EnableSensitiveDataLogging() + .ConfigureWarnings( + b => b.Default(WarningBehavior.Throw) + .Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning) + .Ignore(RelationalEventId.BoolWithDefaultWarning)); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).UseIdentityColumn(); + b.Property(e => e.Identity).HasDefaultValue("Banana Joe"); + b.Property(e => e.IdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); + b.Property(e => e.IdentityReadOnlyAfterSave).HasDefaultValue("Anton"); + b.Property(e => e.AlwaysIdentity).HasDefaultValue("Banana Joe"); + b.Property(e => e.AlwaysIdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); + b.Property(e => e.AlwaysIdentityReadOnlyAfterSave).HasDefaultValue("Anton"); + b.Property(e => e.Computed).HasDefaultValue("Alan"); + b.Property(e => e.ComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); + b.Property(e => e.ComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); + b.Property(e => e.AlwaysComputed).HasDefaultValue("Alan"); + b.Property(e => e.AlwaysComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); + b.Property(e => e.AlwaysComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.OnAdd).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + + b.Property(e => e.OnAddOrUpdate).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + + b.Property(e => e.OnUpdate).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableAsNonNullable).HasComputedColumnSql("1"); + b.Property(e => e.NonNullableAsNullable).HasComputedColumnSql("1"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); + b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); + b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); + b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); + b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); + b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); + b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); + }); + + modelBuilder.Entity().Property(e => e.HasTemp).HasDefaultValue(777); + + modelBuilder.Entity().Property(e => e.Id).UseIdentityColumn(); + + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + + modelBuilder.Entity( + entity => + { + var keyConverter = new ValueConverter( + v => new decimal(v), + v => decimal.ToInt64(v)); + + entity.Property(e => e.Id) + .HasPrecision(18, 0) + .HasConversion(keyConverter); + }); + + base.OnModelCreating(modelBuilder, context); + } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + configurationBuilder.Properties() + .HaveConversion(); + configurationBuilder.Properties() + .HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs index 033e824f294..04a2ee79623 100644 --- a/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs +++ b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs @@ -81,6 +81,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); }); + modelBuilder.Entity( + b => + { + b.Property(e => e.AccessStateWithSentinelId).ValueGeneratedNever(); + b.HasData(new AccessStateWithSentinel { AccessStateWithSentinelId = 1 }); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.IdUserState).HasDefaultValue(1).Metadata.Sentinel = 667; + b.HasOne(e => e.UserState).WithMany(e => e.Users).HasForeignKey(e => e.IdUserState); + }); + modelBuilder.Entity().Property("CategoryId").HasDefaultValue(1); modelBuilder.Entity().Property(e => e.CategoryId).HasDefaultValue(2);; } diff --git a/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs index 654f87d5997..73f9e891480 100644 --- a/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs @@ -33,6 +33,9 @@ protected override void UseTransaction(DatabaseFacade facade, IDbContextTransact public class StoreGeneratedSqliteFixture : StoreGeneratedFixtureBase { + protected override string StoreName + => "StoreGeneratedTest"; + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 1ac67317e30..8a0dca01f8f 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -76,7 +76,7 @@ protected override void Initialize() typeof(CompiledQueryCacheKeyGenerator).GetMethod("GenerateCacheKeyCore", AnyInstance), typeof(InternalEntityEntry).GetMethod("get_Item"), typeof(InternalEntityEntry).GetMethod("set_Item"), - typeof(InternalEntityEntry).GetMethod(nameof(InternalEntityEntry.HasDefaultValue)), + typeof(InternalEntityEntry).GetMethod(nameof(InternalEntityEntry.HasExplicitValue)), typeof(DiagnosticsLogger<>).GetMethod("DispatchEventData", AnyInstance), typeof(DiagnosticsLogger<>).GetMethod("ShouldLog", AnyInstance), typeof(DiagnosticsLogger<>).GetMethod("NeedsEventData", AnyInstance), diff --git a/test/EFCore.Tests/ChangeTracking/EntityEntryTest.cs b/test/EFCore.Tests/ChangeTracking/EntityEntryTest.cs index ae5772810d0..9bb5423ee53 100644 --- a/test/EFCore.Tests/ChangeTracking/EntityEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/EntityEntryTest.cs @@ -12,9 +12,7 @@ public void Non_store_generated_key_is_always_set() { using var context = new KeySetContext(); Assert.True(context.Entry(new NotStoreGenerated()).IsKeySet); - Assert.True( - context.Entry( - new NotStoreGenerated { Id = 1 }).IsKeySet); + Assert.True(context.Entry(new NotStoreGenerated { Id = 1 }).IsKeySet); } [ConditionalFact] @@ -22,15 +20,9 @@ public void Non_store_generated_composite_key_is_always_set() { using var context = new KeySetContext(); Assert.True(context.Entry(new CompositeNotStoreGenerated()).IsKeySet); - Assert.True( - context.Entry( - new CompositeNotStoreGenerated { Id1 = 1 }).IsKeySet); - Assert.True( - context.Entry( - new CompositeNotStoreGenerated { Id2 = true }).IsKeySet); - Assert.True( - context.Entry( - new CompositeNotStoreGenerated { Id1 = 1, Id2 = true }).IsKeySet); + Assert.True(context.Entry(new CompositeNotStoreGenerated { Id1 = 1 }).IsKeySet); + Assert.True(context.Entry(new CompositeNotStoreGenerated { Id2 = true }).IsKeySet); + Assert.True(context.Entry(new CompositeNotStoreGenerated { Id1 = 1, Id2 = true }).IsKeySet); } [ConditionalFact] @@ -38,9 +30,16 @@ public void Store_generated_key_is_set_only_if_non_default_value() { using var context = new KeySetContext(); Assert.False(context.Entry(new StoreGenerated()).IsKeySet); - Assert.True( - context.Entry( - new StoreGenerated { Id = 1 }).IsKeySet); + Assert.True(context.Entry(new StoreGenerated { Id = 1 }).IsKeySet); + } + + [ConditionalFact] + public void Store_generated_key_is_set_only_if_non_sentinel_value() + { + using var context = new KeySetContext(); + Assert.False(context.Entry(new StoreGeneratedWithSentinel { Id = 667 }).IsKeySet); + Assert.True(context.Entry(new StoreGeneratedWithSentinel { Id = 1 }).IsKeySet); + Assert.True(context.Entry(new StoreGeneratedWithSentinel()).IsKeySet); } [ConditionalFact] @@ -48,15 +47,21 @@ public void Composite_store_generated_key_is_set_only_if_non_default_value_in_st { using var context = new KeySetContext(); Assert.False(context.Entry(new CompositeStoreGenerated()).IsKeySet); - Assert.False( - context.Entry( - new CompositeStoreGenerated { Id1 = 1 }).IsKeySet); - Assert.True( - context.Entry( - new CompositeStoreGenerated { Id2 = true }).IsKeySet); - Assert.True( - context.Entry( - new CompositeStoreGenerated { Id1 = 1, Id2 = true }).IsKeySet); + Assert.False(context.Entry(new CompositeStoreGenerated { Id1 = 1 }).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGenerated { Id2 = true }).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGenerated { Id1 = 1, Id2 = true }).IsKeySet); + } + + [ConditionalFact] + public void Composite_store_generated_key_is_set_only_if_non_sentinel_value_in_store_generated_part() + { + using var context = new KeySetContext(); + Assert.False(context.Entry(new CompositeStoreGeneratedWithSentinel { Id2 = true }).IsKeySet); + Assert.False(context.Entry(new CompositeStoreGeneratedWithSentinel { Id1 = 1, Id2 = true }).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGeneratedWithSentinel { Id2 = false }).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGeneratedWithSentinel { Id1 = 1, Id2 = false }).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGeneratedWithSentinel()).IsKeySet); + Assert.True(context.Entry(new CompositeStoreGeneratedWithSentinel { Id1 = 1 }).IsKeySet); } [ConditionalFact] @@ -64,9 +69,16 @@ public void Primary_key_that_is_also_foreign_key_is_set_only_if_non_default_valu { using var context = new KeySetContext(); Assert.False(context.Entry(new Dependent()).IsKeySet); - Assert.True( - context.Entry( - new Dependent { Id = 1 }).IsKeySet); + Assert.True(context.Entry(new Dependent { Id = 1 }).IsKeySet); + } + + [ConditionalFact] + public void Primary_key_that_is_also_foreign_key_is_set_only_if_non_sentinel_value() + { + using var context = new KeySetContext(); + Assert.False(context.Entry(new DependentWithSentinel { Id = 667 }).IsKeySet); + Assert.True(context.Entry(new DependentWithSentinel { Id = 1 }).IsKeySet); + Assert.True(context.Entry(new DependentWithSentinel()).IsKeySet); } private class StoreGenerated @@ -76,6 +88,13 @@ private class StoreGenerated public Dependent Dependent { get; set; } } + private class StoreGeneratedWithSentinel + { + public int Id { get; set; } + + public DependentWithSentinel Dependent { get; set; } + } + private class NotStoreGenerated { public int Id { get; set; } @@ -87,6 +106,12 @@ private class CompositeStoreGenerated public bool Id2 { get; set; } } + private class CompositeStoreGeneratedWithSentinel + { + public int Id1 { get; set; } + public bool Id2 { get; set; } + } + private class CompositeNotStoreGenerated { public int Id1 { get; set; } @@ -100,6 +125,13 @@ private class Dependent public StoreGenerated Principal { get; set; } } + private class DependentWithSentinel + { + public int Id { get; set; } + + public StoreGeneratedWithSentinel Principal { get; set; } + } + private class KeySetContext : DbContext { protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -108,8 +140,10 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu .UseInMemoryDatabase(nameof(KeySetContext)); public DbSet StoreGenerated { get; set; } + public DbSet StoreGeneratedWithSentinel { get; set; } public DbSet NotStoreGenerated { get; set; } public DbSet CompositeStoreGenerated { get; set; } + public DbSet CompositeStoreGeneratedWithSentinel { get; set; } public DbSet CompositeNotStoreGenerated { get; set; } public DbSet Dependent { get; set; } @@ -120,6 +154,17 @@ protected internal override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(e => e.Principal) .HasForeignKey(e => e.Id); + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).Metadata.Sentinel = 667; + b.HasOne(e => e.Dependent) + .WithOne(e => e.Principal) + .HasForeignKey(e => e.Id); + }); + + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = 667; + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); modelBuilder.Entity().HasKey( @@ -128,10 +173,16 @@ protected internal override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity( b => { - b.HasKey( - e => new { e.Id1, e.Id2 }); + b.HasKey(e => new { e.Id1, e.Id2 }); b.Property(e => e.Id2).ValueGeneratedOnAdd(); }); + + modelBuilder.Entity( + b => + { + b.HasKey(e => new { e.Id1, e.Id2 }); + b.Property(e => e.Id2).ValueGeneratedOnAdd().Metadata.Sentinel = true; + }); } } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalClrEntityEntryTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalClrEntityEntryTest.cs index a672ef0a307..ac4dbde2fda 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalClrEntityEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalClrEntityEntryTest.cs @@ -82,17 +82,17 @@ public virtual void Temporary_values_are_not_reset_when_entity_is_detached() entry.SetTemporaryValue(keyProperty, -1); Assert.NotNull(entry[keyProperty]); - Assert.Equal(0, entity.Id); + Assert.Equal(-1, entity.Id); Assert.Equal(-1, entry[keyProperty]); entry.SetEntityState(EntityState.Detached); - Assert.Equal(0, entity.Id); + Assert.Equal(-1, entity.Id); Assert.Equal(-1, entry[keyProperty]); entry.SetEntityState(EntityState.Added); - Assert.Equal(0, entity.Id); + Assert.Equal(-1, entity.Id); Assert.Equal(-1, entry[keyProperty]); } @@ -144,7 +144,7 @@ public void AcceptChanges_handles_different_entity_states_for_owned_types(Entity } [ConditionalFact] - public void Setting_an_explicit_value_on_the_entity_marks_property_as_not_temporary() + public void Setting_an_explicit_value_on_the_entity_does_not_mark_property_as_temporary() { using var context = new KClrContext(); var entry = context.Entry(new SomeEntity()).GetInfrastructure(); @@ -166,11 +166,11 @@ public void Setting_an_explicit_value_on_the_entity_marks_property_as_not_tempor entry.SetEntityState(EntityState.Unchanged); // Does not throw var nameProperty = entry.EntityType.FindProperty(nameof(SomeEntity.Name)); - Assert.True(entry.HasDefaultValue(nameProperty)); + Assert.False(entry.HasExplicitValue(nameProperty)); entity.Name = "Name"; - Assert.False(entry.HasDefaultValue(nameProperty)); + Assert.True(entry.HasExplicitValue(nameProperty)); } [ConditionalFact] diff --git a/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs index f5a828f99c7..e17761d3b2c 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs @@ -16,7 +16,8 @@ public void Can_read_and_manipulate_modification_flags() InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.Unknown, InternalEntityEntry.PropertyFlag.IsLoaded, - InternalEntityEntry.PropertyFlag.IsTemporary); + InternalEntityEntry.PropertyFlag.IsTemporary, + InternalEntityEntry.PropertyFlag.IsStoreGenerated); } } @@ -31,7 +32,8 @@ public void Can_read_and_manipulate_null_flags() InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Unknown, InternalEntityEntry.PropertyFlag.IsLoaded, - InternalEntityEntry.PropertyFlag.IsTemporary); + InternalEntityEntry.PropertyFlag.IsTemporary, + InternalEntityEntry.PropertyFlag.IsStoreGenerated); } } @@ -46,7 +48,8 @@ public void Can_read_and_manipulate_not_set_flags() InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.IsLoaded, - InternalEntityEntry.PropertyFlag.IsTemporary); + InternalEntityEntry.PropertyFlag.IsTemporary, + InternalEntityEntry.PropertyFlag.IsStoreGenerated); } } @@ -61,7 +64,8 @@ public void Can_read_and_manipulate_is_loaded_flags() InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.Unknown, - InternalEntityEntry.PropertyFlag.IsTemporary); + InternalEntityEntry.PropertyFlag.IsTemporary, + InternalEntityEntry.PropertyFlag.IsStoreGenerated); } } @@ -76,7 +80,24 @@ public void Can_read_and_manipulate_temporary_flags() InternalEntityEntry.PropertyFlag.IsLoaded, InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, - InternalEntityEntry.PropertyFlag.Unknown); + InternalEntityEntry.PropertyFlag.Unknown, + InternalEntityEntry.PropertyFlag.IsStoreGenerated); + } + } + + [ConditionalFact] + public void Can_read_and_manipulate_store_generated_flags() + { + for (var i = 0; i < 70; i++) + { + PropertyManipulation( + i, + InternalEntityEntry.PropertyFlag.IsStoreGenerated, + InternalEntityEntry.PropertyFlag.IsLoaded, + InternalEntityEntry.PropertyFlag.Modified, + InternalEntityEntry.PropertyFlag.Null, + InternalEntityEntry.PropertyFlag.Unknown, + InternalEntityEntry.PropertyFlag.IsTemporary); } } @@ -86,7 +107,8 @@ private void PropertyManipulation( InternalEntityEntry.PropertyFlag unusedFlag1, InternalEntityEntry.PropertyFlag unusedFlag2, InternalEntityEntry.PropertyFlag unusedFlag3, - InternalEntityEntry.PropertyFlag unusedFlag4) + InternalEntityEntry.PropertyFlag unusedFlag4, + InternalEntityEntry.PropertyFlag unusedFlag5) { var data = new InternalEntityEntry.StateData(propertyCount, propertyCount); @@ -95,6 +117,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag5)); for (var i = 0; i < propertyCount; i++) { @@ -107,6 +130,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(j, unusedFlag2)); Assert.False(data.IsPropertyFlagged(j, unusedFlag3)); Assert.False(data.IsPropertyFlagged(j, unusedFlag4)); + Assert.False(data.IsPropertyFlagged(j, unusedFlag5)); } Assert.True(data.AnyPropertiesFlagged(propertyFlag)); @@ -114,6 +138,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag5)); } for (var i = 0; i < propertyCount; i++) @@ -127,6 +152,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(j, unusedFlag2)); Assert.False(data.IsPropertyFlagged(j, unusedFlag3)); Assert.False(data.IsPropertyFlagged(j, unusedFlag4)); + Assert.False(data.IsPropertyFlagged(j, unusedFlag5)); } Assert.Equal(i < propertyCount - 1, data.AnyPropertiesFlagged(propertyFlag)); @@ -134,6 +160,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag5)); } for (var i = 0; i < propertyCount; i++) @@ -143,6 +170,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag5)); } data.FlagAllProperties(propertyCount, propertyFlag, flagged: true); @@ -152,6 +180,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag5)); for (var i = 0; i < propertyCount; i++) { @@ -160,6 +189,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag5)); } data.FlagAllProperties(propertyCount, propertyFlag, flagged: false); @@ -169,6 +199,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag5)); for (var i = 0; i < propertyCount; i++) { @@ -177,6 +208,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag5)); } } diff --git a/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs b/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs index aeddc55fe51..899a071a152 100644 --- a/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs +++ b/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs @@ -98,10 +98,6 @@ public void Set_temporary_values_for_indexer_properties() Assert.Equal(77, context.Entry(entity2).Property("NullableValueProperty").CurrentValue); Assert.Equal("Seventy Seven", context.Entry(entity3).Property("ReferenceValueProperty").CurrentValue); - Assert.False(context.Entry(entity1).Property("ValueProperty").IsTemporary); - Assert.False(context.Entry(entity2).Property("NullableValueProperty").IsTemporary); - Assert.False(context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary); - entity1["ValueProperty"] = 78; entity2["NullableValueProperty"] = 78; entity3["ReferenceValueProperty"] = "Seventy Eight"; diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index 4c23e069f88..c1cc2763199 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -19,6 +19,9 @@ namespace Microsoft.EntityFrameworkCore { public partial class DbContextTest { + protected static readonly Guid GuidSentinel = new Guid("56D3784D-6F7F-4935-B7F6-E77DC6E1D91E"); + protected static readonly int IntSentinel = 667; + [ConditionalFact] public void Can_log_debug_events_with_OnConfiguring() => DebugLogTest(useLoggerFactory: false, configureForDebug: false, shouldLog: true); @@ -799,6 +802,30 @@ private class TheGu public string ShirtColor { get; set; } } + private class CategoryWithSentinel + { + public int Id { get; set; } + public string Name { get; set; } + + public List Products { get; set; } + } + + private class ProductWithSentinel + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + public int CategoryId { get; set; } + public CategoryWithSentinel Category { get; set; } + } + + private class TheGuWithSentinel + { + public Guid Id { get; set; } + public string ShirtColor { get; set; } + } + private class EarlyLearningCenter : DbContext { private readonly IServiceProvider _serviceProvider; @@ -821,6 +848,9 @@ public EarlyLearningCenter(IServiceProvider serviceProvider, DbContextOptions op public DbSet Products { get; set; } public DbSet Categories { get; set; } public DbSet Gus { get; set; } + public DbSet ProductWithSentinels { get; set; } + public DbSet CategoryWithSentinels { get; set; } + public DbSet GuWithSentinels { get; set; } protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder @@ -830,8 +860,11 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu .EnableServiceProviderCaching(false); protected internal override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder - .Entity().HasMany(e => e.Products).WithOne(e => e.Category); + { + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = IntSentinel; + modelBuilder.Entity().Property(e => e.Id).Metadata.Sentinel = GuidSentinel; + } } private class FakeEntityMaterializerSource : EntityMaterializerSource diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index e8135fea4e5..06cdb176e39 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -327,23 +327,31 @@ public void Context_can_build_model_using_DbSet_properties() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); Assert.Equal( - new[] { typeof(Category).FullName, typeof(Product).FullName, typeof(TheGu).FullName }, + new[] + { + typeof(Category).FullName, + typeof(CategoryWithSentinel).FullName, + typeof(Product).FullName, + typeof(ProductWithSentinel).FullName, + typeof(TheGu).FullName, + typeof(TheGuWithSentinel).FullName + }, context.Model.GetEntityTypes().Select(e => e.Name).ToArray()); - var categoryType = context.Model.FindEntityType(typeof(Category)); - Assert.Equal("Id", categoryType.FindPrimaryKey().Properties.Single().Name); + var categoryType = context.Model.FindEntityType(typeof(Category))!; + Assert.Equal("Id", categoryType.FindPrimaryKey()!.Properties.Single().Name); Assert.Equal( new[] { "Id", "Name" }, categoryType.GetProperties().Select(p => p.Name).ToArray()); - var productType = context.Model.FindEntityType(typeof(Product)); - Assert.Equal("Id", productType.FindPrimaryKey().Properties.Single().Name); + var productType = context.Model.FindEntityType(typeof(Product))!; + Assert.Equal("Id", productType.FindPrimaryKey()!.Properties.Single().Name); Assert.Equal( new[] { "Id", "CategoryId", "Name", "Price" }, productType.GetProperties().Select(p => p.Name).ToArray()); - var guType = context.Model.FindEntityType(typeof(TheGu)); - Assert.Equal("Id", guType.FindPrimaryKey().Properties.Single().Name); + var guType = context.Model.FindEntityType(typeof(TheGu))!; + Assert.Equal("Id", guType.FindPrimaryKey()!.Properties.Single().Name); Assert.Equal( new[] { "Id", "ShirtColor" }, guType.GetProperties().Select(p => p.Name).ToArray()); diff --git a/test/EFCore.Tests/DbContextTrackingTest.cs b/test/EFCore.Tests/DbContextTrackingTest.cs index bb49f336613..af88bc35889 100644 --- a/test/EFCore.Tests/DbContextTrackingTest.cs +++ b/test/EFCore.Tests/DbContextTrackingTest.cs @@ -226,6 +226,80 @@ private static async Task TrackEntitiesDefaultValueTest( Assert.Same(productEntry1.GetInfrastructure(), context.Entry(product1).GetInfrastructure()); } + [ConditionalFact] + public Task Can_add_existing_entities_with_sentinel_value_to_context_to_be_deleted() + => TrackEntitiesSentinelValueTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + + [ConditionalFact] + public Task Can_add_new_entities_with_sentinel_value_to_context_with_graph_method() + => TrackEntitiesSentinelValueTest((c, e) => c.Add(e), (c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + + [ConditionalFact] + public Task Can_add_new_entities_with_sentinel_value_to_context_with_graph_method_async() + => TrackEntitiesSentinelValueTest((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); + + [ConditionalFact] + public Task Can_add_existing_entities_with_sentinel_value_to_context_to_be_attached_with_graph_method() + => TrackEntitiesSentinelValueTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Added); + + [ConditionalFact] + public Task Can_add_existing_entities_with_sentinel_value_to_context_to_be_updated_with_graph_method() + => TrackEntitiesSentinelValueTest((c, e) => c.Update(e), (c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Added); + + private static Task TrackEntitiesSentinelValueTest( + Func> categoryAdder, + Func> productAdder, + Func> guAdder, + EntityState expectedState) + => TrackEntitiesSentinelValueTest( + (c, e) => new ValueTask>(categoryAdder(c, e)), + (c, e) => new ValueTask>(productAdder(c, e)), + (c, e) => new ValueTask>(guAdder(c, e)), + expectedState); + + // Issue #3890 + private static async Task TrackEntitiesSentinelValueTest( + Func>> categoryAdder, + Func>> productAdder, + Func>> guAdder, + EntityState expectedState) + { + using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); + var category1 = new CategoryWithSentinel { Id = IntSentinel, Name = "Beverages" }; + var product1 = new ProductWithSentinel + { + Id = IntSentinel, + Name = "Marmite", + Price = 7.99m + }; + var gu1 = new TheGuWithSentinel() + { + Id = GuidSentinel, + ShirtColor = "Red" + }; + + var categoryEntry1 = await categoryAdder(context, category1); + var productEntry1 = await productAdder(context, product1); + var guEntry1 = await guAdder(context, gu1); + + Assert.Same(category1, categoryEntry1.Entity); + Assert.Same(product1, productEntry1.Entity); + Assert.Same(gu1, guEntry1.Entity); + + Assert.Same(category1, categoryEntry1.Entity); + Assert.Equal(expectedState, categoryEntry1.State); + + Assert.Same(product1, productEntry1.Entity); + Assert.Equal(expectedState, productEntry1.State); + + Assert.Same(gu1, guEntry1.Entity); + Assert.Equal(expectedState, guEntry1.State); + + Assert.Same(categoryEntry1.GetInfrastructure(), context.Entry(category1).GetInfrastructure()); + Assert.Same(productEntry1.GetInfrastructure(), context.Entry(product1).GetInfrastructure()); + Assert.Same(guEntry1.GetInfrastructure(), context.Entry(gu1).GetInfrastructure()); + } + [ConditionalFact] public Task Can_add_multiple_new_entities_with_default_values_to_context() => TrackMultipleEntitiesDefaultValuesTest((c, e) => c.AddRange(e[0]), (c, e) => c.AddRange(e[0]), EntityState.Added); diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 1361619ac6b..62b61185462 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -20,7 +20,7 @@ private class FakeProperty : Annotatable, IProperty, IClrPropertyGetter public object GetClrValue(object entity) => throw new NotImplementedException(); - public bool HasDefaultValue(object entity) + public bool HasSentinelValue(object entity) => throw new NotImplementedException(); public IEnumerable GetContainingForeignKeys() @@ -108,6 +108,7 @@ public PropertyAccessMode GetPropertyAccessMode() public bool IsNullable { get; } public ValueGenerated ValueGenerated { get; } public bool IsConcurrencyToken { get; } + public object Sentinel { get; } public PropertyInfo PropertyInfo { get; } public FieldInfo FieldInfo { get; } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs index 28597c008f2..9a146889038 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -29,6 +29,7 @@ private class FakeProperty : Annotatable, IProperty, IClrPropertySetter public bool IsStoreGeneratedAlways { get; } public ValueGenerated ValueGenerated { get; } public bool IsConcurrencyToken { get; } + public object Sentinel { get; } public PropertyInfo PropertyInfo { get; } public FieldInfo FieldInfo { get; }