Skip to content

Commit 7fd4cf7

Browse files
papaferstam
andauthored
CSHARP-2096: Make EnumRepresentationConvention also affect collections of Enums (#1574)
Co-authored-by: rstam <robert@robertstam.org>
1 parent dbb7f96 commit 7fd4cf7

File tree

9 files changed

+314
-65
lines changed

9 files changed

+314
-65
lines changed

src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor
6161
/// <returns>A reconfigured serializer.</returns>
6262
protected override IBsonSerializer Apply(IBsonSerializer serializer)
6363
{
64-
var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat));
65-
return reconfiguredSerializer ?? base.Apply(serializer);
64+
return SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure) ?? base.Apply(serializer);
65+
66+
IBsonSerializer Reconfigure(IBsonSerializer s)
67+
=> s is DateOnlySerializer dos ? dos.WithRepresentation(_representation, _documentFormat) : null;
6668
}
6769
}
6870
#endif

src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.Reflection;
1817

1918
namespace MongoDB.Bson.Serialization.Conventions
2019
{
@@ -25,6 +24,7 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention
2524
{
2625
// private fields
2726
private readonly BsonType _representation;
27+
private readonly bool _topLevelOnly;
2828

2929
// constructors
3030
/// <summary>
@@ -33,76 +33,62 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention
3333
/// <param name="representation">The serialization representation. 0 is used to detect representation
3434
/// from the enum itself.</param>
3535
public EnumRepresentationConvention(BsonType representation)
36+
:this(representation, true)
37+
{
38+
}
39+
40+
/// <summary>
41+
/// Initializes a new instance of the <see cref="EnumRepresentationConvention" /> class.
42+
/// </summary>
43+
/// <param name="representation">The serialization representation. 0 is used to detect representation
44+
/// from the enum itself.</param>
45+
/// <param name="topLevelOnly">If set to true, the convention will be applied only to top level enum properties, and not collections of enums, for example.</param>
46+
public EnumRepresentationConvention(BsonType representation, bool topLevelOnly)
3647
{
3748
EnsureRepresentationIsValidForEnums(representation);
3849
_representation = representation;
50+
_topLevelOnly = topLevelOnly;
3951
}
4052

4153
/// <summary>
4254
/// Gets the representation.
4355
/// </summary>
4456
public BsonType Representation => _representation;
4557

58+
/// <summary>
59+
/// Gets a boolean indicating if this convention should be also applied only to the top level enum properties and not to others,
60+
/// collections of enums for example. True by default.
61+
/// </summary>
62+
public bool TopLevelOnly => _topLevelOnly;
63+
4664
/// <summary>
4765
/// Applies a modification to the member map.
4866
/// </summary>
4967
/// <param name="memberMap">The member map.</param>
5068
public void Apply(BsonMemberMap memberMap)
5169
{
52-
var memberType = memberMap.MemberType;
53-
var memberTypeInfo = memberType.GetTypeInfo();
70+
var serializer = memberMap.GetSerializer();
71+
var reconfiguredSerializer = _topLevelOnly && !serializer.ValueType.IsNullableEnum() ?
72+
Reconfigure(serializer) :
73+
SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure);
5474

55-
if (memberTypeInfo.IsEnum)
75+
if (reconfiguredSerializer is not null)
5676
{
57-
var serializer = memberMap.GetSerializer();
58-
var representationConfigurableSerializer = serializer as IRepresentationConfigurable;
59-
if (representationConfigurableSerializer != null)
60-
{
61-
var reconfiguredSerializer = representationConfigurableSerializer.WithRepresentation(_representation);
62-
memberMap.SetSerializer(reconfiguredSerializer);
63-
}
64-
return;
77+
memberMap.SetSerializer(reconfiguredSerializer);
6578
}
6679

67-
if (IsNullableEnum(memberType))
68-
{
69-
var serializer = memberMap.GetSerializer();
70-
var childSerializerConfigurableSerializer = serializer as IChildSerializerConfigurable;
71-
if (childSerializerConfigurableSerializer != null)
72-
{
73-
var childSerializer = childSerializerConfigurableSerializer.ChildSerializer;
74-
var representationConfigurableChildSerializer = childSerializer as IRepresentationConfigurable;
75-
if (representationConfigurableChildSerializer != null)
76-
{
77-
var reconfiguredChildSerializer = representationConfigurableChildSerializer.WithRepresentation(_representation);
78-
var reconfiguredSerializer = childSerializerConfigurableSerializer.WithChildSerializer(reconfiguredChildSerializer);
79-
memberMap.SetSerializer(reconfiguredSerializer);
80-
}
81-
}
82-
return;
83-
}
80+
IBsonSerializer Reconfigure(IBsonSerializer s)
81+
=> s.ValueType.IsEnum ? (s as IRepresentationConfigurable)?.WithRepresentation(_representation) : null;
8482
}
8583

8684
// private methods
87-
private bool IsNullableEnum(Type type)
88-
{
89-
return
90-
type.GetTypeInfo().IsGenericType &&
91-
type.GetGenericTypeDefinition() == typeof(Nullable<>) &&
92-
Nullable.GetUnderlyingType(type).GetTypeInfo().IsEnum;
93-
}
94-
9585
private void EnsureRepresentationIsValidForEnums(BsonType representation)
9686
{
97-
if (
98-
representation == 0 ||
99-
representation == BsonType.String ||
100-
representation == BsonType.Int32 ||
101-
representation == BsonType.Int64)
87+
if (representation is 0 or BsonType.String or BsonType.Int32 or BsonType.Int64)
10288
{
10389
return;
10490
}
105-
throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", "representation");
91+
throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", nameof(representation));
10692
}
10793
}
10894
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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
17+
{
18+
/// <summary>
19+
/// Represents a serializer that has multiple child serializers that configuration attributes can be forwarded to.
20+
/// </summary>
21+
public interface IMultipleChildSerializersConfigurable
22+
{
23+
/// <summary>
24+
/// Gets the child serializers.
25+
/// </summary>
26+
/// <value>
27+
/// The child serializers.
28+
/// </value>
29+
IBsonSerializer[] ChildSerializers { get; }
30+
31+
/// <summary>
32+
/// Returns a serializer that has been reconfigured with the specified child serializers.
33+
/// </summary>
34+
/// <param name="childSerializers">The child serializers.</param>
35+
/// <returns>The reconfigured serializer.</returns>
36+
IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);
37+
}
38+
}

