Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Move ExecuteUpdate/ExecuteDelete to core so non-relational providers can implement #34257

Merged
merged 2 commits into from
Jul 22, 2024
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 @@ -1066,10 +1066,10 @@ or nameof(EntityFrameworkQueryableExtensions.ToListAsync)
method.GetParameters()[1].ParameterType.GenericTypeArguments[0].GenericTypeArguments[1])),

// ExecuteDelete/Update behave just like other scalar-returning operators
nameof(RelationalQueryableExtensions.ExecuteDeleteAsync) when method.DeclaringType == typeof(RelationalQueryableExtensions)
=> RewriteToSync(typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteDelete))),
nameof(RelationalQueryableExtensions.ExecuteUpdateAsync) when method.DeclaringType == typeof(RelationalQueryableExtensions)
=> RewriteToSync(typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteUpdate))),
nameof(EntityFrameworkQueryableExtensions.ExecuteDeleteAsync) when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))),
nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync) when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))),

// In the regular case (sync terminating operator which needs to stay in the query tree), simply compose the terminating
// operator over the penultimate and return that.
Expand Down
15 changes: 5 additions & 10 deletions src/EFCore.Design/Query/Internal/QueryLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@ or nameof(EntityFrameworkQueryableExtensions.SumAsync)
or nameof(EntityFrameworkQueryableExtensions.ForEachAsync)
when IsOnEfQueryableExtensions():

case nameof(RelationalQueryableExtensions.ExecuteDelete)
or nameof(RelationalQueryableExtensions.ExecuteUpdate)
or nameof(RelationalQueryableExtensions.ExecuteDeleteAsync)
or nameof(RelationalQueryableExtensions.ExecuteUpdateAsync)
when IsOnEfRelationalQueryableExtensions():
case nameof(EntityFrameworkQueryableExtensions.ExecuteDelete)
or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate)
or nameof(EntityFrameworkQueryableExtensions.ExecuteDeleteAsync)
or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync)
when IsOnEfQueryableExtensions():
if (ProcessQueryCandidate(invocation))
{
return;
Expand All @@ -202,9 +202,6 @@ bool IsOnQueryable()
bool IsOnEfQueryableExtensions()
=> IsOnTypeSymbol(_symbols.EfQueryableExtensions);

bool IsOnEfRelationalQueryableExtensions()
=> IsOnTypeSymbol(_symbols.EfRelationalQueryableExtensions);

bool IsOnTypeSymbol(ITypeSymbol typeSymbol)
=> _semanticModel.GetSymbolInfo(invocation, _cancellationToken).Symbol is IMethodSymbol methodSymbol
&& methodSymbol.ContainingType.OriginalDefinition.Equals(typeSymbol, SymbolEqualityComparer.Default);
Expand Down Expand Up @@ -342,7 +339,6 @@ private readonly struct Symbols
public readonly INamedTypeSymbol IEnumerableOfT;
public readonly INamedTypeSymbol Queryable;
public readonly INamedTypeSymbol EfQueryableExtensions;
public readonly INamedTypeSymbol EfRelationalQueryableExtensions;
// ReSharper restore InconsistentNaming

private Symbols(Compilation compilation)
Expand All @@ -358,7 +354,6 @@ private Symbols(Compilation compilation)
IEnumerableOfT = GetTypeSymbolOrThrow("System.Collections.Generic.IEnumerable`1");
Queryable = GetTypeSymbolOrThrow("System.Linq.Queryable");
EfQueryableExtensions = GetTypeSymbolOrThrow("Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions");
EfRelationalQueryableExtensions = GetTypeSymbolOrThrow("Microsoft.EntityFrameworkCore.RelationalQueryableExtensions");
}

public static Symbols Load(Compilation compilation)
Expand Down
111 changes: 0 additions & 111 deletions src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,115 +272,4 @@ internal static readonly MethodInfo AsSplitQueryMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSplitQuery))!;

#endregion

#region ExecuteDelete

