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

Add FromSql/ExecuteSql/ExecuteSqlAsync #28677

Merged
1 commit merged into from
Aug 11, 2022
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
10 changes: 7 additions & 3 deletions src/EFCore.Relational/Diagnostics/CommandSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public enum CommandSource

/// <summary>
/// The command was generated from a call to <see cref="DbContext.SaveChanges()"/> or
/// <see cref="DbContext.SaveChangesAsync(System.Threading.CancellationToken)"/>
/// <see cref="DbContext.SaveChangesAsync(CancellationToken)"/>
/// </summary>
SaveChanges,

Expand All @@ -34,15 +34,19 @@ public enum CommandSource
Migrations,

/// <summary>
/// The command was generated from a call to <see cref="RelationalQueryableExtensions.FromSqlRaw{TEntity}"/> or
/// The command was generated from a call to
/// <see cref="RelationalQueryableExtensions.FromSql{TEntity}"/>,
/// <see cref="RelationalQueryableExtensions.FromSqlRaw{TEntity}"/> or
/// <see cref="RelationalQueryableExtensions.FromSqlInterpolated{TEntity}"/>
/// </summary>
FromSqlQuery,

/// <summary>
/// The command was generated from a call to
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlRaw(DatabaseFacade,string,object[])"/>,
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlRawAsync(DatabaseFacade,string,System.Threading.CancellationToken)"/>,
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlRawAsync(DatabaseFacade,string,CancellationToken)"/>,
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSql"/>,
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlAsync"/>,
/// <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolated"/>,
/// or <see cref="RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolatedAsync"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static Task MigrateAsync(
/// <para>
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
/// consider using <see cref="ExecuteSqlInterpolated" /> to create parameters.
/// consider using <see cref="ExecuteSql" /> to create parameters.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
Expand Down Expand Up @@ -190,6 +190,38 @@ public static int ExecuteSqlInterpolated(
FormattableString sql)
=> ExecuteSqlRaw(databaseFacade, sql.Format, sql.GetArguments()!);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
/// <remarks>
/// <para>
/// Note that this method does not start a transaction. To use this method with
/// a transaction, first call <see cref="BeginTransaction" /> or <see cref="O:UseTransaction" />.
/// </para>
/// <para>
/// Note that the current <see cref="ExecutionStrategy" /> is not used by this method
/// since the SQL may not be idempotent and does not run in a transaction. An <see cref="ExecutionStrategy" />
/// can be used explicitly, making sure to also use a transaction if the SQL is not
/// idempotent.
/// </para>
/// <para>
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
/// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
/// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
/// <returns>The number of rows affected.</returns>
public static int ExecuteSql(
this DatabaseFacade databaseFacade,
FormattableString sql)
=> ExecuteSqlRaw(databaseFacade, sql.Format, sql.GetArguments()!);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand All @@ -212,7 +244,7 @@ public static int ExecuteSqlInterpolated(
/// <para>
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
/// consider using <see cref="ExecuteSqlInterpolated" /> to create parameters.
/// consider using <see cref="ExecuteSql" /> to create parameters.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
Expand Down Expand Up @@ -297,6 +329,43 @@ public static Task<int> ExecuteSqlInterpolatedAsync(
CancellationToken cancellationToken = default)
=> ExecuteSqlRawAsync(databaseFacade, sql.Format, sql.GetArguments()!, cancellationToken);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
/// <remarks>
/// <para>
/// Note that this method does not start a transaction. To use this method with
/// a transaction, first call <see cref="BeginTransaction" /> or <see cref="O:UseTransaction" />.
/// </para>
/// <para>
/// Note that the current <see cref="ExecutionStrategy" /> is not used by this method
/// since the SQL may not be idempotent and does not run in a transaction. An <see cref="ExecutionStrategy" />
/// can be used explicitly, making sure to also use a transaction if the SQL is not
/// idempotent.
/// </para>
/// <para>
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
/// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
/// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result is the number of rows affected.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public static Task<int> ExecuteSqlAsync(
this DatabaseFacade databaseFacade,
FormattableString sql,
CancellationToken cancellationToken = default)
=> ExecuteSqlRawAsync(databaseFacade, sql.Format, sql.GetArguments()!, cancellationToken);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand Down Expand Up @@ -354,7 +423,7 @@ public static Task<int> ExecuteSqlRawAsync(
/// <para>
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
/// consider using <see cref="ExecuteSqlInterpolated" /> to create parameters.
/// consider using <see cref="ExecuteSqlAsync" /> to create parameters.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
Expand Down Expand Up @@ -395,7 +464,7 @@ public static Task<int> ExecuteSqlRawAsync(
/// <para>
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
/// consider using <see cref="ExecuteSqlInterpolated" /> to create parameters.
/// consider using <see cref="ExecuteSqlAsync" /> to create parameters.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static DbCommand CreateDbCommand(this IQueryable source)
/// <para>
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
/// consider using <see cref="FromSqlInterpolated{TEntity}" /> to create parameters.
/// consider using <see cref="FromSql{TEntity}" /> to create parameters.
/// </para>
/// <para>
/// This overload also accepts <see cref="DbParameter" /> instances as parameter values. In addition to using positional
Expand Down Expand Up @@ -142,6 +142,46 @@ public static IQueryable<TEntity> FromSqlInterpolated<TEntity>(
sql.GetArguments()));
}

