Skip to content

Commit

Permalink
Implement SqlQuery<T>
Browse files Browse the repository at this point in the history
Resolves #11624
  • Loading branch information
smitpatel committed Aug 15, 2022
1 parent 7430b39 commit 7a4bdf6
Show file tree
Hide file tree
Showing 20 changed files with 576 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -292,6 +294,102 @@ public static int ExecuteSqlRaw(
}
}

/// <summary>
/// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
/// provider.
/// </summary>
/// <remarks>
/// <para>
/// To use this method with a return type that isn't natively supported by the database provider, use the
/// <see cref="ModelConfigurationBuilder.DefaultTypeMapping{TScalar}(Action{TypeMappingConfigurationBuilder{TScalar}})" />
/// method.
/// </para>
/// <para>
/// The returned <see cref="IQueryable{TResult}" /> can be composed over using LINQ to build more complex queries.
/// </para>
/// <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>
/// 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>
/// 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="SqlQuery{TResult}(DatabaseFacade, FormattableString)" /> to create parameters.
/// </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 raw SQL query.</param>
/// <param name="parameters">The values to be assigned to parameters.</param>
/// <returns>An <see cref="IQueryable{T}" /> representing the raw SQL query.</returns>
[StringFormatMethod("sql")]
public static IQueryable<TResult> SqlQueryRaw<TResult>(
this DatabaseFacade databaseFacade,
[NotParameterized] string sql,
params object[] parameters)
{
Check.NotNull(sql, nameof(sql));
Check.NotNull(parameters, nameof(parameters));

var facadeDependencies = GetFacadeDependencies(databaseFacade);

return facadeDependencies.QueryProvider
.CreateQuery<TResult>(new SqlQueryRootExpression(
facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters)));
}

/// <summary>
/// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
/// provider.
/// </summary>
/// <remarks>
/// <para>
/// To use this method with a return type that isn't natively supported by the database provider, use the
/// <see cref="ModelConfigurationBuilder.DefaultTypeMapping{TScalar}(Action{TypeMappingConfigurationBuilder{TScalar}})" />
/// method.
/// </para>
/// <para>
/// The returned <see cref="IQueryable{TResult}" /> can be composed over using LINQ to build more complex queries.
/// </para>
/// <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>
/// 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>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
public static IQueryable<TResult> SqlQuery<TResult>(
this DatabaseFacade databaseFacade,
[NotParameterized] FormattableString sql)
{
Check.NotNull(sql, nameof(sql));
Check.NotNull(sql.Format, nameof(sql.Format));

var facadeDependencies = GetFacadeDependencies(databaseFacade);

return facadeDependencies.QueryProvider
.CreateQuery<TResult>(new SqlQueryRootExpression(
facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments())));
}

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

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

5 changes: 4 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@
<value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
</data>
<data name="FromSqlNonComposable" xml:space="preserve">
<value>'FromSqlRaw' or 'FromSqlInterpolated' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.</value>
<value>'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.</value>
</data>
<data name="FunctionOverrideMismatch" xml:space="preserve">
<value>The property '{propertySpecification}' has specific configuration for the function '{function}', but it isn't mapped to a column on that function return. Remove the specific configuration, or map an entity type that contains this property to '{function}'.</value>
Expand Down Expand Up @@ -951,6 +951,9 @@
<data name="SqlQueryOverrideMismatch" xml:space="preserve">
<value>The property '{propertySpecification}' has specific configuration for the SQL query '{query}', but isn't mapped to a column on that query. Remove the specific configuration, or map an entity type that contains this property to '{query}'.</value>
</data>
<data name="SqlQueryUnmappedType" xml:space="preserve">
<value>The element type '{elementType}' used in 'SqlQuery' method is not natively supported by your database provider. Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type.</value>
</data>
<data name="StoredProcedureConcurrencyTokenNotMapped" xml:space="preserve">
<value>The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.</value>
</data>
Expand Down
7 changes: 6 additions & 1 deletion src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,12 @@ private void InitializeFields()

if (!readerColumns.TryGetValue(column.Name!, out var ordinal))
{
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
if (_columns.Count != 1)
{
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
}

ordinal = 0;
}

newColumnMap[ordinal] = column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ public static int[] BuildIndexMap(IReadOnlyList<string> columnNames, DbDataReade
var columnName = columnNames[i];
if (!readerColumns.TryGetValue(columnName, out var ordinal))
{
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
if (columnNames.Count != 1)
{
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
}

ordinal = 0;
}

indexMap[i] = ordinal;
Expand Down
125 changes: 125 additions & 0 deletions src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Query.Internal;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public sealed class SqlQueryRootExpression : QueryRootExpression
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlQueryRootExpression(
IAsyncQueryProvider queryProvider,
Type elementType,
string sql,
Expression argument)
: base(queryProvider, elementType)
{
Sql = sql;
Argument = argument;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlQueryRootExpression(
Type elementType,
string sql,
Expression argument)
: base(elementType)
{
Sql = sql;
Argument = argument;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public string Sql { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public Expression Argument { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override Expression DetachQueryProvider()
=> new SqlQueryRootExpression(ElementType, Sql, Argument);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var argument = visitor.Visit(Argument);

return argument != Argument
? new SqlQueryRootExpression(ElementType, Sql, argument)
: this;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append($"SqlQuery<{ElementType.ShortDisplayName()}>({Sql}, ");
expressionPrinter.Visit(Argument);
expressionPrinter.AppendLine(")");
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is SqlQueryRootExpression sqlQueryRootExpression
&& Equals(sqlQueryRootExpression));

private bool Equals(SqlQueryRootExpression sqlQueryRootExpression)
=> base.Equals(sqlQueryRootExpression)
&& Sql == sqlQueryRootExpression.Sql
&& ExpressionEqualityComparer.Instance.Equals(Argument, sqlQueryRootExpression.Argument);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Sql, ExpressionEqualityComparer.Instance.GetHashCode(Argument));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
Expand Down Expand Up @@ -151,6 +152,29 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
new QueryExpressionReplacingExpressionVisitor(shapedQueryExpression.QueryExpression, clonedSelectExpression)
.Visit(shapedQueryExpression.ShaperExpression));

case SqlQueryRootExpression sqlQueryRootExpression:
var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
if (typeMapping == null)
{
throw new InvalidOperationException();
}

var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping,
new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));

Expression shaperExpression = new ProjectionBindingExpression(
selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());

if (sqlQueryRootExpression.ElementType != shaperExpression.Type)
{
Check.DebugAssert(sqlQueryRootExpression.ElementType.MakeNullable() == shaperExpression.Type,
"expression.Type must be nullable of targetType");

shaperExpression = Expression.Convert(shaperExpression, sqlQueryRootExpression.ElementType);
}

return new ShapedQueryExpression(selectExpression, shaperExpression);

default:
return base.VisitExtension(extensionExpression);
}
Expand Down
Loading

0 comments on commit 7a4bdf6

Please sign in to comment.