Skip to content

Commit

Permalink
Add FromSql/ExecuteSql/ExecuteSqlAsync (#28677)
Browse files Browse the repository at this point in the history
Resolves #28609
  • Loading branch information
smitpatel authored Aug 11, 2022
1 parent 13c2a5f commit 5b27554
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 9 deletions.
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

0 comments on commit 5b27554

Please sign in to comment.