Skip to content

Commit 4279b89

Browse files
committed
Add support for DateTimeOffset values.
1 parent 77cf215 commit 4279b89

File tree

7 files changed

+196
-1
lines changed

7 files changed

+196
-1
lines changed

Dasher.Tests/DeserialiserTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ public void HandlesDateTime()
7575
Assert.Equal(dateTime, after.Date);
7676
}
7777

78+
[Fact]
79+
public void HandlesDateTimeOffset()
80+
{
81+
var dateTimeOffset = new DateTimeOffset(2015, 12, 25, 0, 0, 0, TimeSpan.FromMinutes(90));
82+
83+
var bytes = PackBytes(packer =>
84+
{
85+
packer.PackMapHeader(1)
86+
.Pack("Date").PackArrayHeader(2)
87+
.Pack(dateTimeOffset.DateTime.ToBinary())
88+
.Pack((short)dateTimeOffset.Offset.TotalMinutes);
89+
});
90+
91+
var after = new Deserialiser<WithDateTimeOffsetProperty>().Deserialise(bytes);
92+
93+
Assert.Equal(dateTimeOffset, after.Date);
94+
Assert.Equal(dateTimeOffset.Offset, after.Date.Offset);
95+
Assert.Equal(dateTimeOffset.DateTime.Kind, after.Date.DateTime.Kind);
96+
Assert.True(dateTimeOffset.EqualsExact(after.Date));
97+
}
98+
7899
[Fact]
79100
public void HandlesTimeSpan()
80101
{

Dasher.Tests/SerialiserTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,39 @@ public void HandlesDateTime()
8080
test(DateTime.UtcNow);
8181
}
8282

83+
[Fact]
84+
public void HandlesDateTimeOffset()
85+
{
86+
Action<DateTimeOffset> test = dto =>
87+
{
88+
var after = RoundTrip(new WithDateTimeOffsetProperty(dto));
89+
90+
Assert.Equal(dto, after.Date);
91+
Assert.Equal(dto.Offset, after.Date.Offset);
92+
Assert.Equal(dto.DateTime.Kind, after.Date.DateTime.Kind);
93+
Assert.True(dto.EqualsExact(after.Date));
94+
};
95+
96+
var offsets = new[]
97+
{
98+
TimeSpan.Zero,
99+
TimeSpan.FromHours(1),
100+
TimeSpan.FromHours(-1),
101+
TimeSpan.FromHours(10),
102+
TimeSpan.FromHours(-10),
103+
TimeSpan.FromMinutes(90),
104+
TimeSpan.FromMinutes(-90)
105+
};
106+
107+
foreach (var offset in offsets)
108+
test(new DateTimeOffset(new DateTime(2015, 12, 25), offset));
109+
110+
test(DateTimeOffset.MinValue);
111+
test(DateTimeOffset.MaxValue);
112+
test(DateTimeOffset.Now);
113+
test(DateTimeOffset.UtcNow);
114+
}
115+
83116
[Fact]
84117
public void HandlesTimeSpan()
85118
{

Dasher.Tests/TestTypes.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public WithDateTimeProperty(DateTime date)
112112
public DateTime Date { get; }
113113
}
114114

