Skip to content

Commit 446be9a

Browse files
authored
Implement SqlQuery<T> (#28665)
Resolves #11624
1 parent f73f511 commit 446be9a

20 files changed

+576
-21
lines changed

src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Data;
5+
using JetBrains.Annotations;
6+
using Microsoft.EntityFrameworkCore.Query.Internal;
57

68
// ReSharper disable once CheckNamespace
79
namespace Microsoft.EntityFrameworkCore;
@@ -292,6 +294,102 @@ public static int ExecuteSqlRaw(
292294
}
293295
}
294296

297+
/// <summary>
298+
/// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
299+
/// provider.
300+
/// </summary>
301+
/// <remarks>
302+
/// <para>
303+
/// To use this method with a return type that isn't natively supported by the database provider, use the
304+
/// <see cref="ModelConfigurationBuilder.DefaultTypeMapping{TScalar}(Action{TypeMappingConfigurationBuilder{TScalar}})" />
305+
/// method.
306+
/// </para>
307+
/// <para>
308+
/// The returned <see cref="IQueryable{TResult}" /> can be composed over using LINQ to build more complex queries.
309+
/// </para>
310+
/// <para>
311+
/// Note that this method does not start a transaction. To use this method with a transaction, first call
312+
/// <see cref="BeginTransaction" /> or <see cref="O:UseTransaction" />.
313+
/// </para>
314+
/// <para>
315+
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
316+
/// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
317+
/// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
318+
/// </para>
319+
/// <para>
320+
/// However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
321+
/// into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
322+
/// consider using <see cref="SqlQuery{TResult}(DatabaseFacade, FormattableString)" /> to create parameters.
323+
/// </para>
324+
/// <para>
325+
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
326+
/// for more information and examples.
327+
/// </para>
328+
/// </remarks>
329+
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
330+
/// <param name="sql">The raw SQL query.</param>
331+
/// <param name="parameters">The values to be assigned to parameters.</param>
332+
/// <returns>An <see cref="IQueryable{T}" /> representing the raw SQL query.</returns>
333+
[StringFormatMethod("sql")]
334+
public static IQueryable<TResult> SqlQueryRaw<TResult>(
335+
this DatabaseFacade databaseFacade,
336+
[NotParameterized] string sql,
337+
params object[] parameters)
338+
{
339+
Check.NotNull(sql, nameof(sql));
340+
Check.NotNull(parameters, nameof(parameters));
341+
342+
var facadeDependencies = GetFacadeDependencies(databaseFacade);
343+
344+
return facadeDependencies.QueryProvider
345+
.CreateQuery<TResult>(new SqlQueryRootExpression(
346+
facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters)));
347+
}
348+
349+
/// <summary>
350+
/// Creates a LINQ query based on a raw SQL query, which returns a result set of a scalar type natively supported by the database
351+
/// provider.
352+
/// </summary>
353+
/// <remarks>
354+
/// <para>
355+
/// To use this method with a return type that isn't natively supported by the database provider, use the
356+
/// <see cref="ModelConfigurationBuilder.DefaultTypeMapping{TScalar}(Action{TypeMappingConfigurationBuilder{TScalar}})" />
357+
/// method.
358+
/// </para>
359+
/// <para>
360+
/// The returned <see cref="IQueryable{TResult}" /> can be composed over using LINQ to build more complex queries.
361+
/// </para>
362+
/// <para>
363+
/// Note that this method does not start a transaction. To use this method with a transaction, first call
364+
/// <see cref="BeginTransaction" /> or <see cref="O:UseTransaction" />.
365+
/// </para>
366+
/// <para>
367+
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
368+
/// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
369+
/// arguments. Any parameter values you supply will automatically be converted to a DbParameter.
370+
/// </para>
371+
/// <para>
372+
/// See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
373+
/// for more information and examples.
374+
/// </para>
375+
/// </remarks>
376+
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
377+
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
378+
/// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
379+
public static IQueryable<TResult> SqlQuery<TResult>(
380+
this DatabaseFacade databaseFacade,
381+
[NotParameterized] FormattableString sql)
382+
{
383+
Check.NotNull(sql, nameof(sql));
384+
Check.NotNull(sql.Format, nameof(sql.Format));
385+
386+
var facadeDependencies = GetFacadeDependencies(databaseFacade);
387+
388+
return facadeDependencies.QueryProvider
389+
.CreateQuery<TResult>(new SqlQueryRootExpression(
390+
facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments())));
391+
}
392+
295393
/// <summary>
296394
/// Executes the given SQL against the database and returns the number of rows affected.
297395
/// </summary>

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@
401401
<value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
402402
</data>
403403
<data name="FromSqlNonComposable" xml:space="preserve">
404-
<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>
404+
<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>
405405
</data>
406406
<data name="FunctionOverrideMismatch" xml:space="preserve">
407407
<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>
@@ -951,6 +951,9 @@
951951
<data name="SqlQueryOverrideMismatch" xml:space="preserve">
952952
<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>
953953
</data>
954+
<data name="SqlQueryUnmappedType" xml:space="preserve">
955+
<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>
956+
</data>
954957
<data name="StoredProcedureConcurrencyTokenNotMapped" xml:space="preserve">
955958
<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>
956959
</data>

src/EFCore.Relational/Query/Internal/BufferedDataReader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,12 @@ private void InitializeFields()
12641264

12651265
if (!readerColumns.TryGetValue(column.Name!, out var ordinal))
12661266
{
1267-
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
1267+
if (_columns.Count != 1)
1268+
{
1269+
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
1270+
}
1271+
1272+
ordinal = 0;
12681273
}
12691274

