Skip to content

Commit a3efd1a

Browse files
committed
Allow opting out of RETURNING/OUTPUT clauses in SaveChanges
Fixes #29916
1 parent 06a41bb commit a3efd1a

20 files changed

+575
-142
lines changed

src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,4 +271,47 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str
271271
/// <returns>The configuration source for the temporal history table schema setting.</returns>
272272
public static ConfigurationSource? GetHistoryTableSchemaConfigurationSource(this IConventionEntityType entityType)
273273
=> entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema)?.GetConfigurationSource();
274+
275+
/// <summary>
276+
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is
277+
/// incompatible with certain SQL Server features, such as tables with triggers.
278+
/// </summary>
279+
/// <param name="entityType">The entity type.</param>
280+
/// <returns><see langword="true" /> if the SQL OUTPUT clause is used to save changes to the table.</returns>
281+
public static bool UseSqlOutputClause(this IReadOnlyEntityType entityType)
282+
=> entityType[SqlServerAnnotationNames.UseSqlOutputClause] as bool? ?? true;
283+
284+
/// <summary>
285+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible
286+
/// with certain SQL Server features, such as tables with triggers.
287+
/// </summary>
288+
/// <param name="entityType">The entity type.</param>
289+
/// <param name="useSqlOutputClause">The value to set.</param>
290+
public static void UseSqlOutputClause(this IMutableEntityType entityType, bool useSqlOutputClause)
291+
=> entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);
292+
293+
/// <summary>
294+
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible
295+
/// with certain SQL Server features, such as tables with triggers.
296+
/// </summary>
297+
/// <param name="entityType">The entity type.</param>
298+
/// <param name="useSqlOutputClause">The value to set.</param>
299+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
300+
/// <returns>The configured value.</returns>
301+
public static bool? UseSqlOutputClause(
302+
this IConventionEntityType entityType,
303+
bool? useSqlOutputClause,
304+
bool fromDataAnnotation = false)
305+
=> (bool?)entityType.SetOrRemoveAnnotation(
306+
SqlServerAnnotationNames.UseSqlOutputClause,
307+
useSqlOutputClause,
308+
fromDataAnnotation)?.Value;
309+
310+
/// <summary>
311+
/// Gets the configuration source for whether to use the SQL OUTPUT clause when saving changes to the table.
312+
/// </summary>
313+
/// <param name="entityType">The entity type.</param>
314+
/// <returns>The configuration source for the memory-optimized setting.</returns>
315+
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityType entityType)
316+
=> entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();
274317
}

src/EFCore.SqlServer/Extensions/SqlServerTableBuilderExtensions.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace Microsoft.EntityFrameworkCore;
1010
/// </summary>
1111
public static class SqlServerTableBuilderExtensions
1212
{
13+
#region IsTemporal
14+
1315
/// <summary>
1416
/// Configures the table as temporal.
1517
/// </summary>
@@ -183,6 +185,10 @@ public static OwnedNavigationTableBuilder<TOwnerEntity, TDependentEntity> IsTemp
183185
return tableBuilder;
184186
}
185187

188+
#endregion IsTemporal
189+
190+
#region IsMemoryOptimized
191+
186192
/// <summary>
187193
/// Configures the table that the entity maps to when targeting SQL Server as memory-optimized.
188194
/// </summary>
@@ -264,4 +270,96 @@ public static OwnedNavigationTableBuilder<TOwnerEntity, TDependentEntity> IsMemo
264270

265271
return tableBuilder;
266272
}
273+
274+
#endregion IsMemoryOptimized
275+
276+
#region UseSqlOutputClause
277+
278+
/// <summary>
279+
/// Configures whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible with
280+
/// certain SQL Server features, such as tables with triggers.
281+
/// </summary>
282+
/// <remarks>
283+
/// See <see href="https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause">Using the SQL OUTPUT clause with SQL Server</see>
284+
/// for more information and examples.
285+
/// </remarks>
286+
/// <param name="tableBuilder">The builder for the table being configured.</param>
287+
/// <param name="useSqlOutputClause">A value indicating whether to use the OUTPUT clause when saving changes to the table.</param>
288+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
289+
public static TableBuilder UseSqlOutputClause(
290+
this TableBuilder tableBuilder,
291+
bool useSqlOutputClause = true)
292+
{
293+
tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);
294+
295+
return tableBuilder;
296+
}
297+
298+
/// <summary>
299+
/// Configures whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible with
300+
/// certain SQL Server features, such as tables with triggers.
301+
/// </summary>
302+
/// <remarks>
303+
/// See <see href="https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause">Using the SQL OUTPUT clause with SQL Server</see>
304+
/// for more information and examples.
305+
/// </remarks>
306+
/// <typeparam name="TEntity">The entity type being configured.</typeparam>
307+
/// <param name="tableBuilder">The builder for the table being configured.</param>
308+
/// <param name="useSqlOutputClause">A value indicating whether to use the OUTPUT clause when saving changes to the table.</param>
309+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
310+
public static TableBuilder<TEntity> UseSqlOutputClause<TEntity>(
311+
this TableBuilder<TEntity> tableBuilder,
312+
bool useSqlOutputClause = true)
313+
where TEntity : class
314+
{
315+
tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);
316+
317+
return tableBuilder;
318+
}
319+
320+
/// <summary>
321+
/// Configures whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible with
322+
/// certain SQL Server features, such as tables with triggers.
323+
/// </summary>
324+
/// <remarks>
325+
/// See <see href="https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause">Using the SQL OUTPUT clause with SQL Server</see>
326+
/// for more information and examples.
327+
/// </remarks>
328+
/// <param name="tableBuilder">The builder for the table being configured.</param>
329+
/// <param name="useSqlOutputClause">A value indicating whether to use the OUTPUT clause when saving changes to the table.</param>
330+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
331+
public static OwnedNavigationTableBuilder UseSqlOutputClause(
332+
this OwnedNavigationTableBuilder tableBuilder,
333+
bool useSqlOutputClause = true)
334+
{
335+
tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);
336+
337+
return tableBuilder;
338+
}
339+
340+
/// <summary>
341+
/// Configures whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible with
342+
/// certain SQL Server features, such as tables with triggers.
343+
/// </summary>
344+
/// <remarks>
345+
/// See <see href="https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause">Using the SQL OUTPUT clause with SQL Server</see>
346+
/// for more information and examples.
347+
/// </remarks>
348+
/// <typeparam name="TOwnerEntity">The entity type owning the relationship.</typeparam>
349+
/// <typeparam name="TDependentEntity">The dependent entity type of the relationship.</typeparam>
350+
/// <param name="tableBuilder">The builder for the table being configured.</param>
351+
/// <param name="useSqlOutputClause">A value indicating whether to use the OUTPUT clause when saving changes to the table.</param>
352+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
353+
public static OwnedNavigationTableBuilder<TOwnerEntity, TDependentEntity> UseSqlOutputClause<TOwnerEntity, TDependentEntity>(
354+
this OwnedNavigationTableBuilder<TOwnerEntity, TDependentEntity> tableBuilder,
355+
bool useSqlOutputClause = true)
356+
where TOwnerEntity : class
357+
where TDependentEntity : class
358+
{
359+
tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);
360+
361+
return tableBuilder;
362+
}
363+
364+
#endregion UseSqlOutputClause
267365
}

