-
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?
Conversation
/// <summary> | ||
/// Gets a boolean indicating if this convention should be also applied to collections of enums. | ||
/// </summary> | ||
public bool ShouldApplyToCollections => _shouldApplyToCollections; |
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 only IChildSerializerConfigurable
that is not a collection serializer, and makes this backwards compatible
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 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.
return null; | ||
} | ||
|
||
// private methods |
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.
Duplicate line
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.
Removed
// private methods | ||
private bool IsNullableEnum(Type type) | ||
{ | ||
return | ||
type.GetTypeInfo().IsGenericType && | ||
type.GetGenericTypeDefinition() == typeof(Nullable<>) && | ||
Nullable.GetUnderlyingType(type).GetTypeInfo().IsEnum; | ||
Nullable.GetUnderlyingType(type)!.GetTypeInfo().IsEnum; |
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.
There is no longer any need to call GetTypeInfo
here (or on line 110). That was a .NET 2.0 thing.
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.
Removed
/// <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 comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this should be deriving from IBsonDictionarySerializer
, or it should be independent from it.
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 instead of an interface that is hard coded to apply just to dictionaries we should consider the more general solution of adding a new IMultipleChildSerializerConfigurableSerializer
that is like IChildSerializerConfigurable
but applies to serializers that have more than one child serializer:
public interface IMultipleChildSerializerConfigurableSerializer
{
IBsonSerializer[] ChildSerializers { get; }
IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);
}
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.
This is a good idea!
Done
/// <summary> | ||
/// Gets a boolean indicating if this convention should be also applied to collections of enums. | ||
/// </summary> | ||
public bool ShouldApplyToCollections => _shouldApplyToCollections; |
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 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?
/// <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 comment
The 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 IMultipleChildSerializerConfigurableSerializer
that is like IChildSerializerConfigurable
but applies to serializers that have more than one child serializer:
public interface IMultipleChildSerializerConfigurableSerializer
{
IBsonSerializer[] ChildSerializers { get; }
IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);
}
/// - 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, | ||
Func<IBsonSerializer, bool> testFunction = null, bool shouldApplyToCollections = true) |
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 don't like the addition of testFunction
. I think the test can be incorporated into the reconfigure
function.
But maybe let's get through my other questions first.
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.
Also not sure shouldApplyToChildSerializer
(or shouldApplyToCollections
) needs to be passed in either. If it is false
this method shouldn't have been called in the first place. If it is true
and this function was called then it always applies to nested children.
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 would prefer to keep shouldApplyTo...
. If we do so, this method becomes a one-stop method for reconfiguring serializers, including when we want to apply reconfigure
to nullable but not child serializers for example. If we keep it out, then we need to keep the code for nullable serializers out, leading to code duplication.
Regarding testFunction
, if we incorporate it into reconfigure
, then reconfigure
would need to return a null value when it does not need to reconfigure the serializer, maybe making the method a little bit less clear. It don't have a super strong opinion on this though
@rstam I've done some changes:
I think the remaining contentious points are about the |
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.
You can see my suggested changes prototyped here:
https://github.com/rstam/mongo-csharp-driver/tree/csharp2096-rstam
/// - 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, | ||
Func<IBsonSerializer, bool> testFunction = null, bool topLevelOnly = false) |
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.
This method has too many knobs. It has 4:
- TSerializer generic type
- reconfigure Func (which might return null)
- testFunction
- topLevelOnly
That's 3 too many.
All we need is the reconfigure
Func
, which centralizes in itself any decisions about whether to reconfigure or not. It either reconfigures the serializer if applicable or returns null.
As far as the rest are concerned:
TSerializer
is redundant because the reconfigure
Func
can decide for itself
testFunction
is redundant because the reconfigure
Func
can decide for itself
topLevelOnly
is redundant because if you only want to configure the top level then don't call this function.
Here's what I think this method should look like:
// 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 serializer implement both interfaces
case IMultipleChildSerializersConfigurableSerializer multipleChildSerializersConfigurable:
{
var newChildSerializers = new List<IBsonSerializer>();
foreach (var childSerializer in multipleChildSerializersConfigurable.ChildSerializers)
{
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure) ?? childSerializer;
newChildSerializers.Add(reconfiguredChildSerializer);
}
return multipleChildSerializersConfigurable.WithChildSerializers(newChildSerializers.ToArray());
}
case IChildSerializerConfigurable childSerializerConfigurable:
{
var childSerializer = childSerializerConfigurable.ChildSerializer;
var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure) ?? childSerializer;
return reconfiguredChildSerializer != null ? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null;
}
default:
return reconfigure(serializer);
}
}
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 still think this method is much simpler and easier to understand if we only have 2 parameters instead of 5.
The only rule the reader needs to understand is that the reconfigure Func should either return a reconfigured serializer or null if it does not apply.
All the decision making about whether to reconfigure or not is centralized in one place (in the caller).
You are concerned about possible duplication of code, which is a valid concern. But if you look at my branch the amount of duplications is close to zero, and in return we only have to look in one place to understand whether and how the serializer is reconfigured.
I've slightly refactored my branch to make the calling code even simpler:
https://github.com/rstam/mongo-csharp-driver/tree/csharp2096-rstam
Look at all the callers of ReconfigureSerializerRecursively
to verify how little duplication there actually is.
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.
Please look at my ReconfigureSerializerRecursively
implementation carefully, in particular the new anyChildSerializerWasReconfigured
variable to return null
if no child was reconfigured.
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 got convinced, I think I was too stubborn on this 😄
Probably it's worth having a little code duplication to make the code clearer.
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.
Just pushed the changes.
@@ -61,7 +61,7 @@ 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)); | |||
var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat), topLevelOnly: false); |
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.
With the other suggestions this method would look like this:
protected override IBsonSerializer Apply(IBsonSerializer serializer)
{
var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure);
return reconfiguredSerializer ?? base.Apply(serializer);
IBsonSerializer Reconfigure(IBsonSerializer serializer) =>
(serializer is DateOnlySerializer dateOnlySerializer) ? dateOnlySerializer.WithRepresentation(_representation, _documentFormat) : null;
}
/// <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 comment
The reason will be displayed to describe this comment to others. Learn more.
With the other suggestions this method would look like this:
public void Apply(BsonMemberMap memberMap)
{
var serializer = memberMap.GetSerializer();
IBsonSerializer reconfiguredSerializer;
if (_topLevelOnly && !serializer.ValueType.IsNullableEnum())
{
reconfiguredSerializer = Reconfigure(serializer);
}
else
{
reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure);
}
if (reconfiguredSerializer is not null)
{
memberMap.SetSerializer(reconfiguredSerializer);
}
IBsonSerializer Reconfigure(IBsonSerializer serializer) =>
(serializer.ValueType.IsEnum && serializer is IRepresentationConfigurable representationConfigurable) ?
representationConfigurable.WithRepresentation(_representation) : 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.
Note that we use _topLevel
locally here to decide whether we want to reconfigure recursively or not (with an exception for nullable enums for backward compatibility).
There is no need to pass the topLevel
argument to ReconfigureSerializerRecursively
.
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.
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 ReconfigureSerializerRecursively
with more parameters we could put all the complexities in one place and we don't need to repeat the same code.
I agree we could remove testFunc
, but if we keep the topLevelOnly
boolean we can keep the reconfiguration code in the method.
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.
@rstam I think this the last point to agree upon.
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.
See comments below.
/// <value> | ||
/// The children serializers. | ||
/// </value> | ||
IBsonSerializer[] ChildrenSerializers { get; } |
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.
This property should be called ChildSerializers
.
It is ungrammatical to say ChildrenSerializers
.
Child
is an adjective here and should not be plural.
Compare "child serializer" to "child seat".
The plural is "child seats", not "children seats".
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.
Yeah, I was unsure about that, I'll correct it!
/// <summary> | ||
/// Represents a serializer that has multiple children serializers that configuration attributes can be forwarded to. | ||
/// </summary> | ||
public interface IMultipleChildrenSerializerConfigurableSerializer |
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.
Class name (and file name) should be:
MultipleChildSerializersConfigurableSerializer
"Child" to "Children"
See below.
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.
Yes
/// </summary> | ||
/// <param name="childrenSerializers">The children serializers.</param> | ||
/// <returns>The reconfigured serializer.</returns> | ||
IBsonSerializer WithChildrenSerializers(IBsonSerializer[] childrenSerializers); |
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.
IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);
/// - 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, | ||
Func<IBsonSerializer, bool> testFunction = null, bool topLevelOnly = false) |
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 still think this method is much simpler and easier to understand if we only have 2 parameters instead of 5.
The only rule the reader needs to understand is that the reconfigure Func should either return a reconfigured serializer or null if it does not apply.
All the decision making about whether to reconfigure or not is centralized in one place (in the caller).
You are concerned about possible duplication of code, which is a valid concern. But if you look at my branch the amount of duplications is close to zero, and in return we only have to look in one place to understand whether and how the serializer is reconfigured.
I've slightly refactored my branch to make the calling code even simpler:
https://github.com/rstam/mongo-csharp-driver/tree/csharp2096-rstam
Look at all the callers of ReconfigureSerializerRecursively
to verify how little duplication there actually is.
/// - <paramref name="topLevelOnly"/> is false; | ||
/// - 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think the method should be called ReconfigureSerializerRecursively
to call attention to the recursive nature of the reconfiguration.
/// - 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, | ||
Func<IBsonSerializer, bool> testFunction = null, bool topLevelOnly = false) |
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.
Please look at my ReconfigureSerializerRecursively
implementation carefully, in particular the new anyChildSerializerWasReconfigured
variable to return null
if no child was reconfigured.
return multipleChildSerializerConfigurable.WithChildSerializers(newSerializers.ToArray()); | ||
} | ||
case IChildSerializerConfigurable childSerializerConfigurable when | ||
!topLevelOnly || Nullable.GetUnderlyingType(serializer.ValueType) != 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.
A nullable enum test doesn't belong here. It only applies to the EnumRepresentationConvention
.
/// <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 comment
The reason will be displayed to describe this comment to others. Learn more.
See comments below.
Co-authored-by: rstam <[email protected]>
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.
Looks good.
I have one question for you before I LGTM. Let me know what you think.
var memberType = memberMap.MemberType; | ||
var memberTypeInfo = memberType.GetTypeInfo(); | ||
var serializer = memberMap.GetSerializer(); | ||
var reconfiguredSerializer = _topLevelOnly && Nullable.GetUnderlyingType(serializer.ValueType) == 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.
Why did you not like:
!serializer.ValueType.IsNullableEnum()
instead of:
Nullable.GetUnderlyingType(serializer.ValueType) == null
The second is longer and it seems suspicious to call Nullable.GetUnderlyingType
without even knowing if the ValueType
is nullable or not.
Yes, after some research I now know Nullable.GetUnderlyingType
returns null
if the ValueType
is not Nullable
but still seems wrong to call a Nullable
method when we don't know if the ValueType
is Nullable
or not. I'm surprised Nullable.GetUnderlyingType
doesn't throw an exception when ValueType
is not Nullable
. I had to research the documentation to figure out what it does in that case.
Calling IsNullableEnum
is not only shorter but it's also self-describing, without having to decipher the obscure behavior of GetUnderlyingType
for non Nullable
types.
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.
Note: IsNullableEnum
is a new extension method added to TypeExtensions.cs
on the branch I shared with you.
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.
Also... isn't the exception for "nullable enums", not for ALL nullable types?
Better to be precise.
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 agree it's better to have it as an extension method, so the intention is clearer. Just pushed it
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.
LGTM
No description provided.