Skip to content

Commit

Permalink
Add a hybrid OnDelete behavior that does SetNull in the StateManager …
Browse files Browse the repository at this point in the history
…and Restrict in the database

Issues #8654, #8632, #8633

The new behavior is called 'ClientSetNull'.
'Restrict' now throws on SaveChanges if EF would have set null. This is the same behavior that has always been there for required relationships, so this is only a break for optional relationships explicitly configured with Restrict, which should be relatively rare since this was the default anyway. The default is now 'ClientSetNull', which matches the old behavior of 'Restrict', so apps that were not setting anything explicitly will not get a breaking change.
  • Loading branch information
ajcvickers committed Jun 5, 2017
1 parent 713e4b1 commit 4f217c1
Show file tree
Hide file tree
Showing 24 changed files with 2,367 additions and 1,426 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ protected virtual void GenerateForeignKey(
}
}

if (foreignKey.DeleteBehavior != DeleteBehavior.Restrict)
if (foreignKey.DeleteBehavior != DeleteBehavior.ClientSetNull)
{
stringBuilder
.AppendLine()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ private void GenerateRelationship(IForeignKey foreignKey, bool useDataAnnotation

var defaultOnDeleteAction = foreignKey.IsRequired
? DeleteBehavior.Cascade
: DeleteBehavior.Restrict;
: DeleteBehavior.ClientSetNull;

if (foreignKey.DeleteBehavior != defaultOnDeleteAction)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ private static void AssignOnDeleteAction(
break;

default:
foreignKey.DeleteBehavior = DeleteBehavior.Restrict;
foreignKey.DeleteBehavior = DeleteBehavior.ClientSetNull;
break;
}
}
Expand Down
3,446 changes: 2,060 additions & 1,386 deletions src/EFCore.Specification.Tests/GraphUpdatesTestBase.cs

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ public virtual void SetPropertyModified(
MarkAsTemporary(property, isTemporary: false);

var index = property.GetOriginalValueIndex();
if (index != -1)
if (index != -1
&& !IsConceptualNull(property))
{
SetOriginalValue(property, this[property], index);
}
Expand Down Expand Up @@ -659,8 +660,7 @@ public virtual object this[[NotNull] IPropertyBase propertyBase]
{
get
{
object value;
return _storeGeneratedValues.TryGetValue(propertyBase, out value)
return _storeGeneratedValues.TryGetValue(propertyBase, out object value)
? value
: ReadPropertyValue(propertyBase);
}
Expand Down Expand Up @@ -694,7 +694,10 @@ public virtual void SetProperty([NotNull] IPropertyBase propertyBase, [CanBeNull
var writeValue = true;

if (asProperty != null
&& !asProperty.IsNullable)
&& (!asProperty.IsNullable
|| asProperty.GetContainingForeignKeys().Any(
p => p.DeleteBehavior == DeleteBehavior.Cascade
|| p.DeleteBehavior == DeleteBehavior.Restrict)))
{
if (value == null)
{
Expand Down Expand Up @@ -833,7 +836,9 @@ public virtual void HandleConceptualNulls()
{
if (_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Null))
{
if (properties.Any(p => p.IsNullable))
if (properties.Any(p => p.IsNullable)
&& foreignKey.DeleteBehavior != DeleteBehavior.Cascade
&& foreignKey.DeleteBehavior != DeleteBehavior.Restrict)
{
foreach (var toNull in properties)
{
Expand Down
9 changes: 7 additions & 2 deletions src/EFCore/ChangeTracking/Internal/NavigationFixer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down Expand Up @@ -324,7 +325,8 @@ public virtual void KeyPropertyChanged(
}
}

if (newPrincipalEntry != null)
if (newPrincipalEntry != null
&& !entry.IsConceptualNull(property))
{
// Add this entity to the collection of the new principal, or set the navigation for a 1:1
SetReferenceOrAddToCollection(newPrincipalEntry, principalToDependent, collectionAccessor, entry.Entity);
Expand Down Expand Up @@ -358,7 +360,10 @@ var targetDependentEntry
}
}

SetNavigation(entry, dependentToPrincipal, newPrincipalEntry.Entity);
if (!entry.IsConceptualNull(property))
{
SetNavigation(entry, dependentToPrincipal, newPrincipalEntry.Entity);
}
}
else if (oldPrincipalEntry != null
&& ReferenceEquals(entry[dependentToPrincipal], oldPrincipalEntry.Entity))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public virtual InternalRelationshipBuilder Apply(InternalRelationshipBuilder rel
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual DeleteBehavior TargetDeleteBehavior([NotNull] ForeignKey foreignKey)
=> foreignKey.IsRequired ? DeleteBehavior.Cascade : DeleteBehavior.Restrict;
=> foreignKey.IsRequired ? DeleteBehavior.Cascade : DeleteBehavior.ClientSetNull;
}
}
90 changes: 78 additions & 12 deletions src/EFCore/Metadata/DeleteBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,98 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore.Metadata
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Indicates how a delete operation is applied to dependent entities in a relationship when the principal is deleted
/// or the relationship is severed.
/// <para>
/// Indicates how a delete operation is applied to dependent entities in a relationship when the
/// principal is deleted or the relationship is severed.
/// </para>
/// <para>
/// Behaviors in the database are dependent on the database schema being created
/// appropriately. Using Entity Framework Migrations or <see cref="DatabaseFacade.EnsureCreated" />
/// will create the appropriate schema.
/// </para>
/// <para>
/// Note that the in-memory behavior for entities that are currently tracked by
/// the <see cref="DbContext" /> can be different from the behavior that happens in the database.
/// See the <see cref="ClientSetNull" /> behavior for more details.
/// </para>
/// </summary>
public enum DeleteBehavior
{
/// <summary>
/// The delete operation is not applied to dependent entities. The dependent entities remain unchanged.
/// <para>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are set to null. This helps keep the graph of entities in a consistent
/// state while they are being tracked, such that a fully consistent graph can then be written to
/// the database. If a property cannot be set to null because it is not a nullable type,
/// then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// This is the same as the <see cref="SetNull" /> behavior.
/// </para>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// This is the same as the <see cref="Restrict" /> behavior.
/// </para>
/// <para>
/// This is the default for optional relationships. That is, for relationships that have
/// nullable foreign keys.
/// </para>
/// </summary>
ClientSetNull,

/// <summary>
/// <para>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are not changed. This can result in an inconsistent graph of entities
/// where the values of foreign key properties do not match the relationships in the
/// graph. If a property remains in this state when <see cref="DbContext.SaveChanges()" />
/// is called, then an exception will be thrown.
/// </para>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// </para>
/// </summary>
Restrict,

/// <summary>
/// The foreign key properties in dependent entities are set to null. This cascading behavior is only applied
/// to entities that are being tracked by the context. A corresponding cascade behavior should be setup in the
/// database to ensure data that is not being tracked by the context has the same action applied. If you use
/// EF to create the database, this cascade behavior will be setup for you.
/// <para>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are set to null. This helps keep the graph of entities in a consistent
/// state while they are being tracked, such that a fully consistent graph can then be written to
/// the database. If a property cannot be set to null because it is not a nullable type,
/// then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// </para>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database is
/// the same as is described above for tracked entities. Keep in mind that some databases cannot easily
/// support this behavior, especially if there are cycles in relationships.
/// </para>
/// </summary>
SetNull,

/// <summary>
/// Dependent entities are also deleted. This cascading behavior is only applied
/// to entities that are being tracked by the context. A corresponding cascade behavior should be setup in the
/// database to ensure data that is not being tracked by the context has the same action applied. If you use
/// EF to create the database, this cascade behavior will be setup for you.
/// <para>
/// For entities being tracked by the <see cref="DbContext" />, the dependent entities
/// will also be deleted when <see cref="DbContext.SaveChanges()" /> is called.
/// </para>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database is
/// the same as is described above for tracked entities. Keep in mind that some databases cannot easily
/// support this behavior, especially if there are cycles in relationships.
/// </para>
/// <para>
/// This is the default for required relationships. That is, for relationships that have
/// non-nullable foreign keys.
/// </para>
/// </summary>
Cascade
}
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/Metadata/Internal/ForeignKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ public virtual void SetDeleteBehavior(DeleteBehavior deleteBehavior, Configurati
UpdateDeleteBehaviorConfigurationSource(configurationSource);
}

private static DeleteBehavior DefaultDeleteBehavior => DeleteBehavior.Restrict;
private static DeleteBehavior DefaultDeleteBehavior => DeleteBehavior.ClientSetNull;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
Expand Down
54 changes: 54 additions & 0 deletions src/EFCore/breakingchanges.netcore.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,58 @@
[
{
"TypeId": "public enum Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior",
"Kind": "Removal"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IForeignKey : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable",
"MemberId": "Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior get_DeleteBehavior()",
"Kind": "Removal"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey : Microsoft.EntityFrameworkCore.Metadata.IForeignKey, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable",
"MemberId": "Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior get_DeleteBehavior()",
"Kind": "Removal"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey : Microsoft.EntityFrameworkCore.Metadata.IForeignKey, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable",
"MemberId": "System.Void set_DeleteBehavior(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior value)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<Microsoft.EntityFrameworkCore.Metadata.IMutableModel>, Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<Microsoft.EntityFrameworkCore.Metadata.Internal.InternalRelationshipBuilder>",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior deleteBehavior)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder<T0, T1> : Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder where T0 : class where T1 : class",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder<T0, T1> OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior deleteBehavior)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceReferenceBuilder : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<Microsoft.EntityFrameworkCore.Metadata.IMutableModel>, Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<Microsoft.EntityFrameworkCore.Metadata.Internal.InternalRelationshipBuilder>",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceReferenceBuilder OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior deleteBehavior)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceReferenceBuilder<T0, T1> : Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceReferenceBuilder where T0 : class where T1 : class",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceReferenceBuilder<T0, T1> OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior deleteBehavior)",
"Kind": "Removal"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IForeignKey : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable",
"MemberId": "Microsoft.EntityFrameworkCore.DeleteBehavior get_DeleteBehavior()",
"Kind": "Addition"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey : Microsoft.EntityFrameworkCore.Metadata.IForeignKey, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable",
"MemberId": "Microsoft.EntityFrameworkCore.DeleteBehavior get_DeleteBehavior()",
"Kind": "Addition"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey : Microsoft.EntityFrameworkCore.Metadata.IForeignKey, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable",
"MemberId": "System.Void set_DeleteBehavior(Microsoft.EntityFrameworkCore.DeleteBehavior value)",
"Kind": "Addition"
},
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Storage.DatabaseProviderServices : Microsoft.EntityFrameworkCore.Storage.IDatabaseProviderServices",
"Kind": "Removal"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ public void Unique_foreign_key()

var fk = Assert.Single(children.GetForeignKeys());
Assert.True(fk.IsUnique);
Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior);
Assert.Equal(DeleteBehavior.ClientSetNull, fk.DeleteBehavior);
}

[Fact]
Expand Down
Loading

0 comments on commit 4f217c1

Please sign in to comment.