/// <summary>
/// Creates a LINQ query based on an interpolated string representing a SQL query.
/// </summary>
/// <remarks>
/// <para>
/// If the database provider supports composing on the supplied SQL, you can compose on top of the raw SQL query using
/// LINQ operators.
/// </para>
/// <para>
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
/// attack. You can include interpolated parameter place holders in the SQL query string. Any interpolated parameter values
/// you supply will automatically be converted to a <see cref="DbParameter" />.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <typeparam name="TEntity">The type of the elements of <paramref name="source" />.</typeparam>
/// <param name="source">
/// An <see cref="IQueryable{T}" /> to use as the base of the interpolated string SQL query (typically a <see cref="DbSet{TEntity}" />).
/// </param>
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
/// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
public static IQueryable<TEntity> FromSql<TEntity>(
this DbSet<TEntity> source,
[NotParameterized] FormattableString sql)
where TEntity : class
{
Check.NotNull(sql, nameof(sql));
Check.NotEmpty(sql.Format, nameof(source));

var queryableSource = (IQueryable)source;
return queryableSource.Provider.CreateQuery<TEntity>(
GenerateFromSqlQueryRoot(
queryableSource,
sql.Format,
sql.GetArguments()));
}

private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot(
IQueryable source,
string sql,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
var methodName = methodCallExpression.Method.Name;
if (methodCallExpression.Method.DeclaringType == typeof(RelationalQueryableExtensions)
&& (methodName == nameof(RelationalQueryableExtensions.FromSqlRaw)
|| methodName == nameof(RelationalQueryableExtensions.FromSqlInterpolated)))
|| methodName == nameof(RelationalQueryableExtensions.FromSqlInterpolated)
|| methodName == nameof(RelationalQueryableExtensions.FromSql)))
{
var newSource = (EntityQueryRootExpression)Visit(methodCallExpression.Arguments[0]);

Expand Down
14 changes: 14 additions & 0 deletions test/EFCore.CrossStore.FunctionalTests/QueryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ public async Task FromSqlInterpolated_throws_for_InMemory(bool async)
Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(FromSqlQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task FromSql_throws_for_InMemory(bool async)
{
using var context = new InMemoryQueryContext();
var query = context.Blogs.FromSql($"Select 1");

var message = async
? (await Assert.ThrowsAsync<InvalidOperationException>(() => query.ToListAsync())).Message
: Assert.Throws<InvalidOperationException>(() => query.ToList()).Message;

Assert.Equal(CoreStrings.QueryUnhandledQueryRootExpression(nameof(FromSqlQueryRootExpression)), message);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TemporalAsOf_throws_for_InMemory(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,27 @@ public virtual async Task FromSqlInterpolated_queryable_with_parameters_interpol
Assert.True(actual.All(c => c.ContactTitle == "Sales Representative"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_queryable_with_parameters_interpolated(bool async)
{
var city = "London";
var contactTitle = "Sales Representative";

using var context = CreateContext();
var query = context.Set<Customer>().FromSql(
NormalizeDelimitersInInterpolatedString(
$"SELECT * FROM [Customers] WHERE [City] = {city} AND [ContactTitle] = {contactTitle}"));

var actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Equal(3, actual.Length);
Assert.True(actual.All(c => c.City == "London"));
Assert.True(actual.All(c => c.ContactTitle == "Sales Representative"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_queryable_with_parameters_inline_interpolated(bool async)
Expand All @@ -636,6 +657,24 @@ public virtual async Task FromSqlInterpolated_queryable_with_parameters_inline_i
Assert.True(actual.All(c => c.ContactTitle == "Sales Representative"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_queryable_with_parameters_inline_interpolated(bool async)
{
using var context = CreateContext();
var query = context.Set<Customer>().FromSql(
NormalizeDelimitersInInterpolatedString(
$"SELECT * FROM [Customers] WHERE [City] = {"London"} AND [ContactTitle] = {"Sales Representative"}"));

var actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Equal(3, actual.Length);
Assert.True(actual.All(c => c.City == "London"));
Assert.True(actual.All(c => c.ContactTitle == "Sales Representative"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(
Expand Down Expand Up @@ -681,6 +720,51 @@ from o in context.Set<Order>().FromSqlInterpolated(
Assert.Single(actual);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(
bool async)
{
var city = "London";
var startDate = new DateTime(1997, 1, 1);
var endDate = new DateTime(1998, 1, 1);

using var context = CreateContext();
var query
= from c in context.Set<Customer>().FromSqlRaw(
NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city)
from o in context.Set<Order>().FromSql(
NormalizeDelimitersInInterpolatedString(
$"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}"))
where c.CustomerID == o.CustomerID
select new { c, o };

var actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Equal(25, actual.Length);

city = "Berlin";
startDate = new DateTime(1998, 4, 1);
endDate = new DateTime(1998, 5, 1);

query
= (from c in context.Set<Customer>().FromSqlRaw(
NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city)
from o in context.Set<Order>().FromSql(
NormalizeDelimitersInInterpolatedString(
$"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}"))
where c.CustomerID == o.CustomerID
select new { c, o });

actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Single(actual);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlRaw_queryable_with_null_parameter(bool async)
Expand Down Expand Up @@ -1119,6 +1203,25 @@ public virtual async Task FromSqlInterpolated_with_inlined_db_parameter(bool asy
Assert.True(actual.All(c => c.City == "Berlin"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_with_inlined_db_parameter(bool async)
{
using var context = CreateContext();
var parameter = CreateDbParameter("@somename", "ALFKI");

var query = context.Customers
.FromSql(
NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Customers] WHERE [CustomerID] = {parameter}"));

var actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Single(actual);
Assert.True(actual.All(c => c.City == "Berlin"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_with_inlined_db_parameter_without_name_prefix(bool async)
Expand All @@ -1138,6 +1241,25 @@ public virtual async Task FromSqlInterpolated_with_inlined_db_parameter_without_
Assert.True(actual.All(c => c.City == "Berlin"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_with_inlined_db_parameter_without_name_prefix(bool async)
{
using var context = CreateContext();
var parameter = CreateDbParameter("somename", "ALFKI");

var query = context.Customers
.FromSql(
NormalizeDelimitersInInterpolatedString($"SELECT * FROM [Customers] WHERE [CustomerID] = {parameter}"));

var actual = async
? await query.ToArrayAsync()
: query.ToArray();

Assert.Single(actual);
Assert.True(actual.All(c => c.City == "Berlin"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_parameterization_issue_12213(bool async)
Expand Down
Loading