/// <summary>
/// Deletes all database rows for the entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <returns>The total number of rows deleted in the database.</returns>
public static int ExecuteDelete<TSource>(this IQueryable<TSource> source)
=> source.Provider.Execute<int>(Expression.Call(ExecuteDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression));

/// <summary>
/// Asynchronously deletes database rows for the entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>The total number of rows deleted in the database.</returns>
public static Task<int> ExecuteDeleteAsync<TSource>(this IQueryable<TSource> source, CancellationToken cancellationToken = default)
=> source.Provider is IAsyncQueryProvider provider
? provider.ExecuteAsync<Task<int>>(
Expression.Call(ExecuteDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression), cancellationToken)
: throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);

internal static readonly MethodInfo ExecuteDeleteMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteDelete))!;

#endregion

#region ExecuteUpdate

/// <summary>
/// Updates all database rows for the entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <param name="setPropertyCalls">A collection of set property statements specifying properties to update.</param>
/// <returns>The total number of rows updated in the database.</returns>
public static int ExecuteUpdate<TSource>(
this IQueryable<TSource> source,
Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> setPropertyCalls)
=> source.Provider.Execute<int>(
Expression.Call(ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls));

/// <summary>
/// Asynchronously updates database rows for the entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <param name="setPropertyCalls">A collection of set property statements specifying properties to update.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>The total number of rows updated in the database.</returns>
public static Task<int> ExecuteUpdateAsync<TSource>(
this IQueryable<TSource> source,
Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> setPropertyCalls,
CancellationToken cancellationToken = default)
=> source.Provider is IAsyncQueryProvider provider
? provider.ExecuteAsync<Task<int>>(
Expression.Call(
ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls), cancellationToken)
: throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);

internal static readonly MethodInfo ExecuteUpdateMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteUpdate))!;

#endregion
}
14 changes: 0 additions & 14 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

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

60 changes: 27 additions & 33 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -957,9 +957,6 @@
<data name="NoneRelationalTypeMappingOnARelationalTypeMappingSource" xml:space="preserve">
<value>'FindMapping' was called on a 'RelationalTypeMappingSource' with a non-relational 'TypeMappingInfo'.</value>
</data>
<data name="NonQueryTranslationFailedWithDetails" xml:space="preserve">
<value>The LINQ expression '{expression}' could not be translated. Additional information: {details} See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.</value>
</data>
<data name="NonScalarFunctionCannotBeNullable" xml:space="preserve">
<value>Cannot set 'IsNullable' on DbFunction '{functionName}' since the function does not represent a scalar function.</value>
</data>
Expand Down Expand Up @@ -1041,9 +1038,6 @@
<data name="SetOperationsRequireAtLeastOneSideWithValidTypeMapping" xml:space="preserve">
<value>A set operation '{setOperationType}' requires valid type mapping for at least one of its sides.</value>
</data>
<data name="SetPropertyMethodInvoked" xml:space="preserve">
<value>The SetProperty&lt;TProperty&gt; method can only be used within 'ExecuteUpdate' method.</value>
</data>
<data name="SplitQueryString" xml:space="preserve">
<value>This LINQ query is being executed in split-query mode, and the SQL shown is for the first query to be executed. Additional queries may also be executed depending on the results of the first query.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Relational/Properties/TypeForwards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
[assembly: TypeForwardedTo(typeof(AttributeCodeFragment))]
[assembly: TypeForwardedTo(typeof(MethodCallCodeFragment))]
[assembly: TypeForwardedTo(typeof(NestedClosureCodeFragment))]
[assembly: TypeForwardedTo(typeof(SetPropertyCalls<>))]
ajcvickers marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 4 additions & 2 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ protected override Expression VisitDelete(DeleteExpression deleteExpression)
}

throw new InvalidOperationException(
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteDelete)));
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete)));
}

/// <summary>
Expand Down Expand Up @@ -1540,7 +1541,8 @@ void LiftPredicate(TableExpressionBase joinTable)
}

throw new InvalidOperationException(
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(
nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate)));
}

/// <summary>
Expand Down
Loading