From 2bedad6e79e05e0ece5ce5c605ca243ed1061902 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 6 Jul 2024 15:25:24 +0200 Subject: [PATCH] Enable Native AOT compatibility (trimming support) This is a squashed merge of PR #1070. --- MoreLinq.Test.Aot/MoreLinq.Test.Aot.csproj | 32 +++ MoreLinq.Test.Aot/ToDataTableTest.cs | 235 +++++++++++++++++++++ MoreLinq.Test/Throws.cs | 9 +- MoreLinq.Test/ToDataTableTest.cs | 32 +-- MoreLinq.sln | 6 + MoreLinq/Extensions.ToDataTable.g.cs | 18 +- MoreLinq/MoreLinq.csproj | 9 + MoreLinq/ToDataTable.cs | 148 ++++++++++--- test.cmd | 21 +- test.sh | 2 + 10 files changed, 456 insertions(+), 56 deletions(-) create mode 100644 MoreLinq.Test.Aot/MoreLinq.Test.Aot.csproj create mode 100644 MoreLinq.Test.Aot/ToDataTableTest.cs diff --git a/MoreLinq.Test.Aot/MoreLinq.Test.Aot.csproj b/MoreLinq.Test.Aot/MoreLinq.Test.Aot.csproj new file mode 100644 index 000000000..9453f8dc7 --- /dev/null +++ b/MoreLinq.Test.Aot/MoreLinq.Test.Aot.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + exe + false + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/MoreLinq.Test.Aot/ToDataTableTest.cs b/MoreLinq.Test.Aot/ToDataTableTest.cs new file mode 100644 index 000000000..163c76598 --- /dev/null +++ b/MoreLinq.Test.Aot/ToDataTableTest.cs @@ -0,0 +1,235 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2024 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + + [TestClass] + public class ToDataTableTest + { + sealed class TestObject(int key) + { + public int KeyField = key; + public Guid? ANullableGuidField = Guid.NewGuid(); + + public string AString { get; } = "ABCDEFGHIKKLMNOPQRSTUVWXYSZ"; + public decimal? ANullableDecimal { get; } = key / 3; + public object Unreadable { set => throw new NotImplementedException(); } + + public object this[int index] { get => new(); set { } } + + public override string ToString() => nameof(TestObject); + } + + readonly IReadOnlyCollection testObjects; + + public ToDataTableTest() => + this.testObjects = Enumerable.Range(0, 3) + .Select(i => new TestObject(i)) + .ToArray(); + + [TestMethod] + public void ToDataTableNullMemberExpressionMethod() + { + Expression>? expression = null; + + [UnconditionalSuppressMessage("Aot", "IL2026")] + void Act() => _ = this.testObjects.ToDataTable(expression!); + + var e = Assert.ThrowsException(Act); + Assert.AreEqual("expressions", e.ParamName); + } + + [TestMethod] + public void ToDataTableTableWithWrongColumnNames() + { + using var dt = new DataTable(); + _ = dt.Columns.Add("Test"); + + [UnconditionalSuppressMessage("Aot", "IL2026")] + void Act() => _ = this.testObjects.ToDataTable(dt); + + var e = Assert.ThrowsException(Act); + Assert.AreEqual("table", e.ParamName); + } + + [TestMethod] + public void ToDataTableTableWithWrongColumnDataType() + { + using var dt = new DataTable(); + _ = dt.Columns.Add("AString", typeof(int)); + + [UnconditionalSuppressMessage("Aot", "IL2026")] + void Act() => _ = this.testObjects.ToDataTable(dt, t => t.AString); + + var e = Assert.ThrowsException(Act); + Assert.AreEqual("table", e.ParamName); + } + + void TestDataTableMemberExpression(Expression> expression) + { + [UnconditionalSuppressMessage("Aot", "IL2026")] + void Act() => _ = this.testObjects.ToDataTable(expression); + + var e = Assert.ThrowsException(Act); + Assert.AreEqual("expressions", e.ParamName); + var innerException = e.InnerException; + Assert.IsNotNull(innerException); + Assert.IsInstanceOfType(innerException); + Assert.AreEqual("lambda", ((ArgumentException)innerException).ParamName); + } + + [TestMethod] + public void ToDataTableMemberExpressionMethod() + { + TestDataTableMemberExpression(t => t.ToString()); + } + + [TestMethod] + public void ToDataTableMemberExpressionNonMember() + { + TestDataTableMemberExpression(t => t.ToString().Length); + } + + [TestMethod] + public void ToDataTableMemberExpressionIndexer() + { + TestDataTableMemberExpression(t => t[0]); + } + + [TestMethod] + public void ToDataTableMemberExpressionStatic() + { + TestDataTableMemberExpression(_ => DateTime.Now); + } + + [TestMethod] + public void ToDataTableSchemaInDeclarationOrder() + { + [UnconditionalSuppressMessage("Aot", "IL2026")] + DataTable Act() => this.testObjects.ToDataTable(); + + var dt = Act(); + + // Assert properties first, then fields, then in declaration order + + Assert.AreEqual("KeyField", dt.Columns[2].Caption); + Assert.AreEqual(typeof(int), dt.Columns[2].DataType); + + Assert.AreEqual("ANullableGuidField", dt.Columns[3].Caption); + Assert.AreEqual(typeof(Guid), dt.Columns[3].DataType); + Assert.IsTrue(dt.Columns[3].AllowDBNull); + + Assert.AreEqual("AString", dt.Columns[0].Caption); + Assert.AreEqual(typeof(string), dt.Columns[0].DataType); + + Assert.AreEqual("ANullableDecimal", dt.Columns[1].Caption); + Assert.AreEqual(typeof(decimal), dt.Columns[1].DataType); + + Assert.AreEqual(4, dt.Columns.Count); + } + + [TestMethod] + public void ToDataTableContainsAllElements() + { + [UnconditionalSuppressMessage("Aot", "IL2026")] + DataTable Act() => this.testObjects.ToDataTable(); + + var dt = Act(); + + Assert.AreEqual(this.testObjects.Count, dt.Rows.Count); + } + + [TestMethod] + public void ToDataTableWithExpression() + { + [UnconditionalSuppressMessage("Aot", "IL2026")] + DataTable Act() => this.testObjects.ToDataTable(t => t.AString); + + var dt = Act(); + + Assert.AreEqual("AString", dt.Columns[0].Caption); + Assert.AreEqual(typeof(string), dt.Columns[0].DataType); + + Assert.AreEqual(1, dt.Columns.Count); + } + + [TestMethod] + public void ToDataTableWithSchema() + { + using var dt = new DataTable(); + var columns = dt.Columns; + _ = columns.Add("Column1", typeof(int)); + _ = columns.Add("Value", typeof(string)); + _ = columns.Add("Column3", typeof(int)); + _ = columns.Add("Name", typeof(string)); + + var vars = Environment.GetEnvironmentVariables() + .Cast() + .ToArray(); + + [UnconditionalSuppressMessage("Aot", "IL2026")] + void Act() => _ = vars.Select(e => new { Name = e.Key.ToString(), Value = e.Value!.ToString() }) + .ToDataTable(dt, e => e.Name, e => e.Value); + + Act(); + + var rows = dt.Rows.Cast().ToArray(); + Assert.AreEqual(vars.Length, rows.Length); + CollectionAssert.AreEqual(vars.Select(e => e.Key).ToArray(), rows.Select(r => r["Name"]).ToArray()); + CollectionAssert.AreEqual(vars.Select(e => e.Value).ToArray(), rows.Select(r => r["Value"]).ToArray()); + } + + readonly struct Point(int x, int y) + { +#pragma warning disable CA1805 // Do not initialize unnecessarily (avoids CS0649) + public static Point Empty = new(); +#pragma warning restore CA1805 // Do not initialize unnecessarily + public bool IsEmpty => X == 0 && Y == 0; + public int X { get; } = x; + public int Y { get; } = y; + } + + [TestMethod] + public void ToDataTableIgnoresStaticMembers() + { + [UnconditionalSuppressMessage("Aot", "IL2026")] + static DataTable Act() => new[] { new Point(12, 34) }.ToDataTable(); + + var points = Act(); + + Assert.AreEqual(3, points.Columns.Count); + var x = points.Columns["X"]; + var y = points.Columns["Y"]; + var empty = points.Columns["IsEmpty"]; + Assert.IsNotNull(x); + Assert.IsNotNull(y); + Assert.IsNotNull(empty); + var row = points.Rows.Cast().Single(); + Assert.AreEqual(12, row[x]); + Assert.AreEqual(34, row[y]); + Assert.AreEqual(row[empty], false); + } + } +} diff --git a/MoreLinq.Test/Throws.cs b/MoreLinq.Test/Throws.cs index 672234e3c..58877a6fc 100644 --- a/MoreLinq.Test/Throws.cs +++ b/MoreLinq.Test/Throws.cs @@ -36,16 +36,19 @@ public static ExactTypeConstraint TypeOf() NUnit.Framework.Throws.TypeOf(); public static EqualConstraint ArgumentException(string expectedParamName) => - NUnit.Framework.Throws.ArgumentException.With.ParamName().EqualTo(expectedParamName); + NUnit.Framework.Throws.ArgumentException.With.ParamName(expectedParamName); public static EqualConstraint ArgumentNullException(string expectedParamName) => - NUnit.Framework.Throws.ArgumentNullException.With.ParamName().EqualTo(expectedParamName); + NUnit.Framework.Throws.ArgumentNullException.With.ParamName(expectedParamName); public static ExactTypeConstraint ArgumentOutOfRangeException() => TypeOf(); public static EqualConstraint ArgumentOutOfRangeException(string expectedParamName) => - ArgumentOutOfRangeException().With.ParamName().EqualTo(expectedParamName); + ArgumentOutOfRangeException().With.ParamName(expectedParamName); + + public static EqualConstraint ParamName(this ConstraintExpression constraint, string expectedParamName) => + constraint.ParamName().EqualTo(expectedParamName); static ResolvableConstraintExpression ParamName(this ConstraintExpression constraint) => constraint.Property(nameof(System.ArgumentException.ParamName)); diff --git a/MoreLinq.Test/ToDataTableTest.cs b/MoreLinq.Test/ToDataTableTest.cs index d6cac4cda..65958f544 100644 --- a/MoreLinq.Test/ToDataTableTest.cs +++ b/MoreLinq.Test/ToDataTableTest.cs @@ -77,33 +77,35 @@ public void ToDataTableTableWithWrongColumnDataType() Throws.ArgumentException("table")); } + void TestDataTableMemberExpression(Expression> expression) + { + Assert.That(() => this.testObjects.ToDataTable(expression), + Throws.ArgumentException("expressions") + .And.InnerException.With.ParamName("lambda")); + } + [Test] public void ToDataTableMemberExpressionMethod() { - Assert.That(() => this.testObjects.ToDataTable(t => t.ToString()), - Throws.ArgumentException("lambda")); + TestDataTableMemberExpression(t => t.ToString()); } - [Test] public void ToDataTableMemberExpressionNonMember() { - Assert.That(() => this.testObjects.ToDataTable(t => t.ToString().Length), - Throws.ArgumentException("lambda")); + TestDataTableMemberExpression(t => t.ToString().Length); } [Test] public void ToDataTableMemberExpressionIndexer() { - Assert.That(() => this.testObjects.ToDataTable(t => t[0]), - Throws.ArgumentException("lambda")); + TestDataTableMemberExpression(t => t[0]); } [Test] public void ToDataTableMemberExpressionStatic() { - Assert.That(() => _ = this.testObjects.ToDataTable(_ => DateTime.Now), - Throws.ArgumentException("lambda")); + TestDataTableMemberExpression(_ => DateTime.Now); } [Test] @@ -113,12 +115,6 @@ public void ToDataTableSchemaInDeclarationOrder() // Assert properties first, then fields, then in declaration order - Assert.That(dt.Columns[0].Caption, Is.EqualTo("AString")); - Assert.That(dt.Columns[0].DataType, Is.EqualTo(typeof(string))); - - Assert.That(dt.Columns[1].Caption, Is.EqualTo("ANullableDecimal")); - Assert.That(dt.Columns[1].DataType, Is.EqualTo(typeof(decimal))); - Assert.That(dt.Columns[2].Caption, Is.EqualTo("KeyField")); Assert.That(dt.Columns[2].DataType, Is.EqualTo(typeof(int))); @@ -126,6 +122,12 @@ public void ToDataTableSchemaInDeclarationOrder() Assert.That(dt.Columns[3].DataType, Is.EqualTo(typeof(Guid))); Assert.That(dt.Columns[3].AllowDBNull, Is.True); + Assert.That(dt.Columns[0].Caption, Is.EqualTo("AString")); + Assert.That(dt.Columns[0].DataType, Is.EqualTo(typeof(string))); + + Assert.That(dt.Columns[1].Caption, Is.EqualTo("ANullableDecimal")); + Assert.That(dt.Columns[1].DataType, Is.EqualTo(typeof(decimal))); + Assert.That(dt.Columns.Count, Is.EqualTo(4)); } diff --git a/MoreLinq.sln b/MoreLinq.sln index ff1b65d6c..d37b3cefb 100644 --- a/MoreLinq.sln +++ b/MoreLinq.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoreLinq.Test", "MoreLinq.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoreLinq.ExtensionsGenerator", "bld\ExtensionsGenerator\MoreLinq.ExtensionsGenerator.csproj", "{5FA8F0E8-648A-4C4F-B1BB-B0C46959A36E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoreLinq.Test.Aot", "MoreLinq.Test.Aot\MoreLinq.Test.Aot.csproj", "{776973A3-AC2E-423E-8106-B4E296CE7752}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {5FA8F0E8-648A-4C4F-B1BB-B0C46959A36E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FA8F0E8-648A-4C4F-B1BB-B0C46959A36E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FA8F0E8-648A-4C4F-B1BB-B0C46959A36E}.Release|Any CPU.Build.0 = Release|Any CPU + {776973A3-AC2E-423E-8106-B4E296CE7752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {776973A3-AC2E-423E-8106-B4E296CE7752}.Debug|Any CPU.Build.0 = Debug|Any CPU + {776973A3-AC2E-423E-8106-B4E296CE7752}.Release|Any CPU.ActiveCfg = Release|Any CPU + {776973A3-AC2E-423E-8106-B4E296CE7752}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MoreLinq/Extensions.ToDataTable.g.cs b/MoreLinq/Extensions.ToDataTable.g.cs index 20380243d..bf5c9ef7b 100644 --- a/MoreLinq/Extensions.ToDataTable.g.cs +++ b/MoreLinq/Extensions.ToDataTable.g.cs @@ -43,7 +43,6 @@ namespace MoreLinq.Extensions [GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")] public static partial class ToDataTableExtension { - /// /// Converts a sequence to a object. /// @@ -54,8 +53,11 @@ public static partial class ToDataTableExtension /// /// This operator uses immediate execution. - public static DataTable ToDataTable(this IEnumerable source) - => MoreEnumerable.ToDataTable(source); + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + public static DataTable + ToDataTable<[DynamicallyAccessedMembers(DynamicallyAccessedPublicPropertiesOrFields)] T>( + this IEnumerable source) + => MoreEnumerable. ToDataTable(source); /// /// Appends elements in the sequence as rows of a given @@ -70,8 +72,10 @@ public static DataTable ToDataTable(this IEnumerable source) /// /// This operator uses immediate execution. + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] public static DataTable ToDataTable(this IEnumerable source, params Expression>[] expressions) => MoreEnumerable.ToDataTable(source, expressions); + /// /// Appends elements in the sequence as rows of a given object. /// @@ -84,9 +88,12 @@ public static DataTable ToDataTable(this IEnumerable source, params Expres /// /// This operator uses immediate execution. - public static TTable ToDataTable(this IEnumerable source, TTable table) + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + public static TTable + ToDataTable<[DynamicallyAccessedMembers(DynamicallyAccessedPublicPropertiesOrFields)] T, + TTable>(this IEnumerable source, TTable table) where TTable : DataTable - => MoreEnumerable.ToDataTable(source, table); + => MoreEnumerable. ToDataTable(source, table); /// /// Appends elements in the sequence as rows of a given @@ -103,6 +110,7 @@ public static TTable ToDataTable(this IEnumerable source, TTable t /// /// This operator uses immediate execution. + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] public static TTable ToDataTable(this IEnumerable source, TTable table, params Expression>[] expressions) where TTable : DataTable => MoreEnumerable.ToDataTable(source, table, expressions); diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index 6c1c604b5..8604ca538 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -126,6 +126,7 @@ true MoreLinq Library + true key.snk true true @@ -170,9 +171,13 @@ System.Index; System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute; + System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; + System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute; System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute; System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; + System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; + System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; @@ -211,6 +216,10 @@ $(DefineConstants);NO_ASYNC_STREAMS;NO_BUFFERS + + $(DefineConstants);DYNAMIC_CODE_FALLBACK + + diff --git a/MoreLinq/ToDataTable.cs b/MoreLinq/ToDataTable.cs index ba2275956..1872c3d85 100644 --- a/MoreLinq/ToDataTable.cs +++ b/MoreLinq/ToDataTable.cs @@ -20,61 +20,83 @@ namespace MoreLinq using System; using System.Collections.Generic; using System.Data; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; + using static Diagnostics; static partial class MoreEnumerable { /// - /// Appends elements in the sequence as rows of a given object. + /// Converts a sequence to a object. /// /// The type of the elements of . - /// /// The source. - /// /// - /// A or subclass representing the source. + /// A representing the source. /// /// This operator uses immediate execution. - public static TTable ToDataTable(this IEnumerable source, TTable table) - where TTable : DataTable + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + public static DataTable + ToDataTable<[DynamicallyAccessedMembers(DynamicallyAccessedPublicPropertiesOrFields)] T>( + this IEnumerable source) { - return ToDataTable(source, table, []); + if (source == null) throw new ArgumentNullException(nameof(source)); + + return source.ToDataTable(new DataTable()); } /// - /// Appends elements in the sequence as rows of a given - /// object with a set of lambda expressions specifying which members (property - /// or field) of each element in the sequence will supply the column values. + /// Appends elements in the sequence as rows of a given object. /// /// The type of the elements of . + /// /// The source. - /// Expressions providing access to element members. + /// /// - /// A representing the source. + /// A or subclass representing the source. /// /// This operator uses immediate execution. - public static DataTable ToDataTable(this IEnumerable source, params Expression>[] expressions) + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + public static TTable + ToDataTable<[DynamicallyAccessedMembers(DynamicallyAccessedPublicPropertiesOrFields)] T, + TTable>(this IEnumerable source, TTable table) + where TTable : DataTable { - return ToDataTable(source, new DataTable(), expressions); + if (source == null) throw new ArgumentNullException(nameof(source)); + if (table == null) throw new ArgumentNullException(nameof(table)); + + const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + + var members = typeof(T).GetProperties(bindingFlags) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .Cast() + .Concat(typeof(T).GetFields(bindingFlags)) + .ToArray(); + + return ToDataTable(source, table, members); } /// - /// Converts a sequence to a object. + /// Appends elements in the sequence as rows of a given + /// object with a set of lambda expressions specifying which members (property + /// or field) of each element in the sequence will supply the column values. /// /// The type of the elements of . /// The source. + /// Expressions providing access to element members. /// /// A representing the source. /// /// This operator uses immediate execution. - public static DataTable ToDataTable(this IEnumerable source) + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + public static DataTable ToDataTable(this IEnumerable source, params Expression>[] expressions) { - return ToDataTable(source, new DataTable()); + return ToDataTable(source, new DataTable(), expressions); } /// @@ -92,6 +114,7 @@ public static DataTable ToDataTable(this IEnumerable source) /// /// This operator uses immediate execution. + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] public static TTable ToDataTable(this IEnumerable source, TTable table, params Expression>[] expressions) where TTable : DataTable { @@ -99,7 +122,13 @@ public static TTable ToDataTable(this IEnumerable source, TTable t if (table == null) throw new ArgumentNullException(nameof(table)); if (expressions == null) throw new ArgumentNullException(nameof(expressions)); - var members = PrepareMemberInfos(expressions).ToArray(); + return ToDataTable(source, table, PrepareMemberInfos(expressions)); + } + + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + static TTable ToDataTable(IEnumerable source, TTable table, MemberInfo[] members) + where TTable : DataTable + { var boundMembers = BuildOrBindSchema(table, members); var shredder = CreateShredder(boundMembers); @@ -127,19 +156,10 @@ public static TTable ToDataTable(this IEnumerable source, TTable t return table; } - static IEnumerable PrepareMemberInfos(ICollection>> expressions) + static MemberInfo[] PrepareMemberInfos(ICollection>> expressions) { - // - // If no lambda expressions supplied then reflect them off the source element type. - // - if (expressions.Count == 0) - { - return from m in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance) - where m.MemberType == MemberTypes.Field - || m is PropertyInfo { CanRead: true } p && p.GetIndexParameters().Length == 0 - select m; - } + return []; // // Ensure none of the expressions is null. @@ -149,7 +169,7 @@ static IEnumerable PrepareMemberInfos(ICollection + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] static MemberInfo?[] BuildOrBindSchema(DataTable table, MemberInfo[] members) { // @@ -224,8 +245,48 @@ var type when Nullable.GetUnderlyingType(type) is { } ut => ut, }; } - static Func CreateShredder(MemberInfo?[] members) + [UnconditionalSuppressMessage("Aot", "IL3050:RequiresDynamicCode", + Justification = "Falls back to reflection-based member access at run-time if the CLR " + + "version does not support dynamic code generation.")] + static Func CreateShredder(MemberInfo?[] members) { +#if DYNAMIC_CODE_FALLBACK + + // + // If the runtime does not support dynamic code generation, then + // fall back to reflection-based member access at run-time. + // + // See also: https://github.com/dotnet/runtime/issues/17973#issuecomment-1330799386 + // + + if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + { + return obj => + { + var values = new object?[members.Length]; + + for (var i = 0; i < members.Length; i++) + { + var member = members[i]; + values[i] = member switch + { + null => null, + PropertyInfo pi => pi.GetValue(obj), + FieldInfo fi => fi.GetValue(obj), + _ => throw new UnreachableException(), + }; + } + + return values; + }; + } +#endif + + // + // Otherwise compile a lambda expression that will extract the + // values of the specified members from an object instance. + // + var parameter = Expression.Parameter(typeof(T), "e"); // @@ -240,7 +301,7 @@ static Func CreateShredder(MemberInfo?[] members) var array = Expression.NewArrayInit(typeof(object), initializers); - var lambda = Expression.Lambda>(array, parameter); + var lambda = Expression.Lambda>(array, parameter); return lambda.Compile(); @@ -251,4 +312,27 @@ UnaryExpression CreateMemberAccessor(MemberInfo member) } } } + + namespace Extensions + { + partial class ToDataTableExtension + { + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedPublicPropertiesOrFields + = DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.PublicFields; + + internal const string RequiresUnreferencedCodeMessage = + "This method uses reflection to access public properties and fields of the source " + + "type, and in turn the types of those properties and fields. That latter could be " + + "problematic and require root descriptors for some custom and complex types " + + "(although columns usually store simple, scalar types). For more, see: " + + "https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options#root-descriptors"; + } + } + + file static class Diagnostics + { + public const DynamicallyAccessedMemberTypes DynamicallyAccessedPublicPropertiesOrFields = MoreLinq.Extensions.ToDataTableExtension.DynamicallyAccessedPublicPropertiesOrFields; + public const string RequiresUnreferencedCodeMessage = MoreLinq.Extensions.ToDataTableExtension.RequiresUnreferencedCodeMessage; + } } diff --git a/test.cmd b/test.cmd index a6d28730d..209719ca3 100644 --- a/test.cmd +++ b/test.cmd @@ -7,6 +7,10 @@ popd & exit /b %ERRORLEVEL% setlocal if not defined SKIP_TEST_BUILD set SKIP_TEST_BUILD=false if %SKIP_TEST_BUILD%==false call build || exit /b 1 +if not "%~1"=="aot" goto :test-all +call :test-aot +exit /b %ERRORLEVEL% +:test-all call :clean ^ && call :test net8.0 Debug ^ && call :test net8.0 Release ^ @@ -14,7 +18,8 @@ call :clean ^ && call :test net6.0 Release ^ && call :test net471 Debug ^ && call :test net471 Release ^ - && call :report-cover + && call :report-cover ^ + && call :test-aot exit /b %ERRORLEVEL% :clean @@ -51,3 +56,17 @@ dotnet reportgenerator -reports:coverage-*.opencover.xml ^ -targetdir:reports ^ && type reports\Summary.txt exit /b %ERRORLEVEL% + +:test-aot +setlocal +cd MoreLinq.Test.Aot +dotnet publish +if not ERRORLEVEL==0 exit /b %ERRORLEVEL% +set AOT_TEST_PUBLISH_DIR= +for /f %%d in ('dir /ad /s /b publish') do if not defined AOT_TEST_PUBLISH_DIR set AOT_TEST_PUBLISH_DIR=%%~d +if not defined AOT_TEST_PUBLISH_DIR ( + echo>&2 Published binary directory not found! + exit /b 1 +) +"%AOT_TEST_PUBLISH_DIR%\MoreLinq.Test.Aot.exe" +exit /b %ERRORLEVEL% diff --git a/test.sh b/test.sh index 9bd9806a0..8427d1849 100755 --- a/test.sh +++ b/test.sh @@ -31,3 +31,5 @@ else mono MoreLinq.Test/bin/$c/net471/MoreLinq.Test.exe done fi +dotnet publish MoreLinq.Test.Aot +"$(find MoreLinq.Test.Aot -type d -name publish)/MoreLinq.Test.Aot"