Skip to content

Commit a701c2e

Browse files
authored
CSHARP-5345: Unable to deserialize C# DateOnly objects (#1534)
1 parent b721799 commit a701c2e

File tree

6 files changed

+370
-38
lines changed

6 files changed

+370
-38
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using MongoDB.Bson.Serialization.Options;
18+
using MongoDB.Bson.Serialization.Serializers;
19+
20+
namespace MongoDB.Bson.Serialization.Attributes
21+
{
22+
#if NET6_0_OR_GREATER
23+
/// <summary>
24+
/// Specifies the external representation and related options for a DateOnly field or property.
25+
/// </summary>
26+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
27+
public class BsonDateOnlyOptionsAttribute : BsonSerializationOptionsAttribute
28+
{
29+
// private fields
30+
private BsonType _representation;
31+
private DateOnlyDocumentFormat _documentFormat;
32+
33+
// constructors
34+
35+
/// <summary>
36+
/// Initializes a new instance of the BsonDateOnlyOptionsAttribute class.
37+
/// </summary>
38+
/// <param name="representation">The external representation.</param>
39+
/// <param name="documentDocumentFormat">The format to use with document representation.</param>
40+
public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFormat documentDocumentFormat = DateOnlyDocumentFormat.DateTimeTicks)
41+
{
42+
_representation = representation;
43+
_documentFormat = documentDocumentFormat;
44+
}
45+
46+
// public properties
47+
/// <summary>
48+
/// Gets the external representation.
49+
/// </summary>
50+
public BsonType Representation => _representation;
51+
52+
/// <summary>
53+
/// Gets the document format.
54+
/// </summary>
55+
public DateOnlyDocumentFormat DocumentFormat => _documentFormat;
56+
57+
/// <summary>
58+
/// Reconfigures the specified serializer by applying this attribute to it.
59+
/// </summary>
60+
/// <param name="serializer">The serializer.</param>
61+
/// <returns>A reconfigured serializer.</returns>
62+
protected override IBsonSerializer Apply(IBsonSerializer serializer)
63+
{
64+
var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat));
65+
return reconfiguredSerializer ?? base.Apply(serializer);
66+
}
67+
}
68+
#endif
69+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace MongoDB.Bson.Serialization.Options
17+
{
18+
/// <summary>
19+
/// Represents the format to use with a DateOnly serializer when the representation is BsonType.Document.
20+
/// </summary>
21+
public enum DateOnlyDocumentFormat
22+
{
23+
/// <summary>
24+
/// The document will contain "DateTime" (BsonType.DateTime) and "Ticks" (BsonType.Int64).
25+
/// </summary>
26+
DateTimeTicks,
27+
28+
/// <summary>
29+
/// The document will contain "Year", "Month" and "Day" (all BsonType.Int32).
30+
/// </summary>
31+
YearMonthDay
32+
}
33+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
18+
namespace MongoDB.Bson.Serialization
19+
{
20+
internal static class SerializerConfigurator
21+
{
22+
/// <summary>
23+
/// Reconfigures a serializer using the specified <paramref name="reconfigure"/> method.
24+
/// If the serializer implements <see cref="IChildSerializerConfigurable"/>,
25+
/// the method traverses and applies the reconfiguration to its child serializers recursively until an appropriate leaf serializer is found.
26+
/// </summary>
27+
/// <param name="serializer">The input serializer to be reconfigured.</param>
28+
/// <param name="reconfigure">A function that defines how the serializer of type <typeparamref name="TSerializer"/> should be reconfigured.</param>
29+
/// <typeparam name="TSerializer">The input type for the reconfigure method.</typeparam>
30+
/// <returns>
31+
/// The reconfigured serializer, or <c>null</c> if no leaf serializer could be reconfigured.
32+
/// </returns>
33+
internal static IBsonSerializer ReconfigureSerializer<TSerializer>(IBsonSerializer serializer, Func<TSerializer, IBsonSerializer> reconfigure)
34+
{
35+
switch (serializer)
36+
{
37+
case IChildSerializerConfigurable childSerializerConfigurable:
38+
var childSerializer = childSerializerConfigurable.ChildSerializer;
39+
var reconfiguredChildSerializer = ReconfigureSerializer(childSerializer, reconfigure);
40+
return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null;
41+
42+
case TSerializer typedSerializer:
43+
return reconfigure(typedSerializer);
44+
45+
default:
46+
return null;
47+
}
48+
}
49+
}
50+
}

src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
using System;
1717
using MongoDB.Bson.IO;
18+
using MongoDB.Bson.Serialization.Attributes;
1819
using MongoDB.Bson.Serialization.Options;
1920

2021
namespace MongoDB.Bson.Serialization.Serializers
@@ -26,7 +27,7 @@ namespace MongoDB.Bson.Serialization.Serializers
2627
public sealed class DateOnlySerializer : StructSerializerBase<DateOnly>, IRepresentationConfigurable<DateOnlySerializer>
2728
{
2829
// static
29-
private static readonly DateOnlySerializer __instance = new DateOnlySerializer();
30+
private static readonly DateOnlySerializer __instance = new();
3031

3132
/// <summary>
3233
/// Gets the default DateOnlySerializer.
@@ -38,19 +39,26 @@ private static class Flags
3839
{
3940
public const long DateTime = 1;
4041
public const long Ticks = 2;
42+
public const long Year = 4;
43+
public const long Month = 8;
44+
public const long Day = 16;
45+
46+
public const long DateTimeTicks = DateTime | Ticks;
47+
public const long YearMonthDay = Year | Month | Day;
4148
}
4249

4350
// private fields
4451
private readonly RepresentationConverter _converter;
4552
private readonly SerializerHelper _helper;
4653
private readonly BsonType _representation;
54+
private readonly DateOnlyDocumentFormat _documentFormat;
4755

4856
// constructors
4957
/// <summary>
5058
/// Initializes a new instance of the <see cref="DateOnlySerializer"/> class.
5159
/// </summary>
5260
public DateOnlySerializer()
53-
: this(BsonType.DateTime)
61+
: this(BsonType.DateTime, DateOnlyDocumentFormat.DateTimeTicks)
5462
{
5563
}
5664

@@ -59,6 +67,16 @@ public DateOnlySerializer()
5967
/// </summary>
6068
/// <param name="representation">The representation.</param>
6169
public DateOnlySerializer(BsonType representation)
70+
: this(representation, DateOnlyDocumentFormat.DateTimeTicks)
71+
{
72+
}
73+
74+
/// <summary>
75+
/// Initializes a new instance of the <see cref="DateOnlySerializer"/> class.
76+
/// </summary>
77+
/// <param name="representation">The representation.</param>
78+
/// <param name="documentFormat">The format to use with the BsonType.Document representation. It will be ignored if the representation is different.</param>
79+
public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat documentFormat)
6280
{
6381
switch (representation)
6482
{
@@ -73,73 +91,84 @@ public DateOnlySerializer(BsonType representation)
7391
}
7492

7593
_representation = representation;
94+
_documentFormat = documentFormat;
7695
_converter = new RepresentationConverter(false, false);
77-
7896
_helper = new SerializerHelper
7997
(
80-
new SerializerHelper.Member("DateTime", Flags.DateTime),
81-
new SerializerHelper.Member("Ticks", Flags.Ticks)
98+
new SerializerHelper.Member("DateTime", Flags.DateTime, isOptional: true),
99+
new SerializerHelper.Member("Ticks", Flags.Ticks, isOptional: true),
100+
new SerializerHelper.Member("Year", Flags.Year, isOptional: true),
101+
new SerializerHelper.Member("Month", Flags.Month, isOptional: true),
102+
new SerializerHelper.Member("Day", Flags.Day, isOptional: true)
82103
);
83104
}
84105

85106
// public properties
86107
/// <inheritdoc />
87108
public BsonType Representation => _representation;
88109

110+
/// <summary>
111+
/// The format to use for the BsonType.Document representation. It will be ignored if the representation is different.
112+
/// </summary>
113+
public DateOnlyDocumentFormat DocumentFormat => _documentFormat;
114+
89115
//public methods
90116
/// <inheritdoc />
91117
public override DateOnly Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
92118
{
93119
var bsonReader = context.Reader;
94-
DateOnly value;
95120

96121
var bsonType = bsonReader.GetCurrentBsonType();
97122

98123
switch (bsonType)
99124
{
100125
case BsonType.DateTime:
101-
value = VerifyAndMakeDateOnly(BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(bsonReader.ReadDateTime()));
102-
break;
126+
return VerifyAndMakeDateOnly(BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(bsonReader.ReadDateTime()));
103127

104128
case BsonType.Document:
105-
value = default;
106-
_helper.DeserializeMembers(context, (_, flag) =>
129+
var ticks = 0L;
130+
var year = 0;
131+
var month = 0;
132+
var day = 0;
133+
134+
var foundMemberFlags = _helper.DeserializeMembers(context, (_, flag) =>
107135
{
108136
switch (flag)
109137
{
110-
case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead)
111-
case Flags.Ticks:
112-
value = VerifyAndMakeDateOnly(new DateTime(Int64Serializer.Instance.Deserialize(context), DateTimeKind.Utc));
113-
break;
138+
case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead)
139+
case Flags.Ticks: ticks = Int64Serializer.Instance.Deserialize(context); break;
140+
case Flags.Year: year = Int32Serializer.Instance.Deserialize(context); break;
141+
case Flags.Month: month = Int32Serializer.Instance.Deserialize(context); break;
142+
case Flags.Day: day = Int32Serializer.Instance.Deserialize(context); break;
114143
}
115144
});
116-
break;
145+
146+
return foundMemberFlags switch
147+
{
148+
Flags.DateTimeTicks => VerifyAndMakeDateOnly(new DateTime(ticks, DateTimeKind.Utc)),
149+
Flags.YearMonthDay => new DateOnly(year, month, day),
150+
_ => throw new FormatException("Invalid document format.")
151+
};
117152

118153
case BsonType.Decimal128:
119-
value = VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDecimal128()), DateTimeKind.Utc));
120-
break;
154+
return VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDecimal128()), DateTimeKind.Utc));
121155

