Skip to content

Commit

Permalink
Add support for DateTimeOffset values.
Browse files Browse the repository at this point in the history
  • Loading branch information
drewnoakes committed Jun 7, 2016
1 parent 77cf215 commit 4279b89
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 1 deletion.
21 changes: 21 additions & 0 deletions Dasher.Tests/DeserialiserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ public void HandlesDateTime()
Assert.Equal(dateTime, after.Date);
}

[Fact]
public void HandlesDateTimeOffset()
{
var dateTimeOffset = new DateTimeOffset(2015, 12, 25, 0, 0, 0, TimeSpan.FromMinutes(90));

var bytes = PackBytes(packer =>
{
packer.PackMapHeader(1)
.Pack("Date").PackArrayHeader(2)
.Pack(dateTimeOffset.DateTime.ToBinary())
.Pack((short)dateTimeOffset.Offset.TotalMinutes);
});

var after = new Deserialiser<WithDateTimeOffsetProperty>().Deserialise(bytes);

Assert.Equal(dateTimeOffset, after.Date);
Assert.Equal(dateTimeOffset.Offset, after.Date.Offset);
Assert.Equal(dateTimeOffset.DateTime.Kind, after.Date.DateTime.Kind);
Assert.True(dateTimeOffset.EqualsExact(after.Date));
}

[Fact]
public void HandlesTimeSpan()
{
Expand Down
33 changes: 33 additions & 0 deletions Dasher.Tests/SerialiserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,39 @@ public void HandlesDateTime()
test(DateTime.UtcNow);
}

[Fact]
public void HandlesDateTimeOffset()
{
Action<DateTimeOffset> test = dto =>
{
var after = RoundTrip(new WithDateTimeOffsetProperty(dto));
Assert.Equal(dto, after.Date);
Assert.Equal(dto.Offset, after.Date.Offset);
Assert.Equal(dto.DateTime.Kind, after.Date.DateTime.Kind);
Assert.True(dto.EqualsExact(after.Date));
};

var offsets = new[]
{
TimeSpan.Zero,
TimeSpan.FromHours(1),
TimeSpan.FromHours(-1),
TimeSpan.FromHours(10),
TimeSpan.FromHours(-10),
TimeSpan.FromMinutes(90),
TimeSpan.FromMinutes(-90)
};

foreach (var offset in offsets)
test(new DateTimeOffset(new DateTime(2015, 12, 25), offset));

test(DateTimeOffset.MinValue);
test(DateTimeOffset.MaxValue);
test(DateTimeOffset.Now);
test(DateTimeOffset.UtcNow);
}

[Fact]
public void HandlesTimeSpan()
{
Expand Down
10 changes: 10 additions & 0 deletions Dasher.Tests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ public WithDateTimeProperty(DateTime date)
public DateTime Date { get; }
}

public sealed class WithDateTimeOffsetProperty
{
public WithDateTimeOffsetProperty(DateTimeOffset date)
{
Date = date;
}

public DateTimeOffset Date { get; }
}

public sealed class WithTimeSpanProperty
{
public WithTimeSpanProperty(TimeSpan time)
Expand Down
1 change: 1 addition & 0 deletions Dasher/Dasher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<Compile Include="ILGeneratorExtensions.cs" />
<Compile Include="SerialiserEmitter.cs" />
<Compile Include="TypeProviders\ComplexTypeProvider.cs" />
<Compile Include="TypeProviders\DateTimeOffsetProvider.cs" />
<Compile Include="TypeProviders\DateTimeProvider.cs" />
<Compile Include="TypeProviders\DecimalProvider.cs" />
<Compile Include="TypeProviders\EnumProvider.cs" />
Expand Down
1 change: 1 addition & 0 deletions Dasher/DasherContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public DasherContext(IEnumerable<ITypeProvider> typeProviders = null)
new MsgPackTypeProvider(),
new DecimalProvider(),
new DateTimeProvider(),
new DateTimeOffsetProvider(),
new TimeSpanProvider(),
new IntPtrProvider(),
new GuidProvider(),
Expand Down
129 changes: 129 additions & 0 deletions Dasher/TypeProviders/DateTimeOffsetProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Reflection;
using System.Reflection.Emit;

namespace Dasher.TypeProviders
{
internal sealed class DateTimeOffsetProvider : ITypeProvider
{
private const int TicksPerMinute = 600000000;

public bool CanProvide(Type type) => type == typeof(DateTimeOffset);

public void Serialise(ILGenerator ilg, LocalBuilder value, LocalBuilder packer, LocalBuilder contextLocal, DasherContext context)
{
// We need to write both the date and the offset
// - dto.DateTime always has unspecified kind (so we can just use Ticks rather than ToBinary and ignore internal flags)
// - dto.Offset is a timespan but always has integral minutes (minutes will be a smaller number than ticks so uses fewer bytes on the wire)

ilg.Emit(OpCodes.Ldloc, packer);
ilg.Emit(OpCodes.Dup);
ilg.Emit(OpCodes.Dup);

// Write the array header
ilg.Emit(OpCodes.Ldc_I4_2);
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.PackArrayHeader)));

// Write ticks
ilg.Emit(OpCodes.Ldloca, value);
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetProperty(nameof(DateTimeOffset.Ticks)).GetMethod);
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.Pack), new[] {typeof(long)}));

