Skip to content

Commit 1d0763c

Browse files
committed
Allow opting out of RETURNING/OUTPUT clauses in SaveChanges
Fixes dotnet#29916
1 parent 4db857b commit 1d0763c

File tree

54 files changed

+1839
-163
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1839
-163
lines changed

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1458,8 +1458,7 @@ public static bool IsTableExcludedFromMigrations(this IReadOnlyEntityType entity
14581458
}
14591459

14601460
var ownership = entityType.FindOwnership();
1461-
if (ownership != null
1462-
&& ownership.IsUnique)
1461+
if (ownership is { IsUnique: true })
14631462
{
14641463
return ownership.PrincipalEntityType.IsTableExcludedFromMigrations();
14651464
}

src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,14 @@ public static IEnumerable<IForeignKeyConstraint> GetMappedConstraints(this IFore
154154
this IReadOnlyForeignKey foreignKey,
155155
in StoreObjectIdentifier storeObject)
156156
{
157+
if (foreignKey.PrincipalEntityType.GetTableName() is not { } principalTableName)
158+
{
159+
return null;
160+
}
161+
157162
var foreignKeyName = foreignKey.GetConstraintName(
158163
storeObject,
159-
StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName()!, foreignKey.PrincipalEntityType.GetSchema()));
164+
StoreObjectIdentifier.Table(principalTableName, foreignKey.PrincipalEntityType.GetSchema()));
160165
var rootForeignKey = foreignKey;
161166

