diff --git a/src/BccCode.Linq/Server/OperandToExpressionResolver.cs b/src/BccCode.Linq/Server/OperandToExpressionResolver.cs index c2bf4b5..4a52713 100644 --- a/src/BccCode.Linq/Server/OperandToExpressionResolver.cs +++ b/src/BccCode.Linq/Server/OperandToExpressionResolver.cs @@ -20,6 +20,42 @@ public static class OperandToExpressionResolver if (valueType == type) return value; + Type? nullableType = Nullable.GetUnderlyingType(type); + if (nullableType != null) + { + if (value is string strValue && strValue == "null") + return null; + + var newValue = ConvertValue(nullableType, value); + + return nullableType switch + { + Type when nullableType == typeof(bool) => (bool?)newValue, + Type when nullableType == typeof(sbyte) => (sbyte?)newValue, + Type when nullableType == typeof(byte) => (byte?)newValue, + Type when nullableType == typeof(char) => (char?)newValue, + Type when nullableType == typeof(short) => (short?)newValue, + Type when nullableType == typeof(ushort) => (ushort?)newValue, + Type when nullableType == typeof(int) => (int?)newValue, + Type when nullableType == typeof(uint) => (uint?)newValue, + Type when nullableType == typeof(long) => (long?)newValue, + Type when nullableType == typeof(ulong) => (ulong?)newValue, + Type when nullableType == typeof(nint) => (nint?)newValue, + Type when nullableType == typeof(nuint) => (nuint?)newValue, + Type when nullableType == typeof(float) => (float?)newValue, + Type when nullableType == typeof(double) => (double?)newValue, + Type when nullableType == typeof(decimal) => (decimal?)newValue, + Type when nullableType == typeof(Guid) => (Guid?)newValue, + Type when nullableType == typeof(DateTime) => (DateTime?)newValue, + Type when nullableType == typeof(TimeSpan) => (TimeSpan?)newValue, +#if NET6_0_OR_GREATER + Type when nullableType == typeof(DateOnly) => (DateOnly?)newValue, + Type when nullableType == typeof(TimeOnly) => (TimeOnly?)newValue, +#endif + _ => newValue, + }; + } + // converts value to an array if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IList<>)) { @@ -123,8 +159,16 @@ public static class OperandToExpressionResolver { Type when type == typeof(bool) && value is string strVal => strVal == "1", Type when type == typeof(int) && value is string strVal => (int)double.Parse(strVal, CultureInfo.InvariantCulture), + Type when type == typeof(float) && value is string strVal => float.Parse(strVal, CultureInfo.InvariantCulture), + Type when type == typeof(double) && value is string strVal => double.Parse(strVal, CultureInfo.InvariantCulture), Type when type == typeof(decimal) && value is string strVal => decimal.Parse(strVal, CultureInfo.InvariantCulture), + Type when type == typeof(Guid) && value is string strVal && Guid.TryParse(strVal, out var uuid) => uuid, Type when type == typeof(DateTime) && value is string strVal && DateTime.TryParse(strVal, out var dateTime) => dateTime, + Type when type == typeof(TimeSpan) && value is string strVal && TimeSpan.TryParse(strVal, out var dateTime) => dateTime, +#if NET6_0_OR_GREATER + Type when type == typeof(DateOnly) && value is string strVal && DateOnly.TryParse(strVal, out var dateTime) => dateTime, + Type when type == typeof(TimeOnly) && value is string strVal && TimeOnly.TryParse(strVal, out var dateTime) => dateTime, +#endif Type when type == typeof(int) && value is ValueTuple tuple => new ValueTuple( (int)ConvertValue(type, tuple.Item1), (int)ConvertValue(type, tuple.Item2)), Type when type == typeof(double) && value is ValueTuple tuple => new ValueTuple( @@ -135,7 +179,6 @@ public static class OperandToExpressionResolver _ => value }; - value = Convert.ChangeType(value, type, CultureInfo.InvariantCulture); } catch (Exception ex) diff --git a/tests/BccCode.Linq.Tests/FilterTests.cs b/tests/BccCode.Linq.Tests/FilterTests.cs index 002b930..b727ad8 100644 --- a/tests/BccCode.Linq.Tests/FilterTests.cs +++ b/tests/BccCode.Linq.Tests/FilterTests.cs @@ -171,6 +171,41 @@ public void should_not_cast_value_to_date() var value = ((Filter)filter.Properties["AnyDate"]).Properties["_eq"]; }); } + + [Fact] + public void should_cast_value_to_nullable_date() + { + var json = @"{ ""DateNullable"": { ""_eq"": ""2009-06-15T13:45:30"" } }"; + var filter = new Filter(json); + + var value = ((Filter)filter.Properties["DateNullable"]).Properties["_eq"]; + + Assert.IsType(value); + } + +#if NET6_0_OR_GREATER + [Fact] + public void should_cast_value_to_date_only() + { + var json = @"{ ""DateOnly"": { ""_eq"": ""2009-06-15"" } }"; + var filter = new Filter(json); + + var value = ((Filter)filter.Properties["DateOnly"]).Properties["_eq"]; + + Assert.IsType(value); + } + + [Fact] + public void should_cast_value_to_time_only() + { + var json = @"{ ""TimeOnly"": { ""_eq"": ""13:45:30"" } }"; + var filter = new Filter(json); + + var value = ((Filter)filter.Properties["TimeOnly"]).Properties["_eq"]; + + Assert.IsType(value); + } +#endif [Fact] public void should_filter_on_nested_class() diff --git a/tests/BccCode.Linq.Tests/Helpers/TestClass.cs b/tests/BccCode.Linq.Tests/Helpers/TestClass.cs index d753534..46e64b1 100644 --- a/tests/BccCode.Linq.Tests/Helpers/TestClass.cs +++ b/tests/BccCode.Linq.Tests/Helpers/TestClass.cs @@ -23,6 +23,7 @@ public class TestClass public Guid? UuidNullable { get; set; } #if NET6_0_OR_GREATER public DateOnly DateOnly { get; set; } + public TimeOnly TimeOnly { get; set; } #endif [DataMember(Name = "custom_name")] diff --git a/tests/BccCode.Linq.Tests/LinqTests.cs b/tests/BccCode.Linq.Tests/LinqTests.cs index 0c18b73..b516dfe 100644 --- a/tests/BccCode.Linq.Tests/LinqTests.cs +++ b/tests/BccCode.Linq.Tests/LinqTests.cs @@ -515,11 +515,11 @@ public void SingleTest() } [Fact] - public void SingleAsyncTest() + public async void SingleAsyncTest() { var api = new ApiClientMockup(); - Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { // ReSharper disable once UnusedVariable var persons = await api.Persons.SingleAsync(); @@ -567,11 +567,11 @@ public void SingleOrDefaultEmptyTest() } [Fact] - public void SingleOrDefaultAsyncTest() + public async void SingleOrDefaultAsyncTest() { var api = new ApiClientMockup(); - Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { // ReSharper disable once UnusedVariable var persons = await api.Persons.SingleOrDefaultAsync(); @@ -1363,6 +1363,26 @@ where p.DateNullable.ToString() == "2023-12-04T04:02:05Z" Assert.Null(api.ClientQuery?.Limit); Assert.Empty(persons); } + + [Fact] + public void WhereDateTimeNullableToStringGreaterThanToDateTimeTest() + { + var api = new ApiClientMockup(); + + var query = + from p in api.Empty + where p.DateNullable >= DateTime.Parse("2023-12-04T04:02:05Z").ToUniversalTime() + select p; + + var persons = query.ToList(); + Assert.Equal("empty", api.PageEndpoint); + Assert.Equal("{\"dateNullable\": {\"_gte\": \"2023-12-04T04:02:05.0000000Z\"}}", api.ClientQuery?.Filter); + Assert.Equal("*", api.ClientQuery?.Fields); + Assert.Null(api.ClientQuery?.Sort); + Assert.Null(api.ClientQuery?.Offset); + Assert.Null(api.ClientQuery?.Limit); + Assert.Empty(persons); + } #if NET6_0_OR_GREATER [Fact]