[CSHARP-3526] Constructor argument with same name as property but different types fails to serialize Created: 03/Apr/21  Updated: 20/Jan/23  Resolved: 10/Nov/22

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

Type: Bug Priority: Major - P3
Reporter: Boris Dogadov Assignee: James Kovacs
Resolution: Won't Fix Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
Epic Link: Improve Serialization

 Description   

 

public class A
 {
  public int[] Data { get; }
  public A(IEnumerable<int> data)
  {
    Data = data.ToArray();
  }
 }
 
 
public class B
{
  public int[] Data { get; }
  public B(IEnumerable<int> data2)
  {
    Data = data2.ToArray();
  }
}

A.ToBson() fails while B.ToBson() works.
Consider same type and name matching in both ImmutableTypeClassMapConvention and NamedParameterCreatorMapConvention

 



 Comments   
Comment by James Kovacs [ 10/Nov/22 ]

See workarounds offered in the previous comments.

Comment by Robert Stam [ 20/Jul/21 ]

`b.ToBson` doesn't really work, at least not in both directions. It doesn't call the constructor during deserialization.

I don't think it's reasonable to expect either one of these classes to be automatically mapped. For mapping to work classes have to follow certain conventions. One of those conventions is that for immutable classes the property names have to match the constructor argument names (case insensitively) and the types have to agree.

`B` violates these conventions because both the names and the types don't agree.

`A` violates these conventions because the types don't agree.

The annotations to get these classes to map correctly aren't "workarounds". They are how you guide the mapper when the conventions aren't being followed.

Comment by Boris Dogadov [ 05/Apr/21 ]

The original example is not accurate. Attaching an updated example:

public class B
 {
  public int[] Data { get; private set; }
  public B(IEnumerable<int> data2)
  {
    Data = data2.ToArray();
  }
 }
 
public class A
 {
  public int[] Data { get; private set; }
  public A(IEnumerable<int> data)
  {
    Data = data.ToArray();
   }
 }

B.ToBson works, A.ToBson throws.
While there are various workaround and knobs to to make A.ToBson work, A.ToBson should work frictionlessly as B.ToBson does.

Comment by Robert Stam [ 05/Apr/21 ]

Both of these classes can be successfully mapped (and therefore serialized and round tripped) if a few annotation attributes are added to instruct the mapper to override the automatic matching algorithms.

Class `A` can be annotated like this:

public class A
{
    public int[] Data { get; }
 
    [BsonConstructor("Data")]
    public A(IEnumerable<int> data)
    {
        Data = data.ToArray();
    }
}

The `[BsonConstructor]` attribute indicates that this constructor should be used (even if this constructor was not automatically selected). The `"Data"` argument to the `[BsonConstructor]` attribute indicates that the `data` constructor parameter should be matched to the `Data` property (even though the types don't match).

Class `B` can be annotated like this:

public class B
{
    [BsonElement]
    public int[] Data { get; }
 
    [BsonConstructor("Data")]
    public B(IEnumerable<int> data2)
    {
        Data = data2.ToArray();
    }
} 

The additional `[BsonElement]` attribute is required because the `Data` property would not be automatically included in the class map because there is no matching constructor parameter (by case insensitive name matching).

Comment by Robert Stam [ 05/Apr/21 ]

This isn't really a bug, but rather examples of classes that don't meet the requirements to be automatically mapped by our class mapping algorithms.

`A` cannot be mapped at all because an exception is thrown during mapping. An exception is thrown because while the `data` constructor parameter can be mapped to the `Data` property (by case insensitive name matching), the types don't match.

No exception is thrown while mapping `B` but the class map is not what you might expect. In this case the constructor is ignored because its parameter cannot be matched to any property. But that doesn't mean the mapping worked. The property is also ignored because it can't be matched to any constructor argument, resulting in an empty class map.

For `B` you would expect the serialized form to be `{ Data : [1, 2, 3] }` but instead the serialized form is `{ }`, which means instance of this class cannot be round tripped to the database without data loss.

It could be argued that instead of an exact match between the type of a constructor parameter and a class property, we should allow some looser form of type matching (in this example you might argue that a constructor argument of type `IEnumerable<int>` is "compatible" with `int[]` for some definition of "compatible"). If we were to proceed down this path extreme care would have to be taken to ensure that we don't accidentally deem mismatched types to be "compatible" when they shouldn't be.

In summary: we require constructor parameters to be matched to properties using case insensitive name matching and to be of the exact same type.

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