-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
CSHARP-2096: Make EnumRepresentationConvention also affect collections of Enums #1574
base: main
Are you sure you want to change the base?
Changes from 9 commits
ec4f48e
91daba7
cdce373
f807853
d7b1739
6fd0000
5378c0c
6b81730
db63623
48c71a9
faf6095
9f94025
e86f3fa
2d11321
5234bdb
1f0041f
38d35f4
0d117d0
c2711c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,6 @@ | |
*/ | ||
|
||
using System; | ||
using System.Reflection; | ||
|
||
namespace MongoDB.Bson.Serialization.Conventions | ||
{ | ||
|
@@ -25,6 +24,7 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention | |
{ | ||
// private fields | ||
private readonly BsonType _representation; | ||
private readonly bool _shouldApplyToCollections; | ||
|
||
// constructors | ||
/// <summary> | ||
|
@@ -33,76 +33,58 @@ 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, false) | ||
{ | ||
} | ||
|
||
/// <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="shouldApplyToCollections">If set to true, the convention will be applied also to collection of enums, recursively.</param> | ||
public EnumRepresentationConvention(BsonType representation, bool shouldApplyToCollections) | ||
{ | ||
EnsureRepresentationIsValidForEnums(representation); | ||
_representation = representation; | ||
_shouldApplyToCollections = shouldApplyToCollections; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the representation. | ||
/// </summary> | ||
public BsonType Representation => _representation; | ||
|
||
/// <summary> | ||
/// Gets a boolean indicating if this convention should be also applied to collections of enums. | ||
/// </summary> | ||
public bool ShouldApplyToCollections => _shouldApplyToCollections; | ||
|
||
/// <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 reconfiguredSerializer = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the other suggestions this method would look like this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that we use There is no need to pass the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My only comment on this is that this way we'll need to have this code copied more or less in the same way wherever we need to reconfigure serializers recursively. If we have a I agree we could remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rstam I think this the last point to agree upon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comments below. |
||
SerializerConfigurator.ReconfigureSerializer<IRepresentationConfigurable>(memberMap.GetSerializer(), | ||
s => s.WithRepresentation(_representation), | ||
s => s.ValueType.IsEnum, _shouldApplyToCollections); | ||
|
||
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; | ||
} | ||
|
||
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; | ||
memberMap.SetSerializer(reconfiguredSerializer); | ||
} | ||
} | ||
|
||
// private methods | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate line There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,31 @@ | ||
/* 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 a key and a value serializer that configuration attributes can be forwarded to. | ||
/// </summary> | ||
public interface IKeyAndValueSerializerConfigurable : IBsonDictionarySerializer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this should be deriving from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think instead of an interface that is hard coded to apply just to dictionaries we should consider the more general solution of adding a new
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good idea! |
||
{ | ||
/// <summary> | ||
/// Returns a serializer that has been reconfigured with the specified key and value serializers. | ||
/// </summary> | ||
/// <param name="keySerializer">The key serializer.</param> | ||
/// <param name="valueSerializer">The value serializer.</param> | ||
/// <returns>The reconfigured serializer.</returns> | ||
IBsonSerializer WithKeyAndValueSerializers(IBsonSerializer keySerializer, IBsonSerializer valueSerializer); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,29 +19,40 @@ 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 using the specified <paramref name="reconfigure"/> method if the result of <paramref name="testFunction"/> is true or the function is null. | ||
/// If the serializer implements <see cref="IChildSerializerConfigurable"/> and either: | ||
/// - is a collection serializer and <paramref name="shouldApplyToCollections"/> is true; | ||
/// - or is a <see cref="Nullable"/> serializer; | ||
/// the method traverses and applies the reconfiguration to its child serializers recursively. | ||
internal static IBsonSerializer ReconfigureSerializer<TSerializer>(IBsonSerializer serializer, Func<TSerializer, IBsonSerializer> reconfigure, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the method should be called |
||
Func<IBsonSerializer, bool> testFunction = null, bool shouldApplyToCollections = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like the addition of But maybe let's get through my other questions first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also not sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to keep |
||
{ | ||
switch (serializer) | ||
{ | ||
case IChildSerializerConfigurable childSerializerConfigurable: | ||
var childSerializer = childSerializerConfigurable.ChildSerializer; | ||
var reconfiguredChildSerializer = ReconfigureSerializer(childSerializer, reconfigure); | ||
return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; | ||
|
||
case TSerializer typedSerializer: | ||
case TSerializer typedSerializer when testFunction?.Invoke(serializer) ?? true: | ||
return reconfigure(typedSerializer); | ||
case IChildSerializerConfigurable childSerializerConfigurable when | ||
(shouldApplyToCollections && childSerializerConfigurable is IBsonArraySerializer) | ||
|| Nullable.GetUnderlyingType(serializer.ValueType) != null: | ||
{ | ||
if (childSerializerConfigurable is IKeyAndValueSerializerConfigurable keyAndValueSerializerConfigurable) | ||
{ | ||
var keySerializer = keyAndValueSerializerConfigurable.KeySerializer; | ||
var valueSerializer = keyAndValueSerializerConfigurable.ValueSerializer; | ||
|
||
var reconfiguredKeySerializer = ReconfigureSerializer(keySerializer, reconfigure, testFunction, | ||
shouldApplyToCollections); | ||
var reconfiguredValueSerializer = ReconfigureSerializer(valueSerializer, reconfigure, testFunction, | ||
shouldApplyToCollections); | ||
|
||
return keyAndValueSerializerConfigurable.WithKeyAndValueSerializers( | ||
reconfiguredKeySerializer ?? keySerializer, reconfiguredValueSerializer ?? valueSerializer); | ||
} | ||
|
||
var childSerializer = childSerializerConfigurable.ChildSerializer; | ||
var reconfiguredChildSerializer = ReconfigureSerializer(childSerializer, reconfigure, testFunction, shouldApplyToCollections); | ||
return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; | ||
} | ||
default: | ||
return null; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this property should be named
ShouldApplyToChildSerializers
because it applies to child serializers in general and not just collections.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a discussion going on slack, but I think it would make sense to keep the name we have now, since we are special casing the
NullableSerializer
that is the onlyIChildSerializerConfigurable
that is not a collection serializer, and makes this backwards compatibleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think this should be generalized to
ShouldApplyToChildSerializers
.For example, while a dictionary clearly has key and value child serializers it's not clear that a dictionary is a collection (depends on how strictly or loosely you want to define a collection).
Has anyone else weighed in on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but we can discuss it during the standup maybe. If we do it though, we need to special case the nullable serializer, otherwise it would be a breaking change.