1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- using System . Diagnostics . CodeAnalysis ;
5
4
using Microsoft . EntityFrameworkCore . Query . SqlExpressions ;
6
5
using Microsoft . EntityFrameworkCore . Storage . Internal ;
7
6
8
7
namespace Microsoft . EntityFrameworkCore . Query . Internal ;
9
8
10
9
/// <summary>
10
+ /// This visitor processes the parameters of <see cref="FromSqlExpression" />, expanding them and creating the appropriate
11
+ /// <see cref="IRelationalParameter" /> for them, and ensures parameter names are unique across the SQL tree.
12
+ /// </summary>
13
+ /// <remarks>
11
14
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
12
15
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
13
16
/// any release. You should only use it directly in your code with extreme caution and knowing that
14
17
/// doing so can result in application failures when updating to a new Entity Framework Core release.
15
- /// </summary >
16
- public class FromSqlParameterExpandingExpressionVisitor : ExpressionVisitor
18
+ /// </remarks >
19
+ public class RelationalParameterProcessor : ExpressionVisitor
17
20
{
18
21
private readonly IDictionary < FromSqlExpression , Expression > _visitedFromSqlExpressions
19
22
= new Dictionary < FromSqlExpression , Expression > ( ReferenceEqualityComparer . Instance ) ;
20
23
21
24
private readonly ISqlExpressionFactory _sqlExpressionFactory ;
22
25
private readonly IRelationalTypeMappingSource _typeMappingSource ;
26
+ private readonly ISqlGenerationHelper _sqlGenerationHelper ;
23
27
private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory ;
24
28
29
+ /// <summary>
30
+ /// Contains parameter names seen so far, for uniquification. These parameter names have already gone through
31
+ /// <see cref="ISqlGenerationHelper.GenerateParameterName(string)"/> (i.e. they're prefixed), since
32
+ /// <see cref="DbParameter.ParameterName" /> can be prefixed or not.
33
+ /// </summary>
34
+ private readonly HashSet < string > _prefixedParameterNames = new ( ) ;
35
+
36
+ private readonly Dictionary < string , SqlParameterExpression > _sqlParameters = new ( ) ;
37
+
25
38
private IReadOnlyDictionary < string , object ? > _parametersValues ;
26
39
private ParameterNameGenerator _parameterNameGenerator ;
27
40
private bool _canCache ;
@@ -32,14 +45,15 @@ private readonly IDictionary<FromSqlExpression, Expression> _visitedFromSqlExpre
32
45
/// any release. You should only use it directly in your code with extreme caution and knowing that
33
46
/// doing so can result in application failures when updating to a new Entity Framework Core release.
34
47
/// </summary>
35
- public FromSqlParameterExpandingExpressionVisitor (
48
+ public RelationalParameterProcessor (
36
49
RelationalParameterBasedSqlProcessorDependencies dependencies )
37
50
{
38
51
Dependencies = dependencies ;
39
52
40
53
_sqlExpressionFactory = dependencies . SqlExpressionFactory ;
41
54
_typeMappingSource = dependencies . TypeMappingSource ;
42
55
_parameterNameGeneratorFactory = dependencies . ParameterNameGeneratorFactory ;
56
+ _sqlGenerationHelper = dependencies . SqlGenerationHelper ;
43
57
_parametersValues = default ! ;
44
58
_parameterNameGenerator = default ! ;
45
59
}
@@ -61,6 +75,8 @@ public virtual Expression Expand(
61
75
out bool canCache )
62
76
{
63
77
_visitedFromSqlExpressions . Clear ( ) ;
78
+ _prefixedParameterNames . Clear ( ) ;
79
+ _sqlParameters . Clear ( ) ;
64
80
_parameterNameGenerator = _parameterNameGeneratorFactory . Create ( ) ;
65
81
_parametersValues = parameterValues ;
66
82
_canCache = true ;
@@ -77,19 +93,55 @@ public virtual Expression Expand(
77
93
/// any release. You should only use it directly in your code with extreme caution and knowing that
78
94
/// doing so can result in application failures when updating to a new Entity Framework Core release.
79
95
/// </summary>
80
- [ return : NotNullIfNotNull ( nameof ( expression ) ) ]
81
- public override Expression ? Visit ( Expression ? expression )
82
- {
83
- if ( expression is not FromSqlExpression fromSql )
96
+ protected override Expression VisitExtension ( Expression expression )
97
+ => expression switch
84
98
{
85
- return base . Visit ( expression ) ;
86
- }
99
+ FromSqlExpression fromSql
100
+ => _visitedFromSqlExpressions . TryGetValue ( fromSql , out var visitedFromSql )
101
+ ? visitedFromSql
102
+ : _visitedFromSqlExpressions [ fromSql ] = VisitFromSql ( fromSql ) ,
103
+
104
+ SqlParameterExpression parameter => VisitSqlParameter ( parameter ) ,
105
+
106
+ _ => base . VisitExtension ( expression )
107
+ } ;
87
108
88
- if ( _visitedFromSqlExpressions . TryGetValue ( fromSql , out var visitedFromSql ) )
109
+ private SqlParameterExpression VisitSqlParameter ( SqlParameterExpression parameter )
110
+ {
111
+ var typeMapping = parameter . TypeMapping ! ;
112
+
113
+ // Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
114
+ // data twice.
115
+ // Note that if the type mapping differs, we do send the same data twice (e.g. the same string may be sent once as Unicode, once as
116
+ // non-Unicode).
117
+ // TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for
118
+ // that we need to ensure that there's only ever one type mapping instance (i.e. no type mappings are ever instantiated out of the
119
+ // type mapping source). See #30677.
120
+ if ( _sqlParameters . TryGetValue ( parameter . InvariantName , out var existingParameter )
121
+ && existingParameter is { TypeMapping : RelationalTypeMapping existingTypeMapping }
122
+ && string . Equals ( existingTypeMapping . StoreType , typeMapping . StoreType , StringComparison . OrdinalIgnoreCase )
123
+ && ( existingTypeMapping . Converter is null && typeMapping . Converter is null
124
+ || existingTypeMapping . Converter is not null && existingTypeMapping . Converter . Equals ( typeMapping . Converter ) ) )
89
125
{
90
- return visitedFromSql ;
126
+ return parameter ;
91
127
}
92
128
129
+ var uniquifiedName = UniquifyParameterName ( parameter . Name ) ;
130
+ var newParameter = uniquifiedName == parameter . Name
131
+ ? parameter
132
+ : new SqlParameterExpression (
133
+ parameter . InvariantName ,
134
+ uniquifiedName ,
135
+ parameter . Type ,
136
+ parameter . IsNullable ,
137
+ parameter . ShouldBeConstantized ,
138
+ parameter . TypeMapping ) ;
139
+
140
+ return _sqlParameters [ newParameter . InvariantName ] = newParameter ;
141
+ }
142
+
143
+ private FromSqlExpression VisitFromSql ( FromSqlExpression fromSql )
144
+ {
93
145
switch ( fromSql . Arguments )
94
146
{
95
147
case QueryParameterExpression queryParameter :
@@ -101,22 +153,14 @@ public virtual Expression Expand(
101
153
// ReSharper disable once ForCanBeConvertedToForeach
102
154
for ( var i = 0 ; i < parameterValues . Length ; i ++ )
103
155
{
104
- var parameterName = _parameterNameGenerator . GenerateNext ( ) ;
105
156
if ( parameterValues [ i ] is DbParameter dbParameter )
106
157
{
107
- if ( string . IsNullOrEmpty ( dbParameter . ParameterName ) )
108
- {
109
- dbParameter . ParameterName = parameterName ;
110
- }
111
- else
112
- {
113
- parameterName = dbParameter . ParameterName ;
114
- }
115
-
116
- subParameters . Add ( new RawRelationalParameter ( parameterName , dbParameter ) ) ;
158
+ ProcessDbParameter ( dbParameter ) ;
159
+ subParameters . Add ( new RawRelationalParameter ( dbParameter . ParameterName , dbParameter ) ) ;
117
160
}
118
161
else
119
162
{
163
+ var parameterName = GenerateNewParameterName ( ) ;
120
164
subParameters . Add (
121
165
new TypeMappedRelationalParameter (
122
166
parameterName ,
@@ -126,18 +170,18 @@ public virtual Expression Expand(
126
170
}
127
171
}
128
172
129
- return _visitedFromSqlExpressions [ fromSql ] = fromSql . Update (
130
- Expression . Constant ( new CompositeRelationalParameter ( queryParameter . Name ! , subParameters ) ) ) ;
173
+ return fromSql . Update ( Expression . Constant ( new CompositeRelationalParameter ( queryParameter . Name , subParameters ) ) ) ;
131
174
132
175
case ConstantExpression { Value : object ? [ ] existingValues } :
133
176
{
134
177
var constantValues = new object ? [ existingValues . Length ] ;
178
+
135
179
for ( var i = 0 ; i < existingValues . Length ; i ++ )
136
180
{
137
181
constantValues [ i ] = ProcessConstantValue ( existingValues [ i ] ) ;
138
182
}
139
183
140
- return _visitedFromSqlExpressions [ fromSql ] = fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
184
+ return fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
141
185
}
142
186
143
187
case NewArrayExpression { Expressions : var expressions } :
@@ -154,35 +198,60 @@ public virtual Expression Expand(
154
198
constantValues [ i ] = ProcessConstantValue ( existingValue ) ;
155
199
}
156
200
157
- return _visitedFromSqlExpressions [ fromSql ] = fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
201
+ return fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
158
202
}
159
203
160
204
default :
161
- Check . DebugFail ( "FromSql.Arguments must be Constant/QueryParameterExpression" ) ;
162
- return null ;
205
+ throw new UnreachableException ( "FromSql.Arguments must be Constant/QueryParameterExpression" ) ;
163
206
}
164
207
165
208
object ProcessConstantValue ( object ? existingConstantValue )
166
209
{
167
210
if ( existingConstantValue is DbParameter dbParameter )
168
211
{
169
- var parameterName = _parameterNameGenerator . GenerateNext ( ) ;
170
- if ( string . IsNullOrEmpty ( dbParameter . ParameterName ) )
171
- {
172
- dbParameter . ParameterName = parameterName ;
173
- }
174
- else
175
- {
176
- parameterName = dbParameter . ParameterName ;
177
- }
178
-
179
- return new RawRelationalParameter ( parameterName , dbParameter ) ;
212
+ ProcessDbParameter ( dbParameter ) ;
213
+ return new RawRelationalParameter ( dbParameter . ParameterName , dbParameter ) ;
180
214
}
181
215
182
216
return _sqlExpressionFactory . Constant (
183
217
existingConstantValue ,
184
218
existingConstantValue ? . GetType ( ) ?? typeof ( object ) ,
185
219
_typeMappingSource . GetMappingForValue ( existingConstantValue ) ) ;
186
220
}
221
+
222
+ void ProcessDbParameter ( DbParameter dbParameter )
223
+ {
224
+ dbParameter . ParameterName = string . IsNullOrEmpty ( dbParameter . ParameterName )
225
+ ? GenerateNewParameterName ( )
226
+ : UniquifyParameterName ( dbParameter . ParameterName ) ;
227
+ }
228
+ }
229
+
230
+ private string GenerateNewParameterName ( )
231
+ {
232
+ string name , prefixedName ;
233
+ do
234
+ {
235
+ name = _parameterNameGenerator . GenerateNext ( ) ;
236
+ prefixedName = _sqlGenerationHelper . GenerateParameterName ( name ) ;
237
+ }
238
+ while ( _prefixedParameterNames . Contains ( prefixedName ) ) ;
239
+
240
+ _prefixedParameterNames . Add ( prefixedName ) ;
241
+ return name ;
242
+ }
243
+
244
+ private string UniquifyParameterName ( string originalName )
245
+ {
246
+ var parameterName = originalName ;
247
+ var prefixedName = _sqlGenerationHelper . GenerateParameterName ( originalName ) ;
248
+ for ( var j = 0 ; _prefixedParameterNames . Contains ( prefixedName ) ; j ++ )
249
+ {
250
+ parameterName = originalName + j ;
251
+ prefixedName = _sqlGenerationHelper . GenerateParameterName ( parameterName ) ;
252
+ }
253
+
254
+ _prefixedParameterNames . Add ( prefixedName ) ;
255
+ return parameterName ;
187
256
}
188
257
}
0 commit comments