Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3fa967f
Refactor NodaTime scalars
glen-84 Mar 10, 2026
90a7309
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 Mar 11, 2026
90ef757
Remove unnecessary using directives
glen-84 Mar 11, 2026
d80daea
Use shared resources
glen-84 Mar 11, 2026
ba28122
Remove unnecessary constant
glen-84 Mar 11, 2026
52a191d
Update dictionary
glen-84 Mar 11, 2026
c865b99
Use ternary
glen-84 Mar 11, 2026
d166bdc
Refactor LocalDateTimeType
glen-84 Mar 11, 2026
938ccc5
Refactor LocalDateType
glen-84 Mar 11, 2026
d3c7ce3
Use single GetFormat method
glen-84 Mar 11, 2026
6c16ef9
Refactor LocalTimeType
glen-84 Mar 12, 2026
8378224
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 Mar 12, 2026
35693b7
Fix BindingBehavior
glen-84 Mar 12, 2026
7a409a6
Implemented custom parser and formatter
michaelstaib Mar 17, 2026
551b196
Align Duration types
glen-84 Mar 18, 2026
de469e4
Polish formatters and parsers
glen-84 Mar 18, 2026
c6acf45
Add tests for duration parsers and formatters
glen-84 Mar 19, 2026
a5c73bb
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 Mar 19, 2026
081ef64
Align DurationTypeTests
glen-84 Mar 19, 2026
8a4a1b6
Delete NodaTime scalars from the previous implementation
glen-84 Mar 19, 2026
45892bd
Throw if TryFormat fails inside Format
glen-84 Mar 19, 2026
ae5bed5
Update StrawberryShake DurationSerializer to use new parser/formatter
glen-84 Mar 19, 2026
d089334
Add `IRequestExecutorBuilder` extension method
glen-84 Mar 19, 2026
8802de4
Fix parser bugs
glen-84 Mar 20, 2026
63f1765
Fix casing bug in DateTimeType
glen-84 Mar 20, 2026
72f9943
Simplify `IRequestExecutorBuilder` extension method
glen-84 Mar 20, 2026
d229c3a
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 Mar 20, 2026
e5bb919
Address Copilot feedback
glen-84 Mar 20, 2026
e53a338
Address more Copilot feedback
glen-84 Mar 20, 2026
7a85ebc
Fix regression
glen-84 Mar 20, 2026
217eea9
Add section to migration docs
glen-84 Mar 20, 2026
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
2 changes: 2 additions & 0 deletions dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ streamable
Structs
subgraphs
sublicensable
Subsecond
subselections
supergraph
Swashbuckle
Expand All @@ -215,6 +216,7 @@ uppercased
Upsert
upvote
URQL
uuuu
vsix
VXNlcjox
websockets
Expand Down
65 changes: 65 additions & 0 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using HotChocolate.Properties;

namespace HotChocolate.Types.NodaTime;

/// <summary>
/// Defines options for configuring the behavior of date and time scalar types, such as
/// <c>DateTime</c>, <c>LocalDateTime</c>, and <c>LocalTime</c>.
/// </summary>
public struct DateTimeOptions
{
public const byte DefaultInputPrecision = 9;
public const byte DefaultOutputPrecision = 9;

public DateTimeOptions()
{
}

/// <summary>
/// Gets the maximum number of fractional second digits to expect when parsing date and time
/// input values.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value is greater than 9.
/// </exception>
public byte InputPrecision
{
get;
init
{
if (value > 9)
{
throw new ArgumentOutOfRangeException(
nameof(InputPrecision),
value,
TypeResources.DateTimeOptions_InputPrecision_InvalidValue);
}

field = value;
}
} = DefaultInputPrecision;

/// <summary>
/// Gets the maximum number of fractional second digits to include when serializing date and
/// time output values.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value is greater than 9.
/// </exception>
public byte OutputPrecision
{
get;
init
{
if (value > 9)
{
throw new ArgumentOutOfRangeException(
nameof(OutputPrecision),
value,
NodaTimeResources.DateTimeOptions_OutputPrecision_InvalidValue);
}

field = value;
}
} = DefaultOutputPrecision;
}
131 changes: 131 additions & 0 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Globalization;
using System.Text.Json;
using HotChocolate.Features;
using HotChocolate.Language;
using HotChocolate.Properties;
using HotChocolate.Text.Json;
using NodaTime;
using NodaTime.Text;
using static HotChocolate.Utilities.ThrowHelper;

namespace HotChocolate.Types.NodaTime;

/// <summary>
/// The <c>DateTime</c> scalar type represents a date and time with time zone offset information. It
/// is intended for scenarios where representing a specific instant in time is required, such as
/// recording when an event occurred, scheduling future events across time zones, or storing
/// timestamps for auditing purposes.
/// </summary>
/// <seealso href="https://scalars.graphql.org/chillicream/date-time.html">Specification</seealso>
public class DateTimeType : ScalarType<OffsetDateTime, StringValueNode>
{
private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/date-time.html";

private readonly DateTimeOptions _options;
private readonly OffsetDateTimePattern _inputPattern;
private readonly string _outputFormat;

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
public DateTimeType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit,
DateTimeOptions? options = null)
: base(name, bind)
{
_options = options ?? new DateTimeOptions();
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
public DateTimeType(DateTimeOptions options)
: this(
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit,
options: options)
Comment thread
glen-84 marked this conversation as resolved.
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
[ActivatorUtilitiesConstructor]
public DateTimeType()
: this(
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit)
{
}

protected override OffsetDateTime OnCoerceInputLiteral(StringValueNode valueLiteral)
{
if (TryParseStringValue(valueLiteral.Value, out var value))
{
return value;
}

throw Scalar_Cannot_CoerceInputLiteral(this, valueLiteral);
}

protected override OffsetDateTime OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context)
{
if (TryParseStringValue(inputValue.GetString()!, out var value))
{
return value;
}

throw Scalar_Cannot_CoerceInputValue(this, inputValue);
}

protected override void OnCoerceOutputValue(OffsetDateTime runtimeValue, ResultElement resultValue)
{
resultValue.SetStringValue(
runtimeValue.ToString(
_outputFormat,
CultureInfo.InvariantCulture));
}

protected override StringValueNode OnValueToLiteral(OffsetDateTime runtimeValue)
{
return new StringValueNode(
runtimeValue.ToString(
_outputFormat,
CultureInfo.InvariantCulture));
}

private bool TryParseStringValue(string serialized, out OffsetDateTime value)
{
var result = _inputPattern.Parse(serialized.Replace('t', 'T').Replace('z', 'Z'));

if (result.Success)
{
value = result.Value;
return true;
}

value = default;
return false;
}

private string GetPattern()
=> _options.InputPrecision == 0
? @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:[Zz]|[+-]\d{2}:\d{2})$"
: @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,"
+ _options.InputPrecision
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";

private static string GetFormat(byte precision)
=> precision == 0
? "uuuu-MM-dd'T'HH:mm:sso<Z+HH:mm>"
: $"uuuu-MM-dd'T'HH:mm:ss.{new string('F', precision)}o<Z+HH:mm>";
}
40 changes: 0 additions & 40 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeZoneType.cs

This file was deleted.

96 changes: 55 additions & 41 deletions src/HotChocolate/Core/src/Types.NodaTime/DurationType.cs
Original file line number Diff line number Diff line change
@@ -1,69 +1,83 @@
using System.Diagnostics.CodeAnalysis;
using HotChocolate.Types.NodaTime.Properties;
using System.Text.Json;
using HotChocolate.Features;
using HotChocolate.Language;
using HotChocolate.Properties;
using HotChocolate.Text.Json;
using NodaTime;
using NodaTime.Text;
using static HotChocolate.Utilities.ThrowHelper;

namespace HotChocolate.Types.NodaTime;

