diff --git a/README.md b/README.md
index 9bb4fef..a6b9367 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ The following features are supported:
- Tables from classes (supported annotations: `Table`)
- Columns from properties (supported annotations: `Column`, `Key`, `MaxLength`, `Required`, `NotMapped`, `DatabaseGenerated`, `Index`)
- PrimaryKey constraint (`Key` annotation, key composites are supported)
-- ForeignKey constraint (1-n relationships, support for 'Cascade on delete')
+- ForeignKey constraint (1-n relationships, support for 'Cascade on delete' and 'Cascade on update')
- Not Null constraint
- Auto increment (An int PrimaryKey will automatically be incremented and you can explicitly set the "AUTOINCREMENT" constraint to a PrimaryKey using the Autoincrement-Attribute)
- Index (Decorate columns with the `Index` attribute. Indices are automatically created for foreign keys by default. To prevent this you can remove the convention `ForeignKeyIndexConvention`)
@@ -36,6 +36,7 @@ The following features are supported:
- Collate constraint (Decorate columns with the `CollateAttribute`, which is part of this library. Use `CollationFunction.Custom` to specify your own collation function.)
- Default collation (pass an instance of Collation as constructor parameter for an initializer to specify a default collation).
- SQL default value (Decorate columns with the `SqlDefaultValueAttribute`, which is part of this library)
+- Cascade on update (Decorate the foreign key property with the `CascadeOnUpdateAttribute`, which is part of this library. Entity Framework cannot express `ON UPDATE CASCADE`, so this opt-in attribute must be placed on the dependent foreign key property, e.g. `TeamId`, not on the navigation property. It requires an explicit foreign key property.)
## Install
diff --git a/SQLite.CodeFirst.Test/IntegrationTests/CascadeOnUpdateTest.cs b/SQLite.CodeFirst.Test/IntegrationTests/CascadeOnUpdateTest.cs
new file mode 100644
index 0000000..1e7b45c
--- /dev/null
+++ b/SQLite.CodeFirst.Test/IntegrationTests/CascadeOnUpdateTest.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Data.SQLite;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace SQLite.CodeFirst.Test.IntegrationTests
+{
+ ///
+ /// Verifies that the placed on a foreign key property
+ /// results in 'ON UPDATE CASCADE' being emitted for the corresponding foreign key constraint.
+ /// This exercises the full pipeline: the attribute is registered as a column annotation, survives
+ /// the EF model build and is read back from the store model when the SQL is generated.
+ ///
+ [TestClass]
+ public class CascadeOnUpdateTest
+ {
+ private static string generatedSql;
+
+ [TestMethod]
+ public void CascadeOnUpdateAttributeEmitsOnUpdateCascade()
+ {
+ using (DbConnection connection = new SQLiteConnection("FullUri=file::memory:"))
+ {
+ // This is important! Else the in memory database will not work.
+ connection.Open();
+
+ using (var context = new CascadeDbContext(connection))
+ {
+ // Touching the set forces the initializer (and therefore the SQL generation) to run.
+ context.Set().FirstOrDefault();
+ }
+ }
+
+ // Decorated FK with cascade-on-delete: both keywords, delete before update.
+ StringAssert.Contains(generatedSql, "FOREIGN KEY ([CustomerId]) REFERENCES \"Customers\"([Id]) ON DELETE CASCADE ON UPDATE CASCADE");
+
+ // Decorated FK without cascade-on-delete: only the update keyword.
+ StringAssert.Contains(generatedSql, "FOREIGN KEY ([WarehouseId]) REFERENCES \"Warehouses\"([Id]) ON UPDATE CASCADE");
+
+ // Undecorated FK: no cascade keyword at all.
+ Assert.IsFalse(generatedSql.Contains("[RegionId]) REFERENCES \"Regions\"([Id]) ON"),
+ "The foreign key without the attribute must not emit any cascade clause.");
+ }
+
+ private class Customer
+ {
+ public int Id { get; set; }
+ }
+
+ private class Warehouse
+ {
+ public int Id { get; set; }
+ }
+
+ private class Region
+ {
+ public int Id { get; set; }
+ }
+
+ private class Order
+ {
+ public int Id { get; set; }
+
+ [CascadeOnUpdate]
+ public int CustomerId { get; set; }
+ public Customer Customer { get; set; }
+
+ [CascadeOnUpdate]
+ public int? WarehouseId { get; set; }
+ public Warehouse Warehouse { get; set; }
+
+ public int? RegionId { get; set; }
+ public Region Region { get; set; }
+ }
+
+ private class CascadeDbContext : DbContext
+ {
+ public CascadeDbContext(DbConnection connection)
+ : base(connection, false)
+ {
+ }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasRequired(o => o.Customer)
+ .WithMany()
+ .HasForeignKey(o => o.CustomerId)
+ .WillCascadeOnDelete(true);
+
+ modelBuilder.Entity()
+ .HasOptional(o => o.Warehouse)
+ .WithMany()
+ .HasForeignKey(o => o.WarehouseId)
+ .WillCascadeOnDelete(false);
+
+ modelBuilder.Entity()
+ .HasOptional(o => o.Region)
+ .WithMany()
+ .HasForeignKey(o => o.RegionId)
+ .WillCascadeOnDelete(false);
+
+ Database.SetInitializer(new CaptureInitializer(modelBuilder));
+ }
+ }
+
+ private class CaptureInitializer : SqliteInitializerBase
+ {
+ public CaptureInitializer(DbModelBuilder modelBuilder)
+ : base(modelBuilder)
+ {
+ }
+
+ public override void InitializeDatabase(CascadeDbContext context)
+ {
+ DbModel model = ModelBuilder.Build(context.Database.Connection);
+ generatedSql = new SqliteSqlGenerator().Generate(model.StoreModel);
+ base.InitializeDatabase(context);
+ }
+ }
+ }
+}
diff --git a/SQLite.CodeFirst.Test/UnitTests/Statement/ForeignKeyStatementTest.cs b/SQLite.CodeFirst.Test/UnitTests/Statement/ForeignKeyStatementTest.cs
index 0a8ac9f..65dd2d7 100644
--- a/SQLite.CodeFirst.Test/UnitTests/Statement/ForeignKeyStatementTest.cs
+++ b/SQLite.CodeFirst.Test/UnitTests/Statement/ForeignKeyStatementTest.cs
@@ -37,6 +37,37 @@ public void CreateStatementOneForeignKeyCascadeDeleteTest()
Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON DELETE CASCADE", output);
}
+ [TestMethod]
+ public void CreateStatementOneForeignKeyCascadeUpdateTest()
+ {
+ var foreignKeyStatement = new ForeignKeyStatement
+ {
+ CascadeUpdate = true,
+ ForeignKey = new List { "dummyForeignKey1" },
+ ForeignPrimaryKey = new List { "dummForeignPrimaryKey1" },
+ ForeignTable = "dummyForeignTable"
+ };
+
+ string output = foreignKeyStatement.CreateStatement();
+ Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON UPDATE CASCADE", output);
+ }
+
+ [TestMethod]
+ public void CreateStatementOneForeignKeyCascadeDeleteAndUpdateTest()
+ {
+ var foreignKeyStatement = new ForeignKeyStatement
+ {
+ CascadeDelete = true,
+ CascadeUpdate = true,
+ ForeignKey = new List { "dummyForeignKey1" },
+ ForeignPrimaryKey = new List { "dummForeignPrimaryKey1" },
+ ForeignTable = "dummyForeignTable"
+ };
+
+ string output = foreignKeyStatement.CreateStatement();
+ Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON DELETE CASCADE ON UPDATE CASCADE", output);
+ }
+
[TestMethod]
public void CreateStatementTwoForeignKeyTest()
{
diff --git a/SQLite.CodeFirst/Internal/Builder/ForeignKeyStatementBuilder.cs b/SQLite.CodeFirst/Internal/Builder/ForeignKeyStatementBuilder.cs
index e5fd81a..0dd595c 100644
--- a/SQLite.CodeFirst/Internal/Builder/ForeignKeyStatementBuilder.cs
+++ b/SQLite.CodeFirst/Internal/Builder/ForeignKeyStatementBuilder.cs
@@ -29,7 +29,8 @@ private IEnumerable GetForeignKeyStatements()
ForeignKey = associationType.ForeignKey,
ForeignTable = associationType.FromTableName,
ForeignPrimaryKey = associationType.ForeignPrimaryKey,
- CascadeDelete = associationType.CascadeDelete
+ CascadeDelete = associationType.CascadeDelete,
+ CascadeUpdate = associationType.CascadeUpdate
};
}
}
diff --git a/SQLite.CodeFirst/Internal/Statement/ForeignKeyStatement.cs b/SQLite.CodeFirst/Internal/Statement/ForeignKeyStatement.cs
index 58b4974..6b862a7 100644
--- a/SQLite.CodeFirst/Internal/Statement/ForeignKeyStatement.cs
+++ b/SQLite.CodeFirst/Internal/Statement/ForeignKeyStatement.cs
@@ -9,11 +9,13 @@ internal class ForeignKeyStatement : IStatement
{
private const string Template = "FOREIGN KEY ({foreign-key}) REFERENCES {referenced-table}({referenced-id})";
private const string CascadeDeleteStatement = "ON DELETE CASCADE";
+ private const string CascadeUpdateStatement = "ON UPDATE CASCADE";
public IEnumerable ForeignKey { get; set; }
public string ForeignTable { get; set; }
public IEnumerable ForeignPrimaryKey { get; set; }
public bool CascadeDelete { get; set; }
+ public bool CascadeUpdate { get; set; }
public string CreateStatement()
{
@@ -25,6 +27,10 @@ public string CreateStatement()
{
sb.Append(" " + CascadeDeleteStatement);
}
+ if (CascadeUpdate)
+ {
+ sb.Append(" " + CascadeUpdateStatement);
+ }
return sb.ToString();
}
diff --git a/SQLite.CodeFirst/Internal/Utility/SqliteAssociationType.cs b/SQLite.CodeFirst/Internal/Utility/SqliteAssociationType.cs
index 6ab4f49..7532614 100644
--- a/SQLite.CodeFirst/Internal/Utility/SqliteAssociationType.cs
+++ b/SQLite.CodeFirst/Internal/Utility/SqliteAssociationType.cs
@@ -2,6 +2,7 @@
using System.Data.Entity.Core.Metadata.Edm;
using System.Linq;
using SQLite.CodeFirst.Builder.NameCreators;
+using SQLite.CodeFirst.Extensions;
namespace SQLite.CodeFirst.Utility
{
@@ -30,6 +31,13 @@ public SqliteAssociationType(AssociationType associationType, EntityContainer co
ForeignKey = associationType.Constraint.ToProperties.Select(x => x.Name);
ForeignPrimaryKey = associationType.Constraint.FromProperties.Select(x => x.Name);
CascadeDelete = associationType.Constraint.FromRole.DeleteBehavior == OperationAction.Cascade;
+
+ // EF has no notion of update-cascade, so it is opt-in via the CascadeOnUpdateAttribute placed on the
+ // dependent foreign key property. The attribute is registered as a column annotation and therefore
+ // available on the store model foreign key columns (the constraint's ToProperties).
+ CascadeUpdate = associationType.Constraint.ToProperties
+ .Select(property => property.GetCustomAnnotation())
+ .Any(attribute => attribute != null && attribute.CanCascade);
}
private static bool IsSelfReferencing(AssociationType associationType)
@@ -47,5 +55,6 @@ private static bool IsSelfReferencing(AssociationType associationType)
public string ToTableName { get; }
public IEnumerable ForeignPrimaryKey { get; }
public bool CascadeDelete { get; }
+ public bool CascadeUpdate { get; }
}
}
\ No newline at end of file
diff --git a/SQLite.CodeFirst/Public/Attributes/CascadeOnUpdateAttribute.cs b/SQLite.CodeFirst/Public/Attributes/CascadeOnUpdateAttribute.cs
new file mode 100644
index 0000000..5870ccf
--- /dev/null
+++ b/SQLite.CodeFirst/Public/Attributes/CascadeOnUpdateAttribute.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace SQLite.CodeFirst
+{
+ ///
+ /// Adds 'ON UPDATE CASCADE' to the foreign key constraint of the relationship the decorated property belongs to.
+ /// Entity Framework has no concept of update-cascade (it assumes primary keys are immutable), so neither the
+ /// nor the fluent API can express it.
+ /// This opt-in attribute is the only way to emit the keyword.
+ /// Place it on the dependent foreign key property (e.g. the 'TeamId' property), not on the navigation property (e.g. 'Team').
+ /// Requires an explicit foreign key property; relationships with a shadow foreign key column cannot be decorated.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class CascadeOnUpdateAttribute : Attribute
+ {
+ public CascadeOnUpdateAttribute()
+ {
+ CanCascade = true;
+ }
+
+ public CascadeOnUpdateAttribute(bool canCascade)
+ {
+ CanCascade = canCascade;
+ }
+
+ public bool CanCascade { get; }
+ }
+}
diff --git a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs
index 1309b1f..4e20b33 100644
--- a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs
+++ b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs
@@ -40,6 +40,7 @@ protected SqliteInitializerBase(DbModelBuilder modelBuilder, Collation defaultCo
modelBuilder.RegisterAttributeAsColumnAnnotation();
modelBuilder.RegisterAttributeAsColumnAnnotation();
modelBuilder.RegisterAttributeAsColumnAnnotation();
+ modelBuilder.RegisterAttributeAsColumnAnnotation();
// By default there is a 'ForeignKeyIndexConvention' but it can be removed.
// And there is no "Contains" and no way to enumerate the ConventionsCollection.