src/MongoDB.Bson/Serialization/SerializerConfigurator.cs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,45 @@
1414
*/
1515

1616
using System;
17+
using System.Collections.Generic;
1718

1819
namespace MongoDB.Bson.Serialization
1920
{
2021
internal static class SerializerConfigurator
2122
{
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)
23+
// Reconfigures a serializer recursively.
24+
// The reconfigure Func should return null if it does not apply to a given serializer.
25+
internal static IBsonSerializer ReconfigureSerializerRecursively(
26+
IBsonSerializer serializer,
27+
Func<IBsonSerializer, IBsonSerializer> reconfigure)
3428
{
3529
switch (serializer)
3630
{
31+
// check IMultipleChildSerializersConfigurableSerializer first because some serializers implement both interfaces
32+
case IMultipleChildSerializersConfigurable multipleChildSerializerConfigurable:
33+
{
34+
var anyChildSerializerWasReconfigured = false;
35+
var reconfiguredChildSerializers = new List<IBsonSerializer>();
36+
37+
foreach (var childSerializer in multipleChildSerializerConfigurable.ChildSerializers)
38+
{
39+
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure);
40+
anyChildSerializerWasReconfigured |= reconfiguredChildSerializer != null;
41+
reconfiguredChildSerializers.Add(reconfiguredChildSerializer ?? childSerializer);
42+
}
43+
44+
return anyChildSerializerWasReconfigured ? multipleChildSerializerConfigurable.WithChildSerializers(reconfiguredChildSerializers.ToArray()) : null;
45+
}
46+
3747
case IChildSerializerConfigurable childSerializerConfigurable:
48+
{
3849
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);
50+
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure);
51+
return reconfiguredChildSerializer != null ? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null;
52+
}
4453

