Skip to content
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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

papafe
Copy link
Contributor

@papafe papafe commented Dec 12, 2024

No description provided.

@papafe papafe marked this pull request as ready for review December 12, 2024 16:58
@papafe papafe requested a review from a team as a code owner December 12, 2024 16:58
@papafe papafe requested review from JamesKovacs and rstam and removed request for a team and JamesKovacs December 12, 2024 16:58
/// <summary>
/// Gets a boolean indicating if this convention should be also applied to collections of enums.
/// </summary>
public bool ShouldApplyToCollections => _shouldApplyToCollections;
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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
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 methods
private bool IsNullableEnum(Type type)
{
return
type.GetTypeInfo().IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>) &&
Nullable.GetUnderlyingType(type).GetTypeInfo().IsEnum;
Nullable.GetUnderlyingType(type)!.GetTypeInfo().IsEnum;
Copy link
Contributor

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.

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

/// <summary>
/// Represents a serializer that has a key and a value serializer that configuration attributes can be forwarded to.
/// </summary>
public interface IKeyAndValueSerializerConfigurable : IBsonDictionarySerializer
Copy link
Contributor Author

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.

Copy link
Contributor

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);
}

Copy link
Contributor Author

@papafe papafe Dec 20, 2024

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

@papafe papafe requested a review from rstam December 17, 2024 11:01
/// <summary>
/// Gets a boolean indicating if this convention should be also applied to collections of enums.
/// </summary>
public bool ShouldApplyToCollections => _shouldApplyToCollections;
Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@papafe papafe Dec 20, 2024

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

@papafe
Copy link
Contributor Author

papafe commented Dec 20, 2024

@rstam I've done some changes:

  • changed IKeyAndValue... to IMultipleChildren... so it has a broader use
  • I've modified Reconfigure to work with IChidSerializable.., IMultipleChildrenSerializable... and Nullable serializer (always)
  • I've changed the name to TopLevelOnly. Still open to changing the name, but at least I flipped all the booleans so it should not be a problem.

I think the remaining contentious points are about the Reconfigure method: if it should have the testFunction parameter and if it should have the topLevelOnly boolean, but we can discuss in the threads.

@papafe papafe requested a review from rstam December 20, 2024 13:35
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

/// - 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)
Copy link
Contributor

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:

  1. TSerializer generic type
  2. reconfigure Func (which might return null)
  3. testFunction
  4. 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);                                                                                                      
    }                                                                                                                                            
}                                                                                                                                                

@@ -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);
Copy link
Contributor

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 =
Copy link
Contributor

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;                                
}                                                                                                                 

Copy link
Contributor

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.

/// <value>
/// The children serializers.
/// </value>
IBsonSerializer[] ChildrenSerializers { get; }
Copy link
Contributor

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".

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants