Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions src/Linq2OData.Core/Expressions/FilterExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,22 @@ protected override Expression VisitConstant(ConstantExpression c)
return c;
}

protected override Expression VisitNew(NewExpression n)
{
try
{
var lambda = Expression.Lambda(n);
var compiled = lambda.Compile();
var value = compiled.DynamicInvoke();
AppendByValueType(value, sb);
return n;
}
catch
{
throw new NotSupportedException($"The constructor for '{n.Type.Name}' is not supported");
}
}

protected override Expression VisitMember(MemberExpression m)
{
// Handle string.Length property on entity properties
Expand Down Expand Up @@ -578,6 +594,14 @@ private void AppendByValueType(object? value, StringBuilder sb)
{
sb.Append(FilterHelper.ToODataFilter(dateTimeOffset, odataVersion));
}
else if (value is TimeOnly timeOnly)
{
sb.Append(FilterHelper.ToODataFilter(timeOnly, odataVersion));
}
else if (value is DateOnly dateOnly)
{
sb.Append(FilterHelper.ToODataFilter(dateOnly, odataVersion));
}
else
{
throw new NotSupportedException($"The constant for '{value}' is not supported");
Expand All @@ -599,18 +623,24 @@ private static string GetODataMemberName(MemberInfo member)
{
foreach (var accessName in memberAccessNames)
{
var property = value.GetType().GetProperty(accessName);
if (property == null)
var type = value.GetType();
var property = type.GetProperty(accessName);
if (property != null)
{
throw new NotSupportedException($"Property '{accessName}' not found on type '{value.GetType().Name}'");
var nextValue = property.GetValue(value);
if (nextValue == null) return null;
value = nextValue;
}

var nextValue = property.GetValue(value);
if (nextValue == null)
else
{
return null;
var field = type.GetField(accessName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field == null)
throw new NotSupportedException($"Property or field '{accessName}' not found on type '{type.Name}'");

var nextValue = field.GetValue(value);
if (nextValue == null) return null;
value = nextValue;
}
value = nextValue;
}
return value;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Linq2OData.Core/Expressions/FilterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ public static string ToODataFilter(DateTimeOffset date, ODataVersion version)
}
}

public static string ToODataFilter(TimeOnly time, ODataVersion version)
{
return $"time'{time:HH:mm:ss}'";
}

public static string ToODataFilter(DateOnly date, ODataVersion version)
{
return $"date'{date:yyyy-MM-dd}'";
}



}
223 changes: 223 additions & 0 deletions test/Linq2OData.Tests/FilterExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class TestProduct : IODataEntitySet
public DateTime? DiscontinuedDate { get; set; }
public DateTimeOffset LastModified { get; set; }
public DateTimeOffset? LastChecked { get; set; }
public TimeOnly OpenTime { get; set; }
public TimeOnly? CloseTime { get; set; }
public DateOnly OpenDate { get; set; }
public DateOnly? CloseDate { get; set; }
public TestCategory? Category { get; set; }
public string __Key => $"ID={ID}";
}
Expand Down Expand Up @@ -256,6 +260,24 @@ public void ODataFilterVisitor_GreaterThanOrEqual_GeneratesCorrectFilter()
Assert.Equal("(Stock ge 10)", result);
}

[Fact]
public void ODataFilterVisitor_ConstantAsObject_GeneratesCorrectFilter()
{

var product = new TestProduct { ID = 10, Name = "Test", Price = 9.99m, Stock = 100, IsAvailable = true, CreatedDate = DateTime.Now, LastModified = DateTimeOffset.Now, OpenTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(9)), OpenDate = DateOnly.FromDateTime(DateTime.Today) };

// Arrange
var visitor = new ODataFilterVisitor();
Expression<Func<TestProduct, bool>> expression = p => p.Stock >= product.ID;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(Stock ge 10)", result);
}


#endregion

#region Boolean Tests
Expand Down Expand Up @@ -1419,4 +1441,205 @@ public void ODataFilterVisitor_EnumInComplexExpression_GeneratesCorrectFilter()
}

#endregion

#region DateOnly Tests

[Fact]
public void ODataFilterVisitor_DateOnlyConstant_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var openDate = new DateOnly(2024, 6, 15);
Expression<Func<TestProduct, bool>> expression = p => p.OpenDate == openDate;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenDate eq date'2024-06-15')", result);
}

[Fact]
public void ODataFilterVisitor_DateOnlyConstant_V2_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var openDate = new DateOnly(2024, 6, 15);
Expression<Func<TestProduct, bool>> expression = p => p.OpenDate == openDate;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V2);

// Assert
Assert.Equal("(OpenDate eq date'2024-06-15')", result);
}

[Fact]
public void ODataFilterVisitor_DateOnlyGreaterThan_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var cutoff = new DateOnly(2024, 1, 1);
Expression<Func<TestProduct, bool>> expression = p => p.OpenDate > cutoff;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenDate gt date'2024-01-01')", result);
}

[Fact]
public void ODataFilterVisitor_DateOnlyLessThan_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var cutoff = new DateOnly(2024, 12, 31);
Expression<Func<TestProduct, bool>> expression = p => p.OpenDate < cutoff;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenDate lt date'2024-12-31')", result);
}

[Fact]
public void ODataFilterVisitor_NullableDateOnly_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var closeDate = new DateOnly(2024, 9, 30);
Expression<Func<TestProduct, bool>> expression = p => p.CloseDate == closeDate;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(CloseDate eq date'2024-09-30')", result);
}

[Fact]
public void ODataFilterVisitor_DateOnlyInlineConstant_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
Expression<Func<TestProduct, bool>> expression = p => p.OpenDate >= new DateOnly(2024, 3, 1);

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenDate ge date'2024-03-01')", result);
}

#endregion

#region TimeOnly Tests

[Fact]
public void ODataFilterVisitor_TimeOnlyConstant_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var openTime = new TimeOnly(9, 0, 0);
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime == openTime;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenTime eq time'09:00:00')", result);
}

[Fact]
public void ODataFilterVisitor_TimeOnlyConstant_V2_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var openTime = new TimeOnly(9, 0, 0);
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime == openTime;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V2);

// Assert
Assert.Equal("(OpenTime eq time'09:00:00')", result);
}

[Fact]
public void ODataFilterVisitor_TimeOnlyGreaterThan_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var cutoff = new TimeOnly(17, 30, 0);
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime > cutoff;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenTime gt time'17:30:00')", result);
}

[Fact]
public void ODataFilterVisitor_TimeOnlyLessThan_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var cutoff = new TimeOnly(12, 0, 0);
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime < cutoff;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenTime lt time'12:00:00')", result);
}

[Fact]
public void ODataFilterVisitor_TimeOnlyWithSeconds_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var closeTime = new TimeOnly(23, 59, 59);
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime <= closeTime;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenTime le time'23:59:59')", result);
}

[Fact]
public void ODataFilterVisitor_NullableTimeOnly_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
var closeTime = new TimeOnly(18, 0, 0);
Expression<Func<TestProduct, bool>> expression = p => p.CloseTime == closeTime;

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(CloseTime eq time'18:00:00')", result);
}

[Fact]
public void ODataFilterVisitor_TimeOnlyInlineConstant_V4_GeneratesCorrectFilter()
{
// Arrange
var visitor = new ODataFilterVisitor();
Expression<Func<TestProduct, bool>> expression = p => p.OpenTime >= new TimeOnly(8, 30, 0);

// Act
var result = visitor.ToFilter(expression, ODataVersion.V4);

// Assert
Assert.Equal("(OpenTime ge time'08:30:00')", result);
}

#endregion
}
Loading