-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-4495: Add conventions and attributes to configure ObjectSerial… #1545
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
|
This is an incredibly quick and dirty proof of concept for a new convention that allows users to define the allowed types for serialization/deserialization in multiple ways:
I have also thought if it would make sense to give users the possibility of setting the allowed types with attributes, but I suppose that most of the times that list of types would be valid for the whole project, not only for certain POCOs. As said, this just a very fast POC, so there's a lot missing, but it's to get some initial feedback. |
rstam
left a comment
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.
Everything looks very reasonable!
| /// <param name="allowedSerializationTypes"></param> | ||
| public AllowedTypesConvention(IEnumerable<Type> allowedDeserializationTypes, IEnumerable<Type> allowedSerializationTypes) | ||
| { | ||
| var allowedDeserializationTypesArray = allowedDeserializationTypes as Type[] ?? allowedDeserializationTypes.ToArray(); |
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.
Interesting optimization...
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.
However... we should probably make a defensive copy anyway because the caller might alter the array they passed in later.
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, we should just make a copy
src/MongoDB.Bson/Serialization/Conventions/AllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
| /// | ||
| /// </summary> | ||
| #pragma warning disable CA1044 | ||
| public bool AllowDefaultFrameworkTypes |
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 idea of giving them a way to opt-in the default framework types.
src/MongoDB.Bson/Serialization/Conventions/AllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/AllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
|
@rstam This is ready for review now. Apart from all your notes, I've also allowed the convention to work with collections and nested collection (as we did with the |
BorisDog
left a comment
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 overall
.../MongoDB.Bson.Tests/Serialization/Conventions/ObjectSerializerAllowedTypesConventionTests.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
| /// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class | ||
| /// that allows all types contained in the calling assembly. | ||
| /// </summary> | ||
| public ObjectSerializerAllowedTypesConvention() : this(Assembly.GetCallingAssembly()) |
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 we should define that the default behavior does not configure any allowed types (except for DefaultFrameworkAllowedTypes). So the configuration needs to be explicit.
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.
Makes sense
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 have removed the automatic add of the types in the calling assembly and I have made AllowDefaultFrameworkTypes default to true, similar to the behaviour of the ObjectSerializer. For this reason now the delegates that are passed to the ObjectSerializer are built lazily.
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Show resolved
Hide resolved
| ? ObjectSerializer.DefaultAllowedTypes | ||
| : t => _allowedDeserializationTypes(t) || ObjectSerializer.DefaultAllowedTypes(t) | ||
| : _allowedDeserializationTypes ?? ObjectSerializer.NoAllowedTypes; | ||
| }); |
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.
Hope that this is readable enough
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 it's fine. minor optimization possible:
_lazyAllowedDeserializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedDeserializationTypes));
_lazyAllowedSerializationTypes= new Lazy<Func<Type, bool>>(() => Construct(_allowedSerializationTypes));
Func<Type, bool> Construct(Func<Type, bool> allowedTypes) => AllowDefaultFrameworkTypes
? (allowedTypes != null ? t => allowedTypes(t) || ObjectSerializer.DefaultAllowedTypes) : ObjectSerializer.DefaultAllowedTypes
: allowedTypes ?? ObjectSerializer.NoAllowedTypes;
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Show resolved
Hide resolved
| ? ObjectSerializer.DefaultAllowedTypes | ||
| : t => _allowedDeserializationTypes(t) || ObjectSerializer.DefaultAllowedTypes(t) | ||
| : _allowedDeserializationTypes ?? ObjectSerializer.NoAllowedTypes; | ||
| }); |
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 it's fine. minor optimization possible:
_lazyAllowedDeserializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedDeserializationTypes));
_lazyAllowedSerializationTypes= new Lazy<Func<Type, bool>>(() => Construct(_allowedSerializationTypes));
Func<Type, bool> Construct(Func<Type, bool> allowedTypes) => AllowDefaultFrameworkTypes
? (allowedTypes != null ? t => allowedTypes(t) || ObjectSerializer.DefaultAllowedTypes) : ObjectSerializer.DefaultAllowedTypes
: allowedTypes ?? ObjectSerializer.NoAllowedTypes;
| /// <summary> | ||
| /// A convention that allows to set the types that can be safely serialized and deserialized with the <see cref="ObjectSerializer"/>. | ||
| /// </summary> | ||
| public sealed class ObjectSerializerAllowedTypesConvention |
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.
Do we want to have convenience presets, like:
ObjectSerializerAllowedTypesConvention.AllowAllTypes
ObjectSerializerAllowedTypesConvention.AllowAllExecutingAssemblyTypes
ObjectSerializerAllowedTypesConvention.DefaultAllowedTypes
...
?
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 that would make sense
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 class needs to implement IMemberMapConvention.
Follow the pattern from other similar classes:
public sealed class ObjectSerializerAllowedTypesConvention : ConventionBase, IMemberMapConvention
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 verify why implementing IMemberMapConvention is required try writing a test that registers this convention and see if it gets used during automapping.
This test will have to be disabled and only run manually in isolation, because registering this convention globally would affect other tests.
.../MongoDB.Bson.Tests/Serialization/Conventions/ObjectSerializerAllowedTypesConventionTests.cs
Outdated
Show resolved
Hide resolved
| /// <summary> | ||
| /// Default <see cref="ObjectSerializerAllowedTypesConvention"/> where all calling assembly types and default framework types are allowed for both serialization and deserialization. | ||
| /// </summary> | ||
| public static ObjectSerializerAllowedTypesConvention AllowAllCallingAssemblyAndDefaultFrameworkTypes => new(Assembly.GetCallingAssembly()); |
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 it makes sense to have both AllowAllCallingAssemblyAndDefaultFrameworkTypes and AllowAllCallingAssemblyTypes or keep only the second one (but then we need to decide if the default framework types are included or not)
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 we need only need AllowAllCallingAssemblyAndDefaultFrameworkTypes
| /// <summary> | ||
| /// Default <see cref="ObjectSerializerAllowedTypesConvention"/> where all calling assembly types and default framework types are allowed for both serialization and deserialization. | ||
| /// </summary> | ||
| public static ObjectSerializerAllowedTypesConvention AllowAllCallingAssemblyAndDefaultFrameworkTypes => new(Assembly.GetCallingAssembly()); |
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 we need only need AllowAllCallingAssemblyAndDefaultFrameworkTypes
| { | ||
| _lazyAllowedDeserializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedDeserializationTypes)); | ||
| _lazyAllowedSerializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedSerializationTypes)); | ||
| return; |
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 need for return
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 mostly to separate "visually" the method body from the local functions. I'm not super sold either way to be honest, so I'll remove it
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
| public ObjectSerializerAllowedTypesConvention() | ||
| { | ||
| _lazyAllowedDeserializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedDeserializationTypes)); | ||
| _lazyAllowedSerializationTypes = new Lazy<Func<Type, bool>>(() => Construct(_allowedSerializationTypes)); |
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.
Are we sure that the values of _allowedDeserialization and _allowedSerializationTypes have already been stored when this code fetches them?
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 Func is built when it's used the first time, so the variables will be set by then
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Bson/Serialization/Conventions/ObjectSerializerAllowedTypesConvention.cs
Outdated
Show resolved
Hide resolved
…izer AllowedTypes
| /// <summary> | ||
| /// A convention that allows to set the types that can be safely serialized and deserialized with the <see cref="ObjectSerializer"/>. | ||
| /// </summary> | ||
| public sealed class ObjectSerializerAllowedTypesConvention |
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 class needs to implement IMemberMapConvention.
Follow the pattern from other similar classes:
public sealed class ObjectSerializerAllowedTypesConvention : ConventionBase, IMemberMapConvention
| /// <summary> | ||
| /// A convention that allows to set the types that can be safely serialized and deserialized with the <see cref="ObjectSerializer"/>. | ||
| /// </summary> | ||
| public sealed class ObjectSerializerAllowedTypesConvention |
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 verify why implementing IMemberMapConvention is required try writing a test that registers this convention and see if it gets used during automapping.
This test will have to be disabled and only run manually in isolation, because registering this convention globally would affect other tests.
| /// A predefined <see cref="ObjectSerializerAllowedTypesConvention"/> where no types are allowed for both serialization and deserialization. | ||
| /// </summary> | ||
| public static ObjectSerializerAllowedTypesConvention AllowNoTypes { get; } = new(ObjectSerializer.NoAllowedTypes) | ||
| { AllowDefaultFrameworkTypes = 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 formatting threw me off. I didn't realize at first that the line was continued on the next line.
Maybe this way:
public static ObjectSerializerAllowedTypesConvention AllowNoTypes { get; } =
new(ObjectSerializer.NoAllowedTypes) { AllowDefaultFrameworkTypes = false };
| private readonly Lazy<Func<Type, bool>> _effectiveAllowedDeserializationTypes; | ||
| private readonly Lazy<Func<Type, bool>> _effectiveAllowedSerializationTypes; | ||
|
|
||
| private readonly bool _allowDefaultFrameworkTypes = 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.
Consider placing line 61 immediately after line 56.
| [Fact] | ||
| public void TestMappingUsesMemberDefaultValueConvention() | ||
| { | ||
| var conventionName = Guid.NewGuid().ToString(); |
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 found these tests that were registering conventions but never unregistering them
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 was not a problem because of t => t == typeof(A)
| } | ||
|
|
||
| [Fact] | ||
| public void Convention_should_be_applied_during_automapping() |
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've added this test to verify that the convention gets registered during automap. I know you've said we should only run it manually, but can't we register the convention and unregister it at the end of the test like here?
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.
As i commented above I'm surprised we even have a method to unregister a convention. You shouldn't be able to alter conventions in the middle of a program run.
We can either unregister it as you discovered (even though I didn't think you should be able to do that), or you can simply program the convention to only apply to the class in this test and just leave it registered forever.
| Assert.IsType<int>(defaultValue); | ||
| Assert.Equal(1, defaultValue); | ||
|
|
||
| ConventionRegistry.Remove(conventionName); |
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.
Wow I am surprised the Remove method even exists.
A general principle of our whole serialization configuration machinery is that you shouldn't be able to change how things are serialized in the middle of the program run.
Though I guess this Remove method is somewhat less harmful because it doesn't change how anything that is already configured is serialized, only how future classes that have not yet been configured will end up being configured after the convention has been removed.
Note also that this convention does not HAVE to be unregistered. It is already programmed to ONLY apply to this one class with t => t == typeof(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.
I understand we could have a convention that applies to a certain type and leave it registered, and while this likely does not affect anything else, shouldn’t we aim to keep the tests as independent as possible?
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, tests should be as independent as possible. That's why we should use t => t == typeof(A) instead of t => true here.
It's fine if you want to unregister this convention at the end of the test. I just wanted to make the following points:
- The test was not inherently wrong because the registered convention only applied to this test
- I was surprised we even had a
Removemethod
The Remove method concerns me because the way serialization is supposed to work is that an application configures serialization at startup and then leaves it alone. Removing a convention at some later time sounds like it would destabilize the application because POCOs might be serialized differently depending on whether the POCO is mapped before or after the convention was removed.
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 all the points :)
I modified the predicate to be more restrictive, but kept the Remove to have a clean state after each test. I can agree also that maybe that method should not be there, but for now I'd say it's better to use it
| } | ||
|
|
||
| [Fact] | ||
| public void Convention_should_be_applied_during_automapping() |
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.
As i commented above I'm surprised we even have a method to unregister a convention. You shouldn't be able to alter conventions in the middle of a program run.
We can either unregister it as you discovered (even though I didn't think you should be able to do that), or you can simply program the convention to only apply to the class in this test and just leave it registered forever.
| var conventionName = Guid.NewGuid().ToString(); | ||
|
|
||
| var subject = new ObjectSerializerAllowedTypesConvention(Assembly.GetExecutingAssembly()); | ||
| ConventionRegistry.Register(conventionName, new ConventionPack {subject}, _ => 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.
Can't we use something more restrictive that t => true?
If we think about a time in the future where we want to run our tests in parallel relying on unregistering will no longer work.
| [Fact] | ||
| public void TestMappingUsesMemberDefaultValueConvention() | ||
| { | ||
| var conventionName = Guid.NewGuid().ToString(); |
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 was not a problem because of t => t == typeof(A)
rstam
left a comment
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
…izer AllowedTypes