// Write offset minutes
var offset = ilg.DeclareLocal(typeof(TimeSpan));
ilg.Emit(OpCodes.Ldloca, value);
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetProperty(nameof(DateTimeOffset.Offset)).GetMethod);
ilg.Emit(OpCodes.Stloc, offset);
ilg.Emit(OpCodes.Ldloca, offset);
ilg.Emit(OpCodes.Call, typeof(TimeSpan).GetProperty(nameof(TimeSpan.Ticks)).GetMethod);
ilg.Emit(OpCodes.Ldc_I4, TicksPerMinute);
ilg.Emit(OpCodes.Conv_I8);
ilg.Emit(OpCodes.Div);
ilg.Emit(OpCodes.Conv_I2);
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.Pack), new[] { typeof(short) }));
}

public void Deserialise(ILGenerator ilg, string name, Type targetType, LocalBuilder value, LocalBuilder unpacker, LocalBuilder contextLocal, DasherContext context, UnexpectedFieldBehaviour unexpectedFieldBehaviour)
{
// Ensure we have an array of two values
var arrayLength = ilg.DeclareLocal(typeof(int));

ilg.Emit(OpCodes.Ldloc, unpacker);
ilg.Emit(OpCodes.Ldloca, arrayLength);
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadArrayLength)));

// If the unpacker method failed (returned false), throw
var lbl1 = ilg.DefineLabel();
ilg.Emit(OpCodes.Brtrue, lbl1);
{
ilg.Emit(OpCodes.Ldstr, $"Expecting array header for DateTimeOffset property {name}");
ilg.LoadType(targetType);
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] { typeof(string), typeof(Type) }));
ilg.Emit(OpCodes.Throw);
}
ilg.MarkLabel(lbl1);

ilg.Emit(OpCodes.Ldloc, arrayLength);
ilg.Emit(OpCodes.Ldc_I4_2);
ilg.Emit(OpCodes.Ceq);

var lbl2 = ilg.DefineLabel();
ilg.Emit(OpCodes.Brtrue, lbl2);
{
ilg.Emit(OpCodes.Ldstr, $"Expecting array to contain two items for DateTimeOffset property {name}");
ilg.LoadType(targetType);
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] { typeof(string), typeof(Type) }));
ilg.Emit(OpCodes.Throw);
}
ilg.MarkLabel(lbl2);

// Read ticks
var ticks = ilg.DeclareLocal(typeof(long));

ilg.Emit(OpCodes.Ldloc, unpacker);
ilg.Emit(OpCodes.Ldloca, ticks);
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadInt64)));

// If the unpacker method failed (returned false), throw
var lbl3 = ilg.DefineLabel();
ilg.Emit(OpCodes.Brtrue, lbl3);
{
ilg.Emit(OpCodes.Ldstr, $"Expecting Int64 value for ticks component of DateTimeOffset property {name}");
ilg.LoadType(targetType);
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] {typeof(string), typeof(Type)}));
ilg.Emit(OpCodes.Throw);
}
ilg.MarkLabel(lbl3);

// Read offset
var minutes = ilg.DeclareLocal(typeof(short));

ilg.Emit(OpCodes.Ldloc, unpacker);
ilg.Emit(OpCodes.Ldloca, minutes);
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadInt16)));

// If the unpacker method failed (returned false), throw
var lbl4 = ilg.DefineLabel();
ilg.Emit(OpCodes.Brtrue, lbl4);
{
ilg.Emit(OpCodes.Ldstr, $"Expecting Int16 value for offset component of DateTimeOffset property {name}");
ilg.LoadType(targetType);
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] {typeof(string), typeof(Type)}));
ilg.Emit(OpCodes.Throw);
}
ilg.MarkLabel(lbl4);

// Compose the final DateTimeOffset
ilg.Emit(OpCodes.Ldloca, value);
ilg.Emit(OpCodes.Ldloc, ticks);
ilg.Emit(OpCodes.Ldloc, minutes);
ilg.Emit(OpCodes.Conv_I8);
ilg.Emit(OpCodes.Ldc_I4, TicksPerMinute);
ilg.Emit(OpCodes.Conv_I8);
ilg.Emit(OpCodes.Mul);
ilg.Emit(OpCodes.Conv_I8);
ilg.Emit(OpCodes.Call, typeof(TimeSpan).GetMethod(nameof(TimeSpan.FromTicks), BindingFlags.Static | BindingFlags.Public));
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetConstructor(new[] {typeof(long), typeof(TimeSpan)}));
}
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public sealed class Holiday

# Supported types

Both serialiser and deserialiser support the core built-in types of `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, as well as `DateTime`, `TimeSpan`, `Guid`, `IntPtr`, `Version`, `Nullable<T>`, `IReadOnlyList<T>`, `IReadOnlyDictionary<TKey, TValue>`, `Tuple<...>` and enum types.
Both serialiser and deserialiser support the core built-in types of `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `string`, as well as `DateTime`, `DateTimeOffset`, `TimeSpan`, `Guid`, `IntPtr`, `Version`, `Nullable<T>`, `IReadOnlyList<T>`, `IReadOnlyDictionary<TKey, TValue>`, `Tuple<...>` and enum types.

Types may contain fields of further complex types, which are nested.

Expand Down

0 comments on commit 4279b89

Please sign in to comment.