diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index ab637e81812..850860e3d8a 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -176,17 +176,13 @@ private static Expression BuildEqualToKeyExpr( // SQL: WHERE key IS NOT NULL AND key = cursorValue. keyExpr = Expression.AndAlso( Expression.NotEqual(keyExpr, nullConst), - Expression.Equal( - Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), - s_zero)); + BuildEqualComparison(cursorKey, keyValueExpr, cursorExpr)); } } else { // SQL: WHERE key = cursorValue. - keyExpr = Expression.Equal( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); + keyExpr = BuildEqualComparison(cursorKey, keyExpr, cursorExpr); } return keyExpr; @@ -229,9 +225,7 @@ private static Expression BuildGreaterThanKeyExpr( if (nullOrdering == NullOrdering.NativeNullsFirst) { // SQL: WHERE key > cursorValue. - keyExpr = Expression.GreaterThan( - Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); + keyExpr = BuildGreaterThanComparison(cursorKey, keyValueExpr, cursorExpr); } else { @@ -239,18 +233,14 @@ private static Expression BuildGreaterThanKeyExpr( // SQL: WHERE key IS NULL OR key > cursorValue. keyExpr = Expression.OrElse( Expression.Equal(keyExpr, nullConst), - Expression.GreaterThan( - Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), - s_zero)); + BuildGreaterThanComparison(cursorKey, keyValueExpr, cursorExpr)); } } } else { // SQL: WHERE key > cursorValue. - keyExpr = Expression.GreaterThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); + keyExpr = BuildGreaterThanComparison(cursorKey, keyExpr, cursorExpr); } return keyExpr; @@ -296,25 +286,19 @@ private static Expression BuildLessThanKeyExpr( // SQL: WHERE key IS NULL OR key < cursorValue. keyExpr = Expression.OrElse( Expression.Equal(keyExpr, nullConst), - Expression.LessThan( - Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), - s_zero)); + BuildLessThanComparison(cursorKey, keyValueExpr, cursorExpr)); } else { // SQL: WHERE key < cursorValue. - keyExpr = Expression.LessThan( - Expression.Call(keyValueExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); + keyExpr = BuildLessThanComparison(cursorKey, keyValueExpr, cursorExpr); } } } else { // SQL: WHERE key < cursorValue. - keyExpr = Expression.LessThan( - Expression.Call(keyExpr, cursorKey.CompareMethod, cursorExpr), - s_zero); + keyExpr = BuildLessThanComparison(cursorKey, keyExpr, cursorExpr); } return keyExpr; @@ -639,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) diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs index 4afb644a8a8..8fb6c10f240 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeySerializerRegistration.cs @@ -26,7 +26,15 @@ public static class CursorKeySerializerRegistration new BoolCursorKeySerializer(), new UShortCursorKeySerializer(), new UIntCursorKeySerializer(), - new ULongCursorKeySerializer() + 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..24ba8a4acd3 --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/EnumCursorKeySerializer.cs @@ -0,0 +1,63 @@ +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) + { + var enumType = Nullable.GetUnderlyingType(type) ?? type; + return enumType.IsEnum && Enum.GetUnderlyingType(enumType) == 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 28e6b74305d..ed9f416e537 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs @@ -417,7 +417,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 @@ -435,7 +443,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) @@ -476,19 +493,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, @@ -497,7 +514,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__/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 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" } ] } 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 + } +} diff --git a/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md b/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md index 7bbfc915049..cd55b9dd6db 100644 --- a/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md +++ b/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md @@ -53,6 +53,7 @@ Things that have been removed or had a change in behavior that may cause your co - Since the `RegisterService` method is no longer required, it has been removed, along with the `ServiceKind` enum. - Scoped services injected into query resolvers are now resolver-scoped by default (not request scoped). For mutation resolvers, services are request-scoped by default. - The default scope can be changed in two ways: + 1. Globally, using `ModifyOptions`: ```csharp