Skip to content

"Scaffold" triggers for SQL Server #28253

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the annotation value here is HashSet<string>, which is different from what we do via the Fluent API (Dictionary<string, ITrigger>), because ITrigger needs to reference an IMutableEntityType which we don't have here. The translation from this to that happens in RelationalScaffoldingModelFactory.


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