From 9cf37b33d76e576e060db5803107e9be13a7f0dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:30:24 +0000 Subject: [PATCH 1/5] Initial plan From 2050e637aad1c77d580b20c131c1fdc1ac1dab5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:36:55 +0000 Subject: [PATCH 2/5] Fix nullable IComparable types not working with relational operators (>, >=, <, <=) When comparing two values of the same nullable type (e.g., Instant?) using relational operators, the check for IComparable<> interface was done on the nullable type itself (Nullable), which doesn't directly implement IComparable<>. The fix uses TypeHelper.GetNonNullableType() to get the underlying type first, so that if T implements IComparable, nullable T? also works correctly with relational operators. Fixes: Operator '>' incompatible with operand types 'Instant?' and 'Instant?' Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/b497145d-ba3d-430a-b608-eda596efffdd Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --- .../Parser/ExpressionParser.cs | 3 +- .../TypeConvertors/NodaTimeConverterTests.cs | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) 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..537a2b7d 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs @@ -158,6 +158,59 @@ public void FilterByNullableLocalDate_WithDynamicExpressionParser_CompareWithNul result.Should().HaveCount(numberOfEntities); } + private class EntityWithInstant + { + public Instant Timestamp { get; set; } + public Instant? TimestampNullable { get; set; } + } + + [Theory] + [InlineData(">")] + [InlineData(">=")] + [InlineData("<")] + [InlineData("<=")] + public void FilterByInstant_WithRelationalOperator(string op) + { + // 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(); + + // Act + var result = data.Where($"Timestamp {op} @0", now).ToList(); + + // Assert + result.Should().NotBeNull(); + } + + [Theory] + [InlineData(">")] + [InlineData(">=")] + [InlineData("<")] + [InlineData("<=")] + public void FilterByNullableInstant_WithRelationalOperator(string op) + { + // Arrange + var now = SystemClock.Instance.GetCurrentInstant(); + var data = new List + { + 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(); + + // Assert + result.Should().NotBeNull(); + } + public class LocalDateConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); From 7dc086016aff59c99899147ee55e52a5adcfc0ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:41:01 +0000 Subject: [PATCH 3/5] Improve test assertions: verify expected result counts for Instant comparisons Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/b497145d-ba3d-430a-b608-eda596efffdd Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --- .../TypeConvertors/NodaTimeConverterTests.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs index 537a2b7d..de5fd175 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs @@ -165,11 +165,11 @@ private class EntityWithInstant } [Theory] - [InlineData(">")] - [InlineData(">=")] - [InlineData("<")] - [InlineData("<=")] - public void FilterByInstant_WithRelationalOperator(string op) + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + public void FilterByInstant_WithRelationalOperator(string op, int expectedCount) { // Arrange var now = SystemClock.Instance.GetCurrentInstant(); @@ -184,15 +184,15 @@ public void FilterByInstant_WithRelationalOperator(string op) var result = data.Where($"Timestamp {op} @0", now).ToList(); // Assert - result.Should().NotBeNull(); + result.Should().HaveCount(expectedCount); } [Theory] - [InlineData(">")] - [InlineData(">=")] - [InlineData("<")] - [InlineData("<=")] - public void FilterByNullableInstant_WithRelationalOperator(string op) + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + public void FilterByNullableInstant_WithRelationalOperator(string op, int expectedCount) { // Arrange var now = SystemClock.Instance.GetCurrentInstant(); @@ -207,8 +207,8 @@ public void FilterByNullableInstant_WithRelationalOperator(string op) // Act var result = data.Where($"TimestampNullable {op} @0", now).ToList(); - // Assert - result.Should().NotBeNull(); + // Assert - null values are excluded from comparison results + result.Should().HaveCount(expectedCount); } public class LocalDateConverter : TypeConverter From 2ed978b47dcdf070908b4a5e6b45dfb0e10d8119 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:46:57 +0000 Subject: [PATCH 4/5] Add == and != test cases for Instant and Instant? comparison tests Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/4e543367-6cb4-4164-bd33-fbf0fecca256 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --- .../TypeConvertors/NodaTimeConverterTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs index de5fd175..2d15149e 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs @@ -169,6 +169,8 @@ private class EntityWithInstant [InlineData(">=", 2)] [InlineData("<", 1)] [InlineData("<=", 2)] + [InlineData("==", 1)] + [InlineData("!=", 2)] public void FilterByInstant_WithRelationalOperator(string op, int expectedCount) { // Arrange @@ -192,6 +194,8 @@ public void FilterByInstant_WithRelationalOperator(string op, int expectedCount) [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 From c530137150713aff845cb3464fbdbaffea1cd711 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 28 Mar 2026 11:56:12 +0100 Subject: [PATCH 5/5] LocalDateConverter --- .../TypeConvertors/NodaTimeConverterTests.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs index 2d15149e..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; @@ -217,18 +214,23 @@ public void FilterByNullableInstant_WithRelationalOperator(string op, int expect public class LocalDateConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { - var result = LocalDatePattern.Iso.Parse(value as string); - - return result.Success - ? result.Value - : throw new FormatException(value?.ToString()); + 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())); + } } } }