12701275
newColumnMap[ordinal] = column;

src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,12 @@ public static int[] BuildIndexMap(IReadOnlyList<string> columnNames, DbDataReade
134134
var columnName = columnNames[i];
135135
if (!readerColumns.TryGetValue(columnName, out var ordinal))
136136
{
137-
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
137+
if (columnNames.Count != 1)
138+
{
139+
throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName));
140+
}
141+
142+
ordinal = 0;
138143
}
139144

140145
indexMap[i] = ordinal;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public sealed class SqlQueryRootExpression : QueryRootExpression
13+
{
14+
/// <summary>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </summary>
20+
public SqlQueryRootExpression(
21+
IAsyncQueryProvider queryProvider,
22+
Type elementType,
23+
string sql,
24+
Expression argument)
25+
: base(queryProvider, elementType)
26+
{
27+
Sql = sql;
28+
Argument = argument;
29+
}
30+
31+
/// <summary>
32+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
33+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
34+
/// any release. You should only use it directly in your code with extreme caution and knowing that
35+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
36+
/// </summary>
37+
public SqlQueryRootExpression(
38+
Type elementType,
39+
string sql,
40+
Expression argument)
41+
: base(elementType)
42+
{
43+
Sql = sql;
44+
Argument = argument;
45+
}
46+
47+
/// <summary>
48+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
49+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
50+
/// any release. You should only use it directly in your code with extreme caution and knowing that
51+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
52+
/// </summary>
53+
public string Sql { get; }
54+
55+
/// <summary>
56+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
57+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
58+
/// any release. You should only use it directly in your code with extreme caution and knowing that
59+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
60+
/// </summary>
61+
public Expression Argument { get; }
62+
63+
/// <summary>
64+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
65+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
66+
/// any release. You should only use it directly in your code with extreme caution and knowing that
67+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
68+
/// </summary>
69+
public override Expression DetachQueryProvider()
70+
=> new SqlQueryRootExpression(ElementType, Sql, Argument);
71+
72+
/// <summary>
73+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
74+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
75+
/// any release. You should only use it directly in your code with extreme caution and knowing that
76+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
77+
/// </summary>
78+
protected override Expression VisitChildren(ExpressionVisitor visitor)
79+
{
80+
var argument = visitor.Visit(Argument);
81+
82+
return argument != Argument
83+
? new SqlQueryRootExpression(ElementType, Sql, argument)
84+
: this;
85+
}
86+
87+
/// <summary>
88+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
89+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
90+
/// any release. You should only use it directly in your code with extreme caution and knowing that
91+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
92+
/// </summary>
93+
protected override void Print(ExpressionPrinter expressionPrinter)
94+
{
95+
expressionPrinter.Append($"SqlQuery<{ElementType.ShortDisplayName()}>({Sql}, ");
96+
expressionPrinter.Visit(Argument);
97+
expressionPrinter.AppendLine(")");
98+
}
99+
100+
/// <summary>
101+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
102+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
103+
/// any release. You should only use it directly in your code with extreme caution and knowing that
104+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
105+
/// </summary>
106+
public override bool Equals(object? obj)
107+
=> obj != null
108+
&& (ReferenceEquals(this, obj)
109+
|| obj is SqlQueryRootExpression sqlQueryRootExpression
110+
&& Equals(sqlQueryRootExpression));
111+
112+
private bool Equals(SqlQueryRootExpression sqlQueryRootExpression)
113+
=> base.Equals(sqlQueryRootExpression)
114+
&& Sql == sqlQueryRootExpression.Sql
115+
&& ExpressionEqualityComparer.Instance.Equals(Argument, sqlQueryRootExpression.Argument);
116+
117+
/// <summary>
118+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
119+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
120+
/// any release. You should only use it directly in your code with extreme caution and knowing that
121+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
122+
/// </summary>
123+
public override int GetHashCode()
124+
=> HashCode.Combine(base.GetHashCode(), Sql, ExpressionEqualityComparer.Instance.GetHashCode(Argument));
125+
}

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using System.Linq.Expressions;
56
using Microsoft.EntityFrameworkCore.Metadata.Internal;
67
using Microsoft.EntityFrameworkCore.Query.Internal;
78
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -151,6 +152,30 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
151152
new QueryExpressionReplacingExpressionVisitor(shapedQueryExpression.QueryExpression, clonedSelectExpression)
152153
.Visit(shapedQueryExpression.ShaperExpression));
153154

155+
case SqlQueryRootExpression sqlQueryRootExpression:
156+
var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
157+
if (typeMapping == null)
158+
{
159+
throw new InvalidOperationException(
160+
RelationalStrings.SqlQueryUnmappedType(sqlQueryRootExpression.ElementType.DisplayName()));
161+
}
162+
163+
var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping,
164+
new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));
165+
166+
Expression shaperExpression = new ProjectionBindingExpression(
167+
selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());
168+
169+
if (sqlQueryRootExpression.ElementType != shaperExpression.Type)
170+
{
171+
Check.DebugAssert(sqlQueryRootExpression.ElementType.MakeNullable() == shaperExpression.Type,
172+
"expression.Type must be nullable of targetType");
173+
174+
shaperExpression = Expression.Convert(shaperExpression, sqlQueryRootExpression.ElementType);
175+
}
176+
177+
return new ShapedQueryExpression(selectExpression, shaperExpression);
178+
154179
default:
155180
return base.VisitExtension(extensionExpression);
156181
}

0 commit comments

Comments
 (0)