4554
default:
46-
return null;
55+
return reconfigure(serializer);
4756
}
4857
}
4958
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ namespace MongoDB.Bson.Serialization.Serializers
2727
public sealed class DictionaryInterfaceImplementerSerializer<TDictionary> :
2828
DictionarySerializerBase<TDictionary>,
2929
IChildSerializerConfigurable,
30+
IMultipleChildSerializersConfigurable,
3031
IDictionaryRepresentationConfigurable
3132
where TDictionary : class, IDictionary, new()
3233
{
@@ -153,6 +154,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
153154
{
154155
return WithDictionaryRepresentation(dictionaryRepresentation);
155156
}
157+
158+
IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];
159+
160+
IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
161+
{
162+
if (childSerializers.Length != 2)
163+
{
164+
throw new Exception("Wrong number of child serializers passed.");
165+
}
166+
167+
var newKeySerializer = childSerializers[0];
168+
var newValueSerializer = childSerializers[1];
169+
170+
return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
171+
? this
172+
: new DictionaryInterfaceImplementerSerializer<TDictionary>(DictionaryRepresentation, newKeySerializer,
173+
newValueSerializer);
174+
}
156175
}
157176

158177
/// <summary>
@@ -164,6 +183,7 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
164183
public class DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue> :
165184
DictionarySerializerBase<TDictionary, TKey, TValue>,
166185
IChildSerializerConfigurable,
186+
IMultipleChildSerializersConfigurable,
167187
IDictionaryRepresentationConfigurable<DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>>
168188
where TDictionary : class, IDictionary<TKey, TValue>
169189
{
@@ -281,6 +301,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
281301
return WithDictionaryRepresentation(dictionaryRepresentation);
282302
}
283303

304+
IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];
305+
306+
IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
307+
{
308+
if (childSerializers.Length != 2)
309+
{
310+
throw new Exception("Wrong number of child serializers passed.");
311+
}
312+
313+
var newKeySerializer = (IBsonSerializer<TKey>)childSerializers[0];
314+
var newValueSerializer = (IBsonSerializer<TValue>)childSerializers[1];
315+
316+
return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
317+
? this
318+
: new DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>(DictionaryRepresentation, newKeySerializer,
319+
newValueSerializer);
320+
}
321+
284322
/// <inheritdoc/>
285323
protected override ICollection<KeyValuePair<TKey, TValue>> CreateAccumulator()
286324
{

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace MongoDB.Bson.Serialization.Serializers
2828
public sealed class ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue> :
2929
DictionarySerializerBase<TDictionary, TKey, TValue>,
3030
IChildSerializerConfigurable,
31+
IMultipleChildSerializersConfigurable,
3132
IDictionaryRepresentationConfigurable<ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>>
3233
where TDictionary : class, IReadOnlyDictionary<TKey, TValue>
3334
{
@@ -122,6 +123,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
122123
return WithDictionaryRepresentation(dictionaryRepresentation);
123124
}
124125

126+
IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];
127+
128+
IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
129+
{
130+
if (childSerializers.Length != 2)
131+
{
132+
throw new Exception("Wrong number of child serializers passed.");
133+
}
134+
135+
var newKeySerializer = (IBsonSerializer<TKey>)childSerializers[0];
136+
var newValueSerializer = (IBsonSerializer<TValue>)childSerializers[1];
137+
138+
return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
139+
? this
140+
: new ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>(DictionaryRepresentation, newKeySerializer,
141+
newValueSerializer);
142+
}
143+
125144
/// <inheritdoc/>
126145
protected override ICollection<KeyValuePair<TKey, TValue>> CreateAccumulator()
127146
{

src/MongoDB.Bson/Serialization/TypeExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,17 @@ public static bool IsAnonymousType(this Type type)
2929
type.IsGenericType &&
3030
type.Name.Contains("Anon"); // don't check for more than "Anon" so it works in mono also
3131
}
32+
33+
public static bool IsNullable(this Type type)
34+
{
35+
return type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
36+
}
37+
38+
public static bool IsNullableEnum(this Type type)
39+
{
40+
return
41+
type.IsNullable() &&
42+
Nullable.GetUnderlyingType(type).IsEnum;
43+
}
3244
}
3345
}

0 commit comments

Comments
 (0)