Skip to content

Commit 0bd8b43

Browse files
committed
Implement SqlQuery<T>
Resolves #11624
1 parent 7430b39 commit 0bd8b43

20 files changed

+519
-21
lines changed

src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs

Lines changed: 43 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.Data;
5+
using Microsoft.EntityFrameworkCore.Query.Internal;
56

67
// ReSharper disable once CheckNamespace
78
namespace Microsoft.EntityFrameworkCore;
@@ -292,6 +293,48 @@ public static int ExecuteSqlRaw(
292293
}
293294
}
294295

296+
/// <summary>
297+
/// .
298+
/// </summary>
299+
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
300+
/// <param name="sql">The raw SQL query.</param>
301+
/// <param name="parameters">The values to be assigned to parameters.</param>
302+
/// <returns>An <see cref="IQueryable{T}" /> representing the raw SQL query.</returns>
303+
public static IQueryable<TResult> SqlQueryRaw<TResult>(
304+
this DatabaseFacade databaseFacade,
305+
[NotParameterized] string sql,
306+
params object[] parameters)
307+
{
308+
Check.NotNull(sql, nameof(sql));
309+
Check.NotNull(parameters, nameof(parameters));
310+
311+
var facadeDependencies = GetFacadeDependencies(databaseFacade);
312+
313+
return facadeDependencies.QueryProvider
314+
.CreateQuery<TResult>(new SqlQueryRootExpression(
315+
facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters)));
316+
}
317+
318+
/// <summary>
319+
/// .
320+
/// </summary>
321+
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
322+
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
323+
/// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
324+
public static IQueryable<TResult> SqlQuery<TResult>(
325+
this DatabaseFacade databaseFacade,
326+
[NotParameterized] FormattableString sql)
327+
{
328+
Check.NotNull(sql, nameof(sql));
329+
Check.NotNull(sql.Format, nameof(sql.Format));
330+
331+
var facadeDependencies = GetFacadeDependencies(databaseFacade);
332+
333+
return facadeDependencies.QueryProvider
334+
.CreateQuery<TResult>(new SqlQueryRootExpression(
335+
facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments())));
336+
}
337+
295338
/// <summary>
296339
/// Executes the given SQL against the database and returns the number of rows affected.
297340
/// </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 mapped in current type provider.</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: 24 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,29 @@ 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+
}
161+
162+
var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping,
163+
new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));
164+
165+
Expression shaperExpression = new ProjectionBindingExpression(
166+
selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());
167+
168+
if (sqlQueryRootExpression.ElementType != shaperExpression.Type)
169+
{
170+
Check.DebugAssert(sqlQueryRootExpression.ElementType.MakeNullable() == shaperExpression.Type,
171+
"expression.Type must be nullable of targetType");
172+
173+
shaperExpression = Expression.Convert(shaperExpression, sqlQueryRootExpression.ElementType);
174+
}
175+
176+
return new ShapedQueryExpression(selectExpression, shaperExpression);
177+
154178
default:
155179
return base.VisitExtension(extensionExpression);
156180
}

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend
4747
[EntityFrameworkInternal]
4848
public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
4949
IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory,
50-
ISqlExpressionFactory sqlExpressionFactory)
50+
ISqlExpressionFactory sqlExpressionFactory,
51+
IRelationalTypeMappingSource typeMappingSource)
5152
{
5253
RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory;
5354
SqlExpressionFactory = sqlExpressionFactory;
55+
TypeMappingSource = typeMappingSource;
5456
}
5557

5658
/// <summary>
@@ -62,4 +64,9 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
6264
/// The SQL expression factory.
6365
/// </summary>
6466
public ISqlExpressionFactory SqlExpressionFactory { get; init; }
67+
68+
/// <summary>
69+
/// The relational type mapping souce.
70+
/// </summary>
71+
public IRelationalTypeMappingSource TypeMappingSource { get; init; }
6572
}

0 commit comments

Comments
 (0)