Skip to content

Commit

Permalink
Allow opting out of RETURNING/OUTPUT clauses in SaveChanges
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jan 25, 2023
1 parent f8a5d54 commit 633dc0e
Show file tree
Hide file tree
Showing 56 changed files with 1,964 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1458,8 +1458,7 @@ public static bool IsTableExcludedFromMigrations(this IReadOnlyEntityType entity
}

var ownership = entityType.FindOwnership();
if (ownership != null
&& ownership.IsUnique)
if (ownership is { IsUnique: true })
{
return ownership.PrincipalEntityType.IsTableExcludedFromMigrations();
}
Expand Down
18 changes: 14 additions & 4 deletions src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,14 @@ public static IEnumerable<IForeignKeyConstraint> GetMappedConstraints(this IFore
this IReadOnlyForeignKey foreignKey,
in StoreObjectIdentifier storeObject)
{
if (foreignKey.PrincipalEntityType.GetTableName() is not { } principalTableName)
{
return null;
}

var foreignKeyName = foreignKey.GetConstraintName(
storeObject,
StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName()!, foreignKey.PrincipalEntityType.GetSchema()));
StoreObjectIdentifier.Table(principalTableName, foreignKey.PrincipalEntityType.GetSchema()));
var rootForeignKey = foreignKey;

