-
-
Notifications
You must be signed in to change notification settings - Fork 244
Description
Description and reproduction
Problem: When , scalar parameters are handled correctly but passing an array or as a single parameter often fails or is expanded incorrectly. Queries such as:
var config = new ParsingConfig { UseParameterizedNamesInDynamicQuery = true };
var ids = new[] { 1, 2, 3 };
var q = context.Entities.Where(config, "Ids.Contains(@0)", ids);
sql :
SELECT [e].*
FROM [Entities] AS [e]
WHERE [e].[Id] IN (1, 2, 3)
either throw, produce incorrect expression trees with extra nodes, or are treated as multiple parameters instead of a single parameter holding the array.
Steps to reproduce
- Create an EF Core with an entity that has an property and a collection or a simple property to test .
- Run the query above with .
- Observe exception or incorrect filtering.
Expected behavior
When is enabled, an array or any passed as a single parameter should be treated as a single parameter value. The parser should insert a (or otherwise preserve the runtime enumerable) for so expressions like , , or work reliably without spurious nodes or parameter expansion.
Proposed implementation
High-level approach
• Detect enumerable parameter values early in the parameter mapping path when is true.
• For values implementing (excluding ), create a single typed array or constant and bind it to the parameter name (e.g., ) instead of expanding elements or creating a that later requires .
• Ensure the expression resolver checks for parameter constants first and uses them directly.
• Optionally add parser rewrite for operator to .
Key rules
• Exclude from enumerable handling.
• Preserve element type when possible by detecting and creating a typed array of .
• Fall back to when element type cannot be inferred.
• Keep scalar parameter behavior unchanged.
Conceptual code sketch
private void MapParameters(object[] values)
{
for (int i = 0; i < values.Length; i++)
{
var value = values[i];
var name = $"@{i}";
if (ParsingConfig.UseParameterizedNamesInDynamicQuery && IsEnumerableButNotString(value))
{
var elementType = GetEnumerableElementType(value?.GetType()) ?? typeof(object);
var typedArray = ConvertEnumerableToTypedArray(value, elementType);
_parameterConstants[name] = Expression.Constant(typedArray, typedArray.GetType());
}
else
{
var paramType = value?.GetType() ?? typeof(object);
_parameterValues[name] = value;
// existing parameter handling path
}
}
}
private bool IsEnumerableButNotString(object value)
{
if (value == null) return false;
if (value is string) return false;
return value is System.Collections.IEnumerable;
}
private Type GetEnumerableElementType(Type type)
{
var ienum = type?.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return ienum?.GetGenericArguments()[0];
}
private Array ConvertEnumerableToTypedArray(object value, Type elementType)
{
var items = ((System.Collections.IEnumerable)value).Cast<object>().ToArray();
var typed = Array.CreateInstance(elementType, items.Length);
for (int i = 0; i < items.Length; i++)
typed.SetValue(Convert.ChangeType(items[i], elementType), i);
return typed;
}
Resolver change
• When resolving in the expression parser, check first and use the stored directly. Avoid inserting nodes around these constants.
Tests and migration notes
Unit tests to add
• Scalar parameter unchanged: returns expected results.
• Array parameter with Contains: filters correctly.
• Array parameter with property on left: filters correctly.
• String[] parameter: works and is not treated as enumerable for expansion.
• Nullable element types: behaves correctly.
• Regression test for scenarios that previously produced nodes.
Backward compatibility
• Default scalar behavior remains unchanged.
• Because some users may have relied on enumerable expansion into multiple parameters, consider adding a compatibility flag defaulting to to preserve old semantics if needed.