115+
public sealed class WithDateTimeOffsetProperty
116+
{
117+
public WithDateTimeOffsetProperty(DateTimeOffset date)
118+
{
119+
Date = date;
120+
}
121+
122+
public DateTimeOffset Date { get; }
123+
}
124+
115125
public sealed class WithTimeSpanProperty
116126
{
117127
public WithTimeSpanProperty(TimeSpan time)

Dasher/Dasher.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<Compile Include="ILGeneratorExtensions.cs" />
4545
<Compile Include="SerialiserEmitter.cs" />
4646
<Compile Include="TypeProviders\ComplexTypeProvider.cs" />
47+
<Compile Include="TypeProviders\DateTimeOffsetProvider.cs" />
4748
<Compile Include="TypeProviders\DateTimeProvider.cs" />
4849
<Compile Include="TypeProviders\DecimalProvider.cs" />
4950
<Compile Include="TypeProviders\EnumProvider.cs" />

Dasher/DasherContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public DasherContext(IEnumerable<ITypeProvider> typeProviders = null)
4747
new MsgPackTypeProvider(),
4848
new DecimalProvider(),
4949
new DateTimeProvider(),
50+
new DateTimeOffsetProvider(),
5051
new TimeSpanProvider(),
5152
new IntPtrProvider(),
5253
new GuidProvider(),
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Reflection.Emit;
4+
5+
namespace Dasher.TypeProviders
6+
{
7+
internal sealed class DateTimeOffsetProvider : ITypeProvider
8+
{
9+
private const int TicksPerMinute = 600000000;
10+
11+
public bool CanProvide(Type type) => type == typeof(DateTimeOffset);
12+
13+
public void Serialise(ILGenerator ilg, LocalBuilder value, LocalBuilder packer, LocalBuilder contextLocal, DasherContext context)
14+
{
15+
// We need to write both the date and the offset
16+
// - dto.DateTime always has unspecified kind (so we can just use Ticks rather than ToBinary and ignore internal flags)
17+
// - 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)
18+
19+
ilg.Emit(OpCodes.Ldloc, packer);
20+
ilg.Emit(OpCodes.Dup);
21+
ilg.Emit(OpCodes.Dup);
22+
23+
// Write the array header
24+
ilg.Emit(OpCodes.Ldc_I4_2);
25+
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.PackArrayHeader)));
26+
27+
// Write ticks
28+
ilg.Emit(OpCodes.Ldloca, value);
29+
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetProperty(nameof(DateTimeOffset.Ticks)).GetMethod);
30+
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.Pack), new[] {typeof(long)}));
31+
32+
// Write offset minutes
33+
var offset = ilg.DeclareLocal(typeof(TimeSpan));
34+
ilg.Emit(OpCodes.Ldloca, value);
35+
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetProperty(nameof(DateTimeOffset.Offset)).GetMethod);
36+
ilg.Emit(OpCodes.Stloc, offset);
37+
ilg.Emit(OpCodes.Ldloca, offset);
38+
ilg.Emit(OpCodes.Call, typeof(TimeSpan).GetProperty(nameof(TimeSpan.Ticks)).GetMethod);
39+
ilg.Emit(OpCodes.Ldc_I4, TicksPerMinute);
40+
ilg.Emit(OpCodes.Conv_I8);
41+
ilg.Emit(OpCodes.Div);
42+
ilg.Emit(OpCodes.Conv_I2);
43+
ilg.Emit(OpCodes.Call, typeof(UnsafePacker).GetMethod(nameof(UnsafePacker.Pack), new[] { typeof(short) }));
44+
}
45+
46+
public void Deserialise(ILGenerator ilg, string name, Type targetType, LocalBuilder value, LocalBuilder unpacker, LocalBuilder contextLocal, DasherContext context, UnexpectedFieldBehaviour unexpectedFieldBehaviour)
47+
{
48+
// Ensure we have an array of two values
49+
var arrayLength = ilg.DeclareLocal(typeof(int));
50+
51+
ilg.Emit(OpCodes.Ldloc, unpacker);
52+
ilg.Emit(OpCodes.Ldloca, arrayLength);
53+
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadArrayLength)));
54+
55+
// If the unpacker method failed (returned false), throw
56+
var lbl1 = ilg.DefineLabel();
57+
ilg.Emit(OpCodes.Brtrue, lbl1);
58+
{
59+
ilg.Emit(OpCodes.Ldstr, $"Expecting array header for DateTimeOffset property {name}");
60+
ilg.LoadType(targetType);
61+
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] { typeof(string), typeof(Type) }));
62+
ilg.Emit(OpCodes.Throw);
63+
}
64+
ilg.MarkLabel(lbl1);
65+
66+
ilg.Emit(OpCodes.Ldloc, arrayLength);
67+
ilg.Emit(OpCodes.Ldc_I4_2);
68+
ilg.Emit(OpCodes.Ceq);
69+
70+
var lbl2 = ilg.DefineLabel();
71+
ilg.Emit(OpCodes.Brtrue, lbl2);
72+
{
73+
ilg.Emit(OpCodes.Ldstr, $"Expecting array to contain two items for DateTimeOffset property {name}");
74+
ilg.LoadType(targetType);
75+
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] { typeof(string), typeof(Type) }));
76+
ilg.Emit(OpCodes.Throw);
77+
}
78+
ilg.MarkLabel(lbl2);
79+
80+
// Read ticks
81+
var ticks = ilg.DeclareLocal(typeof(long));
82+
83+
ilg.Emit(OpCodes.Ldloc, unpacker);
84+
ilg.Emit(OpCodes.Ldloca, ticks);
85+
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadInt64)));
86+
87+
// If the unpacker method failed (returned false), throw
88+
var lbl3 = ilg.DefineLabel();
89+
ilg.Emit(OpCodes.Brtrue, lbl3);
90+
{
91+
ilg.Emit(OpCodes.Ldstr, $"Expecting Int64 value for ticks component of DateTimeOffset property {name}");
92+
ilg.LoadType(targetType);
93+
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] {typeof(string), typeof(Type)}));
94+
ilg.Emit(OpCodes.Throw);
95+
}
96+
ilg.MarkLabel(lbl3);
97+
98+
// Read offset
99+
var minutes = ilg.DeclareLocal(typeof(short));
100+
101+
ilg.Emit(OpCodes.Ldloc, unpacker);
102+
ilg.Emit(OpCodes.Ldloca, minutes);
103+
ilg.Emit(OpCodes.Call, typeof(Unpacker).GetMethod(nameof(Unpacker.TryReadInt16)));
104+
105+
// If the unpacker method failed (returned false), throw
106+
var lbl4 = ilg.DefineLabel();
107+
ilg.Emit(OpCodes.Brtrue, lbl4);
108+
{
109+
ilg.Emit(OpCodes.Ldstr, $"Expecting Int16 value for offset component of DateTimeOffset property {name}");
110+
ilg.LoadType(targetType);
111+
ilg.Emit(OpCodes.Newobj, typeof(DeserialisationException).GetConstructor(new[] {typeof(string), typeof(Type)}));
112+
ilg.Emit(OpCodes.Throw);
113+
}
114+
ilg.MarkLabel(lbl4);
115+
116+
// Compose the final DateTimeOffset
117+
ilg.Emit(OpCodes.Ldloca, value);
118+
ilg.Emit(OpCodes.Ldloc, ticks);
119+
ilg.Emit(OpCodes.Ldloc, minutes);
120+
ilg.Emit(OpCodes.Conv_I8);
121+
ilg.Emit(OpCodes.Ldc_I4, TicksPerMinute);
122+
ilg.Emit(OpCodes.Conv_I8);
123+
ilg.Emit(OpCodes.Mul);
124+
ilg.Emit(OpCodes.Conv_I8);
125+
ilg.Emit(OpCodes.Call, typeof(TimeSpan).GetMethod(nameof(TimeSpan.FromTicks), BindingFlags.Static | BindingFlags.Public));
126+
ilg.Emit(OpCodes.Call, typeof(DateTimeOffset).GetConstructor(new[] {typeof(long), typeof(TimeSpan)}));
127+
}
128+
}
129+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public sealed class Holiday
4444

4545
# Supported types
4646

47-
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.
47+
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.
4848

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

0 commit comments

Comments
 (0)