diff --git a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs
index 52271345704..45d135b1c2f 100644
--- a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs
@@ -236,19 +236,43 @@ internal static readonly MethodInfo AsSplitQueryMethodInfo
#region BulkDelete
///
- /// TBD
+ /// Deletes all entity instances which match the LINQ query from the database.
///
+ ///
+ ///
+ /// This operation executes immediately against the database, rather than being deferred until
+ /// 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.
+ ///
+ ///
+ /// See Executing bulk operations with EF Core
+ /// for more information and examples.
+ ///
+ ///
/// The source query.
- /// TBD
+ /// The total number of entity instances deleted from the database.
public static int BulkDelete(this IQueryable source)
=> source.Provider.Execute(Expression.Call(BulkDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression));
///
- /// TBD
+ /// Asynchronously deletes all entity instances which match the LINQ query from the database.
///
+ ///
+ ///
+ /// This operation executes immediately against the database, rather than being deferred until
+ /// 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.
+ ///
+ ///
+ /// See Executing bulk operations with EF Core
+ /// for more information and examples.
+ ///
+ ///
/// The source query.
/// A to observe while waiting for the task to complete.
- /// TBD
+ /// The total number of entity instances deleted from the database.
public static Task BulkDeleteAsync(this IQueryable source, CancellationToken cancellationToken = default)
=> source.Provider is IAsyncQueryProvider provider
? provider.ExecuteAsync>(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index eed94e6bbd5..5053bf88afe 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -47,6 +47,20 @@ public static string BadSequenceString
public static string BadSequenceType
=> GetString("BadSequenceType");
+ ///
+ /// The bulk operation cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.
+ ///
+ public static string BulkOperationOnKeylessEntityTypeWithUnsupportedOperator(object? entityType)
+ => string.Format(
+ GetString("BulkOperationOnKeylessEntityTypeWithUnsupportedOperator", nameof(entityType)),
+ entityType);
+
+ ///
+ /// The bulk operation contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported in RelationalQueryableMethodTranslatingExpressionVisitor.
+ ///
+ public static string BulkOperationWithUnsupportedOperatorInSqlGeneration
+ => GetString("BulkOperationWithUnsupportedOperatorInSqlGeneration");
+
///
/// The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 5a38d8f7d7a..6b766fc516f 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -127,6 +127,12 @@
Invalid type for sequence. Valid types are long (the default), int, short, byte and decimal.
+
+ The bulk operation cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.
+
+
+ The bulk operation contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported in RelationalQueryableMethodTranslatingExpressionVisitor.
+
The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index a3e1e23ce29..f85e4373be5 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -176,8 +176,7 @@ protected override Expression VisitDelete(DeleteExpression deleteExpression)
}
else
{
- // TODO: Exception message
- throw new InvalidOperationException();
+ throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration);
}
return deleteExpression;
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs
index 603e1d506d9..fe29f5a2393 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs
@@ -64,8 +64,7 @@ private sealed class SelectExpressionMutableVerifyingExpressionVisitor : Express
{
switch (expression)
{
- case SelectExpression selectExpression
- when selectExpression.IsMutable():
+ case SelectExpression selectExpression when selectExpression.IsMutable():
throw new InvalidDataException(selectExpression.Print());
case ShapedQueryExpression shapedQueryExpression:
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 390185ca04b..88bec48a983 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -1029,8 +1029,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
else
{
- // TODO: Exception message
- throw new InvalidOperationException("specific message about keyless");
+ throw new InvalidOperationException(
+ RelationalStrings.BulkOperationOnKeylessEntityTypeWithUnsupportedOperator(entityType.DisplayName()));
}
}
}
@@ -1042,6 +1042,16 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
///
/// Validates if the current select expression can be used for bulk delete operation or it requires to be pushed into a subquery.
///
+ ///
+ ///
+ /// By default, only single-table select expressions are supported, and only with a predicate.
+ ///
+ ///
+ /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery.
+ /// When doing this, VisitDelete must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for
+ /// the feature.
+ ///
+ ///
/// The select expression to validate.
/// The entity shaper expression on which delete operation is being applied.
/// The table expression from which rows are being deleted.
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
index 9006994de26..419a7487d8a 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs
@@ -49,10 +49,11 @@ protected override Expression VisitExtension(Expression extensionExpression)
: base.VisitExtension(extensionExpression);
///
- /// asdagv
+ /// Visits the given , returning an expression that when compiled, can execute the non-
+ /// query operation against the database.
///
- /// asdasfdsa
- /// asdasd
+ /// The expression to be compiled.
+ /// An expression which executes a non-query operation.
protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression)
{
var relationalCommandCache = new RelationalCommandCache(
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index ce1fcc4686c..83d9250f5c5 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -67,7 +67,7 @@ protected override Expression VisitDelete(DeleteExpression deleteExpression)
}
else
{
- throw new NotSupportedException();
+ throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration);
}
return deleteExpression;