diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index e0573d7..e7c99e8 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -10,9 +10,11 @@ jobs:
uses: actions/checkout@v1
- name: Setup .NET SDK
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 7.0.x
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
- name: Install dependencies
run: dotnet restore
@@ -21,4 +23,4 @@ jobs:
run: dotnet build ./Mindbox.Data.Linq.sln --configuration Release --no-restore
- name: Test
- run: dotnet test --no-restore
+ run: dotnet test --no-restore
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..d1c34ed
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+ 12
+ Mindbox
+ Mindbox
+ Copyright 2019 (c) Mindbox
+ MIT
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..61397e9
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,17 @@
+
+
+ true
+ $(NoWarn);NU1507
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj
index 940b847..1a8bf8e 100644
--- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj
+++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj
@@ -1,23 +1,21 @@
-
+
+ net8.0;net10.0
Mindbox.Data.Linq.Tests
Mindbox.Data.Linq.Tests
- net7.0
false
false
- mindbox.ru
- Itc.Nexus
- ru
- false
+ true
true
true
+ CS0169
-
-
-
-
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs
new file mode 100644
index 0000000..a147c8f
--- /dev/null
+++ b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Data.Linq;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Mindbox.Data.Linq.Tests
+{
+ ///
+ /// Tests that array.Contains(value) in LINQ queries is correctly translated to SQL IN clause.
+ /// In .NET 10, the C# compiler generates MemoryExtensions.Contains(ReadOnlySpan, value)
+ /// for this pattern instead of Enumerable.Contains — this must produce the same SQL.
+ /// Two compiler patterns exist:
+ /// 1. MethodCallExpression(op_Implicit, array) — simple closure capture
+ /// 2. InvocationExpression(ConstantExpression(delegate), []) — pre-compiled closure
+ /// Both must be handled.
+ ///
+ [TestClass]
+ public class ArrayContainsTranslationTests
+ {
+ [TestMethod]
+ public void ArrayContains_TranslatesToSqlIn()
+ {
+ var ids = new[] { 1, 2, 3 };
+
+ using var connection = new DbConnectionStub();
+ using var context = new DataContext(connection);
+
+ var query = context.GetTable().Where(t => ids.Contains(t.Id));
+
+ using var command = context.GetCommand(query);
+
+ Assert.AreEqual(
+ "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine +
+ "FROM [SimpleTable] AS [t0]" + Environment.NewLine +
+ "WHERE [t0].[Id] IN (@p0, @p1, @p2)",
+ command.CommandText);
+ }
+
+ ///
+ /// Reproduces the InvocationExpression pattern: when array is passed as a parameter
+ /// to a helper method and used in a Where clause, .NET 10 compiler may generate
+ /// InvocationExpression(ConstantExpression(pre_compiled_delegate), []) instead of
+ /// MethodCallExpression(op_Implicit, array_expr) for the implicit T[]→ReadOnlySpan conversion.
+ ///
+ [TestMethod]
+ public void ArrayContains_PassedAsParameter_TranslatesToSqlIn()
+ {
+ TranslateContainsQuery(new[] { 1, 2, 3 });
+ }
+
+ private static void TranslateContainsQuery(int[] ids)
+ {
+ using var connection = new DbConnectionStub();
+ using var context = new DataContext(connection);
+
+ var query = context.GetTable()
+ .Where(t => ids.Contains(t.Id));
+
+ using var command = context.GetCommand(query);
+
+ Assert.AreEqual(
+ "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine +
+ "FROM [SimpleTable] AS [t0]" + Environment.NewLine +
+ "WHERE [t0].[Id] IN (@p0, @p1, @p2)",
+ command.CommandText);
+ }
+
+ [TestMethod]
+ public void ArrayContains_EmptyArray_TranslatesToFalse()
+ {
+ var ids = Array.Empty();
+
+ using var connection = new DbConnectionStub();
+ using var context = new DataContext(connection);
+
+ var query = context.GetTable().Where(t => ids.Contains(t.Id));
+
+ using var command = context.GetCommand(query);
+
+ Assert.AreEqual(
+ "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine +
+ "FROM [SimpleTable] AS [t0]" + Environment.NewLine +
+ "WHERE 0 = 1",
+ command.CommandText);
+ }
+ }
+}
diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj
index a773987..fc5788e 100644
--- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj
+++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj
@@ -1,30 +1,22 @@
-
+
- netstandard2.0
- Mindbox.Data.Linq
- Mindbox
+ net8.0
+ Mindbox.Data.Linq
3.2.0
A clone of Microsoft System.Data.Linq to allow multi-DLL extensibility and EF compatibility.
false
- Copyright 2019 (c) Mindbox
- Mindbox
- latest
true
true
snupkg
- 10.7.2$(VersionTag)
+ 10.8.1$(VersionTag)
+ SYSLIB0003;SYSLIB0011
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs
index bf2f79d..5064bd5 100644
--- a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs
+++ b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs
@@ -1774,6 +1774,38 @@ private SqlExpression GetAggregate(SqlNodeType aggType, Type clrType, SqlExpress
return new SqlUnary(aggType, clrType, sqlType, exp, this.dominatingExpression);
}
+ ///
+ /// Extracts the underlying array/collection expression from a ReadOnlySpan expression.
+ /// In .NET 10, array.Contains() may generate two patterns for the implicit T[]→ReadOnlySpan conversion:
+ /// 1. MethodCallExpression(op_Implicit, [array_expr]) — extract array_expr directly
+ /// 2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — extract array
+ /// from the delegate's closure target via reflection
+ ///
+ private static Expression TryExtractArrayFromSpanExpression(Expression spanExpr) {
+ // Pattern 1: MethodCallExpression — op_Implicit(array)
+ if (spanExpr is MethodCallExpression mc && mc.Arguments.Count == 1)
+ return mc.Arguments[0];
+
+ // Pattern 2: InvocationExpression(ConstantExpression(delegate), [])
+ // The compiler pre-compiled the implicit conversion to a zero-arg delegate.
+ // Extract the array from the delegate's closure.
+ if (spanExpr is InvocationExpression { Arguments.Count: 0 } invoke
+ && invoke.Expression is ConstantExpression constExpr
+ && constExpr.Value is Delegate del
+ && del.Target != null) {
+ var target = del.Target;
+ var elementType = spanExpr.Type.GetGenericArguments()[0];
+ var arrayType = elementType.MakeArrayType();
+ var arrayField = target.GetType()
+ .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ .FirstOrDefault(f => f.FieldType == arrayType);
+ if (arrayField != null)
+ return Expression.Constant(arrayField.GetValue(target), arrayType);
+ }
+
+ return null;
+ }
+
private SqlNode VisitContains(Expression sequence, Expression value) {
Type elemType = TypeSystem.GetElementType(sequence.Type);
SqlNode seqNode = this.Visit(sequence);
@@ -1879,6 +1911,21 @@ private SqlNode VisitMethodCall(MethodCallExpression mc) {
if (this.IsSequenceOperatorCall(mc)) {
return this.VisitSequenceOperatorCall(mc);
}
+ // In .NET 10, array.Contains(value) in expression trees compiles to
+ // MemoryExtensions.Contains(ReadOnlySpan span, value).
+ // Two compiler patterns exist for the span argument:
+ // 1. MethodCallExpression(op_Implicit, [array]) — simple local capture
+ // 2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — nested closure
+ // Both must be unwrapped to extract the underlying array for SQL IN translation.
+ else if (mc.Method.DeclaringType == typeof(MemoryExtensions)
+ && mc.Method.Name == "Contains"
+ && mc.Arguments[0].Type is { IsGenericType: true } spanType
+ && spanType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)) {
+ var arrayExpr = TryExtractArrayFromSpanExpression(mc.Arguments[0]);
+ if (arrayExpr != null) {
+ return this.VisitContains(arrayExpr, mc.Arguments[1]);
+ }
+ }
else if (IsDataManipulationCall(mc)) {
return this.VisitDataManipulationCall(mc);
}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e855fb2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# Mindbox.Data.Linq
+
+A fork of Microsoft's `System.Data.Linq` (LINQ to SQL) that enables multi-assembly extensibility and compatibility with modern .NET.
+
+## Why this fork?
+
+The original `System.Data.Linq` ships as a single sealed assembly. This fork splits the internals across multiple DLLs, making it possible to extend the query pipeline — custom SQL translators, mapping providers, and diagnostics hooks — without reflection hacks.
+
+## Target framework
+
+`net8.0`
+
+## Installation
+
+```
+dotnet add package Mindbox.Data.Linq
+```
+
+## Usage
+
+Drop-in replacement for `System.Data.Linq`. Replace the namespace import and use `DataContext` as usual:
+
+```csharp
+using System.Data.Linq;
+
+using var context = new DataContext(connectionString, mappingSource);
+var results = context.GetTable()
+ .Where(o => o.CustomerId == customerId)
+ .ToList();
+```
+
+## Key differences from System.Data.Linq
+
+| Feature | System.Data.Linq | Mindbox.Data.Linq |
+|---|---|---|
+| Multi-assembly extensibility | ✗ | ✓ |
+| .NET 10 `array.Contains()` in queries | ✗ | ✓ |
+| Target framework | netstandard2.0 | net8.0 |
+
+## Building
+
+```bash
+dotnet restore
+dotnet build --configuration Release
+dotnet test
+```
+
+## License
+
+MIT — see [LICENSE.txt](LICENSE.txt).