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
80 changes: 80 additions & 0 deletions Fluid.Tests/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,86 @@ public async Task ShouldEvaluateTimeSpanValue()
Assert.Equal("1970-01-01 01:00:00Z", result);
}

[Fact]
public async Task ShouldHandleDateTimeMinValueWithPositiveTimezoneOffset()
{
// Set a timezone offset of +2 hours (like EET - Eastern European Time)
var plusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom+2", TimeSpan.FromHours(2), "UTC+2", "UTC+2");

_parser.TryParse("{{ foo }} {{ date }}", out var template, out var error);

var context = new TemplateContext { TimeZone = plusTwoTimezone };
context.SetValue("foo", "bar");
context.SetValue("date", DateTime.MinValue);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot what it the value is DateTime.MinValue + 1hour, there would still be an exception. The constructor need to check if there is an overflow, not just special values like MinValue and MaxValue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the implementation to use a try-catch approach instead of checking for exact MinValue/MaxValue. Now handles any DateTime value that would cause overflow when converted to DateTimeOffset, including cases like DateTime.MinValue + 1 hour.

Added tests for these scenarios:

  • DateTime.MinValue + 1 hour with +2 offset
  • DateTime.MaxValue - 1 hour with -2 offset

Fixed in commit 710c03e


// This should not throw ArgumentOutOfRangeException
var result = await template.RenderAsync(context);

// DateTime.MinValue should be rendered as the minimum DateTimeOffset value
Assert.Contains("bar", result);
Assert.Contains("0001-01-01", result);
}

[Fact]
public async Task ShouldHandleDateTimeNearMinValueWithPositiveTimezoneOffset()
{
// Set a timezone offset of +2 hours (like EET - Eastern European Time)
var plusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom+2", TimeSpan.FromHours(2), "UTC+2", "UTC+2");

_parser.TryParse("{{ foo }} {{ date }}", out var template, out var error);

var context = new TemplateContext { TimeZone = plusTwoTimezone };
context.SetValue("foo", "bar");
context.SetValue("date", DateTime.MinValue.AddHours(1));

// This should not throw ArgumentOutOfRangeException even with DateTime.MinValue + 1 hour
var result = await template.RenderAsync(context);

// DateTime near MinValue should be rendered as the minimum DateTimeOffset value
Assert.Contains("bar", result);
Assert.Contains("0001-01-01", result);
}

[Fact]
public async Task ShouldHandleDateTimeMaxValueWithNegativeTimezoneOffset()
{
// Set a timezone offset of -2 hours (like Brazil Standard Time)
var minusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom-2", TimeSpan.FromHours(-2), "UTC-2", "UTC-2");

_parser.TryParse("{{ foo }} {{ date }}", out var template, out var error);

var context = new TemplateContext { TimeZone = minusTwoTimezone };
context.SetValue("foo", "bar");
context.SetValue("date", DateTime.MaxValue);

// This should not throw ArgumentOutOfRangeException
var result = await template.RenderAsync(context);

// DateTime.MaxValue should be rendered as the maximum DateTimeOffset value
Assert.Contains("bar", result);
Assert.Contains("9999-12-31", result);
}

[Fact]
public async Task ShouldHandleDateTimeNearMaxValueWithNegativeTimezoneOffset()
{
// Set a timezone offset of -2 hours (like Brazil Standard Time)
var minusTwoTimezone = TimeZoneInfo.CreateCustomTimeZone("Custom-2", TimeSpan.FromHours(-2), "UTC-2", "UTC-2");

_parser.TryParse("{{ foo }} {{ date }}", out var template, out var error);

var context = new TemplateContext { TimeZone = minusTwoTimezone };
context.SetValue("foo", "bar");
context.SetValue("date", DateTime.MaxValue.AddHours(-1));

// This should not throw ArgumentOutOfRangeException even with DateTime.MaxValue - 1 hour
var result = await template.RenderAsync(context);

// DateTime near MaxValue should be rendered as the maximum DateTimeOffset value
Assert.Contains("bar", result);
Assert.Contains("9999-12-31", result);
}

[Fact]
public async Task ShouldEvaluateObjectProperty()
{
Expand Down
39 changes: 39 additions & 0 deletions Fluid/Values/DateTimeValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ public DateTimeValue(DateTimeOffset value)
_value = value;
}

public DateTimeValue(DateTime value)
Copy link
Owner

@sebastienros sebastienros Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Check for values less than one day of the boundaries before doing the try/catch. Otherwise just do the conversion directly.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Check for values less than one day of the boundaries before doing the try/catch. Otherwise just do the conversion directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimized the implementation to check if the value is within one day of the boundaries before using try-catch. Normal DateTime values now use direct conversion without exception overhead, while values near MinValue/MaxValue still use try-catch to handle potential overflow.

Fixed in commit 4b66dc8

{
// Handle edge cases where DateTime cannot be safely converted to DateTimeOffset
// with local timezone offset due to overflow (e.g., DateTime.MinValue with positive offset)

// Check if the value is within one day of the boundaries where overflow might occur
if (value <= DateTime.MinValue.AddDays(1))
{
// Value is close to MinValue - attempt conversion with try-catch
try
{
_value = value;
}
catch (ArgumentOutOfRangeException)
{
// Offset caused underflow - use minimum boundary
_value = DateTimeOffset.MinValue;
}
}
else if (value >= DateTime.MaxValue.AddDays(-1))
{
// Value is close to MaxValue - attempt conversion with try-catch
try
{
_value = value;
}
catch (ArgumentOutOfRangeException)
{
// Offset caused overflow - use maximum boundary
_value = DateTimeOffset.MaxValue;
}
}
else
{
// Normal case - direct conversion without try-catch overhead
_value = value;
}
}

public override FluidValues Type => FluidValues.DateTime;

public override bool Equals(FluidValue other)
Expand Down