diff --git a/src/LinqTests/Bugs/Bug_4332_string_array_contains_under_nullable_method_init.cs b/src/LinqTests/Bugs/Bug_4332_string_array_contains_under_nullable_method_init.cs
new file mode 100644
index 0000000000..8fbc503ad1
--- /dev/null
+++ b/src/LinqTests/Bugs/Bug_4332_string_array_contains_under_nullable_method_init.cs
@@ -0,0 +1,101 @@
+#nullable enable
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Marten;
+using Marten.Testing.Documents;
+using Marten.Testing.Harness;
+using Shouldly;
+
+namespace LinqTests.Bugs;
+
+public class Bug_4332_string_array_contains_under_nullable_method_init: BugIntegrationContext
+{
+ private static string[] MakeNames(params string[] names) => names;
+
+ // Reproduces https://github.com/JasperFx/marten/issues/4332.
+ //
+ // Under C# 14 with enable, when a string[] local is
+ // initialised from a method-typed return (var v = MakeNames(...);), the
+ // compiler emits the Where predicate with an extra Convert() wrapper
+ // around the captured closure field:
+ //
+ // s => op_Implicit(Convert(closureField, String[])).Contains(s.UserName)
+ //
+ // MemoryExtensionsContains.UnwrapConversions only peels op_Implicit, so
+ // the receiver fails to reduce to a constant and the parser falls through
+ // to ValueCollectionMember.ParseWhereForContains, which CompileFasts the
+ // whole lambda and throws InvalidOperationException about 's' not being
+ // defined in scope.
+ [Fact]
+ public async Task can_query_when_string_array_local_is_method_initialised()
+ {
+ theSession.Store(new User { UserName = "alice" });
+ theSession.Store(new User { UserName = "bob" });
+ await theSession.SaveChangesAsync();
+
+ var values = MakeNames("alice");
+
+ var results = await theSession.Query()
+ .Where(u => values.Contains(u.UserName))
+ .ToListAsync();
+
+ results.Count.ShouldBe(1);
+ results[0].UserName.ShouldBe("alice");
+ }
+
+ // Same scenario, but the failing expression shape is constructed by hand
+ // so the bug reproduces deterministically regardless of which target
+ // framework / C# language version the test project was built against.
+ [Fact]
+ public async Task can_query_when_collection_expression_has_convert_wrapper()
+ {
+ theSession.Store(new User { UserName = "alice" });
+ theSession.Store(new User { UserName = "bob" });
+ await theSession.SaveChangesAsync();
+
+ var holder = new ValuesHolder { Values = new[] { "alice" } };
+ var lambda = BuildPredicateWithConvertWrapper(holder);
+
+ var results = await theSession.Query().Where(lambda).ToListAsync();
+
+ results.Count.ShouldBe(1);
+ results[0].UserName.ShouldBe("alice");
+ }
+
+ private static Expression> BuildPredicateWithConvertWrapper(ValuesHolder holder)
+ {
+ var s = Expression.Parameter(typeof(User), "s");
+
+ // The captured-closure field, then a Convert() wrapper to string[]:
+ // Convert(holder.Values, String[])
+ var fieldAccess = Expression.Field(Expression.Constant(holder), nameof(ValuesHolder.Values));
+ var converted = Expression.Convert(fieldAccess, typeof(string[]));
+
+ // Implicit string[] -> ReadOnlySpan, exactly what C# emits.
+ var opImplicit = typeof(ReadOnlySpan).GetMethod(
+ "op_Implicit",
+ new[] { typeof(string[]) })!;
+ var asSpan = Expression.Call(opImplicit, converted);
+
+ // MemoryExtensions.Contains(ReadOnlySpan, string)
+ var containsOpen = typeof(MemoryExtensions).GetMethods()
+ .Single(m => m.Name == "Contains"
+ && m.IsGenericMethodDefinition
+ && m.GetParameters().Length == 2
+ && m.GetParameters()[0].ParameterType.IsGenericType
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition()
+ == typeof(ReadOnlySpan<>));
+ var contains = containsOpen.MakeGenericMethod(typeof(string));
+
+ var nameMember = Expression.Property(s, nameof(User.UserName));
+ var body = Expression.Call(contains, asSpan, nameMember);
+ return Expression.Lambda>(body, s);
+ }
+
+ private class ValuesHolder
+ {
+ public string[] Values = Array.Empty();
+ }
+}
diff --git a/src/Marten/Linq/Parsing/Methods/MemoryExtensionsContains.cs b/src/Marten/Linq/Parsing/Methods/MemoryExtensionsContains.cs
index 4553bc10d9..cb2044e5cb 100644
--- a/src/Marten/Linq/Parsing/Methods/MemoryExtensionsContains.cs
+++ b/src/Marten/Linq/Parsing/Methods/MemoryExtensionsContains.cs
@@ -44,11 +44,34 @@ public ISqlFragment Parse(IQueryableMemberCollection memberCollection, IReadOnly
private static Expression UnwrapConversions(Expression expression)
{
- // Unwrap op_Implicit method calls
- if (expression is MethodCallExpression { Method.Name: "op_Implicit", Arguments.Count: > 0 } methodCall)
+ // C# 14 with enable can wrap captured-closure
+ // string[] locals with an extra Convert() before the implicit
+ // string[] -> ReadOnlySpan conversion, e.g.
+ // op_Implicit(Convert(closureField, String[])).Contains(s.Name)
+ // Strip both forms (and any stacked combination) so the receiver can
+ // still be reduced to a constant.
+ while (true)
{
- return methodCall.Arguments[0];
+ if (expression is MethodCallExpression
+ {
+ Method.Name: "op_Implicit" or "op_Explicit",
+ Arguments.Count: > 0
+ } methodCall)
+ {
+ expression = methodCall.Arguments[0];
+ continue;
+ }
+
+ if (expression is UnaryExpression
+ {
+ NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked
+ } unary)
+ {
+ expression = unary.Operand;
+ continue;
+ }
+
+ return expression;
}
- return expression;
}
}