162167
// Limit traversal to avoid getting stuck in a cycle (validation will throw for these later)
@@ -168,11 +173,16 @@ public static IEnumerable<IForeignKeyConstraint> GetMappedConstraints(this IFore
168173
.FindRowInternalForeignKeys(storeObject)
169174
.SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys()))
170175
{
176+
principalTableName = otherForeignKey.PrincipalEntityType.GetTableName();
177+
178+
if (principalTableName is null)
179+
{
180+
return null;
181+
}
182+
171183
if (otherForeignKey.GetConstraintName(
172184
storeObject,
173-
StoreObjectIdentifier.Table(
174-
otherForeignKey.PrincipalEntityType.GetTableName()!,
175-
otherForeignKey.PrincipalEntityType.GetSchema()))
185+
StoreObjectIdentifier.Table(principalTableName, otherForeignKey.PrincipalEntityType.GetSchema()))
176186
== foreignKeyName)
177187
{
178188
linkedForeignKey = otherForeignKey;

src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs

+13-3
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,19 @@ public static void SetDatabaseName(this IMutableTrigger trigger, string? name)
108108
/// </summary>
109109
/// <param name="trigger">The trigger.</param>
110110
/// <returns>The name of the table on which this trigger is defined.</returns>
111-
public static string? GetTableName(this IReadOnlyTrigger trigger)
112-
=> (string?)trigger.FindAnnotation(RelationalAnnotationNames.TableName)?.Value
113-
?? trigger.EntityType.GetTableName()!;
111+
public static string GetTableName(this IReadOnlyTrigger trigger)
112+
{
113+
if (trigger.FindAnnotation(RelationalAnnotationNames.TableName) is { Value: string tableName })
114+
{
115+
return tableName;
116+
}
117+
118+
var mainTableName = trigger.EntityType.GetTableName();
119+
120+
Check.DebugAssert(mainTableName is not null, "Trigger defined on entity not mapped to a table");
121+
122+
return mainTableName;
123+
}
114124

115125
/// <summary>
116126
/// Sets the name of the table on which this trigger is defined.

src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -2474,8 +2474,15 @@ protected override void ValidateTriggers(
24742474
IModel model,
24752475
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
24762476
{
2477-
foreach (var entityType in model.GetEntityTypes())
2477+
foreach (var entityType in model.GetEntityTypes().Where(e => e.GetDeclaredTriggers().Any()))
24782478
{
2479+
if (entityType.BaseType is not null
2480+
&& entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
2481+
{
2482+
throw new InvalidOperationException(
2483+
RelationalStrings.CannotConfigureTriggerNonRootTphEntity(entityType.DisplayName()));
2484+
}
2485+
24792486
var tableName = entityType.GetTableName();
24802487
var tableSchema = entityType.GetSchema();
24812488

src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public virtual string? Schema
5757
public virtual IMutableEntityTypeMappingFragment MappingFragment
5858
=> InternalMappingFragment;
5959

60+
/// <summary>
61+
/// The entity type being configured.
62+
/// </summary>
63+
public virtual IMutableEntityType Metadata
64+
=> EntityTypeBuilder.Metadata;
65+
6066
private EntityTypeBuilder EntityTypeBuilder { get; }
6167

6268
/// <summary>

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@
130130
<data name="CannotChangeWhenOpen" xml:space="preserve">
131131
<value>The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.</value>
132132
</data>
133+
<data name="CannotConfigureTriggerNonRootTphEntity" xml:space="preserve">
134+
<value>Can't configure a trigger on entity type '{entityType}', which is in a TPH hierarchy and isn't the root. Configure the trigger on the TPH root entity type instead.</value>
135+
</data>
133136
<data name="ClientGroupByNotSupported" xml:space="preserve">
134137
<value>Unable to translate the given 'GroupBy' pattern. Call 'AsEnumerable' before 'GroupBy' to evaluate it client-side.</value>
135138
</data>

src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs

+152
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
45
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
56

67
// ReSharper disable once CheckNamespace
@@ -18,6 +19,8 @@ public static class SqlServerEntityTypeExtensions
1819
{
1920
private const string DefaultHistoryTableNameSuffix = "History";
2021

22+
#region Memory-optimized table
23+
2124
/// <summary>
2225
/// Returns a value indicating whether the entity type is mapped to a memory-optimized table.
2326
/// </summary>
@@ -58,6 +61,10 @@ public static void SetIsMemoryOptimized(this IMutableEntityType entityType, bool
5861
public static ConfigurationSource? GetIsMemoryOptimizedConfigurationSource(this IConventionEntityType entityType)
5962
=> entityType.FindAnnotation(SqlServerAnnotationNames.MemoryOptimized)?.GetConfigurationSource();
6063

64+
#endregion Memory-optimized table
65+
66+
#region Temporal table
67+
6168
/// <summary>
6269
/// Returns a value indicating whether the entity type is mapped to a temporal table.
6370
/// </summary>
@@ -271,4 +278,149 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str
271278
/// <returns>The configuration source for the temporal history table schema setting.</returns>
272279
public static ConfigurationSource? GetHistoryTableSchemaConfigurationSource(this IConventionEntityType entityType)
273280
=> entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema)?.GetConfigurationSource();
281+
282+
#endregion Temporal table
283+
284+
#region SQL OUTPUT clause
285+
286+
/// <summary>
287+
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
288+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
289+
/// </summary>
290+
/// <param name="entityType">The entity type.</param>
291+
/// <returns><see langword="true" /> if the SQL OUTPUT clause is used to save changes to the table.</returns>
292+
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType)
293+
{
294+
if (entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is { Value: bool useSqlOutputClause })
295+
{
296+
return useSqlOutputClause;
297+
}
298+
299+
if (entityType.FindOwnership() is { } ownership
300+
&& StoreObjectIdentifier.Create(entityType, StoreObjectType.Table) is { } tableIdentifier
301+
&& ownership.FindSharedObjectRootForeignKey(tableIdentifier) is { } rootForeignKey)
302+
{
303+
return rootForeignKey.PrincipalEntityType.IsSqlOutputClauseUsed();
304+
}
305+
306+
if (entityType.BaseType is not null && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
307+
{
308+
return entityType.GetRootType().IsSqlOutputClauseUsed();
309+
}
310+
311+
return true;
312+
}
313+
314+
/// <summary>
315+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
316+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
317+
/// </summary>
318+
/// <param name="entityType">The entity type.</param>
319+
/// <param name="useSqlOutputClause">The value to set.</param>
320+
public static void UseSqlOutputClause(this IMutableEntityType entityType, bool? useSqlOutputClause)
321+
=> entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);
322+
323+
/// <summary>
324+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
325+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
326+
/// </summary>
327+
/// <param name="entityType">The entity type.</param>
328+
/// <param name="useSqlOutputClause">The value to set.</param>
329+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
330+
/// <returns>The configured value.</returns>
331+
public static bool? UseSqlOutputClause(
332+
this IConventionEntityType entityType,
333+
bool? useSqlOutputClause,
334+
bool fromDataAnnotation = false)
335+
=> (bool?)entityType.SetOrRemoveAnnotation(
336+
SqlServerAnnotationNames.UseSqlOutputClause,
337+
useSqlOutputClause,
338+
fromDataAnnotation)?.Value;
339+
340+
/// <summary>
341+
/// Gets the configuration source for whether to use the SQL OUTPUT clause when saving changes to the table.
342+
/// </summary>
343+
/// <param name="entityType">The entity type.</param>
344+
/// <returns>The configuration source for the memory-optimized setting.</returns>
345+
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityType entityType)
346+
=> entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();
347+
348+
/// <summary>
349+
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the specified table.
350+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
351+
/// </summary>
352+
/// <param name="entityType">The entity type.</param>
353+
/// <param name="storeObject">The identifier of the table-like store object.</param>
354+
/// <returns>A value indicating whether the SQL OUTPUT clause is used to save changes to the associated table.</returns>
355+
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType, in StoreObjectIdentifier storeObject)
356+
{
357+
if (entityType.FindMappingFragment(storeObject) is { } overrides
358+
&& overrides.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is { Value: bool useSqlOutputClause })
359+
{
360+
return useSqlOutputClause;
361+
}
362+
363+
if (StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject)
364+
{
365+
return entityType.IsSqlOutputClauseUsed();
366+
}
367+
368+
if (entityType.FindOwnership() is { } ownership
369+
&& ownership.FindSharedObjectRootForeignKey(storeObject) is { } rootForeignKey)
370+
{
371+
return rootForeignKey.PrincipalEntityType.IsSqlOutputClauseUsed(storeObject);
372+
}
373+
374+
if (entityType.BaseType is not null && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
375+
{
376+
return entityType.GetRootType().IsSqlOutputClauseUsed(storeObject);
377+
}
378+
379+
return true;
380+
}
381+
382+
/// <summary>
383+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
384+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
385+
/// </summary>
386+
/// <param name="entityType">The entity type.</param>
387+
/// <param name="useSqlOutputClause">The value to set.</param>
388+
/// <param name="storeObject">The identifier of the table-like store object.</param>
389+
public static void UseSqlOutputClause(
390+
this IMutableEntityType entityType,
391+
bool? useSqlOutputClause,
392+
in StoreObjectIdentifier storeObject)
393+
{
394+
if (StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject)
395+
{
396+
entityType.UseSqlOutputClause(useSqlOutputClause);
397+
return;
398+
}
399+
400+
entityType
401+
.GetOrCreateMappingFragment(storeObject)
402+
.UseSqlOutputClause(useSqlOutputClause);
403+
}
404+
405+
/// <summary>
406+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
407+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
408+
/// </summary>
409+
/// <param name="entityType">The entity type.</param>
410+
/// <param name="useSqlOutputClause">The value to set.</param>
411+
/// <param name="storeObject">The identifier of the table-like store object.</param>
412+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
413+
/// <returns>The configured value.</returns>
414+
public static bool? UseSqlOutputClause(
415+
this IConventionEntityType entityType,
416+
bool? useSqlOutputClause,
417+
in StoreObjectIdentifier storeObject,
418+
bool fromDataAnnotation = false)
419+
=> StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject
420+
? entityType.UseSqlOutputClause(useSqlOutputClause, fromDataAnnotation)
421+
: entityType
422+
.GetOrCreateMappingFragment(storeObject, fromDataAnnotation)
423+
.UseSqlOutputClause(useSqlOutputClause, fromDataAnnotation);
424+
425+
#endregion SQL OUTPUT clause
274426
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
5+
6+
// ReSharper disable once CheckNamespace
7+
namespace Microsoft.EntityFrameworkCore;
8+
9+
/// <summary>
10+
/// SQL Server specific extension methods for <see cref="IReadOnlyEntityTypeMappingFragment" />.
11+
/// </summary>
12+
public static class SqlServerEntityTypeMappingFragmentExtensions
13+
{
14+
/// <summary>
15+
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the associated table.
16+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
17+
/// </summary>
18+
/// <param name="fragment">The entity type mapping fragment.</param>
19+
/// <returns>The configured value.</returns>
20+
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityTypeMappingFragment fragment)
21+
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is not { Value: false };
22+
23+
/// <summary>
24+
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
25+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
26+
/// </summary>
27+
/// <param name="fragment">The entity type mapping fragment.</param>
28+
/// <param name="useSqlOutputClause">The value to set.</param>
29+
public static void UseSqlOutputClause(this IMutableEntityTypeMappingFragment fragment, bool? useSqlOutputClause)
30+
=> fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);
31+
32+
/// <summary>
33+
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
34+
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
35+
/// </summary>
36+
/// <param name="fragment">The entity type mapping fragment.</param>
37+
/// <param name="useSqlOutputClause">The value to set.</param>
38+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
39+
/// <returns>The configured value.</returns>
40+
public static bool? UseSqlOutputClause(
41+
this IConventionEntityTypeMappingFragment fragment,
42+
bool? useSqlOutputClause,
43+
bool fromDataAnnotation = false)
44+
=> (bool?)fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause, fromDataAnnotation)?.Value;
45+
46+
/// <summary>
47+
/// Gets the configuration source for the setting whether to use the SQL OUTPUT clause when saving changes to the associated table.
48+
/// </summary>
49+
/// <param name="fragment">The entity type mapping fragment.</param>
50+
/// <returns>The configuration source for the configured value.</returns>
51+
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityTypeMappingFragment fragment)
52+
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();
53+
}

0 commit comments

Comments
 (0)