Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
27 changes: 27 additions & 0 deletions Fluid/Values/DateTimeValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ 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)

try
{
// Attempt normal conversion - implicit conversion uses local timezone for Unspecified kind
_value = value;
}
catch (ArgumentOutOfRangeException)
{
// If conversion fails due to offset overflow, use the appropriate boundary value
// This happens when the UTC representation would be outside the valid range
if (value < DateTime.MinValue.AddDays(1))
{
// Value is close to MinValue and offset caused underflow
_value = DateTimeOffset.MinValue;
}
else
{
// Value is close to MaxValue and offset caused overflow
_value = DateTimeOffset.MaxValue;
}
}
}

public override FluidValues Type => FluidValues.DateTime;

public override bool Equals(FluidValue other)
Expand Down