122156
case BsonType.Double:
123-
value = VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDouble()), DateTimeKind.Utc));
124-
break;
157+
return VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDouble()), DateTimeKind.Utc));
125158

126159
case BsonType.Int32:
127-
value = VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt32(), DateTimeKind.Utc));
128-
break;
160+
return VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt32(), DateTimeKind.Utc));
129161

130162
case BsonType.Int64:
131-
value = VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt64(), DateTimeKind.Utc));
132-
break;
163+
return VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt64(), DateTimeKind.Utc));
133164

134165
case BsonType.String:
135-
value = DateOnly.ParseExact(bsonReader.ReadString(), "yyyy-MM-dd");
136-
break;
166+
return DateOnly.ParseExact(bsonReader.ReadString(), "yyyy-MM-dd");
137167

138168
default:
139169
throw CreateCannotDeserializeFromBsonTypeException(bsonType);
140170
}
141171

142-
return value;
143172

144173
DateOnly VerifyAndMakeDateOnly(DateTime dt)
145174
{
@@ -160,7 +189,8 @@ public override bool Equals(object obj)
160189
return
161190
base.Equals(obj) &&
162191
obj is DateOnlySerializer other &&
163-
_representation.Equals(other._representation);
192+
_representation.Equals(other._representation) &&
193+
_documentFormat.Equals(other._documentFormat);
164194
}
165195

