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-5375: Remove KnownSerializerRegistry. #1552

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

Conversation

rstam
Copy link
Contributor

@rstam rstam commented Nov 20, 2024

No description provided.

@rstam rstam requested a review from a team as a code owner November 20, 2024 22:23
@@ -32,6 +32,7 @@ internal static class PartialEvaluator
typeof(DateTimeExtensions),
typeof(LinqExtensions),
typeof(MongoEnumerable),
typeof(Mql),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To keep Mql.Constant(...) from being evaluated client-side.

@@ -102,7 +103,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
var funcLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[2]);
var funcParameters = funcLambda.Parameters;
var accumulatorParameter = funcParameters[0];
var accumulatorSerializer = context.KnownSerializersRegistry.GetSerializer(accumulatorParameter);
var accumulatorSerializer = BsonSerializer.LookupSerializer(accumulatorParameter.Type);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might make sense to have a factory for numeric return types to protect ourselves from users registering weird serializers for numeric types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the previous comment is moot. What we should be doing is using the serializer from the seedTranslation as the serializer for the accumulator.

}
else
{
throw new ExpressionNotSupportedException(expression, because: "the registred serializer is not representation configurable");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, contains a typo... will fix in next commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

AggregationExpression ifTrueTranslation;
AggregationExpression ifFalseTranslation;
IBsonSerializer resultSerializer;
if (ifTrueExpression is ConstantExpression ifTrueConstantExpression)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If one result is a constant use the serializer of the other expression to serialize the constant.

@@ -57,7 +57,7 @@ public static AggregationExpression Translate(TranslationContext context, NewExp
var millisecondTranslation = millisecondExpression != null ? ExpressionToAggregationExpressionTranslator.Translate(context, millisecondExpression) : null;

var ast = AstExpression.DateFromParts(yearTranslation.Ast, monthTranslation.Ast, dayTranslation.Ast, hourTranslation?.Ast, minuteTranslation?.Ast, secondTranslation?.Ast, millisecondTranslation?.Ast);
var serializer = context.KnownSerializersRegistry.GetSerializer(expression);
var serializer = DateTimeSerializer.Instance;
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 need to look up anything. We know which serializer we need.

@@ -27,7 +27,7 @@ public static AstFilter Translate(TranslationContext context, ParameterExpressio
{
if (context.SymbolTable.TryGetSymbol(expression, out var symbol))
{
var serializer = context.KnownSerializersRegistry.GetSerializer(expression);
var serializer = symbol.Serializer;
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 need to look up anything. We know which serializer we need.

_translationOptions = translationOptions ?? new ExpressionTranslationOptions();
_data = data; // can be null
}

// public properties
public TranslationContextData Data => _data;
public KnownSerializersRegistry KnownSerializersRegistry => _knownSerializersRegistry;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think of making the serializer registry part of the context so we're not tied to using the global registry?

In the short term it's the same thing, but it looks towards the future where we want to specify different registries for different collections.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this idea. Up to you whether you want to include it in this PR or create a separate one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to postpone doing anything like this. I don't even want to create a ticket for it until we know we need it.

By the way, I've been able to eliminate even more references to the serializer registry in the latest commit.

@@ -71,7 +72,7 @@ public void Predicate_should_use_correct_representation(int i, string projection
(d => new R<E> { N = d.Id, V = d.I1 == E.E1 ? d.S1 : E.E2 }, "{ $project : { N : '$_id', V : { $cond : { if : { $eq : ['$I1', 1] }, then : '$S1', else : 'E2' } }, _id : 0 } }", new[] { E.E1, E.E2 }),
(d => new R<E> { N = d.Id, V = d.I1 == E.E1 ? E.E1 : d.S2 }, "{ $project : { N : '$_id', V : { $cond : { if : { $eq : ['$I1', 1] }, then : 'E1', else : '$S2' } }, _id : 0 } }", new[] { E.E1, E.E2 }),
(d => new R<E> { N = d.Id, V = d.I1 == E.E1 ? d.S1 : d.S2 }, "{ $project : { N : '$_id', V : { $cond : { if : { $eq : ['$I1', 1] }, then : '$S1', else : '$S2' } }, _id : 0 } }", new[] { E.E1, E.E2 }),
(d => new R<E> { N = d.Id, V = d.S1 == E.E1 ? E.E1 : E.E2 }, "{ $project : { N : '$_id', V : { $cond : { if : { $eq : ['$S1', 'E1'] }, then : 'E1', else : 'E2' } }, _id : 0 } }", new[] { E.E1, E.E2 }),
(d => new R<E> { N = d.Id, V = d.S1 == E.E1 ? Mql.Constant(E.E1, BsonType.String) : E.E2 }, "{ $project : { N : '$_id', V : { $cond : { if : { $eq : ['$S1', 'E1'] }, then : 'E1', else : 'E2' } }, _id : 0 } }", new[] { E.E1, E.E2 }),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the scenario we were discussing in Slack.

Since the ternary operator has two constants we now default to using the registered serializer, but using Mql.Constant the user can override that.

Copy link
Contributor

@JamesKovacs JamesKovacs left a comment

Choose a reason for hiding this comment

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

Overall looks great and allows us to remove a difficult-to-reason-about piece of code. One question around the TranslationContext and the now unused serializer parameter.

}
var knownSerializersRegistry = KnownSerializerFinder.FindKnownSerializers(expression, serializer);
return new TranslationContext(symbolTable, nameGenerator, knownSerializersRegistry, translationOptions, data);
return new TranslationContext(symbolTable, nameGenerator, translationOptions, data);
Copy link
Contributor

Choose a reason for hiding this comment

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

serializer is never used. Are we still translating $setWindowFields correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I've removed the unused serializer paramater.

$setWindowFields translators weren't using the KnownSerializerRegisty so they should continue to be translated correctly.

_translationOptions = translationOptions ?? new ExpressionTranslationOptions();
_data = data; // can be null
}

// public properties
public TranslationContextData Data => _data;
public KnownSerializersRegistry KnownSerializersRegistry => _knownSerializersRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this idea. Up to you whether you want to include it in this PR or create a separate one.

/// <returns>The value.</returns>
public static TValue Constant<TValue>(TValue value, IBsonSerializer<TValue> serializer)
{
throw new NotSupportedException("This method is not functional. It is only usable in MongoDB LINQ queries.");
Copy link
Member

Choose a reason for hiding this comment

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

Haven't we moved this to helper method?

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... in a different PR that was not yet pushed to main when this code was written.

I will change this now that that PR has been pushed to main.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In order to do this I'm rebasing the PR on main.

Copy link
Contributor Author

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

Addressed all comments.

Probably needs at least one more careful pass before we decide it's ready to merge (for example the TODO comment).

@@ -102,7 +103,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
var funcLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[2]);
var funcParameters = funcLambda.Parameters;
var accumulatorParameter = funcParameters[0];
var accumulatorSerializer = context.KnownSerializersRegistry.GetSerializer(accumulatorParameter);
var accumulatorSerializer = BsonSerializer.LookupSerializer(accumulatorParameter.Type);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the previous comment is moot. What we should be doing is using the serializer from the seedTranslation as the serializer for the accumulator.

}
else
{
throw new ExpressionNotSupportedException(expression, because: "the registred serializer is not representation configurable");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

_translationOptions = translationOptions ?? new ExpressionTranslationOptions();
_data = data; // can be null
}

// public properties
public TranslationContextData Data => _data;
public KnownSerializersRegistry KnownSerializersRegistry => _knownSerializersRegistry;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to postpone doing anything like this. I don't even want to create a ticket for it until we know we need it.

By the way, I've been able to eliminate even more references to the serializer registry in the latest commit.

/// <returns>The value.</returns>
public static TValue Constant<TValue>(TValue value, IBsonSerializer<TValue> serializer)
{
throw new NotSupportedException("This method is not functional. It is only usable in MongoDB LINQ queries.");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In order to do this I'm rebasing the PR on main.

@@ -64,7 +64,7 @@ private AstStage RenderProjectStage(
out IBsonSerializer<TOutput> outputSerializer)
{
var partiallyEvaluatedOutput = (Expression<Func<TGrouping, TOutput>>)PartialEvaluator.EvaluatePartially(_output);
var context = TranslationContext.Create(partiallyEvaluatedOutput, inputSerializer, translationOptions);
var context = TranslationContext.Create(partiallyEvaluatedOutput, translationOptions);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create method no longer has a serializer parameter.

@@ -130,7 +130,7 @@ public static AggregationExpression Translate(TranslationContext context, Binary
Type t when t == typeof(decimal) => DecimalSerializer.Instance,
Type { IsConstructedGenericType: true } t when t.GetGenericTypeDefinition() == typeof(Nullable<>) => (IBsonSerializer)Activator.CreateInstance(typeof(NullableSerializer<>).MakeGenericType(t.GenericTypeArguments[0])),
Type { IsArray: true } t => (IBsonSerializer)Activator.CreateInstance(typeof(ArraySerializer<>).MakeGenericType(t.GetElementType())),
_ => context.KnownSerializersRegistry.GetSerializer(expression) // Required for Coalesce
_ => BsonSerializer.LookupSerializer(expression.Type) // Required for Coalesce
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: look into this a bit more and confirm whether a lookup is justified.

@@ -102,7 +103,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
var funcLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[2]);
var funcParameters = funcLambda.Parameters;
var accumulatorParameter = funcParameters[0];
var accumulatorSerializer = context.KnownSerializersRegistry.GetSerializer(accumulatorParameter);
var accumulatorSerializer = seedTranslation.Serializer;
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 lookup is needed. We know which serializer to use.

@in: resultSelectorTranslation.Ast);
serializer = context.KnownSerializersRegistry.GetSerializer(resultSelectorLambda);
serializer = resultSelectorTranslation.Serializer;
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 lookup is needed. We know which serializer to use.

var resultSelectorSymbol = context.CreateSymbol(resultSelectorParameter, resultSelectorParameterSerializer);
var resultSelectorContext = context.WithSymbol(resultSelectorSymbol);
var resultSelectorAccumulatorParameter = resultSelectorLambda.Parameters[0];
var resultSelectorAccumulatorSymbol = context.CreateSymbol(resultSelectorAccumulatorParameter, accumulatorSerializer);
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 lookup is needed. We know which serializer to use.

@@ -130,7 +130,7 @@ public static (AstFilterField, AstFilter) Translate(TranslationContext context,
_ => throw new ExpressionNotSupportedException(sourceExpression, because: "OfType method is not supported with the configured discriminator convention")
};

var actualTypeSerializer = context.KnownSerializersRegistry.GetSerializer(sourceExpression);
var actualTypeSerializer = BsonSerializer.LookupSerializer(actualType);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the future we might add some way to ask a serializer which serializer to use for a derived type.

For now a lookup is the only option available.

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM + minor suggestion


if (type.IsNullable(out var valueType) && TryGetSerializer(valueType, out var valueSerializer))
{
serializer = NullableSerializer.Create(valueSerializer);
Copy link
Member

Choose a reason for hiding this comment

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

Does it makes sense to use singletons for Nullable<> in the same way as we have for other standard serializers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made a number of changes to this class based on discussions in Slack.

@rstam rstam requested a review from sanych-sun December 11, 2024 04:01
__standardSerializers.Add(typeof(uint), new UInt32Serializer(representation: BsonType.Int32, strictConverter));
__standardSerializers.Add(typeof(ulong), new UInt64Serializer(representation: BsonType.Int64, strictConverter));

var standardTypes = __standardSerializers.Keys.ToArray(); // call ToArray to make a copy of the current Keys
Copy link
Member

Choose a reason for hiding this comment

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

What if we implement local method RegisterSerializer instead, like:

            void RegisterSerializer(IBsonSerializer serializer)
            {
                __standardSerializers.Add(serializer.ValueType, serializer);

                var nullableSerializer = NullableSerializer.Create(serializer);
                __standardSerializers.Add(nullableSerializer.ValueType, nullableSerializer);

                var arraySerializer = ArraySerializerHelper.CreateSerializer(serializer);
                __standardSerializers.Add(arraySerializer.ValueType, nullableSerializer);

                var arrayOfNullableSerializer = ArraySerializerHelper.CreateSerializer(itemSerializer: nullableSerializer);
                __standardSerializers.Add(arrayOfNullableSerializer.ValueType, arrayOfNullableSerializer);
            }

As far as I understood serializer.ValueType - this is the exact type that serializer will process, so we do not have to construct it here, but can relay on what serializer says.
And then simply call it for each serializer, like:

            RegisterSerializer(new BooleanSerializer(representation: BsonType.Boolean));
            RegisterSerializer(new ByteSerializer(representation: BsonType.Int32));
            RegisterSerializer(new CharSerializer(representation: BsonType.Int32));
            RegisterSerializer(new DecimalSerializer(representation: BsonType.Decimal128, allowTruncationConverter));
            RegisterSerializer(new DoubleSerializer(representation: BsonType.Double, strictConverter));
            RegisterSerializer(new Int16Serializer(representation: BsonType.Int32, strictConverter));
            RegisterSerializer(new Int32Serializer(representation: BsonType.Int32, strictConverter));
            RegisterSerializer(new Int64Serializer(representation: BsonType.Int64, strictConverter));
            RegisterSerializer(new SByteSerializer(representation: BsonType.Int32));
            RegisterSerializer(new SingleSerializer(representation: BsonType.Double, allowTruncationConverter));
            RegisterSerializer(new UInt16Serializer(representation: BsonType.Int32, strictConverter));
            RegisterSerializer(new UInt32Serializer(representation: BsonType.Int32, strictConverter));
            RegisterSerializer(new UInt64Serializer(representation: BsonType.Int64, strictConverter));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like it! Done.

Only difference is I named the local method AddStandardSerializers instead.

@rstam rstam requested a review from sanych-sun December 11, 2024 23:23
Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM, but please check the failed tests.

@rstam rstam requested a review from sanych-sun December 13, 2024 16:45
Copy link
Contributor

@JamesKovacs JamesKovacs left a comment

Choose a reason for hiding this comment

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

LGTM

@rstam rstam requested a review from adelinowona December 19, 2024 16:20
Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM

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.

3 participants