-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-4475: Add an AllowedTypes filter to ObjectSerializer. #1008
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
Conversation
src/MongoDB.Bson/Serialization/Serializers/DiscriminatedInterfaceSerializer.cs
Show resolved
Hide resolved
static CSharp1559Tests() | ||
{ | ||
TestObjectSerializerRegisterer.EnsureTestObjectSerializerIsRegistered(); | ||
} |
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 static constructor here and similarly in many classes below ensures that the correct ObjectSerializer is registered before running these tests.
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 like 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.
Turns out that if you don't actually need to use the fixture in your tests you can just omit this constructor.
We're only using the collection fixture for its side effect of registering the object serializer.
I'm going to remove all these constructors.
cm.MapProperty(t => t.ROD).SetSerializer(readOnlyDictionarySerializer); | ||
cm.MapProperty(t => t.SD).SetSerializer(sortedDictionarySerializer); | ||
cm.MapProperty(t => t.SL).SetSerializer(sortedListSerializer); | ||
}); |
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.
The problem here is that while line 43 ensures that the correct ObjectSerializer is registered, line 43 doesn't help with any Dictionary (and similar) serializers that might already be registered and that have cached the wrong ObjectSerializer (as the key and value serializers) before line 43 was executed.
The workaround is to manually configure a class map for T
that has all the correct serializers regardless of what may or may not already be in the registry.
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.
Could do this whole setup in RegisterObjectSerializerFixture
too?
Replace dictionarySerializer
and the rest for all?
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.
Originally this was done here because this was the only test affected by this.
But it does make sense to do it RegisterObjectSerializerFixture
.
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 a problem though... we can't move the class map registration to the fixture.
I'm going to leave this alone for now. Think about it a bit more and let me know if you want to pursue it further.
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.
Maybe we could just register serializers for Dictionary<>
, IDictionary<>
like we do with ObjectSerializer
and skip the mapping?
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.
Reminder.
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 could register those serializers also in the fixture, but... they only apply to this one test. Note also, that they are not registered... only used in this one test.
Let's leave this alone for now.
When we add new conventions and attributes we can remove the code that creates these serializers and maps the class in code and let the conventions/attributes take care of mapping correctly.
cm.MapProperty(t => t.SL).SetSerializer(sortedListDictionarySerializer); | ||
}); | ||
} | ||
|
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.
Similar issue as above.
tests/MongoDB.Bson.Tests/Serialization/Serializers/ObjectSerializerTests.cs
Show resolved
Hide resolved
tests/MongoDB.Bson.Tests/Serialization/Serializers/ObjectSerializerTests.cs
Show resolved
Hide resolved
|
||
static bool IsAllowedGenericType(Type type) => | ||
__allowedGenericTypes.Contains(type.GetGenericTypeDefinition()) && | ||
type.GetGenericArguments().All(a => AllowedTypes(a)); |
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.
type.GetGenericArguments().All(AllowedTypes)
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.
Good optimization. Done.
{ | ||
if (type == null) | ||
{ | ||
throw new ArgumentNullException("type"); |
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.
nameof(...)
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.
Done.
For the record this was just copy pasted from existing code.
var existingSerializer = _cache[type]; | ||
if (!existingSerializer.Equals(serializer)) | ||
{ | ||
var message = string.Format("There is already a different serializer registered for type {0}.", BsonUtils.GetFriendlyTypeName(type)); |
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.
Maybe we can use string interpolation here and elsewhere?
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.
Sure, I like string interpolation a lot.
This was just copy/pasted from existing code and adapted.
I will change it to string interpolation here in this new code, but I'm reluctant to change existing code.
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.
Done
static CSharp1559Tests() | ||
{ | ||
TestObjectSerializerRegisterer.EnsureTestObjectSerializerIsRegistered(); | ||
} |
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 like the _
|
||
namespace MongoDB.Bson.Tests | ||
{ | ||
[CollectionDefinition("RegisterObjectSerializer")] |
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 recommend defining a const for "RegisterObjectSerializer".
Maybe RegisterObjectSerializerFixture.CollectionName
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.
Done
cm.MapProperty(t => t.ROD).SetSerializer(readOnlyDictionarySerializer); | ||
cm.MapProperty(t => t.SD).SetSerializer(sortedDictionarySerializer); | ||
cm.MapProperty(t => t.SL).SetSerializer(sortedListSerializer); | ||
}); |
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.
Could do this whole setup in RegisterObjectSerializerFixture
too?
Replace dictionarySerializer
and the rest for all?
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
cm.MapProperty(t => t.ROD).SetSerializer(readOnlyDictionarySerializer); | ||
cm.MapProperty(t => t.SD).SetSerializer(sortedDictionarySerializer); | ||
cm.MapProperty(t => t.SL).SetSerializer(sortedListSerializer); | ||
}); |
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.
Reminder.
@@ -165,6 +217,20 @@ public override object Deserialize(BsonDeserializationContext context, BsonDeser | |||
} | |||
} | |||
|
|||
/// <inheritdoc/> | |||
public override bool Equals(object obj) => | |||
obj is ObjectSerializer other && |
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.
For non-sealed classes obj is ObjectSerializer
is not a good enough test because obj
might be a subclass of ObjectSerializer
in which case we want to return false
.
Because ObjectSerializer
is sealed
we know that if obj is an ObjectSerializer
it can't be a subclass of ObjectSerializer
.
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 could add simple type as alternative to sealed (might be safer in terms of BC).
obj is ObjectSerializer other &&
this.GetType() == other.GetType()
But I would not worry about this. The choice on whether override Equals
method or not should be of the derived type.
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.
To avoid the sealed
breaking change, would the following not be sufficient?
obj is ObjectSerializer other &&
GetType() == obj.GetType() &&
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 implementing Equals in a class hierarchy is non-trivial. If a derived class fails to override Equals then Equals is broken. And if a derived class does override Equals (as it must, I don't think it's an option), then does it leverage the base class by calling base.Equals
? But for that to work the base Equals must be implemented to allow subclasses to call it and not return false unintentionally.
I'm fine with adding the extra check... and will do so, but we should note that implementing Equals for sealed class allows some assumptions that simplify the implementation.
/// <inheritdoc/> | ||
public override int GetHashCode() => | ||
new Hasher() | ||
.Hash(_discriminatorConvention) |
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.
Deliberately left _alowedTypes
out of the computation of the hash code because we don't know anything about the behavior of GetHashCode
for delegates (and it's not worth finding out).
Technically public override int GetHashCode() => 0
would be correct also if we don't anticipate serializers being put into a hash table (and even if they are 0
would just be a small performance hit).
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 with your remark that GetHashCode() => 0
would work, as this class is not designed to be used for lookup in hash tables.
Would not => base.GetHashCode()
be sufficient then?
The default implementation calculates the hash based on reference, so I would recommend to offload this to .net framework.
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.
Would not => base.GetHashCode() be sufficient then?
I think relying on the base implementation of GetHashCode
would be a bug.
If Equals returns true for x
and y
then x.GetHashCode()
MUST EQUAL y.GetHashCode()
. Calling base.GetHashCode()
violates that.
A hash code of 0
is never "wrong", but it also isn't very efficient in cases where a random distribution of hashcodes would be better.
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.
If Equals returns true for x and y then x.GetHashCode() MUST EQUAL y.GetHashCode(). Calling base.GetHashCode() violates that.
I think this only applies to cases where Hashcode
is actually used, which is lookups. But I agree that it's a "bon ton" not to violate this in any case.
I think 0 is a good option, to state that "we don't care", otherwise using Hasher
leads to wrong impression that this class is designed and optimized to be used for lookups.
|
||
namespace MongoDB.Bson.Serialization.Serializers | ||
{ | ||
/// <summary> | ||
/// Represents a serializer for objects. | ||
/// </summary> | ||
public class ObjectSerializer : ClassSerializerBase<object> | ||
public sealed class ObjectSerializer : ClassSerializerBase<object> |
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.
Changed to sealed
in part to simplify the implementation of Equals
.
Any concern that this could be considered a breaking change?
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.
It will break anyone with a IBsonSerializer<T>
deriving from ObjectSerializer
. A quick google for mongodb objectserializer
returns our API docs and a few SO questions, but no one recommending deriving a new serializer from ObjectSerializer
. It's probably not common, but we can't know for certain.
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 can avoid this by checking other.GetType()
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. But if this class is sealed we don't need to do that.
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.
Removing sealed to avoid possible though unlikely breaking change.
@@ -165,6 +217,20 @@ public override object Deserialize(BsonDeserializationContext context, BsonDeser | |||
} | |||
} | |||
|
|||
/// <inheritdoc/> |
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 another ticket to implement Equals
for all serializers.
In this PR I'm only implementing Equals
for ObjectSerializer
because it is required for the tests of TryRegisterSerializer
to pass.
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.
Agreed.
obj is ObjectSerializer other && | ||
_allowedTypes.Equals(other._allowedTypes) && | ||
_discriminatorConvention.Equals(other._discriminatorConvention) && | ||
_guidRepresentation == other._guidRepresentation; |
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 firmly believe Equals
should be implemented by calling Equals
on fields in order to preserve the semantics of Equals
(without being forced to analyze whether ==
is semantically equivalent to Equals
or not on a case by case basis).
But... in the case of enum
fields calling Equals
would involve boxing. It would still be correct, but there would be a small performance hit.
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.
While I suspect that no one has subclassed ObjectSerializer
there is no way to know for sure. See my suggested change to Equals
avoids the need for this breaking change.
|
||
namespace MongoDB.Bson.Serialization.Serializers | ||
{ | ||
/// <summary> | ||
/// Represents a serializer for objects. | ||
/// </summary> | ||
public class ObjectSerializer : ClassSerializerBase<object> | ||
public sealed class ObjectSerializer : ClassSerializerBase<object> |
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.
It will break anyone with a IBsonSerializer<T>
deriving from ObjectSerializer
. A quick google for mongodb objectserializer
returns our API docs and a few SO questions, but no one recommending deriving a new serializer from ObjectSerializer
. It's probably not common, but we can't know for certain.
@@ -165,6 +217,20 @@ public override object Deserialize(BsonDeserializationContext context, BsonDeser | |||
} | |||
} | |||
|
|||
/// <inheritdoc/> |
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.
Agreed.
@@ -165,6 +217,20 @@ public override object Deserialize(BsonDeserializationContext context, BsonDeser | |||
} | |||
} | |||
|
|||
/// <inheritdoc/> | |||
public override bool Equals(object obj) => | |||
obj is ObjectSerializer other && |
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.
To avoid the sealed
breaking change, would the following not be sufficient?
obj is ObjectSerializer other &&
GetType() == obj.GetType() &&
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
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.
Comment re. GetHashcode
for your consideration.
No description provided.