Skip to content

Support array and IEnumerable parameters when UseParameterizedNamesInDynamicQuery = true #972

@mahdiyar021

Description

@mahdiyar021

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

  1. Create an EF Core with an entity that has an property and a collection or a simple property to test .
  2. Run the query above with .
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions