-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-3985: Support multiple SerializerRegistries #1592
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
base: main
Are you sure you want to change the base?
Conversation
This is still a super quick and dirty proof of concept, just to verify we can actually create a custom domain and move it along down the call stack. Of course this broke down several things, and works only in certain cases, but it's a starting point.
(This works similar for `BsonSerializationContext) I'm not trying to say that this is the perfect way, but I just tried to come up with the "most direct" way to go from the collection to the serialization contexts, so it's mostly a proof of concept. Also, I still did not touch the conventions, the discriminators and the class maps, that are also statics and need to be part of the serialization domain. |
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 didn't review every single file. I tried to pick the most relevant ones.
Overall this looks like the right direction.
But... it's going to be a pain to finish and to review.
Now the PR is open for reviews. There are still a couple of open questions, but given the size of this I thought it would be better to start getting feedback the earliest possible. The next section will be both a summary and partially a guide on how to read the PR itself. The aim of this PR is to move away from the global serialization settings we use right now (
The main idea behind the PR is to create a serialization domain, represented by the In order for the domain to be accessible where it is needed, I've added a domain property in multiple classes, among which:
These new properties allow to pass down the domain where it is needed. This also required enriching interfaces with methods or properties that use the domain. For public interface, this was obtained by creating an internal interface that derives from the public one, and that contains the new method that take the domain as input, as you can see with Another thing to note is that I did not remove calls to the global state (mostly to In order to keep the changes private and let As a final note, I've left lots of comments in the code, in order to remember why certain decisions were taken, and a couple of questions left. Those comments have specific tags so they can be found and removed. |
src/MongoDB.Bson/IO/IBsonReader.cs
Outdated
/// <summary> | ||
/// Gets the settings of the reader. | ||
/// </summary> | ||
BsonReaderSettings Settings { 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.
I think it was just an omission.
In any case I don't think we need 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.
Removed
src/MongoDB.Bson/IO/IBsonReader.cs
Outdated
@@ -62,7 +62,7 @@ public interface IBsonReader : IDisposable | |||
/// <summary> | |||
/// Pops the settings. | |||
/// </summary> | |||
void PopSettings(); | |||
void PopSettings(); //TODO Why do we have push and pop methods? They are not used. We should remove 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.
Something for a different PR maybe?
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, it was mostly a reminder. I'll remove 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.
Done
src/MongoDB.Bson/IO/BsonReader.cs
Outdated
@@ -25,7 +25,7 @@ namespace MongoDB.Bson.IO | |||
/// <summary> | |||
/// Represents a BSON reader for some external format (see subclasses). | |||
/// </summary> | |||
public abstract class BsonReader : IBsonReader | |||
public abstract class BsonReader : IBsonReaderInternal |
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.
IBsonReaderInternal not needed.
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
@@ -77,6 +79,16 @@ public BsonReaderSettings FrozenCopy() | |||
} | |||
} | |||
|
|||
internal IBsonSerializationDomain SerializationDomain |
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 should not be a reader setting.
Classes in this directory are low level I/O classes that just do I/O.
Serialization is one level up and is built on TOP of these I/O classes.
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> | ||
/// //TODO | ||
/// </summary> | ||
internal IBsonSerializationDomain SerializationDomain |
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 should not be a writer setting.
Classes in this directory are low level I/O classes that just do I/O.
Serialization is one level up and is built on TOP of these I/O classes.
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
@@ -0,0 +1,317 @@ | |||
using System; |
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.
Add copyright.
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.
Added
{ | ||
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); | ||
} | ||
// Commented out because there is an identical method in Bson assembly (and also in this assembly...). |
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 CSHARP-5632.
The duplication between Bson and Driver was because they were internal.
Be careful if you remove any of the duplication here. I don't think all the duplicate methods are 100% identical and there might be subtle reasons why (but I don't know them if there are).
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. It seems that the method works the same.
{ | ||
var discriminator = discriminatorConvention.GetDiscriminator(nominalType, actualType); | ||
var discriminator = discriminatorConvention.GetDiscriminatorInternal(nominalType, actualType, serializationDomain); |
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's going the VERY difficult and fragile to remember NOT to call GetDiscriminator
and to call GetDiscriminatorInternal
(or whatever we decide to call it) instead...
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 this. One of the things I was thinking, is that maybe we could use something like:
#if DEBUG
[Obsolete("Please use GetDiscriminatorInternal)]
#endif
public void GetDiscriminator() { }
Not necessarily this, but something to remind us not to use this internally (while eventually allowing 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.
Another possibility is to use something like https://www.nuget.org/packages/Microsoft.CodeAnalysis.BannedApiAnalyzers/ that will raise an error while compiling. What do you think?
|
||
namespace MongoDB.Driver.Support | ||
{ | ||
internal static class InternalExtensions |
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 see this in a file called IPipelineStageDefinitionExtensions.cs
.
I also would not mix extension methods for different types in the same file (that didn't happen here though, but it did in some other file).
<Compile Include="..\MongoDB.Shared\SequenceComparer.cs" Link="Shared\SequenceComparer.cs" /> | ||
<Compile Include="..\MongoDB.Shared\Hasher.cs" Link="Shared\Hasher.cs" /> | ||
</ItemGroup> | ||
<!-- The followings have been removed because they are accessed directly through MongoDB.Bson, otherwise there will be a double definition --> |
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.
Did you mean "INDIRECTLY through MongoDB.Bson"?
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.
InternalsVisibleTo
sucks.
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, "indirectly". And yes, but that's the only way to go to keep all the new things internal :)
/// <returns>All registered class maps.</returns> | ||
public IEnumerable<BsonClassMap> GetRegisteredClassMaps() | ||
{ | ||
_serializationDomain.ConfigLock.EnterReadLock(); |
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 need to synchronize such external collection via shared lock?
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 be honest, I did not give much thought to the whole synchronization/locking that we currently have. I didn't want to make even more changes than we currently have so I just tried to keep what we were doing before.
{ | ||
BsonSerializer.ConfigLock.EnterReadLock(); | ||
var configLock = context.SerializationDomain!.ConfigLock; | ||
configLock.EnterReadLock(); |
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.
_frozen
check can be done outside the lock.
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 the way that it's done currently, and I didn't modify it so it's easier to compare it with what we currently have. I'd suggest to not touch it for now.
__dynamicArraySerializerWasSet = true; | ||
__dynamicArraySerializer = value; | ||
} | ||
get => BsonSerializer.DefaultSerializationDomain.BsonDefaults.DynamicArraySerializer; |
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.
Is it possible for this class to be immutable?
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 when we have builders and we'll have only setters.
|
||
namespace MongoDB.Bson | ||
{ | ||
internal class BsonDefaultsDomain : IBsonDefaults |
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.
Is it possible for this class to be immutable?
Also do we need to maintain Defaults
entities under domain, as opposed to just Settings
(which are initialized to defaults)?
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.
Same as before, we can decide to make this immutable when this is created with builders.
Regarding the second point, would you like the class to be renamed to Settings
and initialize those from BsonDefaults
that are kept static?
/// //TODO | ||
/// </summary> | ||
/// <returns></returns> | ||
internal static IBsonSerializationDomain CreateSerializationDomain() => new BsonSerializationDomain(); |
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.
Future cleanup: No need for factory 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.
Yes, I added a comment that it can be removed.
@@ -20,6 +20,7 @@ | |||
|
|||
namespace MongoDB.Bson.Serialization | |||
{ | |||
//DOMAIN-API We should consider making all our serialization provider classes sealed or internal. |
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.
4.0 ticket?
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.
All the comments left with //DOMAIN-API
are related to changes that should/can't be done now and should be addressed later.
|
||
/* QUESTION I removed the part where we set the dynamic serializers from the BsonDefaults, and delay it until we have a serialization domain (when we build the DeserializationContext). | ||
* This is technically changing the public behaviour, but it's in a builder, I do not thing it will affect anyone. Same done for the serialization context. | ||
*/ |
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 there be an observable effect for this change?
When BsonDeserializationContext
instance is returned by Build
, dynamic serializers are set in both cases?
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 actually not necessary anymore since we're passing the domain here, so I've removed it.
@@ -118,7 +118,8 @@ public object DeserializeValue(BsonValue value) | |||
var tempDocument = new BsonDocument("value", value); | |||
using (var reader = new BsonDocumentReader(tempDocument)) | |||
{ | |||
var context = BsonDeserializationContext.CreateRoot(reader); | |||
//QUESTION Is it correct we only need a default domain here? | |||
var context = BsonDeserializationContext.CreateRoot(reader, BsonSerializer.DefaultSerializationDomain); |
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.
In this case, should the relevant domain be passed along with the _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.
In this case my doubt is that if we actually here need what I called a "standard" domain before.
internal class ConventionRegistryDomain : IConventionRegistryDomain | ||
{ | ||
private readonly List<ConventionPackContainer> _conventionPacks = []; | ||
private readonly object _lock = new(); |
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 can be made lock-free with a simple array. Or concurrent dictionary (lock-free for reads).
This is minor as this is not a hot path.
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, I tried to keep the code as close as possible as it was before to have:
- less contentious points
- less things to review given the size of this PR
// Assert.Equal(expectedVal, toString); | ||
// } | ||
|
||
//The first section demonstrates that the class maps are also separated |
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 also need more explicit testing of multiple domains working side by side.
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. The testing at the moment has been minimal to verify the basic functionalities work.
Some notes about the tags used in comments:
|
JIRA ticket