src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public override ConventionSet CreateConventionSet()
5454
conventionSet.Add(new SqlServerIndexConvention(Dependencies, RelationalDependencies, _sqlGenerationHelper));
5555
conventionSet.Add(new SqlServerMemoryOptimizedTablesConvention(Dependencies, RelationalDependencies));
5656
conventionSet.Add(new SqlServerDbFunctionConvention(Dependencies, RelationalDependencies));
57+
conventionSet.Add(new SqlServerOutputClauseConvention(Dependencies, RelationalDependencies));
5758

5859
conventionSet.Replace<CascadeDeleteConvention>(
5960
new SqlServerOnDeleteConvention(Dependencies, RelationalDependencies));
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
// ReSharper disable once CheckNamespace
5+
namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
6+
7+
/// <summary>
8+
/// A convention that configures tables with triggers to not use the OUTPUT clause when saving changes.
9+
/// </summary>
10+
/// <remarks>
11+
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see>, and
12+
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
13+
/// for more information and examples.
14+
/// </remarks>
15+
public class SqlServerOutputClauseConvention : IModelFinalizingConvention
16+
{
17+
/// <summary>
18+
/// Creates a new instance of <see cref="SqlServerDbFunctionConvention" />.
19+
/// </summary>
20+
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
21+
/// <param name="relationalDependencies"> Parameter object containing relational dependencies for this convention.</param>
22+
public SqlServerOutputClauseConvention(
23+
ProviderConventionSetBuilderDependencies dependencies,
24+
RelationalConventionSetBuilderDependencies relationalDependencies)
25+
{
26+
Dependencies = dependencies;
27+
RelationalDependencies = relationalDependencies;
28+
}
29+
30+
/// <summary>
31+
/// Dependencies for this service.
32+
/// </summary>
33+
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
34+
35+
/// <summary>
36+
/// Relational provider-specific dependencies for this service.
37+
/// </summary>
38+
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
39+
40+
/// <inheritdoc />
41+
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
42+
{
43+
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
44+
{
45+
// TODO: inheritance?
46+
if (entityType.GetDeclaredTriggers().Any())
47+
{
48+
entityType.UseSqlOutputClause(false);
49+
}
50+
}
51+
}
52+
}

src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,12 @@ public static class SqlServerAnnotationNames
258258
/// doing so can result in application failures when updating to a new Entity Framework Core release.
259259
/// </summary>
260260
public const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy";
261+
262+
/// <summary>
263+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
264+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
265+
/// any release. You should only use it directly in your code with extreme caution and knowing that
266+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
267+
/// </summary>
268+
public const string UseSqlOutputClause = Prefix + "UseSqlOutputClause";
261269
}

src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.SqlServer/Properties/SqlServerStrings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,10 @@
282282
<value>SQL Server does not support releasing a savepoint.</value>
283283
</data>
284284
<data name="SaveChangesFailedBecauseOfComputedColumnWithFunction" xml:space="preserve">
285-
<value>Could not save changes because the target table has computed column with a function that performs data access. Please configure your entity type accordingly, see https://aka.ms/efcore-docs-sqlserver-save-changes-and-computed-columns for more information.</value>
285+
<value>Could not save changes because the target table has computed column with a function that performs data access. Please configure your table accordingly, see https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause for more information.</value>
286286
</data>
287287
<data name="SaveChangesFailedBecauseOfTriggers" xml:space="preserve">
288-
<value>Could not save changes because the target table has database triggers. Please configure your entity type accordingly, see https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers for more information.</value>
288+
<value>Could not save changes because the target table has database triggers. Please configure your table accordingly, see https://aka.ms/efcore-docs-sqlserver-save-changes-and-output-clause for more information.</value>
289289
</data>
290290
<data name="SequenceBadType" xml:space="preserve">
291291
<value>SQL Server sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties.</value>

0 commit comments

Comments
 (0)