// Limit traversal to avoid getting stuck in a cycle (validation will throw for these later)
Expand All @@ -168,11 +173,16 @@ public static IEnumerable<IForeignKeyConstraint> GetMappedConstraints(this IFore
.FindRowInternalForeignKeys(storeObject)
.SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys()))
{
principalTableName = otherForeignKey.PrincipalEntityType.GetTableName();

if (principalTableName is null)
{
return null;
}

if (otherForeignKey.GetConstraintName(
storeObject,
StoreObjectIdentifier.Table(
otherForeignKey.PrincipalEntityType.GetTableName()!,
otherForeignKey.PrincipalEntityType.GetSchema()))
StoreObjectIdentifier.Table(principalTableName, otherForeignKey.PrincipalEntityType.GetSchema()))
== foreignKeyName)
{
linkedForeignKey = otherForeignKey;
Expand Down
16 changes: 13 additions & 3 deletions src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,19 @@ public static void SetDatabaseName(this IMutableTrigger trigger, string? name)
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns>The name of the table on which this trigger is defined.</returns>
public static string? GetTableName(this IReadOnlyTrigger trigger)
=> (string?)trigger.FindAnnotation(RelationalAnnotationNames.TableName)?.Value
?? trigger.EntityType.GetTableName()!;
public static string GetTableName(this IReadOnlyTrigger trigger)
{
if (trigger.FindAnnotation(RelationalAnnotationNames.TableName) is { Value: string tableName })
{
return tableName;
}

var mainTableName = trigger.EntityType.GetTableName();

Check.DebugAssert(mainTableName is not null, "Trigger defined on entity not mapped to a table");

return mainTableName;
}

/// <summary>
/// Sets the name of the table on which this trigger is defined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2474,8 +2474,16 @@ protected override void ValidateTriggers(
IModel model,
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
foreach (var entityType in model.GetEntityTypes())
foreach (var entityType in model.GetEntityTypes().Where(e => e.GetDeclaredTriggers().Any()))
{
if (entityType.BaseType is not null
&& entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
{
throw new InvalidOperationException(
RelationalStrings.CannotConfigureTriggerNonRootTphEntity(
entityType.DisplayName(), entityType.GetRootType().DisplayName()));
}

var tableName = entityType.GetTableName();
var tableSchema = entityType.GetSchema();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public virtual string? Schema
public virtual IMutableEntityTypeMappingFragment MappingFragment
=> InternalMappingFragment;

/// <summary>
/// The entity type being configured.
/// </summary>
public virtual IMutableEntityType Metadata
=> OwnedNavigationBuilder.OwnedEntityType;

private OwnedNavigationBuilder OwnedNavigationBuilder { get; }

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public virtual string? Schema
public virtual IMutableEntityTypeMappingFragment MappingFragment
=> InternalMappingFragment;

/// <summary>
/// The entity type being configured.
/// </summary>
public virtual IMutableEntityType Metadata
=> EntityTypeBuilder.Metadata;

private EntityTypeBuilder EntityTypeBuilder { get; }

/// <summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@
<data name="CannotChangeWhenOpen" xml:space="preserve">
<value>The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.</value>
</data>
<data name="CannotConfigureTriggerNonRootTphEntity" xml:space="preserve">
<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 '{rootEntityType}' instead.</value>
</data>
<data name="ClientGroupByNotSupported" xml:space="preserve">
<value>Unable to translate the given 'GroupBy' pattern. Call 'AsEnumerable' before 'GroupBy' to evaluate it client-side.</value>
</data>
Expand Down
152 changes: 152 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;

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

#region Memory-optimized table

/// <summary>
/// Returns a value indicating whether the entity type is mapped to a memory-optimized table.
/// </summary>
Expand Down Expand Up @@ -58,6 +61,10 @@ public static void SetIsMemoryOptimized(this IMutableEntityType entityType, bool
public static ConfigurationSource? GetIsMemoryOptimizedConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.MemoryOptimized)?.GetConfigurationSource();

#endregion Memory-optimized table

#region Temporal table

/// <summary>
/// Returns a value indicating whether the entity type is mapped to a temporal table.
/// </summary>
Expand Down Expand Up @@ -271,4 +278,149 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str
/// <returns>The configuration source for the temporal history table schema setting.</returns>
public static ConfigurationSource? GetHistoryTableSchemaConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema)?.GetConfigurationSource();

#endregion Temporal table

#region SQL OUTPUT clause

/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns><see langword="true" /> if the SQL OUTPUT clause is used to save changes to the table.</returns>
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType)
{
if (entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is { Value: bool useSqlOutputClause })
{
return useSqlOutputClause;
}

if (entityType.FindOwnership() is { } ownership
&& StoreObjectIdentifier.Create(entityType, StoreObjectType.Table) is { } tableIdentifier
&& ownership.FindSharedObjectRootForeignKey(tableIdentifier) is { } rootForeignKey)
{
return rootForeignKey.PrincipalEntityType.IsSqlOutputClauseUsed();
}

if (entityType.BaseType is not null && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
{
return entityType.GetRootType().IsSqlOutputClauseUsed();
}

return true;
}

/// <summary>
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
public static void UseSqlOutputClause(this IMutableEntityType entityType, bool? useSqlOutputClause)
=> entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);

/// <summary>
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? UseSqlOutputClause(
this IConventionEntityType entityType,
bool? useSqlOutputClause,
bool fromDataAnnotation = false)
=> (bool?)entityType.SetOrRemoveAnnotation(
SqlServerAnnotationNames.UseSqlOutputClause,
useSqlOutputClause,
fromDataAnnotation)?.Value;

/// <summary>
/// Gets the configuration source for whether to use the SQL OUTPUT clause when saving changes to the table.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The configuration source for the memory-optimized setting.</returns>
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the specified table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
/// <returns>A value indicating whether the SQL OUTPUT clause is used to save changes to the associated table.</returns>
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType, in StoreObjectIdentifier storeObject)
{
if (entityType.FindMappingFragment(storeObject) is { } overrides
&& overrides.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is { Value: bool useSqlOutputClause })
{
return useSqlOutputClause;
}

if (StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject)
{
return entityType.IsSqlOutputClauseUsed();
}

if (entityType.FindOwnership() is { } ownership
&& ownership.FindSharedObjectRootForeignKey(storeObject) is { } rootForeignKey)
{
return rootForeignKey.PrincipalEntityType.IsSqlOutputClauseUsed(storeObject);
}

if (entityType.BaseType is not null && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
{
return entityType.GetRootType().IsSqlOutputClauseUsed(storeObject);
}

return true;
}

/// <summary>
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
public static void UseSqlOutputClause(
this IMutableEntityType entityType,
bool? useSqlOutputClause,
in StoreObjectIdentifier storeObject)
{
if (StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject)
{
entityType.UseSqlOutputClause(useSqlOutputClause);
return;
}

entityType
.GetOrCreateMappingFragment(storeObject)
.UseSqlOutputClause(useSqlOutputClause);
}

/// <summary>
/// Sets a value indicating whether to use the SQL OUTPUT clause when saving changes to the table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? UseSqlOutputClause(
this IConventionEntityType entityType,
bool? useSqlOutputClause,
in StoreObjectIdentifier storeObject,
bool fromDataAnnotation = false)
=> StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject
? entityType.UseSqlOutputClause(useSqlOutputClause, fromDataAnnotation)
: entityType
.GetOrCreateMappingFragment(storeObject, fromDataAnnotation)
.UseSqlOutputClause(useSqlOutputClause, fromDataAnnotation);

#endregion SQL OUTPUT clause
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.SqlServer.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// SQL Server specific extension methods for <see cref="IReadOnlyEntityTypeMappingFragment" />.
/// </summary>
public static class SqlServerEntityTypeMappingFragmentExtensions
{
/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <returns>The configured value.</returns>
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityTypeMappingFragment fragment)
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is not { Value: false };

/// <summary>
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
public static void UseSqlOutputClause(this IMutableEntityTypeMappingFragment fragment, bool? useSqlOutputClause)
=> fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);

/// <summary>
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? UseSqlOutputClause(
this IConventionEntityTypeMappingFragment fragment,
bool? useSqlOutputClause,
bool fromDataAnnotation = false)
=> (bool?)fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the configuration source for the setting whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <returns>The configuration source for the configured value.</returns>
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityTypeMappingFragment fragment)
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();
}
Loading

0 comments on commit 633dc0e

Please sign in to comment.