166196
/// <inheritdoc/>
@@ -182,8 +212,17 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati
182212

183213
case BsonType.Document:
184214
bsonWriter.WriteStartDocument();
185-
bsonWriter.WriteDateTime("DateTime", millisecondsSinceEpoch);
186-
bsonWriter.WriteInt64("Ticks", utcDateTime.Ticks);
215+
if (_documentFormat is DateOnlyDocumentFormat.DateTimeTicks)
216+
{
217+
bsonWriter.WriteDateTime("DateTime", millisecondsSinceEpoch);
218+
bsonWriter.WriteInt64("Ticks", utcDateTime.Ticks);
219+
}
220+
else
221+
{
222+
bsonWriter.WriteInt32("Year", value.Year);
223+
bsonWriter.WriteInt32("Month", value.Month);
224+
bsonWriter.WriteInt32("Day", value.Day);
225+
}
187226
bsonWriter.WriteEndDocument();
188227
break;
189228

@@ -200,10 +239,28 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati
200239
}
201240
}
202241

242+
/// <summary>
243+
/// Returns a serializer that has been reconfigured with the specified representation and document format.
244+
/// </summary>
245+
/// <param name="representation">The representation.</param>
246+
/// <param name="documentFormat">The document format to use with BsonType.Document representation.</param>
247+
/// <returns>
248+
/// The reconfigured serializer.
249+
/// </returns>
250+
public DateOnlySerializer WithRepresentation(BsonType representation, DateOnlyDocumentFormat documentFormat)
251+
{
252+
if (representation == _representation && documentFormat == _documentFormat)
253+
{
254+
return this;
255+
}
256+
257+
return new DateOnlySerializer(representation, documentFormat);
258+
}
259+
203260
/// <inheritdoc />
204261
public DateOnlySerializer WithRepresentation(BsonType representation)
205262
{
206-
return representation == _representation ? this : new DateOnlySerializer(representation);
263+
return representation == _representation ? this : new DateOnlySerializer(representation, _documentFormat);
207264
}
208265

209266
// explicit interface implementations

0 commit comments

Comments
 (0)