From 6e7f9c2482e4a83921679260983919157b6d967d Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 23 May 2025 11:33:19 +0200 Subject: [PATCH 1/4] Added EnumCursorKeySerializer --- .../Expressions/ExpressionHelpers.cs | 55 +- .../CursorKeySerializerRegistration.cs | 8 + .../Serializers/EnumCursorKeySerializer.cs | 60 ++ .../PagingHelperTests.cs | 37 +- .../TestContext/Test.cs | 64 ++ ...2_Items_Second_Page_Descending_AllTypes.md | 718 ++++-------------- 6 files changed, 351 insertions(+), 591 deletions(-) create mode 100644 src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index e037e1ed18d..37143f3d3ea 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -77,9 +77,21 @@ public static (Expression> WhereExpression, int Offset) BuildWhere { var handledKey = handled[j]; - keyExpr = Expression.Equal( - Expression.Call(ReplaceParameter(handledKey.Expression, parameter), handledKey.CompareMethod, - cursorExpr[j]), zero); + if (handledKey.Expression.ReturnType.IsEnum) + { + keyExpr = Expression.Equal( + ReplaceParameter(handledKey.Expression, parameter), + cursorExpr[j]); + } + else + { + keyExpr = Expression.Equal( + Expression.Call( + ReplaceParameter(handledKey.Expression, parameter), + handledKey.CompareMethod, + cursorExpr[j]), + zero); + } current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr); } @@ -88,13 +100,36 @@ public static (Expression> WhereExpression, int Offset) BuildWhere ? key.Direction == CursorKeyDirection.Ascending : key.Direction == CursorKeyDirection.Descending; - keyExpr = greaterThan - ? Expression.GreaterThan( - Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]), - zero) - : Expression.LessThan( - Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]), - zero); + if (key.Expression.ReturnType.IsEnum) + { + var underlyingType = Enum.GetUnderlyingType(key.Expression.ReturnType); + + keyExpr = greaterThan + ? Expression.GreaterThan( + Expression.Convert( + ReplaceParameter(key.Expression, parameter), underlyingType), + Expression.Convert(cursorExpr[i], underlyingType)) + : Expression.LessThan( + Expression.Convert( + ReplaceParameter(key.Expression, parameter), underlyingType), + Expression.Convert(cursorExpr[i], underlyingType)); + } + else + { + keyExpr = greaterThan + ? Expression.GreaterThan( + Expression.Call( + ReplaceParameter(key.Expression, parameter), + key.CompareMethod, + cursorExpr[i]), + zero) + : Expression.LessThan( + Expression.Call( + ReplaceParameter(key.Expression, parameter), + key.CompareMethod, + cursorExpr[i]), + zero); + } current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr); expression = expression is null ? current : Expression.OrElse(expression, current); diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs index b4b6ff787d4..2c3856a7aef 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs @@ -27,6 +27,14 @@ public static class CursorKeySerializerRegistration new UShortCursorKeySerializer(), new UIntCursorKeySerializer(), new ULongCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer(), + new EnumCursorKeySerializer() ]; /// diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs new file mode 100644 index 00000000000..186bf347479 --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs @@ -0,0 +1,60 @@ +using System.Buffers.Text; +using System.Numerics; +using System.Reflection; + +namespace GreenDonut.Data.Cursors.Serializers; + +internal sealed class EnumCursorKeySerializer : ICursorKeySerializer where T : struct, INumber +{ + private static readonly MethodInfo _compareTo = CompareToResolver.GetCompareToMethod(); + + public bool IsSupported(Type type) + => type.IsEnum && Enum.GetUnderlyingType(type) == typeof(T); + + public MethodInfo GetCompareToMethod(Type type) + => _compareTo; + + public object Parse(ReadOnlySpan formattedKey) + { + var t = typeof(T); + + return t switch + { + _ when t == typeof(byte) && Utf8Parser.TryParse(formattedKey, out byte b, out _) + => b, + _ when t == typeof(sbyte) && Utf8Parser.TryParse(formattedKey, out sbyte sb, out _) + => sb, + _ when t == typeof(short) && Utf8Parser.TryParse(formattedKey, out short s, out _) + => s, + _ when t == typeof(ushort) && Utf8Parser.TryParse(formattedKey, out ushort us, out _) + => us, + _ when t == typeof(int) && Utf8Parser.TryParse(formattedKey, out int i, out _) + => i, + _ when t == typeof(uint) && Utf8Parser.TryParse(formattedKey, out uint ui, out _) + => ui, + _ when t == typeof(long) && Utf8Parser.TryParse(formattedKey, out long l, out _) + => l, + _ when t == typeof(ulong) && Utf8Parser.TryParse(formattedKey, out ulong ul, out _) + => ul, + _ => throw new InvalidOperationException("Unsupported enum type.") + }; + } + + public bool TryFormat(object key, Span buffer, out int written) + { + var t = typeof(T); + + return t switch + { + _ when t == typeof(byte) => Utf8Formatter.TryFormat((byte)key, buffer, out written), + _ when t == typeof(sbyte) => Utf8Formatter.TryFormat((sbyte)key, buffer, out written), + _ when t == typeof(short) => Utf8Formatter.TryFormat((short)key, buffer, out written), + _ when t == typeof(ushort) => Utf8Formatter.TryFormat((ushort)key, buffer, out written), + _ when t == typeof(int) => Utf8Formatter.TryFormat((int)key, buffer, out written), + _ when t == typeof(uint) => Utf8Formatter.TryFormat((uint)key, buffer, out written), + _ when t == typeof(long) => Utf8Formatter.TryFormat((long)key, buffer, out written), + _ when t == typeof(ulong) => Utf8Formatter.TryFormat((ulong)key, buffer, out written), + _ => throw new InvalidOperationException("Unsupported enum type.") + }; + } +} diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs index f2cfc2ec440..a75f6041aa0 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs @@ -412,7 +412,15 @@ public async Task Fetch_First_2_Items_Second_Page_Descending_AllTypes() { "TimeOnly", context.Tests.OrderByDescending(t => t.TimeOnly) }, { "UInt", context.Tests.OrderByDescending(t => t.UInt) }, { "ULong", context.Tests.OrderByDescending(t => t.ULong) }, - { "UShort", context.Tests.OrderByDescending(t => t.UShort) } + { "UShort", context.Tests.OrderByDescending(t => t.UShort) }, + { "ByteEnum", context.Tests.OrderByDescending(t => t.ByteEnum) }, + { "SbyteEnum", context.Tests.OrderByDescending(t => t.SbyteEnum) }, + { "ShortEnum", context.Tests.OrderByDescending(t => t.ShortEnum) }, + { "UshortEnum", context.Tests.OrderByDescending(t => t.UshortEnum) }, + { "IntEnum", context.Tests.OrderByDescending(t => t.IntEnum) }, + { "UintEnum", context.Tests.OrderByDescending(t => t.UintEnum) }, + { "LongEnum", context.Tests.OrderByDescending(t => t.LongEnum) }, + { "UlongEnum", context.Tests.OrderByDescending(t => t.UlongEnum) } }; // Act @@ -430,7 +438,16 @@ public async Task Fetch_First_2_Items_Second_Page_Descending_AllTypes() } // Assert - pages.MatchMarkdownSnapshot(); + pages.ToDictionary( + p => p.Key, + p => + p.Value.Select( + t => + new + { + t.Id, + Value = t.GetType().GetProperty(p.Key)?.GetValue(t) + })).MatchMarkdownSnapshot(); } private static async Task SeedAsync(string connectionString) @@ -471,19 +488,19 @@ private static async Task SeedTestAsync(string connectionString) await using var context = new CatalogContext(connectionString); await context.Database.EnsureCreatedAsync(); - for (var i = 1; i <= 10; i++) + for (var i = 1; i <= 8; i++) { var test = new Test { Id = i, - Bool = i % 2 == 0, + Bool = i > 4, DateOnly = DateOnly.FromDateTime(DateTime.UnixEpoch.AddDays(i - 1)), DateTime = DateTime.UnixEpoch.AddDays(i - 1), DateTimeOffset = DateTimeOffset.UnixEpoch.AddDays(i - 1), Decimal = i, Double = i, Float = i, - Guid = Guid.ParseExact($"0000000000000000000000000000000{i - 1}", "N"), + Guid = Guid.ParseExact($"0000000000000000000000000000000{i}", "N"), Int = i, Long = i, Short = (short)i, @@ -492,7 +509,15 @@ private static async Task SeedTestAsync(string connectionString) TimeSpan = TimeSpan.FromHours(i), UInt = (uint)i, ULong = (ulong)i, - UShort = (ushort)i + UShort = (ushort)i, + ByteEnum = i > 4 ? TestByteEnum.Two : TestByteEnum.One, + SbyteEnum = i > 4 ? TestSbyteEnum.Two : TestSbyteEnum.One, + ShortEnum = i > 4 ? TestShortEnum.Two : TestShortEnum.One, + UshortEnum = i > 4 ? TestUshortEnum.Two : TestUshortEnum.One, + IntEnum = i > 4 ? TestIntEnum.Two : TestIntEnum.One, + UintEnum = i > 4 ? TestUintEnum.Two : TestUintEnum.One, + LongEnum = i > 4 ? TestLongEnum.Two : TestLongEnum.One, + UlongEnum = i > 4 ? TestUlongEnum.Two : TestUlongEnum.One }; context.Tests.Add(test); diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/TestContext/Test.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/TestContext/Test.cs index 4717d18bfb6..538d18636dd 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/TestContext/Test.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/TestContext/Test.cs @@ -37,4 +37,68 @@ public class Test public ulong ULong { get; set; } public ushort UShort { get; set; } + + public TestByteEnum ByteEnum { get; set; } + + public TestSbyteEnum SbyteEnum { get; set; } + + public TestShortEnum ShortEnum { get; set; } + + public TestUshortEnum UshortEnum { get; set; } + + public TestIntEnum IntEnum { get; set; } + + public TestUintEnum UintEnum { get; set; } + + public TestLongEnum LongEnum { get; set; } + + public TestUlongEnum UlongEnum { get; set; } +} + +public enum TestByteEnum : byte +{ + One = 1, + Two = 2 +} + +public enum TestSbyteEnum : sbyte +{ + One = 1, + Two = 2 +} + +public enum TestShortEnum : short +{ + One = 1, + Two = 2 +} + +public enum TestUshortEnum : ushort +{ + One = 1, + Two = 2 +} + +public enum TestIntEnum +{ + One = 1, + Two = 2 +} + +public enum TestUintEnum : uint +{ + One = 1, + Two = 2 +} + +public enum TestLongEnum : long +{ + One = 1, + Two = 2 +} + +public enum TestUlongEnum : ulong +{ + One = 1, + Two = 2 } diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_Descending_AllTypes.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_Descending_AllTypes.md index 382a2404eff..4398d9586da 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_Descending_AllTypes.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_Descending_AllTypes.md @@ -5,673 +5,241 @@ "Bool": [ { "Id": 6, - "Bool": true, - "DateOnly": "1970-01-06", - "DateTime": "1970-01-06T00:00:00Z", - "DateTimeOffset": "1970-01-06T00:00:00+00:00", - "Decimal": 6.0, - "Double": 6.0, - "Float": 6.0, - "Guid": "00000000-0000-0000-0000-000000000005", - "Int": 6, - "Long": 6, - "Short": 6, - "String": "6", - "TimeOnly": "06:00:00", - "TimeSpan": "06:00:00", - "UInt": 6, - "ULong": 6, - "UShort": 6 + "Value": true }, { - "Id": 4, - "Bool": true, - "DateOnly": "1970-01-04", - "DateTime": "1970-01-04T00:00:00Z", - "DateTimeOffset": "1970-01-04T00:00:00+00:00", - "Decimal": 4.0, - "Double": 4.0, - "Float": 4.0, - "Guid": "00000000-0000-0000-0000-000000000003", - "Int": 4, - "Long": 4, - "Short": 4, - "String": "4", - "TimeOnly": "04:00:00", - "TimeSpan": "04:00:00", - "UInt": 4, - "ULong": 4, - "UShort": 4 + "Id": 5, + "Value": true } ], "DateOnly": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": "1970-01-06" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "1970-01-05" } ], "DateTime": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": "1970-01-06T00:00:00Z" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "1970-01-05T00:00:00Z" } ], "DateTimeOffset": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": "1970-01-06T00:00:00+00:00" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "1970-01-05T00:00:00+00:00" } ], "Decimal": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6.0 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5.0 } ], "Double": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6.0 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5.0 } ], "Float": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6.0 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5.0 } ], "Guid": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": "00000000-0000-0000-0000-000000000006" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "00000000-0000-0000-0000-000000000005" } ], "Int": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5 } ], "Long": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5 } ], "Short": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5 } ], "String": [ { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 6, + "Value": "6" }, { - "Id": 6, - "Bool": true, - "DateOnly": "1970-01-06", - "DateTime": "1970-01-06T00:00:00Z", - "DateTimeOffset": "1970-01-06T00:00:00+00:00", - "Decimal": 6.0, - "Double": 6.0, - "Float": 6.0, - "Guid": "00000000-0000-0000-0000-000000000005", - "Int": 6, - "Long": 6, - "Short": 6, - "String": "6", - "TimeOnly": "06:00:00", - "TimeSpan": "06:00:00", - "UInt": 6, - "ULong": 6, - "UShort": 6 + "Id": 5, + "Value": "5" } ], "TimeOnly": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": "06:00:00" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "05:00:00" } ], "UInt": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5 } ], "ULong": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": 5 } ], "UShort": [ { - "Id": 8, - "Bool": true, - "DateOnly": "1970-01-08", - "DateTime": "1970-01-08T00:00:00Z", - "DateTimeOffset": "1970-01-08T00:00:00+00:00", - "Decimal": 8.0, - "Double": 8.0, - "Float": 8.0, - "Guid": "00000000-0000-0000-0000-000000000007", - "Int": 8, - "Long": 8, - "Short": 8, - "String": "8", - "TimeOnly": "08:00:00", - "TimeSpan": "08:00:00", - "UInt": 8, - "ULong": 8, - "UShort": 8 + "Id": 6, + "Value": 6 + }, + { + "Id": 5, + "Value": 5 + } + ], + "ByteEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "SbyteEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "ShortEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "UshortEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "IntEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "UintEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "LongEnum": [ + { + "Id": 6, + "Value": "Two" + }, + { + "Id": 5, + "Value": "Two" + } + ], + "UlongEnum": [ + { + "Id": 6, + "Value": "Two" }, { - "Id": 7, - "Bool": false, - "DateOnly": "1970-01-07", - "DateTime": "1970-01-07T00:00:00Z", - "DateTimeOffset": "1970-01-07T00:00:00+00:00", - "Decimal": 7.0, - "Double": 7.0, - "Float": 7.0, - "Guid": "00000000-0000-0000-0000-000000000006", - "Int": 7, - "Long": 7, - "Short": 7, - "String": "7", - "TimeOnly": "07:00:00", - "TimeSpan": "07:00:00", - "UInt": 7, - "ULong": 7, - "UShort": 7 + "Id": 5, + "Value": "Two" } ] } From 54851a6808714cb519145709fdd67571a226fe2e Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Mar 2026 12:54:38 +0000 Subject: [PATCH 2/4] Integrate enum key comparisons with nullable paging predicates --- .../Expressions/ExpressionHelpers.cs | 150 +++++++++--------- ...Tests.Paging_First_5_After_Id_13_NET8_0.md | 2 +- ...Tests.Paging_First_5_After_Id_13_NET9_0.md | 2 +- ...ests.Paging_First_5_Before_Id_96_NET8_0.md | 2 +- ...ests.Paging_First_5_Before_Id_96_NET9_0.md | 2 +- 5 files changed, 81 insertions(+), 77 deletions(-) diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index 3aecfbbddcf..406b1f3fee8 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -153,6 +153,11 @@ private static Expression BuildEqualToKeyExpr( Expression cursorExpr) { var keyExpr = ReplaceParameter(cursorKey.Expression, parameter); + var enumType = Nullable.GetUnderlyingType(cursorKey.Expression.ReturnType) + ?? cursorKey.Expression.ReturnType; + var enumUnderlyingType = enumType.IsEnum + ? Enum.GetUnderlyingType(enumType) + : null; // Access the value of the key if it is a nullable value type. var keyValueExpr = cursorKey.Expression.ReturnType.IsValueType && keyIsNullable @@ -173,15 +178,29 @@ private static Expression BuildEqualToKeyExpr( else { // SQL: WHERE key IS NOT NULL AND key = cursorValue. + var equalExpr = enumUnderlyingType is null + ? Expression.Equal( + Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.Equal( + Expression.Convert(keyValueExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); + keyExpr = Expression.AndAlso( Expression.NotEqual(keyExpr, nullConst), - BuildEqualToCursorExpr(cursorKey, keyValueExpr, cursorExpr)); + equalExpr); } } else { // SQL: WHERE key = cursorValue. - keyExpr = BuildEqualToCursorExpr(cursorKey, keyExpr, cursorExpr); + keyExpr = enumUnderlyingType is null + ? Expression.Equal( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.Equal( + Expression.Convert(keyExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); } return keyExpr; @@ -196,6 +215,11 @@ private static Expression BuildGreaterThanKeyExpr( Expression cursorExpr) { var keyExpr = ReplaceParameter(cursorKey.Expression, parameter); + var enumType = Nullable.GetUnderlyingType(cursorKey.Expression.ReturnType) + ?? cursorKey.Expression.ReturnType; + var enumUnderlyingType = enumType.IsEnum + ? Enum.GetUnderlyingType(enumType) + : null; // Access the value of the key if it is a nullable value type. var keyValueExpr = @@ -224,22 +248,42 @@ private static Expression BuildGreaterThanKeyExpr( if (nullOrdering == NullOrdering.NativeNullsFirst) { // SQL: WHERE key > cursorValue. - keyExpr = BuildGreaterThanCursorExpr(cursorKey, keyValueExpr, cursorExpr); + keyExpr = enumUnderlyingType is null + ? Expression.GreaterThan( + Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.GreaterThan( + Expression.Convert(keyValueExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); } else { // When nulls are last, null is greater than any non-null value. // SQL: WHERE key IS NULL OR key > cursorValue. + var greaterThanExpr = enumUnderlyingType is null + ? Expression.GreaterThan( + Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.GreaterThan( + Expression.Convert(keyValueExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); + keyExpr = Expression.OrElse( Expression.Equal(keyExpr, nullConst), - BuildGreaterThanCursorExpr(cursorKey, keyValueExpr, cursorExpr)); + greaterThanExpr); } } } else { // SQL: WHERE key > cursorValue. - keyExpr = BuildGreaterThanCursorExpr(cursorKey, keyExpr, cursorExpr); + keyExpr = enumUnderlyingType is null + ? Expression.GreaterThan( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.GreaterThan( + Expression.Convert(keyExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); } return keyExpr; @@ -254,6 +298,11 @@ private static Expression BuildLessThanKeyExpr( Expression cursorExpr) { var keyExpr = ReplaceParameter(cursorKey.Expression, parameter); + var enumType = Nullable.GetUnderlyingType(cursorKey.Expression.ReturnType) + ?? cursorKey.Expression.ReturnType; + var enumUnderlyingType = enumType.IsEnum + ? Enum.GetUnderlyingType(enumType) + : null; // Access the value of the key if it is a nullable value type. var keyValueExpr = @@ -283,91 +332,46 @@ private static Expression BuildLessThanKeyExpr( { // With nulls first, null is less than any non-null value. // SQL: WHERE key IS NULL OR key < cursorValue. + var lessThanExpr = enumUnderlyingType is null + ? Expression.LessThan( + Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.LessThan( + Expression.Convert(keyValueExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); + keyExpr = Expression.OrElse( Expression.Equal(keyExpr, nullConst), - BuildLessThanCursorExpr(cursorKey, keyValueExpr, cursorExpr)); + lessThanExpr); } else { // SQL: WHERE key < cursorValue. - keyExpr = BuildLessThanCursorExpr(cursorKey, keyValueExpr, cursorExpr); + keyExpr = enumUnderlyingType is null + ? Expression.LessThan( + Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.LessThan( + Expression.Convert(keyValueExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); } } } else { // SQL: WHERE key < cursorValue. - keyExpr = BuildLessThanCursorExpr(cursorKey, keyExpr, cursorExpr); + keyExpr = enumUnderlyingType is null + ? Expression.LessThan( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero) + : Expression.LessThan( + Expression.Convert(keyExpr, enumUnderlyingType), + Expression.Convert(cursorExpr, enumUnderlyingType)); } return keyExpr; } - private static Expression BuildEqualToCursorExpr( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - if (TryGetEnumUnderlyingType(cursorKey.Expression.ReturnType, out var enumUnderlyingType)) - { - return Expression.Equal( - Expression.Convert(keyExpr, enumUnderlyingType), - Expression.Convert(cursorExpr, enumUnderlyingType)); - } - - return Expression.Equal( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - - private static Expression BuildGreaterThanCursorExpr( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - if (TryGetEnumUnderlyingType(cursorKey.Expression.ReturnType, out var enumUnderlyingType)) - { - return Expression.GreaterThan( - Expression.Convert(keyExpr, enumUnderlyingType), - Expression.Convert(cursorExpr, enumUnderlyingType)); - } - - return Expression.GreaterThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - - private static Expression BuildLessThanCursorExpr( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - if (TryGetEnumUnderlyingType(cursorKey.Expression.ReturnType, out var enumUnderlyingType)) - { - return Expression.LessThan( - Expression.Convert(keyExpr, enumUnderlyingType), - Expression.Convert(cursorExpr, enumUnderlyingType)); - } - - return Expression.LessThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - - private static bool TryGetEnumUnderlyingType(Type keyType, out Type enumUnderlyingType) - { - var enumType = Nullable.GetUnderlyingType(keyType) ?? keyType; - - if (enumType.IsEnum) - { - enumUnderlyingType = Enum.GetUnderlyingType(enumType); - return true; - } - - enumUnderlyingType = default!; - return false; - } - private static bool IsNullable(LambdaExpression expression) { if (expression.ReturnType.IsValueType) diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET8_0.md index dcc9055a17d..8ebd25adcb9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET8_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.Int32]).value) > 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.Int32]).value) > 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET9_0.md index dcc9055a17d..8ebd25adcb9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_After_Id_13_NET9_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.Int32]).value) > 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.Int32]).value) > 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET8_0.md index ef3c0174609..396d78db6c5 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET8_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.Int32]).value) < 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.Int32]).value) < 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET9_0.md index ef3c0174609..396d78db6c5 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperIntegrationTests.Paging_First_5_Before_Id_96_NET9_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass14_0`1[System.Int32]).value) < 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass16_0`1[System.Int32]).value) < 0)))).Take(6) ``` ## Result 3 From 4b26eea09aa008e6192d61349185703656f02f5e Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Mar 2026 14:51:10 +0000 Subject: [PATCH 3/4] Stabilize paging expression snapshot output --- .../Expressions/ExpressionHelpers.cs | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index 9707af66f3b..850860e3d8a 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -304,63 +304,6 @@ private static Expression BuildLessThanKeyExpr( return keyExpr; } - private static Expression BuildEqualComparison( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; - - if (comparisonType.IsEnum) - { - return Expression.Equal(keyExpr, cursorExpr); - } - - return Expression.Equal( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - - private static Expression BuildGreaterThanComparison( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; - - if (comparisonType.IsEnum) - { - var underlyingType = Enum.GetUnderlyingType(comparisonType); - return Expression.GreaterThan( - Expression.Convert(keyExpr, underlyingType), - Expression.Convert(cursorExpr, underlyingType)); - } - - return Expression.GreaterThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - - private static Expression BuildLessThanComparison( - CursorKey cursorKey, - Expression keyExpr, - Expression cursorExpr) - { - var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; - - if (comparisonType.IsEnum) - { - var underlyingType = Enum.GetUnderlyingType(comparisonType); - return Expression.LessThan( - Expression.Convert(keyExpr, underlyingType), - Expression.Convert(cursorExpr, underlyingType)); - } - - return Expression.LessThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); - } - private static bool IsNullable(LambdaExpression expression) { if (expression.ReturnType.IsValueType) @@ -680,6 +623,63 @@ private static Expression CreateAndConvertParameter(T value) return lambda.Body; } + private static Expression BuildEqualComparison( + CursorKey cursorKey, + Expression keyExpr, + Expression cursorExpr) + { + var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; + + if (comparisonType.IsEnum) + { + return Expression.Equal(keyExpr, cursorExpr); + } + + return Expression.Equal( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero); + } + + private static Expression BuildGreaterThanComparison( + CursorKey cursorKey, + Expression keyExpr, + Expression cursorExpr) + { + var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; + + if (comparisonType.IsEnum) + { + var underlyingType = Enum.GetUnderlyingType(comparisonType); + return Expression.GreaterThan( + Expression.Convert(keyExpr, underlyingType), + Expression.Convert(cursorExpr, underlyingType)); + } + + return Expression.GreaterThan( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero); + } + + private static Expression BuildLessThanComparison( + CursorKey cursorKey, + Expression keyExpr, + Expression cursorExpr) + { + var comparisonType = Nullable.GetUnderlyingType(keyExpr.Type) ?? keyExpr.Type; + + if (comparisonType.IsEnum) + { + var underlyingType = Enum.GetUnderlyingType(comparisonType); + return Expression.LessThan( + Expression.Convert(keyExpr, underlyingType), + Expression.Convert(cursorExpr, underlyingType)); + } + + return Expression.LessThan( + Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), + s_zero); + } + private static Expression ReplaceParameter( LambdaExpression expression, ParameterExpression replacement) From 8a4e5732b928e855079f575abf803e4f58aec458 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Mar 2026 17:29:58 +0000 Subject: [PATCH 4/4] Support nullable enum cursor key serializers --- .../Serializers/EnumCursorKeySerializer.cs | 5 +- .../EnumCursorKeySerializerTests.cs | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/EnumCursorKeySerializerTests.cs diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs index 186bf347479..24ba8a4acd3 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs @@ -9,7 +9,10 @@ internal sealed class EnumCursorKeySerializer : ICursorKeySerializer where T private static readonly MethodInfo _compareTo = CompareToResolver.GetCompareToMethod(); public bool IsSupported(Type type) - => type.IsEnum && Enum.GetUnderlyingType(type) == typeof(T); + { + var enumType = Nullable.GetUnderlyingType(type) ?? type; + return enumType.IsEnum && Enum.GetUnderlyingType(enumType) == typeof(T); + } public MethodInfo GetCompareToMethod(Type type) => _compareTo; diff --git a/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/EnumCursorKeySerializerTests.cs b/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/EnumCursorKeySerializerTests.cs new file mode 100644 index 00000000000..f4b4a115ea1 --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/EnumCursorKeySerializerTests.cs @@ -0,0 +1,67 @@ +using System.Text; +using GreenDonut.Data.Cursors; + +namespace GreenDonut.Data.Cursors.Serializers; + +public class EnumCursorKeySerializerTests +{ + private static readonly EnumCursorKeySerializer s_serializer = new(); + + [Fact] + public void IsSupported_NonNullable_Enum() + { + Assert.True(s_serializer.IsSupported(typeof(TestIntEnum))); + } + + [Fact] + public void IsSupported_Nullable_Enum() + { + Assert.True(s_serializer.IsSupported(typeof(TestIntEnum?))); + } + + [Fact] + public void IsSupported_Different_Underlying_Type() + { + Assert.False(s_serializer.IsSupported(typeof(TestByteEnum))); + } + + [Fact] + public void Registration_Finds_Nullable_Enum_Serializer() + { + var serializer = CursorKeySerializerRegistration.Find(typeof(TestIntEnum?)); + Assert.IsType>(serializer); + } + + [Theory] + [InlineData(TestIntEnum.One, "1")] + [InlineData(TestIntEnum.Two, "2")] + public void TryFormat(TestIntEnum value, string expected) + { + Span buffer = stackalloc byte[16]; + + var success = s_serializer.TryFormat(value, buffer, out var written); + + Assert.True(success); + Assert.Equal(expected, Encoding.UTF8.GetString(buffer[..written])); + } + + [Theory] + [InlineData("1", 1)] + [InlineData("2", 2)] + public void Parse(string formatted, int expected) + { + var result = s_serializer.Parse(Encoding.UTF8.GetBytes(formatted)); + Assert.Equal(expected, Assert.IsType(result)); + } + + public enum TestIntEnum + { + One = 1, + Two = 2 + } + + public enum TestByteEnum : byte + { + One = 1 + } +}