Skip to content

Commit

Permalink
Add a custom convention which adds a blank trigger to all tables
Browse files Browse the repository at this point in the history
Fixes #27531
  • Loading branch information
AndriySvyryd committed Aug 17, 2022
1 parent a511eae commit 2313620
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A convention that makes sure there is a trigger on all entity types.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
/// </remarks>
public class BlankTriggerAddingConvention : IModelFinalizingConvention
{
/// <summary>
/// Creates a new instance of <see cref="BlankTriggerAddingConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="relationalDependencies"> Parameter object containing relational dependencies for this convention.</param>
public BlankTriggerAddingConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies)
{
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}

/// <summary>
/// Dependencies for this convention.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }

/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <summary>
/// Called when a model is being finalized.
/// </summary>
/// <param name="modelBuilder">The builder for the model.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> 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");
}
}
}
}
}
26 changes: 26 additions & 0 deletions src/EFCore/Extensions/Internal/DbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public static class DbContextExtensions
{
/// <summary>
/// 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.
/// </summary>
public static void ConfigureConventions(
this DbContext context,
ModelConfigurationBuilder configurationBuilder)
=> context.ConfigureConventions(configurationBuilder);
}
Original file line number Diff line number Diff line change
@@ -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<Order>().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<Order>().ToView("Orders");
modelBuilder.Entity<Order>().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<ProviderConventionSetBuilderDependencies>(),
p.GetRequiredService<RelationalConventionSetBuilderDependencies>())));
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);

Expand Down
14 changes: 8 additions & 6 deletions test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<Product>(
eb =>
{
eb.ToTable(tb =>
{
tb.HasTrigger("TRG_InsertProduct");
tb.HasTrigger("TRG_UpdateProduct");
tb.HasTrigger("TRG_DeleteProduct");
});
eb.Property(e => e.Version)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
Expand All @@ -122,6 +116,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<ProductBackup>()
.Property(e => e.Id).ValueGeneratedNever();
}

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(p =>
new BlankTriggerAddingConvention(
p.GetRequiredService<ProviderConventionSetBuilderDependencies>(),
p.GetRequiredService<RelationalConventionSetBuilderDependencies>()));
}
}

protected class Product
Expand Down

0 comments on commit 2313620

Please sign in to comment.