diff --git a/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs
index eece3f04c7c..4c27bf2a617 100644
--- a/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.EntityFrameworkCore.Metadata.Internal;
-
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
diff --git a/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs
new file mode 100644
index 00000000000..7e617d5ff7c
--- /dev/null
+++ b/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.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.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+///
+/// A convention that makes sure there is a trigger on all entity types.
+///
+///
+/// See Model building conventions for more information and examples.
+///
+public class BlankTriggerAddingConvention : IModelFinalizingConvention
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// Parameter object containing dependencies for this convention.
+ /// Parameter object containing relational dependencies for this convention.
+ public BlankTriggerAddingConvention(
+ ProviderConventionSetBuilderDependencies dependencies,
+ RelationalConventionSetBuilderDependencies relationalDependencies)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ }
+
+ ///
+ /// Dependencies for this convention.
+ ///
+ protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
+
+ ///
+ /// Called when a model is being finalized.
+ ///
+ /// The builder for the model.
+ /// Additional information associated with convention execution.
+ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context)
+ {
+ foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
+ {
+ var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
+ if (table != null
+ && entityType.GetDeclaredTriggers().All(t => t.GetName(table.Value) == null))
+ {
+ entityType.Builder.HasTrigger(table.Value.Name + "_Trigger");
+ }
+
+ foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table))
+ {
+ if (entityType.GetDeclaredTriggers().All(t => t.GetName(fragment.StoreObject) == null))
+ {
+ entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger");
+ }
+ }
+ }
+ }
+}
diff --git a/src/EFCore/Extensions/Internal/DbContextExtensions.cs b/src/EFCore/Extensions/Internal/DbContextExtensions.cs
new file mode 100644
index 00000000000..e747f45c422
--- /dev/null
+++ b/src/EFCore/Extensions/Internal/DbContextExtensions.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public static class DbContextExtensions
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public static void ConfigureConventions(
+ this DbContext context,
+ ModelConfigurationBuilder configurationBuilder)
+ => context.ConfigureConventions(configurationBuilder);
+}
diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs
new file mode 100644
index 00000000000..529b6084437
--- /dev/null
+++ b/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+// ReSharper disable InconsistentNaming
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+public class BlankTriggerAddingConventionTest
+{
+ [ConditionalFact]
+ public virtual void Adds_triggers_with_table_splitting()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity().SplitToTable("OrderDetails", s => s.Property(o => o.CustomerId));
+
+ var model = modelBuilder.FinalizeModel();
+
+ var entity = model.FindEntityType(typeof(Order))!;
+
+ Assert.Equal(new[] { "OrderDetails_Trigger", "Order_Trigger" }, entity.GetDeclaredTriggers().Select(t => t.ModelName));
+ }
+
+ [ConditionalFact]
+ public virtual void Does_not_add_triggers_without_tables()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity().ToView("Orders");
+ modelBuilder.Entity().SplitToView("OrderDetails", s => s.Property(o => o.CustomerId));
+
+ var model = modelBuilder.FinalizeModel();
+
+ var entity = model.FindEntityType(typeof(Order))!;
+
+ Assert.Empty(entity.GetDeclaredTriggers());
+ }
+
+ protected class Order
+ {
+ public int OrderId { get; set; }
+
+ public int? CustomerId { get; set; }
+ public Guid AnotherCustomerId { get; set; }
+ }
+
+ protected virtual ModelBuilder CreateModelBuilder()
+ => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(
+ configureModel:
+ b => b.Conventions.Add(
+ p => new BlankTriggerAddingConvention(
+ p.GetRequiredService(),
+ p.GetRequiredService())));
+}
diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs
index 41ddbec93d9..fb7f21cdf7e 100644
--- a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs
+++ b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Internal;
+
namespace Microsoft.EntityFrameworkCore.TestUtilities;
public class TestModelSource : ModelSource
@@ -26,6 +28,7 @@ protected override IModel CreateModel(
var modelConfigurationBuilder = new ModelConfigurationBuilder(
conventionSetBuilder.CreateConventionSet(),
context.GetInfrastructure());
+ context.ConfigureConventions(modelConfigurationBuilder);
_configureConventions?.Invoke(modelConfigurationBuilder);
var modelBuilder = modelConfigurationBuilder.CreateModelBuilder(modelDependencies);
diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs
index 17fc19a4dcc..85d914fdc58 100644
--- a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs
@@ -107,12 +107,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity(
eb =>
{
- eb.ToTable(tb =>
- {
- tb.HasTrigger("TRG_InsertProduct");
- tb.HasTrigger("TRG_UpdateProduct");
- tb.HasTrigger("TRG_DeleteProduct");
- });
eb.Property(e => e.Version)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
@@ -122,6 +116,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity()
.Property(e => e.Id).ValueGeneratedNever();
}
+
+ protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
+ {
+ configurationBuilder.Conventions.Add(p =>
+ new BlankTriggerAddingConvention(
+ p.GetRequiredService(),
+ p.GetRequiredService()));
+ }
}
protected class Product