From 23136204b96f9f1405549a5af3eaa859c70ca8c5 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 17 Aug 2022 09:01:44 -0700 Subject: [PATCH] Add a custom convention which adds a blank trigger to all tables Fixes #27531 --- .../Extensions/RelationalTriggerExtensions.cs | 2 - .../BlankTriggerAddingConvention.cs | 62 +++++++++++++++++++ .../Internal/DbContextExtensions.cs | 26 ++++++++ .../BlankTriggerAddingConventionTest.cs | 55 ++++++++++++++++ .../TestUtilities/TestModelSource.cs | 3 + .../SqlServerTriggersTest.cs | 14 +++-- 6 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs create mode 100644 src/EFCore/Extensions/Internal/DbContextExtensions.cs create mode 100644 test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs 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