Skip to content

Commit

Permalink
"Scaffold" triggers for SQL Server
Browse files Browse the repository at this point in the history
HasTrigger with trigger name only, to make the SQL Server SaveChanges work out of the box.

Closes #28185
  • Loading branch information
roji committed Jun 18, 2022
1 parent f755ffc commit d2e0750
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 40 deletions.
37 changes: 37 additions & 0 deletions src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,43 @@ private void GenerateEntityType(IEntityType entityType)
GenerateManyToMany(skipNavigation);
}
}

var triggers = entityType.GetTriggers().ToArray();

if (triggers.Length > 0)
{
using (_builder.Indent())
{
_builder.AppendLine();

_builder.Append($"{EntityLambdaIdentifier}.{nameof(RelationalEntityTypeBuilderExtensions.ToTable)}(tb => ");

// Note: no trigger annotation support as of yet

if (triggers.Length == 1)
{
var trigger = triggers[0];
if (trigger.Name is not null)
{
_builder.AppendLine($"tb.HasTrigger({_code.Literal(trigger.Name)}));");
}
}
else
{
_builder.AppendLine("{");

using (_builder.Indent())
{
foreach (var trigger in entityType.GetTriggers().Where(t => t.Name is not null))
{
_builder.AppendLine($"tb.HasTrigger({_code.Literal(trigger.Name!)});");
}
}

_builder.AppendLine("});");
}
}
}
}

private void AppendMultiLineFluentApi(IEntityType entityType, IList<string> lines)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ protected virtual ModelBuilder VisitTables(ModelBuilder modelBuilder, ICollectio
VisitUniqueConstraints(builder, table.UniqueConstraints);
VisitIndexes(builder, table.Indexes);

if (table.FindAnnotation(RelationalAnnotationNames.Triggers) is { Value: HashSet<string> triggers })
{
foreach (var triggerName in triggers)
{
builder.ToTable(table.Name, table.Schema, tb => tb.HasTrigger(triggerName));
}

table.RemoveAnnotation(RelationalAnnotationNames.Triggers);
}

builder.Metadata.AddAnnotations(table.GetAnnotations());

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ FROM [sys].[views] AS [v]
GetColumns(connection, tables, filter, viewFilter, typeAliases, databaseCollation);
GetIndexes(connection, tables, filter);
GetForeignKeys(connection, tables, filter);
GetTriggers(connection, tables, filter);

foreach (var table in tables)
{
Expand Down Expand Up @@ -1295,6 +1296,48 @@ FROM [sys].[foreign_keys] AS [f]
}
}

private void GetTriggers(DbConnection connection, IReadOnlyList<DatabaseTable> tables, string tableFilter)
{
using var command = connection.CreateCommand();
command.CommandText = @"
SELECT
SCHEMA_NAME([t].[schema_id]) AS [table_schema],
[t].[name] AS [table_name],
[tr].[name] AS [trigger_name]
FROM [sys].[triggers] AS [tr]
JOIN [sys].[tables] AS [t] ON [tr].[parent_id] = [t].[object_id]
WHERE "
+ tableFilter
+ @"
ORDER BY [table_schema], [table_name], [tr].[name]";

using var reader = command.ExecuteReader();
var tableGroups = reader.Cast<DbDataRecord>()
.GroupBy(
ddr => (tableSchema: ddr.GetValueOrDefault<string>("table_schema"),
tableName: ddr.GetFieldValue<string>("table_name")));

foreach (var tableGroup in tableGroups)
{
var tableSchema = tableGroup.Key.tableSchema;
var tableName = tableGroup.Key.tableName;

var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName);

var triggers = new HashSet<string>();
table[RelationalAnnotationNames.Triggers] = triggers;

foreach (var triggerRecord in tableGroup)
{
var triggerName = triggerRecord.GetFieldValue<string>("trigger_name");

// We don't actually scaffold anything beyond the fact that there's a trigger with a given name.
// This is to modify the SaveChanges logic to not use OUTPUT without INTO, which is incompatible with triggers.
triggers.Add(triggerName);
}
}
}

private bool SupportsTemporalTable()
=> _compatibilityLevel >= 130 && _engineEdition != 6;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,88 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
// TODO
})).Message);

[ConditionalFact]
public void Trigger_works()
=> Test(
modelBuilder => modelBuilder
.Entity(
"Employee",
x =>
{
x.Property<int>("Id");
x.ToTable(
tb =>
{
tb.HasTrigger("Trigger1");
tb.HasTrigger("Trigger2");
});
}),
new ModelCodeGenerationOptions { UseDataAnnotations = false },
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace TestNamespace
{
public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
public virtual DbSet<Employee> Employee { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning "
+ DesignStrings.SensitiveInformationWarning
+ @"
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();
entity.ToTable(tb => {
tb.HasTrigger(""Trigger1"");
tb.HasTrigger(""Trigger2"");
});
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
",
code.ContextFile);
},
model =>
{
var entityType = model.FindEntityType("TestNamespace.Employee")!;
var triggers = entityType.GetTriggers();
Assert.Collection(triggers.OrderBy(t => t.Name),
t => Assert.Equal("Trigger1", t.Name),
t => Assert.Equal("Trigger2", t.Name));
});

protected override void AddModelServices(IServiceCollection services)
=> services.Replace(ServiceDescriptor.Singleton<IRelationalAnnotationProvider, TestModelAnnotationProvider>());

Expand Down
Loading

0 comments on commit d2e0750

Please sign in to comment.