[CSHARP-3845] Deserialize Select of anonymous types using default values for missing fields Created: 12/Sep/21  Updated: 28/Oct/23  Resolved: 18/Dec/21

Status: Closed
Project: C# Driver
Component/s: None
Affects Version/s: None
Fix Version/s: 2.15.0

Type: Bug Priority: Unknown
Reporter: James Turner Assignee: Robert Stam
Resolution: Fixed Votes: 1
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
is related to CSHARP-3108 Deserialization throws No matching cr... Closed
is related to CSHARP-3175 Regression in v2.10.2 - No matching c... Closed

 Description   

Take this model:

class A
{
	public string _id { get; set; }
	public string ExistingA { get; set; }
	public string ExistingB { get; set; }
	[BsonDefaultValue(null)]
	public string New { get; set; }
}

This data in the collection:

{
	"_id": "1",
	"ExistingA": "Hello World",
	"ExistingB": "Example"
}

And this LINQ query:

collection.AsQueryable()
	.Select(e => new
	{
		e._id,
		e.ExistingA,
		e.New
	})
	.First()

This throws the "No matching creator found" exception because I can't specify default values for properties on anonymous types. I can't specify attributes on it etc. What I'm thinking is that maybe anonymous types should have assumed default values for all properties or otherwise ignored from the creator map processing.

Anonymous types are an important piece of doing succinct LINQ select statements. I shouldn't need to declare a class manually (so I can add `BsonDefaultValue` attribute to the properties) just because the DB has less data than the model.

Unfortunately this doesn't seem to be behaviour I can override on my end. I maintain MongoFramework and I can't really intercept the type and add default values myself. I mean, unless I'm going to write my own extension to LINQ Select, get the anonymous type out and manually register it myself before the driver even sees it. But that approach is likely error prone.

So in summary, can the driver apply default values for all properties of anonymous types (as we can't set them ourselves).



 Comments   
Comment by Githook User [ 18/Dec/21 ]

Author:

{'name': 'rstam', 'email': 'robert@robertstam.org', 'username': 'rstam'}

Message: CSHARP-3845: Deserialize Select of anonymous types using default values for missing fields.
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/08689b613bb69ea0bc6c674e26c24196f1ca929d

Comment by James Turner [ 13/Sep/21 ]

I've found something else - there is a subtle difference how `BsonClassMap` is created depending which part of the code it is created from.

`SerializerBuilder.BuildProjectedSerializer` creates a `BsonClassMap` that doesn't call auto map which doesn't run any of the conventions (including `ImmutableTypeClassMapConvention`) so the properties are never flagged to have default values.

The best I can gather why this is the case is this comment inside the `BuildProjectSerializer` method:

// We are building a serializer specifically for a projected type based
// on serialization information collected from other serializers.
// We cannot cache this in the serializer registry because the compiler reuses 
// the same anonymous type definition in different contexts as long as they 
// are structurally equatable. As such, it might be that two different queries 
// projecting the same shape might need to be deserialized differently.

While I didn't know about that tidbit of the compiler using the same type if its the same shape (though it makes sense to do so), I'm not actually sure it is a problem. If two anonymous types have the same properties by name and by type, why would they deserialize differently? Or more to the point, how could they? We're inside the logic for building a projected serializer for an anonymous type - we know what the property types are - there shouldn't be another way it can be serialized unless I'm missing something.

If I'm right that it isn't an issue, could the `BsonClassMap` generating code here more simply call `BsonClassMap.LookupClassMap(type)` instead?

Then with the serializer, rather than creating the `BsonClassMapSerializer<T>` directly in the `SerializerBuilder.BuildProjectedSerializer`, would it be better to send the request to `BsonSerializer.LookupSerializer(type)` instead? It helps decouple the BSON class map logic from the serializer builder which also opens the door for overriding the behaviour externally in the future.

To completely avoid the behaviour of this hard reference to creating a `BsonClassMapSerializer` dynamically when building the projection serializer, I'd need to process the LINQ query myself. My only alternative now is letting the driver translate the queryable (getting me an `AggregateQueryableExecutionModel` - still creating that the class map and serializer for the anonymous type in the process) to subsequently throw out and create/select my own serializer for the anonymous type before I pass it to the `PipelineDefinition<TInput, TOutput>.Create(stages, serializer)`.

Comment by James Turner [ 12/Sep/21 ]

One thing I noticed while trying to find creative ways to get around this is that what I'm saying in the issue actually is implemented in the code: https://github.com/mongodb/mongo-csharp-driver/blob/b961b81cb7dc1ffe7262c55a227afad0aab5a994/src/MongoDB.Bson/Serialization/Conventions/ImmutableTypeClassMapConvention.cs#L77-L99

That snippet is of the `ImmutableTypeClassMapConvention` and should be applying a default value for properties of anonymous types. I guess then the next question is: Why doesn't it seem to be happening?

Generated at Wed Feb 07 21:46:28 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.