From d0b5e2bf526a23fbb0f90f0217988450c3ca4e86 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 13:00:57 +0000 Subject: [PATCH 01/13] feat: net8.0 target, MemoryExtensions.Contains support, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TargetFramework: netstandard2.0 → net8.0 - LangVersion: latest → 12 - PackageVersion: 10.7.2 → 10.8.0 - Microsoft.Data.SqlClient: 5.1.0 → 6.1.4 - Removed BCL packages: System.Reflection.Emit, System.Runtime, System.Runtime.Extensions, System.ComponentModel.Annotations, System.Security.Permissions - SecurityUtils: removed CAS (ReflectionPermission) — no-op on .NET 5+ - SYSLIB0011 (BinaryFormatter): suppressed — legacy DB serialization path - Tests: updated to net8.0, suppressed CS0169 QueryConverter: support MemoryExtensions.Contains from .NET 10 In .NET 10, array.Contains(value) in expression trees compiles to MemoryExtensions.Contains(ReadOnlySpan.op_Implicit(array), value) instead of Enumerable.Contains(array, value). Added handling to translate it to SQL IN clause the same way. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../Mindbox.Data.Linq.Tests.csproj | 3 +- Mindbox.Data.Linq/Mindbox.Data.Linq.csproj | 15 +- .../SqlClient/Query/QueryConverter.cs | 11 + Mindbox.Data.Linq/misc/SecurityUtils.cs | 199 +++--------------- 4 files changed, 50 insertions(+), 178 deletions(-) diff --git a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj index 940b847..b95de51 100644 --- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj +++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj @@ -3,7 +3,7 @@ Mindbox.Data.Linq.Tests Mindbox.Data.Linq.Tests - net7.0 + net8.0 false false mindbox.ru @@ -12,6 +12,7 @@ false true true + CS0169 diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj index a773987..485d9da 100644 --- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj +++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net8.0 Mindbox.Data.Linq Mindbox 3.2.0 @@ -8,23 +8,18 @@ false Copyright 2019 (c) Mindbox Mindbox - latest + 12 + SYSLIB0011 true true snupkg - 10.7.2$(VersionTag) + 10.8.0$(VersionTag) - + - - - - - - diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs index bf2f79d..2e03b74 100644 --- a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs +++ b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs @@ -1879,6 +1879,17 @@ 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.op_Implicit(array), value). + // Translate the same as Enumerable.Contains(array, value). + else if (mc.Method.DeclaringType == typeof(MemoryExtensions) + && mc.Method.Name == "Contains" + && mc.Arguments.Count == 2 + && mc.Arguments[0] is MethodCallExpression spanConversion + && spanConversion.Method.Name == "op_Implicit" + && spanConversion.Arguments.Count == 1) { + return this.VisitContains(spanConversion.Arguments[0], mc.Arguments[1]); + } else if (IsDataManipulationCall(mc)) { return this.VisitDataManipulationCall(mc); } diff --git a/Mindbox.Data.Linq/misc/SecurityUtils.cs b/Mindbox.Data.Linq/misc/SecurityUtils.cs index a53858e..8450a32 100644 --- a/Mindbox.Data.Linq/misc/SecurityUtils.cs +++ b/Mindbox.Data.Linq/misc/SecurityUtils.cs @@ -1,214 +1,79 @@ -//------------------------------------------------------------------------------ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -//------------------------------------------------------------------------------ - using System; using System.Reflection; -using System.Security; -using System.Security.Permissions; namespace System.Data.Linq { - /// - /// Useful methods to securely call 'dangerous' managed APIs (especially reflection). - /// See http://wiki/default.aspx/Microsoft.Projects.DotNetClient.SecurityConcernsAroundReflection - /// for more information specifically about why we need to be careful about reflection invocations. - /// - internal static class SecurityUtils { - - private static volatile ReflectionPermission memberAccessPermission = null; - private static volatile ReflectionPermission restrictedMemberAccessPermission = null; - - private static ReflectionPermission MemberAccessPermission + // Code Access Security (ReflectionPermission) is not supported on .NET 5+. + // This simplified version removes CAS checks which were no-ops on modern .NET. + internal static class SecurityUtils + { + internal static object SecureCreateInstance(Type type) { - get { - if (memberAccessPermission == null) { - memberAccessPermission = new ReflectionPermission(ReflectionPermissionFlag.MemberAccess); - } - return memberAccessPermission; - } - } - - private static ReflectionPermission RestrictedMemberAccessPermission { - get { - if (restrictedMemberAccessPermission == null) { - restrictedMemberAccessPermission = new ReflectionPermission(ReflectionPermissionFlag.RestrictedMemberAccess); - } - return restrictedMemberAccessPermission; - } - } - - private static void DemandReflectionAccess(Type type) { - MemberAccessPermission.Demand(); - } - - private static bool HasReflectionPermission(Type type) { - try { - DemandReflectionAccess(type); - return true; - } - catch (SecurityException) { - } - - return false; - } - - - /// - /// This helper method provides safe access to Activator.CreateInstance. - /// NOTE: This overload will work only with public .ctors. - /// - internal static object SecureCreateInstance(Type type) { return SecureCreateInstance(type, null, false); } - - /// - /// This helper method provides safe access to Activator.CreateInstance. - /// Set allowNonPublic to true if you want non public ctors to be used. - /// - internal static object SecureCreateInstance(Type type, object[] args, bool allowNonPublic) { - if (type == null) { + internal static object SecureCreateInstance(Type type, object[] args, bool allowNonPublic) + { + if (type == null) throw new ArgumentNullException("type"); - } BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance; - - // if it's an internal type, we demand reflection permission. - if (!type.IsVisible) { - DemandReflectionAccess(type); - } - else if (allowNonPublic && !HasReflectionPermission(type)) { - // Someone is trying to instantiate a public type in *our* assembly, but does not - // have full reflection permission. We shouldn't pass BindingFlags.NonPublic in this case. - // The reason we don't directly demand the permission here is because we don't know whether - // a public or non-public .ctor will be invoked. We want to allow the public .ctor case to - // succeed. - allowNonPublic = false; - } - - if (allowNonPublic) { + if (allowNonPublic) flags |= BindingFlags.NonPublic; - } return Activator.CreateInstance(type, flags, null, args, null); } - /// - /// This helper method provides safe access to Activator.CreateInstance. - /// NOTE: This overload will work only with public .ctors. - /// - internal static object SecureCreateInstance(Type type, object[] args) { + internal static object SecureCreateInstance(Type type, object[] args) + { return SecureCreateInstance(type, args, false); } - - /// - /// Helper method to safely invoke a .ctor. You should prefer SecureCreateInstance to this. - /// Set allowNonPublic to true if you want non public ctors to be used. - /// - internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, bool allowNonPublic) { + internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, bool allowNonPublic) + { return SecureConstructorInvoke(type, argTypes, args, allowNonPublic, BindingFlags.Default); } - /// - /// Helper method to safely invoke a .ctor. You should prefer SecureCreateInstance to this. - /// Set allowNonPublic to true if you want non public ctors to be used. - /// The 'extraFlags' parameter is used to pass in any other flags you need, - /// besides Public, NonPublic and Instance. - /// - internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, - bool allowNonPublic, BindingFlags extraFlags) { - if (type == null) { + internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, + bool allowNonPublic, BindingFlags extraFlags) + { + if (type == null) throw new ArgumentNullException("type"); - } - - // if it's an internal type, we demand reflection permission. - if (!type.IsVisible) { - DemandReflectionAccess(type); - } - else if (allowNonPublic && !HasReflectionPermission(type)) { - // Someone is trying to invoke a ctor on a public type, but does not - // have full reflection permission. We shouldn't pass BindingFlags.NonPublic in this case. - allowNonPublic = false; - } BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | extraFlags; - if (!allowNonPublic) { + if (!allowNonPublic) flags &= ~BindingFlags.NonPublic; - } ConstructorInfo ctor = type.GetConstructor(flags, null, argTypes, null); - if (ctor != null) { + if (ctor != null) return ctor.Invoke(args); - } return null; } - private static bool GenericArgumentsAreVisible(MethodInfo method) { - if (method.IsGenericMethod) { - Type[] parameterTypes = method.GetGenericArguments(); - foreach (Type type in parameterTypes) { - if (!type.IsVisible) { - return false; - } - } - } - return true; - } - - /// - /// This helper method provides safe access to FieldInfo's GetValue method. - /// - internal static object FieldInfoGetValue(FieldInfo field, object target) { - Type type = field.DeclaringType; - if (type == null) { - // Type is null for Global fields. + internal static object FieldInfoGetValue(FieldInfo field, object target) + { + if (field.DeclaringType == null) throw new NotImplementedException("Global fields are not supported."); - } else if (!(type != null && type.IsVisible && field.IsPublic)) { - DemandReflectionAccess(type); - } + return field.GetValue(target); } - /// - /// This helper method provides safe access to MethodInfo's Invoke method. - /// - internal static object MethodInfoInvoke(MethodInfo method, object target, object[] args) { - Type type = method.DeclaringType; - if (type == null) { - // Type is null for Global methods. In this case we would need to demand grant set on - // the containing assembly for internal methods. - throw new NotImplementedException("Global methods are not supported."); - } else if (!(type.IsVisible && method.IsPublic && GenericArgumentsAreVisible(method))) { - // this demand is required for internal types in system.dll and its friend assemblies. - DemandReflectionAccess(type); - } + internal static object MethodInfoInvoke(MethodInfo method, object target, object[] args) + { + if (method.DeclaringType == null) + throw new NotImplementedException("Global methods are not supported."); + return method.Invoke(target, args); } - /// - /// This helper method provides safe access to ConstructorInfo's Invoke method. - /// Constructors can't be generic, so we don't check if argument types are visible - /// - internal static object ConstructorInfoInvoke(ConstructorInfo ctor, object[] args) { - Type type = ctor.DeclaringType; - if ((type != null) && !(type.IsVisible && ctor.IsPublic)) { - DemandReflectionAccess(type); - } + internal static object ConstructorInfoInvoke(ConstructorInfo ctor, object[] args) + { return ctor.Invoke(args); } - /// - /// This helper method provides safe access to Array.CreateInstance. - /// - internal static object ArrayCreateInstance(Type type, int length) { - if (!type.IsVisible) { - DemandReflectionAccess(type); - } + internal static object ArrayCreateInstance(Type type, int length) + { return Array.CreateInstance(type, length); } } From b71bbb647e4cd28575ae9410bf3b08cc0ff63e5d Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:23:34 +0000 Subject: [PATCH 02/13] refactor: add Directory.Build/Packages.props, restore SecurityUtils, fix CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Directory.Build.props: TargetFramework=net8.0, LangVersion=12 - Add Directory.Packages.props: centralized package versions - Restore original SecurityUtils.cs (add System.Security.Permissions 8.0.0 package; suppress SYSLIB0003 — CAS is a no-op on .NET 5+, types still compile with the compat package) - Simplify csproj files: remove properties now in Directory.Build.props - Update pull-request.yml: setup-dotnet@v4, dotnet-version 8.0.x --- .github/workflows/pull-request.yml | 4 +- Directory.Build.props | 6 + Directory.Packages.props | 17 ++ .../Mindbox.Data.Linq.Tests.csproj | 13 +- Mindbox.Data.Linq/Mindbox.Data.Linq.csproj | 17 +- Mindbox.Data.Linq/misc/SecurityUtils.cs | 199 +++++++++++++++--- 6 files changed, 206 insertions(+), 50 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e0573d7..3fa1947 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,9 +10,9 @@ 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 - name: Install dependencies run: dotnet restore diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..03ac2fb --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + net8.0 + 12 + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..1e340db --- /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 b95de51..df7b7f7 100644 --- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj +++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj @@ -1,9 +1,8 @@ - + Mindbox.Data.Linq.Tests Mindbox.Data.Linq.Tests - net8.0 false false mindbox.ru @@ -16,9 +15,9 @@ - - - - + + + + - \ No newline at end of file + diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj index 485d9da..3d66b4c 100644 --- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj +++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj @@ -1,25 +1,24 @@ - + - net8.0 - Mindbox.Data.Linq + Mindbox.Data.Linq Mindbox 3.2.0 A clone of Microsoft System.Data.Linq to allow multi-DLL extensibility and EF compatibility. false Copyright 2019 (c) Mindbox Mindbox - 12 - SYSLIB0011 true true snupkg 10.8.0$(VersionTag) + SYSLIB0003;SYSLIB0011 - - - - + + + + + diff --git a/Mindbox.Data.Linq/misc/SecurityUtils.cs b/Mindbox.Data.Linq/misc/SecurityUtils.cs index 8450a32..a53858e 100644 --- a/Mindbox.Data.Linq/misc/SecurityUtils.cs +++ b/Mindbox.Data.Linq/misc/SecurityUtils.cs @@ -1,79 +1,214 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + using System; using System.Reflection; +using System.Security; +using System.Security.Permissions; namespace System.Data.Linq { - // Code Access Security (ReflectionPermission) is not supported on .NET 5+. - // This simplified version removes CAS checks which were no-ops on modern .NET. - internal static class SecurityUtils - { - internal static object SecureCreateInstance(Type type) + /// + /// Useful methods to securely call 'dangerous' managed APIs (especially reflection). + /// See http://wiki/default.aspx/Microsoft.Projects.DotNetClient.SecurityConcernsAroundReflection + /// for more information specifically about why we need to be careful about reflection invocations. + /// + internal static class SecurityUtils { + + private static volatile ReflectionPermission memberAccessPermission = null; + private static volatile ReflectionPermission restrictedMemberAccessPermission = null; + + private static ReflectionPermission MemberAccessPermission { + get { + if (memberAccessPermission == null) { + memberAccessPermission = new ReflectionPermission(ReflectionPermissionFlag.MemberAccess); + } + return memberAccessPermission; + } + } + + private static ReflectionPermission RestrictedMemberAccessPermission { + get { + if (restrictedMemberAccessPermission == null) { + restrictedMemberAccessPermission = new ReflectionPermission(ReflectionPermissionFlag.RestrictedMemberAccess); + } + return restrictedMemberAccessPermission; + } + } + + private static void DemandReflectionAccess(Type type) { + MemberAccessPermission.Demand(); + } + + private static bool HasReflectionPermission(Type type) { + try { + DemandReflectionAccess(type); + return true; + } + catch (SecurityException) { + } + + return false; + } + + + /// + /// This helper method provides safe access to Activator.CreateInstance. + /// NOTE: This overload will work only with public .ctors. + /// + internal static object SecureCreateInstance(Type type) { return SecureCreateInstance(type, null, false); } - internal static object SecureCreateInstance(Type type, object[] args, bool allowNonPublic) - { - if (type == null) + + /// + /// This helper method provides safe access to Activator.CreateInstance. + /// Set allowNonPublic to true if you want non public ctors to be used. + /// + internal static object SecureCreateInstance(Type type, object[] args, bool allowNonPublic) { + if (type == null) { throw new ArgumentNullException("type"); + } BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance; - if (allowNonPublic) + + // if it's an internal type, we demand reflection permission. + if (!type.IsVisible) { + DemandReflectionAccess(type); + } + else if (allowNonPublic && !HasReflectionPermission(type)) { + // Someone is trying to instantiate a public type in *our* assembly, but does not + // have full reflection permission. We shouldn't pass BindingFlags.NonPublic in this case. + // The reason we don't directly demand the permission here is because we don't know whether + // a public or non-public .ctor will be invoked. We want to allow the public .ctor case to + // succeed. + allowNonPublic = false; + } + + if (allowNonPublic) { flags |= BindingFlags.NonPublic; + } return Activator.CreateInstance(type, flags, null, args, null); } - internal static object SecureCreateInstance(Type type, object[] args) - { + /// + /// This helper method provides safe access to Activator.CreateInstance. + /// NOTE: This overload will work only with public .ctors. + /// + internal static object SecureCreateInstance(Type type, object[] args) { return SecureCreateInstance(type, args, false); } - internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, bool allowNonPublic) - { + + /// + /// Helper method to safely invoke a .ctor. You should prefer SecureCreateInstance to this. + /// Set allowNonPublic to true if you want non public ctors to be used. + /// + internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, bool allowNonPublic) { return SecureConstructorInvoke(type, argTypes, args, allowNonPublic, BindingFlags.Default); } - internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, - bool allowNonPublic, BindingFlags extraFlags) - { - if (type == null) + /// + /// Helper method to safely invoke a .ctor. You should prefer SecureCreateInstance to this. + /// Set allowNonPublic to true if you want non public ctors to be used. + /// The 'extraFlags' parameter is used to pass in any other flags you need, + /// besides Public, NonPublic and Instance. + /// + internal static object SecureConstructorInvoke(Type type, Type[] argTypes, object[] args, + bool allowNonPublic, BindingFlags extraFlags) { + if (type == null) { throw new ArgumentNullException("type"); + } + + // if it's an internal type, we demand reflection permission. + if (!type.IsVisible) { + DemandReflectionAccess(type); + } + else if (allowNonPublic && !HasReflectionPermission(type)) { + // Someone is trying to invoke a ctor on a public type, but does not + // have full reflection permission. We shouldn't pass BindingFlags.NonPublic in this case. + allowNonPublic = false; + } BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | extraFlags; - if (!allowNonPublic) + if (!allowNonPublic) { flags &= ~BindingFlags.NonPublic; + } ConstructorInfo ctor = type.GetConstructor(flags, null, argTypes, null); - if (ctor != null) + if (ctor != null) { return ctor.Invoke(args); + } return null; } - internal static object FieldInfoGetValue(FieldInfo field, object target) - { - if (field.DeclaringType == null) + private static bool GenericArgumentsAreVisible(MethodInfo method) { + if (method.IsGenericMethod) { + Type[] parameterTypes = method.GetGenericArguments(); + foreach (Type type in parameterTypes) { + if (!type.IsVisible) { + return false; + } + } + } + return true; + } + + /// + /// This helper method provides safe access to FieldInfo's GetValue method. + /// + internal static object FieldInfoGetValue(FieldInfo field, object target) { + Type type = field.DeclaringType; + if (type == null) { + // Type is null for Global fields. throw new NotImplementedException("Global fields are not supported."); - + } else if (!(type != null && type.IsVisible && field.IsPublic)) { + DemandReflectionAccess(type); + } return field.GetValue(target); } - internal static object MethodInfoInvoke(MethodInfo method, object target, object[] args) - { - if (method.DeclaringType == null) - throw new NotImplementedException("Global methods are not supported."); - + /// + /// This helper method provides safe access to MethodInfo's Invoke method. + /// + internal static object MethodInfoInvoke(MethodInfo method, object target, object[] args) { + Type type = method.DeclaringType; + if (type == null) { + // Type is null for Global methods. In this case we would need to demand grant set on + // the containing assembly for internal methods. + throw new NotImplementedException("Global methods are not supported."); + } else if (!(type.IsVisible && method.IsPublic && GenericArgumentsAreVisible(method))) { + // this demand is required for internal types in system.dll and its friend assemblies. + DemandReflectionAccess(type); + } return method.Invoke(target, args); } - internal static object ConstructorInfoInvoke(ConstructorInfo ctor, object[] args) - { + /// + /// This helper method provides safe access to ConstructorInfo's Invoke method. + /// Constructors can't be generic, so we don't check if argument types are visible + /// + internal static object ConstructorInfoInvoke(ConstructorInfo ctor, object[] args) { + Type type = ctor.DeclaringType; + if ((type != null) && !(type.IsVisible && ctor.IsPublic)) { + DemandReflectionAccess(type); + } return ctor.Invoke(args); } - internal static object ArrayCreateInstance(Type type, int length) - { + /// + /// This helper method provides safe access to Array.CreateInstance. + /// + internal static object ArrayCreateInstance(Type type, int length) { + if (!type.IsVisible) { + DemandReflectionAccess(type); + } return Array.CreateInstance(type, length); } } From c848abfb8bf0b163f3c9e9f21dae3eb0376ca781 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:29:59 +0000 Subject: [PATCH 03/13] chore: bump test packages to latest versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Microsoft.NET.Test.Sdk: 17.4.1 → 18.3.0 - MSTest.TestAdapter/TestFramework: 3.0.2 → 3.11.1 - Moq: 4.18.4 → 4.20.72 --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1e340db..7f6792b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,12 +6,12 @@ - + - - - + + + From 39a31c99d69e1a58fb43c534cd10a9a807510ef5 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:34:21 +0000 Subject: [PATCH 04/13] =?UTF-8?q?chore:=20bump=20Mindbox.Expressions=203.3?= =?UTF-8?q?.0=20=E2=86=92=203.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7f6792b..61397e9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 0e5eae43d5137334729c598742a0b838180a3673 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:43:35 +0000 Subject: [PATCH 05/13] refactor: simplify MemoryExtensions.Contains check using pattern matching --- Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs index 2e03b74..3262b67 100644 --- a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs +++ b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs @@ -1884,10 +1884,7 @@ private SqlNode VisitMethodCall(MethodCallExpression mc) { // Translate the same as Enumerable.Contains(array, value). else if (mc.Method.DeclaringType == typeof(MemoryExtensions) && mc.Method.Name == "Contains" - && mc.Arguments.Count == 2 - && mc.Arguments[0] is MethodCallExpression spanConversion - && spanConversion.Method.Name == "op_Implicit" - && spanConversion.Arguments.Count == 1) { + && mc.Arguments[0] is MethodCallExpression { Method.Name: "op_Implicit" } spanConversion) { return this.VisitContains(spanConversion.Arguments[0], mc.Arguments[1]); } else if (IsDataManipulationCall(mc)) { From e8aab88cef1c73077ff261ed85dd0195854d9647 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:48:36 +0000 Subject: [PATCH 06/13] test: add ArrayContainsTranslationTests, multi-target net8.0+net10.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests verify that array.Contains(value) in LINQ queries translates to SQL IN clause on both net8.0 and net10.0. On .NET 10, the compiler generates MemoryExtensions.Contains — the fix in QueryConverter ensures it produces identical SQL to Enumerable.Contains on .NET 8. Also update CI to install both .NET 8 and .NET 10 SDKs. --- .github/workflows/pull-request.yml | 6 ++- .../Mindbox.Data.Linq.Tests.csproj | 1 + .../ArrayContainsTranslationTests.cs | 54 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3fa1947..e7c99e8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,9 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.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/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj index df7b7f7..301493a 100644 --- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj +++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj @@ -2,6 +2,7 @@ Mindbox.Data.Linq.Tests + net8.0;net10.0 Mindbox.Data.Linq.Tests false false diff --git a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs new file mode 100644 index 0000000..64ab3c1 --- /dev/null +++ b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Data.Linq; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Mindbox.Data.Linq.Tests.SqlGeneration +{ + /// + /// 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. + /// + [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); + } + + [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); + } + } +} From 5de6d056049d272a37a37fdeb259e7a1040984e1 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 15:58:06 +0000 Subject: [PATCH 07/13] docs: add README --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README.md 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). From d45b7d78f18f6a2aafc1cf15bfd17842853236bf Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 16:05:55 +0000 Subject: [PATCH 08/13] fix: ensure tests run on both net8.0 and net10.0 - Add to clear Directory.Build.props default - Set AppendTargetFrameworkToOutputPath=true to avoid DLL overwrites --- Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj index 301493a..1c8d33a 100644 --- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj +++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj @@ -2,6 +2,7 @@ Mindbox.Data.Linq.Tests + net8.0;net10.0 Mindbox.Data.Linq.Tests false @@ -9,7 +10,7 @@ mindbox.ru Itc.Nexus ru - false + true true true CS0169 From a27af5d8e7fc7758e759018d150618a01fe4afc5 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 16:39:25 +0000 Subject: [PATCH 09/13] refactor: check ReadOnlySpan<> return type instead of op_Implicit name More robust: matches any conversion to ReadOnlySpan, not just the specific op_Implicit generated by the current C# compiler. --- Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs index 3262b67..4323504 100644 --- a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs +++ b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs @@ -1884,7 +1884,10 @@ private SqlNode VisitMethodCall(MethodCallExpression mc) { // Translate the same as Enumerable.Contains(array, value). else if (mc.Method.DeclaringType == typeof(MemoryExtensions) && mc.Method.Name == "Contains" - && mc.Arguments[0] is MethodCallExpression { Method.Name: "op_Implicit" } spanConversion) { + && mc.Arguments[0] is MethodCallExpression spanConversion + && spanConversion.Method.ReturnType is { IsGenericType: true } returnType + && returnType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>) + && spanConversion.Arguments.Count == 1) { return this.VisitContains(spanConversion.Arguments[0], mc.Arguments[1]); } else if (IsDataManipulationCall(mc)) { From a975bdef21130a7e6316b143ba85c7bd00aa18dc Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 16:43:44 +0000 Subject: [PATCH 10/13] refactor: move common metadata to Directory.Build.props, TargetFramework per project - Directory.Build.props: LangVersion, Company, Authors, Copyright, PackageLicenseExpression - Each csproj now declares its own TargetFramework/TargetFrameworks - Remove duplicate Company/Authors/Copyright from individual csproj files - Remove leftover Product/NeutralLanguage from test csproj --- Directory.Build.props | 5 ++++- Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj | 6 +----- Mindbox.Data.Linq/Mindbox.Data.Linq.csproj | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 03ac2fb..d1c34ed 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,9 @@ - net8.0 12 + Mindbox + Mindbox + Copyright 2019 (c) Mindbox + MIT diff --git a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj index 1c8d33a..1a8bf8e 100644 --- a/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj +++ b/Mindbox.Data.Linq.Tests/Mindbox.Data.Linq.Tests.csproj @@ -1,15 +1,11 @@ - Mindbox.Data.Linq.Tests - net8.0;net10.0 + Mindbox.Data.Linq.Tests Mindbox.Data.Linq.Tests false false - mindbox.ru - Itc.Nexus - ru true true true diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj index 3d66b4c..61f7142 100644 --- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj +++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj @@ -1,12 +1,10 @@ + net8.0 Mindbox.Data.Linq - Mindbox 3.2.0 A clone of Microsoft System.Data.Linq to allow multi-DLL extensibility and EF compatibility. false - Copyright 2019 (c) Mindbox - Mindbox true true snupkg From 7335d2faab0aa8188909f6f433f59d0d33867e9b Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Mon, 23 Mar 2026 16:44:58 +0000 Subject: [PATCH 11/13] fix: use Mindbox.Data.Linq.Tests namespace (not folder-based) --- .../SqlGeneration/ArrayContainsTranslationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs index 64ab3c1..fdee899 100644 --- a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs +++ b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs @@ -3,7 +3,7 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Mindbox.Data.Linq.Tests.SqlGeneration +namespace Mindbox.Data.Linq.Tests { /// /// Tests that array.Contains(value) in LINQ queries is correctly translated to SQL IN clause. From a4bbcf34a5319377325f0853f72d0c99ca9ede42 Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Tue, 24 Mar 2026 10:53:41 +0000 Subject: [PATCH 12/13] fix: handle InvocationExpression pattern for MemoryExtensions.Contains in .NET 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In .NET 10, array.Contains(value) in LINQ expression trees may generate two patterns: 1. MethodCallExpression(op_Implicit, [array]) — for simple local captures 2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — when array is captured in a nested closure (e.g. passed as a method parameter) The original 10.8.0 fix only handled pattern 1. This fix adds: - TryExtractArrayFromSpanExpression() helper that handles both patterns - For pattern 2: extracts the array from the delegate's closure target via reflection - Broader condition check: mc.Arguments[0].Type is ReadOnlySpan (not just MethodCallExpression) New test ArrayContains_PassedAsParameter_TranslatesToSqlIn reproduces pattern 2. --- .../ArrayContainsTranslationTests.cs | 33 ++++++++++++ .../SqlClient/Query/QueryConverter.cs | 50 ++++++++++++++++--- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs index fdee899..a147c8f 100644 --- a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs +++ b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs @@ -9,6 +9,10 @@ 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 @@ -32,6 +36,35 @@ public void ArrayContains_TranslatesToSqlIn() 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() { diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs index 4323504..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); @@ -1880,15 +1912,19 @@ private SqlNode VisitMethodCall(MethodCallExpression mc) { return this.VisitSequenceOperatorCall(mc); } // In .NET 10, array.Contains(value) in expression trees compiles to - // MemoryExtensions.Contains(ReadOnlySpan.op_Implicit(array), value). - // Translate the same as Enumerable.Contains(array, value). + // 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] is MethodCallExpression spanConversion - && spanConversion.Method.ReturnType is { IsGenericType: true } returnType - && returnType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>) - && spanConversion.Arguments.Count == 1) { - return this.VisitContains(spanConversion.Arguments[0], mc.Arguments[1]); + && 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); From 68a9f82c48a33b9b595a3b9637cc45321c1a9a6a Mon Sep 17 00:00:00 2001 From: Aleksandr Korchak Date: Tue, 24 Mar 2026 10:54:04 +0000 Subject: [PATCH 13/13] chore: bump version to 10.8.1 --- Mindbox.Data.Linq/Mindbox.Data.Linq.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj index 61f7142..fc5788e 100644 --- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj +++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj @@ -8,7 +8,7 @@ true true snupkg - 10.8.0$(VersionTag) + 10.8.1$(VersionTag) SYSLIB0003;SYSLIB0011