/// <summary>
/// Represents a fixed (and calendar-independent) length of time.
/// The <c>Duration</c> scalar type represents a duration of time. It is intended for scenarios
/// where you need to represent time intervals, such as elapsed time, timeout durations, scheduling
/// intervals, or any measurement of time that is not tied to a specific date or time.
/// </summary>
public class DurationType : StringToStructBaseType<Duration>
/// <seealso href="https://scalars.graphql.org/chillicream/duration.html">Specification</seealso>
public class DurationType : ScalarType<Duration, StringValueNode>
{
private readonly IPattern<Duration>[] _allowedPatterns;
private readonly IPattern<Duration> _serializationPattern;
private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/duration.html";

/// <summary>
/// Initializes a new instance of <see cref="DurationType"/>.
/// Initializes a new instance of the <see cref="DurationType"/> class.
/// </summary>
public DurationType(params IPattern<Duration>[] allowedPatterns) : base("Duration")
public DurationType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit)
: base(name, bind)
{
if (allowedPatterns.Length == 0)
{
throw ThrowHelper.PatternCannotBeEmpty(this);
}

_allowedPatterns = allowedPatterns;
_serializationPattern = allowedPatterns[0];

Description = CreateDescription(
allowedPatterns,
NodaTimeResources.DurationType_Description,
NodaTimeResources.DurationType_Description_Extended);
Description = description;
Pattern =
@"^-?P(?:-?\d+Y)?(?:-?\d+M)?(?:-?\d+W)?(?:-?\d+D)?(?:T(?:-?\d+H)?(?:-?\d+M)?(?:-?\d+(?:[.,]\d+)?S)?)?$";
Comment thread
glen-84 marked this conversation as resolved.
SpecifiedBy = new Uri(SpecifiedByUri);
}

/// <summary>
/// Initializes a new instance of <see cref="DurationType"/>.
/// Initializes a new instance of the <see cref="DurationType"/> class.
/// </summary>
[ActivatorUtilitiesConstructor]
public DurationType() : this(DurationPattern.Roundtrip)
public DurationType()
: this(
ScalarNames.Duration,
TypeResources.DurationType_Description,
BindingBehavior.Implicit)
{
}

/// <inheritdoc />
protected override bool TryCoerceRuntimeValue(
string resultValue,
[NotNullWhen(true)] out Duration? runtimeValue)
=> _allowedPatterns.TryParse(resultValue, out runtimeValue);
protected override Duration OnCoerceInputLiteral(StringValueNode valueLiteral)
{
// Parse directly from UTF-8 bytes to avoid the string allocation.
if (Iso8601DurationParser.TryParse(valueLiteral.AsSpan(), out var value))
{
return value;
}

throw Scalar_Cannot_CoerceInputLiteral(this, valueLiteral);
}

/// <inheritdoc />
protected override bool TryCoerceOutputValue(
Duration runtimeValue,
[NotNullWhen(true)] out string? resultValue)
protected override Duration OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context)
{
resultValue = _serializationPattern.Format(runtimeValue);
return true;
if (Iso8601DurationParser.TryParse(inputValue.GetString()!.AsSpan(), out var value))
{
return value;
}

throw Scalar_Cannot_CoerceInputValue(this, inputValue);
}

protected override Dictionary<IPattern<Duration>, string> PatternMap => new()
/// <inheritdoc />
protected override void OnCoerceOutputValue(Duration runtimeValue, ResultElement resultValue)
{
{ DurationPattern.Roundtrip, "-D:hh:mm:ss.sssssssss" },
{ DurationPattern.JsonRoundtrip, "-hh:mm:ss.sssssssss" }
};
// Format directly to UTF-8 bytes on the stack to avoid allocation.
Span<byte> buffer = stackalloc byte[64];
Iso8601DurationFormatter.TryFormat(runtimeValue, buffer, out var bytesWritten);
resultValue.SetStringValue(buffer[..bytesWritten]);
}

protected override Dictionary<IPattern<Duration>, string> ExampleMap => new()
{
{ DurationPattern.Roundtrip, "-1:20:00:00.999999999" },
{ DurationPattern.JsonRoundtrip, "-44:00:00.999999999" }
};
/// <inheritdoc />
protected override StringValueNode OnValueToLiteral(Duration runtimeValue)
=> new StringValueNode(Iso8601DurationFormatter.Format(runtimeValue));
}
Loading
Loading