11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- using System . Diagnostics . CodeAnalysis ;
54using Microsoft . EntityFrameworkCore . Query . SqlExpressions ;
65using Microsoft . EntityFrameworkCore . Storage . Internal ;
76
87namespace Microsoft . EntityFrameworkCore . Query . Internal ;
98
109/// <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>
1114/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1215/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1316/// any release. You should only use it directly in your code with extreme caution and knowing that
1417/// 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
1720{
1821 private readonly IDictionary < FromSqlExpression , Expression > _visitedFromSqlExpressions
1922 = new Dictionary < FromSqlExpression , Expression > ( ReferenceEqualityComparer . Instance ) ;
2023
2124 private readonly ISqlExpressionFactory _sqlExpressionFactory ;
2225 private readonly IRelationalTypeMappingSource _typeMappingSource ;
26+ private readonly ISqlGenerationHelper _sqlGenerationHelper ;
2327 private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory ;
2428
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+
2538 private IReadOnlyDictionary < string , object ? > _parametersValues ;
2639 private ParameterNameGenerator _parameterNameGenerator ;
2740 private bool _canCache ;
@@ -32,14 +45,15 @@ private readonly IDictionary<FromSqlExpression, Expression> _visitedFromSqlExpre
3245 /// any release. You should only use it directly in your code with extreme caution and knowing that
3346 /// doing so can result in application failures when updating to a new Entity Framework Core release.
3447 /// </summary>
35- public FromSqlParameterExpandingExpressionVisitor (
48+ public RelationalParameterProcessor (
3649 RelationalParameterBasedSqlProcessorDependencies dependencies )
3750 {
3851 Dependencies = dependencies ;
3952
4053 _sqlExpressionFactory = dependencies . SqlExpressionFactory ;
4154 _typeMappingSource = dependencies . TypeMappingSource ;
4255 _parameterNameGeneratorFactory = dependencies . ParameterNameGeneratorFactory ;
56+ _sqlGenerationHelper = dependencies . SqlGenerationHelper ;
4357 _parametersValues = default ! ;
4458 _parameterNameGenerator = default ! ;
4559 }
@@ -61,6 +75,8 @@ public virtual Expression Expand(
6175 out bool canCache )
6276 {
6377 _visitedFromSqlExpressions . Clear ( ) ;
78+ _prefixedParameterNames . Clear ( ) ;
79+ _sqlParameters . Clear ( ) ;
6480 _parameterNameGenerator = _parameterNameGeneratorFactory . Create ( ) ;
6581 _parametersValues = parameterValues ;
6682 _canCache = true ;
@@ -77,19 +93,55 @@ public virtual Expression Expand(
7793 /// any release. You should only use it directly in your code with extreme caution and knowing that
7894 /// doing so can result in application failures when updating to a new Entity Framework Core release.
7995 /// </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
8498 {
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+ } ;
87108
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 ) ) )
89125 {
90- return visitedFromSql ;
126+ return parameter ;
91127 }
92128
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+ {
93145 switch ( fromSql . Arguments )
94146 {
95147 case QueryParameterExpression queryParameter :
@@ -101,22 +153,14 @@ public virtual Expression Expand(
101153 // ReSharper disable once ForCanBeConvertedToForeach
102154 for ( var i = 0 ; i < parameterValues . Length ; i ++ )
103155 {
104- var parameterName = _parameterNameGenerator . GenerateNext ( ) ;
105156 if ( parameterValues [ i ] is DbParameter dbParameter )
106157 {
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 ) ) ;
117160 }
118161 else
119162 {
163+ var parameterName = GenerateNewParameterName ( ) ;
120164 subParameters . Add (
121165 new TypeMappedRelationalParameter (
122166 parameterName ,
@@ -126,18 +170,18 @@ public virtual Expression Expand(
126170 }
127171 }
128172
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 ) ) ) ;
131174
132175 case ConstantExpression { Value : object ? [ ] existingValues } :
133176 {
134177 var constantValues = new object ? [ existingValues . Length ] ;
178+
135179 for ( var i = 0 ; i < existingValues . Length ; i ++ )
136180 {
137181 constantValues [ i ] = ProcessConstantValue ( existingValues [ i ] ) ;
138182 }
139183
140- return _visitedFromSqlExpressions [ fromSql ] = fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
184+ return fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
141185 }
142186
143187 case NewArrayExpression { Expressions : var expressions } :
@@ -154,35 +198,60 @@ public virtual Expression Expand(
154198 constantValues [ i ] = ProcessConstantValue ( existingValue ) ;
155199 }
156200
157- return _visitedFromSqlExpressions [ fromSql ] = fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
201+ return fromSql . Update ( Expression . Constant ( constantValues , typeof ( object [ ] ) ) ) ;
158202 }
159203
160204 default :
161- Check . DebugFail ( "FromSql.Arguments must be Constant/QueryParameterExpression" ) ;
162- return null ;
205+ throw new UnreachableException ( "FromSql.Arguments must be Constant/QueryParameterExpression" ) ;
163206 }
164207
165208 object ProcessConstantValue ( object ? existingConstantValue )
166209 {
167210 if ( existingConstantValue is DbParameter dbParameter )
168211 {
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 ) ;
180214 }
181215
182216 return _sqlExpressionFactory . Constant (
183217 existingConstantValue ,
184218 existingConstantValue ? . GetType ( ) ?? typeof ( object ) ,
185219 _typeMappingSource . GetMappingForValue ( existingConstantValue ) ) ;
186220 }
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 ;
187256 }
188257}
0 commit comments