Skip to content

Commit

Permalink
Merge pull request #298 from MartinM85/feature/280-datetime-invariant…
Browse files Browse the repository at this point in the history
…-culture

Use invariant culture for (de)serialization
  • Loading branch information
andrueastman authored Jul 18, 2024
2 parents e06f086 + cf1c752 commit 10b879b
Show file tree
Hide file tree
Showing 24 changed files with 2,065 additions and 111 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.9.10] - 2024-07-18

- Fix DateTime serialization and deserialization

## [1.9.9] - 2024-07-12

- Fix enum deserialization for SendPrimitiveAsync and SendPrimitiveCollectionAsync
Expand All @@ -23,4 +27,4 @@ Refer to the following for earlier releases of libraries in the project.
1. [Serialization - JSON](./src/serialization/json/Changelog-old.md)
1. [Serialization - FORM](./src/serialization/form/Changelog-old.md)
1. [Serialization - TEXT](./src/serialization/text/Changelog-old.md)
1. [Serialization - MULTIPART](./src/serialization/multipart/Changelog-old.md)
1. [Serialization - MULTIPART](./src/serialization/multipart/Changelog-old.md)
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- Common default project properties for ALL projects-->
<PropertyGroup>
<VersionPrefix>1.9.9</VersionPrefix>
<VersionPrefix>1.9.10</VersionPrefix>
<VersionSuffix></VersionSuffix>
<!-- This is overidden in test projects by setting to true-->
<IsTestProject>false</IsTestProject>
Expand Down
25 changes: 24 additions & 1 deletion src/abstractions/Time.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.Kiota.Abstractions
/// <summary>
/// Model to represent only the date component of a DateTime
/// </summary>
public struct Time
public struct Time : IEquatable<Time>
{
#if NET6_0_OR_GREATER
/// <summary>
Expand All @@ -26,6 +26,29 @@ public struct Time
/// <returns>A new <see cref="TimeOnly"/> structure whose hours, minutes, seconds and milliseconds are equal to those of the supplied time.</returns>
public static implicit operator TimeOnly(Time time) => new(time.DateTime.Hour, time.DateTime.Minute, time.DateTime.Second, time.DateTime.Millisecond);
#endif
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
public bool Equals(Time other) => Hour == other.Hour && Minute == other.Minute && Second == other.Second;

/// <inheritdoc />
public override bool Equals(object? o) => (o is Time other) && Equals(other);

/// <inheritdoc />
public override int GetHashCode()
{
#if NET6_0_OR_GREATER
return HashCode.Combine(Hour, Minute, Second);
#else
int hash = 17;
hash = hash * 23 + Hour.GetHashCode();
hash = hash * 23 + Minute.GetHashCode();
hash = hash * 23 + Second.GetHashCode();
return hash;
#endif
}
/// <summary>
/// Create a new Time from hours, minutes, and seconds.
/// </summary>
Expand Down
30 changes: 16 additions & 14 deletions src/serialization/form/FormParseNode.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Globalization;
using System.Xml;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Extensions;
Expand Down Expand Up @@ -143,21 +143,21 @@ public IEnumerable<T> GetCollectionOfPrimitiveValues<T>()
}
}
/// <inheritdoc/>
public DateTimeOffset? GetDateTimeOffsetValue() => DateTimeOffset.TryParse(DecodedValue, out var result) ? result : null;
public DateTimeOffset? GetDateTimeOffsetValue() => DateTimeOffset.TryParse(DecodedValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? result : null;
/// <inheritdoc/>
public Date? GetDateValue() => DateTime.TryParse(DecodedValue, out var result) ? new Date(result) : null;
public Date? GetDateValue() => DateTime.TryParse(DecodedValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? new Date(result) : null;
/// <inheritdoc/>
public decimal? GetDecimalValue() => decimal.TryParse(DecodedValue, out var result) ? result : null;
public decimal? GetDecimalValue() => decimal.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc/>
public double? GetDoubleValue() => double.TryParse(DecodedValue, out var result) ? result : null;
public double? GetDoubleValue() => double.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc/>
public float? GetFloatValue() => float.TryParse(DecodedValue, out var result) ? result : null;
public float? GetFloatValue() => float.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc/>
public Guid? GetGuidValue() => Guid.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public int? GetIntValue() => int.TryParse(DecodedValue, out var result) ? result : null;
public int? GetIntValue() => int.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc/>
public long? GetLongValue() => long.TryParse(DecodedValue, out var result) ? result : null;
public long? GetLongValue() => long.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc/>
public T GetObjectValue<T>(ParsableFactory<T> factory) where T : IParsable
{
Expand Down Expand Up @@ -205,7 +205,7 @@ private void AssignFieldValues<T>(T item) where T : IParsable
}

/// <inheritdoc/>
public sbyte? GetSbyteValue() => sbyte.TryParse(DecodedValue, out var result) ? result : null;
public sbyte? GetSbyteValue() => sbyte.TryParse(DecodedValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;

/// <inheritdoc/>
public string GetStringValue() => DecodedValue;
Expand All @@ -222,22 +222,24 @@ private void AssignFieldValues<T>(T item) where T : IParsable
}

/// <inheritdoc/>
public Time? GetTimeValue() => DateTime.TryParse(DecodedValue, out var result) ? new Time(result) : null;
public Time? GetTimeValue() => DateTime.TryParse(DecodedValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? new Time(result) : null;

/// <inheritdoc/>
#if NET5_0_OR_GREATER
IEnumerable<T?> IParseNode.GetCollectionOfEnumValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>()
public IEnumerable<T?> GetCollectionOfEnumValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>() where T : struct, Enum
#else
IEnumerable<T?> IParseNode.GetCollectionOfEnumValues<T>()
public IEnumerable<T?> GetCollectionOfEnumValues<T>() where T : struct, Enum
#endif
{
foreach(var v in DecodedValue.Split(ComaSeparator, StringSplitOptions.RemoveEmptyEntries))
yield return EnumHelpers.GetEnumValue<T>(v);
}

/// <inheritdoc/>
#if NET5_0_OR_GREATER
T? IParseNode.GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>()
public T? GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>() where T : struct, Enum
#else
T? IParseNode.GetEnumValue<T>()
public T? GetEnumValue<T>() where T : struct, Enum
#endif
{
return EnumHelpers.GetEnumValue<T>(DecodedValue);
Expand Down
6 changes: 4 additions & 2 deletions src/serialization/form/FormSerializationWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ private void WriteAnyValue(string? key, object value)
case IEnumerable<object> coll:
WriteCollectionOfPrimitiveValues(key, coll);
break;
case byte[] coll:
WriteByteArrayValue(key, coll);
break;
case IParsable:
throw new InvalidOperationException("Form serialization does not support nested objects.");
default:
WriteStringValue(key, value.ToString());// works for Date and String types
break;

}
}

Expand Down Expand Up @@ -134,7 +136,7 @@ public void WriteCollectionOfPrimitiveValues<T>(string? key, IEnumerable<T>? val
public void WriteDateTimeOffsetValue(string? key, DateTimeOffset? value)
{
if(value.HasValue)
WriteStringValue(key, value.Value.ToString("o"));
WriteStringValue(key, value.Value.ToString("o", CultureInfo.InvariantCulture));
}
/// <inheritdoc/>
public void WriteDateValue(string? key, Date? value)
Expand Down
52 changes: 31 additions & 21 deletions src/serialization/json/JsonParseNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Xml;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Extensions;
Expand Down Expand Up @@ -148,17 +150,11 @@ public JsonParseNode(JsonElement node, KiotaJsonSerializationContext jsonSeriali
if(_jsonNode.ValueKind != JsonValueKind.String)
return null;

if(_jsonNode.TryGetDateTimeOffset(out var dateTimeOffset))
if(TryGetUsingTypeInfo(_jsonNode, _jsonSerializerContext.DateTimeOffset, out var dateTimeOffset))
return dateTimeOffset;

var dateTimeOffsetStr = _jsonNode.GetString();
if(string.IsNullOrEmpty(dateTimeOffsetStr))
return null;

if(DateTimeOffset.TryParse(dateTimeOffsetStr, out dateTimeOffset))
return dateTimeOffset;

return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);
else if(DateTimeOffset.TryParse(_jsonNode.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dto))
return dto;
else return null;
}

/// <summary>
Expand All @@ -181,14 +177,14 @@ public JsonParseNode(JsonElement node, KiotaJsonSerializationContext jsonSeriali
/// <returns>A <see cref="Date"/> value</returns>
public Date? GetDateValue()
{
var dateString = _jsonNode.GetString();
if(string.IsNullOrEmpty(dateString))
if(_jsonNode.ValueKind != JsonValueKind.String)
return null;

if(DateTime.TryParse(dateString, out var result))
return new Date(result);

return _jsonNode.Deserialize(_jsonSerializerContext.Date);
if(TryGetUsingTypeInfo(_jsonNode, _jsonSerializerContext.Date, out var date))
return date;
else if(DateTime.TryParse(_jsonNode.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt))
return new Date(dt);
else return null;
}

/// <summary>
Expand All @@ -197,14 +193,14 @@ public JsonParseNode(JsonElement node, KiotaJsonSerializationContext jsonSeriali
/// <returns>A <see cref="Time"/> value</returns>
public Time? GetTimeValue()
{
var dateString = _jsonNode.GetString();
if(string.IsNullOrEmpty(dateString))
if(_jsonNode.ValueKind != JsonValueKind.String)
return null;

if(DateTime.TryParse(dateString, out var result))
if(TryGetUsingTypeInfo(_jsonNode, _jsonSerializerContext.Time, out var time))
return time;
if(DateTime.TryParse(_jsonNode.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result))
return new Time(result);

return _jsonNode.Deserialize(_jsonSerializerContext.Time);
else return null;
}

/// <summary>
Expand Down Expand Up @@ -581,5 +577,19 @@ private static string ToEnumRawName<T>(string value) where T : struct, Enum

return value;
}

private static bool TryGetUsingTypeInfo<T>(JsonElement currentElement, JsonTypeInfo<T>? typeInfo, out T? deserializedValue)
{
try
{
deserializedValue = currentElement.Deserialize(typeInfo!);
return true;
}
catch(Exception)
{
deserializedValue = default;
return false;
}
}
}
}
19 changes: 10 additions & 9 deletions src/serialization/text/TextParseNode.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Helpers;
Expand Down Expand Up @@ -45,31 +46,31 @@ public TextParseNode(string? text)
public IEnumerable<T> GetCollectionOfPrimitiveValues<T>() => throw new InvalidOperationException(NoStructuredDataMessage);
#endif
/// <inheritdoc />
public DateTimeOffset? GetDateTimeOffsetValue() => DateTimeOffset.TryParse(Text, out var result) ? result : null;
public DateTimeOffset? GetDateTimeOffsetValue() => DateTimeOffset.TryParse(Text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? result : null;
/// <inheritdoc />
public Date? GetDateValue() => DateTime.TryParse(Text, out var result) ? new Date(result) : null;
public Date? GetDateValue() => DateTime.TryParse(Text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? new Date(result) : null;
/// <inheritdoc />
public decimal? GetDecimalValue() => decimal.TryParse(Text, out var result) ? result : null;
public decimal? GetDecimalValue() => decimal.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public double? GetDoubleValue() => double.TryParse(Text, out var result) ? result : null;
public double? GetDoubleValue() => double.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public float? GetFloatValue() => float.TryParse(Text, out var result) ? result : null;
public float? GetFloatValue() => float.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public Guid? GetGuidValue() => Guid.TryParse(Text, out var result) ? result : null;
/// <inheritdoc />
public int? GetIntValue() => int.TryParse(Text, out var result) ? result : null;
public int? GetIntValue() => int.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public long? GetLongValue() => long.TryParse(Text, out var result) ? result : null;
public long? GetLongValue() => long.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public T GetObjectValue<T>(ParsableFactory<T> factory) where T : IParsable => throw new InvalidOperationException(NoStructuredDataMessage);
/// <inheritdoc />
public sbyte? GetSbyteValue() => sbyte.TryParse(Text, out var result) ? result : null;
public sbyte? GetSbyteValue() => sbyte.TryParse(Text, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? result : null;
/// <inheritdoc />
public string? GetStringValue() => Text;
/// <inheritdoc />
public TimeSpan? GetTimeSpanValue() => string.IsNullOrEmpty(Text) ? null : XmlConvert.ToTimeSpan(Text);
/// <inheritdoc />
public Time? GetTimeValue() => DateTime.TryParse(Text, out var result) ? new Time(result) : null;
public Time? GetTimeValue() => DateTime.TryParse(Text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result) ? new Time(result) : null;
/// <inheritdoc />
#if NET5_0_OR_GREATER
public IEnumerable<T?> GetCollectionOfEnumValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>() where T : struct, Enum
Expand Down
17 changes: 9 additions & 8 deletions src/serialization/text/TextSerializationWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using Microsoft.Kiota.Abstractions;
Expand Down Expand Up @@ -59,33 +60,33 @@ public Stream GetSerializedContent()
/// <inheritdoc />
public void WriteByteArrayValue(string? key, byte[]? value) => WriteStringValue(key, value?.Length > 0 ? Convert.ToBase64String(value) : string.Empty);
/// <inheritdoc />
public void WriteByteValue(string? key, byte? value) => WriteStringValue(key, value?.ToString());
public void WriteByteValue(string? key, byte? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteCollectionOfObjectValues<T>(string? key, IEnumerable<T>? values) where T : IParsable => throw new InvalidOperationException(TextParseNode.NoStructuredDataMessage);
/// <inheritdoc />
public void WriteCollectionOfPrimitiveValues<T>(string? key, IEnumerable<T>? values) => throw new InvalidOperationException(TextParseNode.NoStructuredDataMessage);
/// <inheritdoc />
public void WriteDateTimeOffsetValue(string? key, DateTimeOffset? value) => WriteStringValue(key, value.HasValue ? value.Value.ToString() : null);
public void WriteDateTimeOffsetValue(string? key, DateTimeOffset? value) => WriteStringValue(key, value.HasValue ? value.Value.ToString("o", CultureInfo.InvariantCulture) : null);
/// <inheritdoc />
public void WriteDateValue(string? key, Date? value) => WriteStringValue(key, value?.ToString());
/// <inheritdoc />
public void WriteDecimalValue(string? key, decimal? value) => WriteStringValue(key, value?.ToString());
public void WriteDecimalValue(string? key, decimal? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteDoubleValue(string? key, double? value) => WriteStringValue(key, value?.ToString());
public void WriteDoubleValue(string? key, double? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteFloatValue(string? key, float? value) => WriteStringValue(key, value?.ToString());
public void WriteFloatValue(string? key, float? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteGuidValue(string? key, Guid? value) => WriteStringValue(key, value?.ToString());
/// <inheritdoc />
public void WriteIntValue(string? key, int? value) => WriteStringValue(key, value?.ToString());
public void WriteIntValue(string? key, int? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteLongValue(string? key, long? value) => WriteStringValue(key, value?.ToString());
public void WriteLongValue(string? key, long? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteNullValue(string? key) => WriteStringValue(key, "null");
/// <inheritdoc />
public void WriteObjectValue<T>(string? key, T? value, params IParsable?[] additionalValuesToMerge) where T : IParsable => throw new InvalidOperationException(TextParseNode.NoStructuredDataMessage);
/// <inheritdoc />
public void WriteSbyteValue(string? key, sbyte? value) => WriteStringValue(key, value?.ToString());
public void WriteSbyteValue(string? key, sbyte? value) => WriteStringValue(key, value?.ToString(CultureInfo.InvariantCulture));
/// <inheritdoc />
public void WriteStringValue(string? key, string? value)
{
Expand Down
28 changes: 28 additions & 0 deletions tests/abstractions/TimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using Xunit;

namespace Microsoft.Kiota.Abstractions.Tests
{
public class TimeTests
{
[Fact]
public void TestTimeEquality()
{
var time1 = new Time(10, 30, 0);
var time2 = new Time(new DateTime(2024, 7, 17, 10, 30, 0));
var time3 = new Time(12, 0, 0);

Assert.Equal(time1, time2);
Assert.NotEqual(time1, time3);
}

[Fact]
public void TestTimeToString()
{
var time = new Time(15, 45, 30);
var expectedString = "15:45:30";

Assert.Equal(expectedString, time.ToString());
}
}
}
Loading

0 comments on commit 10b879b

Please sign in to comment.