Skip to content

CSHARP-2096: Make EnumRepresentationConvention also affect collections of Enums #1574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor
/// <returns>A reconfigured serializer.</returns>
protected override IBsonSerializer Apply(IBsonSerializer serializer)
{
var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat));
return reconfiguredSerializer ?? base.Apply(serializer);
return SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure) ?? base.Apply(serializer);

IBsonSerializer Reconfigure(IBsonSerializer s)
=> s is DateOnlySerializer dos ? dos.WithRepresentation(_representation, _documentFormat) : null;
}
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
*/

using System;
using System.Reflection;

namespace MongoDB.Bson.Serialization.Conventions
{
Expand All @@ -25,6 +24,7 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention
{
// private fields
private readonly BsonType _representation;
private readonly bool _topLevelOnly;

// constructors
/// <summary>
Expand All @@ -33,76 +33,62 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention
/// <param name="representation">The serialization representation. 0 is used to detect representation
/// from the enum itself.</param>
public EnumRepresentationConvention(BsonType representation)
:this(representation, true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="EnumRepresentationConvention" /> class.
/// </summary>
/// <param name="representation">The serialization representation. 0 is used to detect representation
/// from the enum itself.</param>
/// <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>
public EnumRepresentationConvention(BsonType representation, bool topLevelOnly)
{
EnsureRepresentationIsValidForEnums(representation);
_representation = representation;
_topLevelOnly = topLevelOnly;
}

/// <summary>
/// Gets the representation.
/// </summary>
public BsonType Representation => _representation;

/// <summary>
/// Gets a boolean indicating if this convention should be also applied only to the top level enum properties and not to others,
/// collections of enums for example. True by default.
/// </summary>
public bool TopLevelOnly => _topLevelOnly;

/// <summary>
/// Applies a modification to the member map.
/// </summary>
/// <param name="memberMap">The member map.</param>
public void Apply(BsonMemberMap memberMap)
{
var memberType = memberMap.MemberType;
var memberTypeInfo = memberType.GetTypeInfo();
var serializer = memberMap.GetSerializer();
var reconfiguredSerializer = _topLevelOnly && !serializer.ValueType.IsNullableEnum() ?
Reconfigure(serializer) :
SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure);

if (memberTypeInfo.IsEnum)
if (reconfiguredSerializer is not null)
{
var serializer = memberMap.GetSerializer();
var representationConfigurableSerializer = serializer as IRepresentationConfigurable;
if (representationConfigurableSerializer != null)
{
var reconfiguredSerializer = representationConfigurableSerializer.WithRepresentation(_representation);
memberMap.SetSerializer(reconfiguredSerializer);
}
return;
memberMap.SetSerializer(reconfiguredSerializer);
}

if (IsNullableEnum(memberType))
{
var serializer = memberMap.GetSerializer();
var childSerializerConfigurableSerializer = serializer as IChildSerializerConfigurable;
if (childSerializerConfigurableSerializer != null)
{
var childSerializer = childSerializerConfigurableSerializer.ChildSerializer;
var representationConfigurableChildSerializer = childSerializer as IRepresentationConfigurable;
if (representationConfigurableChildSerializer != null)
{
var reconfiguredChildSerializer = representationConfigurableChildSerializer.WithRepresentation(_representation);
var reconfiguredSerializer = childSerializerConfigurableSerializer.WithChildSerializer(reconfiguredChildSerializer);
memberMap.SetSerializer(reconfiguredSerializer);
}
}
return;
}
IBsonSerializer Reconfigure(IBsonSerializer s)
=> s.ValueType.IsEnum ? (s as IRepresentationConfigurable)?.WithRepresentation(_representation) : null;
}

// private methods
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

private bool IsNullableEnum(Type type)
{
return
type.GetTypeInfo().IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>) &&
Nullable.GetUnderlyingType(type).GetTypeInfo().IsEnum;
}

private void EnsureRepresentationIsValidForEnums(BsonType representation)
{
if (
representation == 0 ||
representation == BsonType.String ||
representation == BsonType.Int32 ||
representation == BsonType.Int64)
if (representation is 0 or BsonType.String or BsonType.Int32 or BsonType.Int64)
{
return;
}
throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", "representation");
throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", nameof(representation));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace MongoDB.Bson.Serialization
{
/// <summary>
/// Represents a serializer that has multiple child serializers that configuration attributes can be forwarded to.
/// </summary>
public interface IMultipleChildSerializersConfigurable
{
/// <summary>
/// Gets the child serializers.
/// </summary>
/// <value>
/// The child serializers.
/// </value>
IBsonSerializer[] ChildSerializers { get; }

/// <summary>
/// Returns a serializer that has been reconfigured with the specified child serializers.
/// </summary>
/// <param name="childSerializers">The child serializers.</param>
/// <returns>The reconfigured serializer.</returns>
IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);
}
}
45 changes: 27 additions & 18 deletions src/MongoDB.Bson/Serialization/SerializerConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,45 @@
*/

using System;
using System.Collections.Generic;

namespace MongoDB.Bson.Serialization
{
internal static class SerializerConfigurator
{
/// <summary>
/// Reconfigures a serializer using the specified <paramref name="reconfigure"/> method.
/// If the serializer implements <see cref="IChildSerializerConfigurable"/>,
/// the method traverses and applies the reconfiguration to its child serializers recursively until an appropriate leaf serializer is found.
/// </summary>
/// <param name="serializer">The input serializer to be reconfigured.</param>
/// <param name="reconfigure">A function that defines how the serializer of type <typeparamref name="TSerializer"/> should be reconfigured.</param>
/// <typeparam name="TSerializer">The input type for the reconfigure method.</typeparam>
/// <returns>
/// The reconfigured serializer, or <c>null</c> if no leaf serializer could be reconfigured.
/// </returns>
internal static IBsonSerializer ReconfigureSerializer<TSerializer>(IBsonSerializer serializer, Func<TSerializer, IBsonSerializer> reconfigure)
// Reconfigures a serializer recursively.
// The reconfigure Func should return null if it does not apply to a given serializer.
internal static IBsonSerializer ReconfigureSerializerRecursively(
IBsonSerializer serializer,
Func<IBsonSerializer, IBsonSerializer> reconfigure)
{
switch (serializer)
{
// check IMultipleChildSerializersConfigurableSerializer first because some serializers implement both interfaces
case IMultipleChildSerializersConfigurable multipleChildSerializerConfigurable:
{
var anyChildSerializerWasReconfigured = false;
var reconfiguredChildSerializers = new List<IBsonSerializer>();

foreach (var childSerializer in multipleChildSerializerConfigurable.ChildSerializers)
{
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure);
anyChildSerializerWasReconfigured |= reconfiguredChildSerializer != null;
reconfiguredChildSerializers.Add(reconfiguredChildSerializer ?? childSerializer);
}

return anyChildSerializerWasReconfigured ? multipleChildSerializerConfigurable.WithChildSerializers(reconfiguredChildSerializers.ToArray()) : null;
}

case IChildSerializerConfigurable childSerializerConfigurable:
{
var childSerializer = childSerializerConfigurable.ChildSerializer;
var reconfiguredChildSerializer = ReconfigureSerializer(childSerializer, reconfigure);
return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null;

case TSerializer typedSerializer:
return reconfigure(typedSerializer);
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure);
return reconfiguredChildSerializer != null ? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null;
}

default:
return null;
return reconfigure(serializer);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace MongoDB.Bson.Serialization.Serializers
public sealed class DictionaryInterfaceImplementerSerializer<TDictionary> :
DictionarySerializerBase<TDictionary>,
IChildSerializerConfigurable,
IMultipleChildSerializersConfigurable,
IDictionaryRepresentationConfigurable
where TDictionary : class, IDictionary, new()
{
Expand Down Expand Up @@ -153,6 +154,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
{
return WithDictionaryRepresentation(dictionaryRepresentation);
}

IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];

IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
{
if (childSerializers.Length != 2)
{
throw new Exception("Wrong number of child serializers passed.");
}

var newKeySerializer = childSerializers[0];
var newValueSerializer = childSerializers[1];

return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
? this
: new DictionaryInterfaceImplementerSerializer<TDictionary>(DictionaryRepresentation, newKeySerializer,
newValueSerializer);
}
}

/// <summary>
Expand All @@ -164,6 +183,7 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
public class DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue> :
DictionarySerializerBase<TDictionary, TKey, TValue>,
IChildSerializerConfigurable,
IMultipleChildSerializersConfigurable,
IDictionaryRepresentationConfigurable<DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>>
where TDictionary : class, IDictionary<TKey, TValue>
{
Expand Down Expand Up @@ -281,6 +301,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
return WithDictionaryRepresentation(dictionaryRepresentation);
}

IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];

IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
{
if (childSerializers.Length != 2)
{
throw new Exception("Wrong number of child serializers passed.");
}

var newKeySerializer = (IBsonSerializer<TKey>)childSerializers[0];
var newValueSerializer = (IBsonSerializer<TValue>)childSerializers[1];

return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
? this
: new DictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>(DictionaryRepresentation, newKeySerializer,
newValueSerializer);
}

/// <inheritdoc/>
protected override ICollection<KeyValuePair<TKey, TValue>> CreateAccumulator()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace MongoDB.Bson.Serialization.Serializers
public sealed class ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue> :
DictionarySerializerBase<TDictionary, TKey, TValue>,
IChildSerializerConfigurable,
IMultipleChildSerializersConfigurable,
IDictionaryRepresentationConfigurable<ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>>
where TDictionary : class, IReadOnlyDictionary<TKey, TValue>
{
Expand Down Expand Up @@ -122,6 +123,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati
return WithDictionaryRepresentation(dictionaryRepresentation);
}

IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer];

IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers)
{
if (childSerializers.Length != 2)
{
throw new Exception("Wrong number of child serializers passed.");
}

var newKeySerializer = (IBsonSerializer<TKey>)childSerializers[0];
var newValueSerializer = (IBsonSerializer<TValue>)childSerializers[1];

return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer)
? this
: new ReadOnlyDictionaryInterfaceImplementerSerializer<TDictionary, TKey, TValue>(DictionaryRepresentation, newKeySerializer,
newValueSerializer);
}

/// <inheritdoc/>
protected override ICollection<KeyValuePair<TKey, TValue>> CreateAccumulator()
{
Expand Down
12 changes: 12 additions & 0 deletions src/MongoDB.Bson/Serialization/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,17 @@ public static bool IsAnonymousType(this Type type)
type.IsGenericType &&
type.Name.Contains("Anon"); // don't check for more than "Anon" so it works in mono also
}

public static bool IsNullable(this Type type)
{
return type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}

public static bool IsNullableEnum(this Type type)
{
return
type.IsNullable() &&
Nullable.GetUnderlyingType(type).IsEnum;
}
}
}
Loading