diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index f9179323..71828940 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -607,7 +607,8 @@ private Expression ParseComparisonOperator() bool typesAreSameAndImplementCorrectInterface = false; if (left.Type == right.Type) { - var interfaces = left.Type.GetInterfaces().Where(x => x.GetTypeInfo().IsGenericType); + var typeToCheck = TypeHelper.GetNonNullableType(left.Type); + var interfaces = typeToCheck.GetInterfaces().Where(x => x.GetTypeInfo().IsGenericType); if (isEquality) { typesAreSameAndImplementCorrectInterface = interfaces.Any(x => x.GetGenericTypeDefinition() == typeof(IEquatable<>)); diff --git a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs index fdfdaa0f..4cff6311 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs @@ -1,10 +1,7 @@ #if !NET452 using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq.Dynamic.Core.CustomTypeProviders; -using System.Reflection; using FluentAssertions; using NodaTime; using NodaTime.Text; @@ -158,20 +155,82 @@ public void FilterByNullableLocalDate_WithDynamicExpressionParser_CompareWithNul result.Should().HaveCount(numberOfEntities); } - public class LocalDateConverter : TypeConverter + private class EntityWithInstant + { + public Instant Timestamp { get; set; } + public Instant? TimestampNullable { get; set; } + } + + [Theory] + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + [InlineData("==", 1)] + [InlineData("!=", 2)] + public void FilterByInstant_WithRelationalOperator(string op, int expectedCount) { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); + // Arrange + var now = SystemClock.Instance.GetCurrentInstant(); + var data = new List + { + new EntityWithInstant { Timestamp = now - Duration.FromHours(1) }, + new EntityWithInstant { Timestamp = now }, + new EntityWithInstant { Timestamp = now + Duration.FromHours(1) } + }.AsQueryable(); - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + // Act + var result = data.Where($"Timestamp {op} @0", now).ToList(); + + // Assert + result.Should().HaveCount(expectedCount); + } + + [Theory] + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + [InlineData("==", 1)] + [InlineData("!=", 3)] // null != now evaluates to true in C# nullable semantics + public void FilterByNullableInstant_WithRelationalOperator(string op, int expectedCount) + { + // Arrange + var now = SystemClock.Instance.GetCurrentInstant(); + var data = new List { - var result = LocalDatePattern.Iso.Parse(value as string); + new EntityWithInstant { TimestampNullable = now - Duration.FromHours(1) }, + new EntityWithInstant { TimestampNullable = now }, + new EntityWithInstant { TimestampNullable = now + Duration.FromHours(1) }, + new EntityWithInstant { TimestampNullable = null } + }.AsQueryable(); + + // Act + var result = data.Where($"TimestampNullable {op} @0", now).ToList(); - return result.Success - ? result.Value - : throw new FormatException(value?.ToString()); + // Assert - null values are excluded from comparison results + result.Should().HaveCount(expectedCount); + } + + public class LocalDateConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var result = Convert(value); + return result.Success ? result.Value : throw new FormatException(value?.ToString()); } - protected ParseResult Convert(object value) => LocalDatePattern.Iso.Parse(value as string); + private static ParseResult Convert(object value) + { + if (value is string stringValue) + { + return LocalDatePattern.Iso.Parse(stringValue); + } + + return ParseResult.ForException(() => new FormatException(value?.ToString())); + } } } }