[CSHARP-1328] Property names for dynamic types are not deserialized with PascalCasing Created: 20/Jun/15  Updated: 31/Mar/22

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

Type: Bug Priority: Minor - P4
Reporter: Lenny Granovsky Assignee: Unassigned
Resolution: Unresolved Votes: 1
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: File MongoDB-dynamic test.linq    

 Description   

Serialization with default settings keeps the original property name casing, Pascal in particular. Using camelCasing convention works properly, and ensures the camel casing fields in the DB.

Unfortunately the deserialization leaves the names in the camel casing making the data incompatible with the domain that expects PascalCasing in the code.

The goal is to have camelCasing in the DB and PascalCasing in the code.

Serialization/Deserialization process should be compatible with each other.



 Comments   
Comment by Craig Wilson [ 22/Jun/15 ]

Great. That is exactly what I was expecting to see. Ok, we'll have to figure out how to handle this. Thanks for the report.

Craig

Comment by Lenny Granovsky [ 22/Jun/15 ]

I ended up creating a little test from LinqPad and attached it.

Comment by Lenny Granovsky [ 22/Jun/15 ]

Craig,

Your last comment made me question my code and I re-tested the whole thing. So, I have to apologize for incorrectness. It DOES produce different outcome, but actually the CamelCaseElementNameConvention is NOT what triggers it. It depends on the way the dynamic is declared.

     liability.Details = new ExpandoObject();
     liability.Details.First = "something"; // using PascalCase
     liability.Details.Second = "something else"; // using PascalCase
 
//produces:
{
    "_id" : NUUID("403138c4-59c6-49e7-b892-15f99e812409"),
    "totalAmount" : 0,
    "totalItemsCount" : 0,
    "details" : {
        "First" : "something",
        "Second" : "something else"
    }
}

     liability.Details = new { First = "something", Second = "something else" };
 
//produces:
{
    "_id" : NUUID("a30c87e0-9869-4957-933b-1388ebbd96bf"),
    "totalAmount" : 0,
    "totalItemsCount" : 0,
    "details" : {
        "first" : "something",
        "second" : "something else"
    }
}

Sorry for confusion. I was changing multiple things while testing and it led me to a mistake.

Comment by Lenny Granovsky [ 22/Jun/15 ]

I understand it, but maybe it can be solved via special convention for dynamic type, or via an attribute parameter that can tell deserializer to deserialize all sub-nodes within dynamic type from "camel" to "Pascal". Otherwise it creates integration issues when data is exchanged between multiple systems.

Comment by Craig Wilson [ 22/Jun/15 ]

That's not what I expected to hear, especially with an ExpandoObject() as the type of Details object. I'll have to play with this.

There is a good chance we can't do anything about this. Any convention that applies to element names is run during setup and uses reflection. For dynamic objects, we don't actually know the names used until they are actually used, which is long past when the conventions have been run. I understand your desire, so we'll try and figure something out.

Craig

Comment by Lenny Granovsky [ 22/Jun/15 ]

It will depend on whether or not I use CamelCaseElementNameConvention. The above will be saved to the DB as:

1. WITHOUT CamelCaseElementNameConvention:

{
   details: {
        First: "something",
        Second: "something else"
   }
}

2. WITH CamelCaseElementNameConvention:

{
   details: {
        first: "something",
        second: "something else"
   }
}

Then deserialization ALWAYS uses what's in the DB AS IS.

Is this helpful?

Comment by Craig Wilson [ 22/Jun/15 ]

Thanks Lenny. One more question: When you insert an item from the .NET code, can you confirm whether the dynamic properties are saved in PascalCase or camelCase form . For instance, if you did this, what would the document look like in the database:

var liability = new Liability();
liability.Details = new ExpandoObject();
 
liability.Details.First = "something"; // using PascalCase
liability.Details.Second = "something else"; // using PascalCase
 
await items.InsertOneAsync(liability);

Craig

Comment by Lenny Granovsky [ 22/Jun/15 ]

I simplified for clarity to keep emphases on the issue.

Given the DB has the following document (edited for simplicity):

{
    "_id" : NUUID("00000000-5586-eaf2-8d7d-b62ff8d9b414"),
    "totalAmount" : 897658.46,
    "totalItemsCount" : 563,
    "type" : 30,
    "details" : {
        "ddTotalAmount" : 628360.92,
        "checkTotalAmount" : 269297.54,
        "ddTotalCount" : 563,
        "checkTotalCount" : 563
    }
}

Given that C# code has this class defined:

    [DataContract]
    [BsonIgnoreExtraElements]
    [Serializable]
    public class Liability
    {
        [BsonId]
        [DataMember]
        public Guid LiabilityId { get; set; }
 
        [BsonElement("totalAmount")]
        [BsonRepresentation(BsonType.Double)]
        [DataMember]
        public decimal TotalAmount { get; set; }
 
        [BsonElement("totalItemsCount")]
        [DataMember]
        public int TotalItemsCount { get; set; }
 
        [BsonElement("type")]
        [DataMember]
        public int Type { get; set; } //this is enum in real life, but I changed it for simplicity
 
        [BsonElement("details")]
        [DataMember]
        public dynamic Details { get; set; }
 
        public Liability() 
        {
        }
    }

Then use the following code to retrieve the data:

        MongoClient client = new MongoClient("mongodb://localhost/hub");
        IMongoDatabase mongo = client.GetDatabase("hub");
        IMongoCollection<Liability> items = mongo.GetCollection<Liability>("Liabilities");								
        var data = await items.FindAsync(l => l.LiabilityId == Guid.Parse("00000000-5586-eaf2-8d7d-b62ff8d9b414"));	

or this code:

        var pack = new ConventionPack();
        pack.Add(new CamelCaseElementNameConvention());
        ConventionRegistry.Register("camel case", pack, t => true);	
        MongoClient client = new MongoClient("mongodb://localhost/hub");
        IMongoDatabase mongo = client.GetDatabase("hub");
        IMongoCollection<Liability> items = mongo.GetCollection<Liability>("Liabilities");								
        var data = await items.FindAsync(l => l.LiabilityId == Guid.Parse("00000000-5586-eaf2-8d7d-b62ff8d9b414"));	

The issue is that in both cases the properties within Details object that is declared as dynamic come as camelCasing (the same as DB has it): "ddTotalAmount", "checkTotalAmount", "ddTotalCount", "checkTotalCount". The desired result is pascalCasing. If I save this entity from the code without use of ConventionPack, then all items come to the DB in PascalCasing and being retrieved correctly, but with the Pack, it saves to the DB in camelCasing and being retrieved incorrectly.

The desired result: PascalCasing should be used in the C# code, and camelCasing in the DB. Whether it requires use of ConventionPack or not is not important to me - I mean I can use it if it's required to make it work this way.

Please let me know should I provide more details or any related info.

Thank you.

Comment by Craig Wilson [ 21/Jun/15 ]

Hi Lenny,

Could you provide some code for us? I don't want to make any assumptions about what you're doing.

Craig

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