Click here to Skip to main content
Click here to Skip to main content
Go to top

SignalR .NET deserialization of objects with private setters / member variables

, 27 Aug 2014
Rate this:
Please Sign up or sign in to vote.
If you use deserialize objects with private setters in a .NET SignalR client you may find those member variables are still set to default.

Introduction

The other day my boss asked me to look into an issue he was having when creating a .NET client for a SignalR publishing server. Some of the member variables in the deserialized message classes were still at their default values whereas others were at their serialized values. Not only did these classes have private setters for their properties and member variables (perfectly reasonable for most classes if not DTOs) they also did not have a default constructor and the provided constructor only initialised some of the variables, with the rest of the variables set when creating from a static factory Create method. That's legacy code for you ...

This is a known issue

I'm definitely not the first person to run into this, as this feature request demonstrates. It has been accepted onto the backlog for v3, but until that comes out you'll have to work around it (see below).

Json.NET

The JSON serialization in SignalR is done using the excellent Json.NET library. Nothing in this tip/article should be seen as a criticism of Json.NET (or SignalR for that matter); without a method of tagging the classes you're going to send via SignalR (like for a DataContractSerializer) then I don't see how anything could reliably deserialize the TestMessageObject I'll talk about later. The proposed feature/fix mentioned above is to allow injection of your own serialization into SignalR rather than any complaints about Json.NET. Also note that the version of Json.NET that ships with the SignalR demos is 5.0.1 (and so that's what I've looked at); the latest version may well behave differently.

This tip is more of a "hey, you might want to watch out for this" and not a "hey, this thing is rubbish and I demand the moon-on-a-stick".

Testing

The issue can be seen if you create a .NET client for the SignalR stock ticker demo (available as the Microsoft.AspNet.SignalR.Sample NuGet package). The code for a simple C# console client is attached as a zip of a VS2013 Express for Desktop solution: to use just enable NuGet Restore, restore the packages, build and then run after the stock ticker demo project is running.

The Stock class sent to clients has private setters for all the properties that are calculated when setting the Price property (e.g. DayOpen):

public class Stock
{
    private decimal _price;

    public string Symbol { get; set; }
    public decimal DayOpen { get; private set; }
    public decimal DayLow { get; private set; }
    public decimal DayHigh { get; private set; }
    public decimal LastChange { get; private set; }
    public decimal Change { get { return Price - DayOpen; } }
    public double PercentChange
    {
        get { return (double)Math.Round(Change / Price, 4); }
    }

    public decimal Price
    {
        get
        {
            return _price;
        }
        set
        {
            if (_price == value)
            {
                return;
            }

            LastChange = value - _price;
            _price = value;

            if (DayOpen == 0)
            {
                DayOpen = _price;
            }
            if (_price < DayLow || DayLow == 0)
            {
                DayLow = _price;
            }
            if (_price > DayHigh)
            {
                DayHigh = _price;
            }
        }
    }
}
The browser client happily shows the correct values for all these properties, including derived ones such as PercentChange; whereas the C# console client always has DayOpen etc. set to the Price and the Change is always zero:

JS client vs C# client

A quick step through of the Json.NET code shows that the readonly member variables are skipped on deserialization and their values set when the Price setter is called; hence Price == DayOpen == DayLow == DayHigh and Change == PercentChange == 0.

A trivial, DTO-like class such as TestMessageObject2 shown below can be sent and deserialized without any issues for a C# (console or WPF) or a JS client.

public class TestMessageObject2
{
    public int ValueInt { get; set; }
    public double ValueDouble { get; set; }
    public string ValueString { get; set; }
}

JS client deserialization issues - a contrived example

When testing out the serialization I created a class to see how that various types of members were dealt with. Although it is rather contrived, it's not entirely unlike some of the legacy classes mentioned in the introduction:

public class TestMessageObject
{
    public int PublicFieldCtor;
    public int PublicField;
    public int PublicAutoPropertyCtor { get; set; }
    public int PublicAutoProperty { get; set; }
    public int PrivateSetAutoPropertyCtor { get; private set; }
    public int PrivateSetAutoProperty { get; private set; }

    private int _publicPropertyCtorBacking;
    public int PublicPropertyCtor
    {
        get { return _publicPropertyCtorBacking; }
        set { _publicPropertyCtorBacking = value; }
    }

    private int _publicPropertyBacking;
    public int PublicProperty
    {
        get { return _publicPropertyBacking; }
        set { _publicPropertyBacking = value; }
    }

    private int _privateFieldCtor;
    private int _privateField;

    public int GetPrivateFieldCtor() { return _privateFieldCtor; }
    public int GetPrivateField() { return _privateField; }
    
    // Partial parameter factory method
    public static TestMessageObject Create()
    {
        return new TestMessageObject(1, 2, 3, 4, 5)
        {
            PublicField = 6,
            PublicAutoProperty = 7,
            PrivateSetAutoProperty = 8,
            PublicProperty = 9,
            _privateField = 10
        };
    }

    public override string ToString()
    { 
        return string.Format(
            "pfc:{0},pf:{1},papc:{2},pap:{3},psapc:{4},psap:{5},ppc:{6},pp:{7},_pfc:{8},_pf:{9}",
            PublicFieldCtor,
            PublicField,
            PublicAutoPropertyCtor,
            PublicAutoProperty,
            PrivateSetAutoPropertyCtor,
            PrivateSetAutoProperty,
            _publicPropertyCtorBacking,
            _publicPropertyBacking,
            _privateFieldCtor,
            _privateField);
    }

    public TestMessageObject(
        int publicField,
        int publicAutoProperty,
        int privateSetAutoProperty,
        int publicProperty,
        int privateField)
    {
        PublicFieldCtor = publicField;
        PublicAutoPropertyCtor = publicAutoProperty;
        PrivateSetAutoPropertyCtor = privateSetAutoProperty;
        PublicPropertyCtor = publicProperty;
        _privateFieldCtor = privateField;
    }

    //public TestMessageObject() { }
}    
The default constructor was then commented in and out as needed to test. The code for this example is attached; you'll need VS 2013 (Express for Web for the server and JS client, and Express for Desktop for the WPF client).

Unsurprisingly TestMessageObject2 that we talked about before deserializes exactly as it exists on the server for both the WPF and the JS client. However, also unsurprisingly, TestMessageObject does not.

Default constructor

If TestMessageObject has a default constructor then the values of all publicly gettable and settable members are correct for both the WPF and the JS client. Any fields without a public setter for the C# class are at their default value of 0.

  • C# server object: pfc:1,pf:6,papc:2,pap:7,psapc:3,psap:8,ppc:4,pp:9,_pfc:5,_pf:10
  • C# client object: pfc:1,pf:6,papc:2,pap:7,psapc:0,psap:0,ppc:4,pp:9,_pfc:0,_pf:0
JS client:
{"PublicFieldCtor":1,
 "PublicField":6,
 "PublicAutoPropertyCtor":2,
 "PublicAutoProperty":7,
 "PrivateSetAutoPropertyCtor":0,
 "PrivateSetAutoProperty":0,
 "PublicPropertyCtor":4,
 "PublicProperty":9}

No default constructor

If you don't have a default constructor then the Json.NET deserialization code does its best and calls the non-default constructor with the best match from whatever parameters were serialized, and then considers the object to be fully deserialized (a reasonable assumption since usually non-default ctors fully initialize the object). Note that this example class is particularly awkward since the constructor parameter names more closely match the non-xxxCtor fields than xxxCtor ones.

For the 5 parameter constructor we end up with:

  • Breaking in the ctor shows it is being called with arguments 6,7,8,9,0
  • C# server object: pfc:1,pf:6,papc:2,pap:7,psapc:3,psap:8,ppc:4,pp:9,_pfc:5,_pf:10
  • C# client object: pfc:1,pf:0,papc:2,pap:0,psapc:0,psap:0,ppc:4,pp:0,_pfc:0,_pf:0
Interestingly the JS client object has the value for the PrivateSetAutoPropertyCtor set to that of the PrivateSetAutoProperty:
{"PublicFieldCtor":1,
 "PublicField":0,
 "PublicAutoPropertyCtor":2,
 "PublicAutoProperty":0,
 "PrivateSetAutoPropertyCtor":8,
 "PrivateSetAutoProperty":0,
 "PublicPropertyCtor":4,
 "PublicProperty":0}
Given the extremely un-DTO nature of TestMessageObject, especially with the 5 parameter constructor and its parameter names, I'd be amazed if this had worked. Without studying the Json.NET serialization code more than I have time to (and possibly learning a lot more about JS) I'm not sure why the values all end up as they do; however it was enough for me to know that they might well end up not set as we expected.

So how can I work around it?

If the requested feature mentioned above makes it into version 3 then there will no doubt be an official way to handle private fields / setters, but until then you'll need to work around the problem. First and foremost, if you have access to the code for the serialized message object classes, and can modify them then, switch them to standard DTO-style objects with a default constructor and public getters and setters and everything will be fine.

If, like us, you're unable to modify the message object classes (for whatever reason) then you could add a wrapper class around the object and serialize that instead. If you happen to have something that is already set to serialize to XML (say via a DataContractSerializer) then your wrapper class could have a single string property that contains the XML representation of the class (don't forget to use something like HttpUtility.HtmlEncode). This way you should be able to more easily handle things like backwards compatibility for older clients if new server fields are added.

Failing that, another way would be to dump the contents of the class to a byte[] and send that in the wrapper class. This is what we ended up doing, but it does make backwards compatibility / versioning more tricky and invasive. Needless to say this would also make adding a JavaScript client a lot more awkward.

There may well be other simpler and/or better workarounds; if you can think of any then please feel free to share them!

History

  • 2014-08-27 Initial Version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mark_Shield
Software Developer (Senior) Heathmill Ltd
United Kingdom United Kingdom
Mark Shield is a senior developer at the Heathmill Ltd consultancy. He enjoys coding / tinkering in a variety of languages (C++, C#, Python, R to name a few) and playing his ever increasing bass guitar collection in his ever decreasing spare time.
 
Heathmill is a software development consultancy, working across the financial, life sciences, and engineering industries, based in London UK.

Comments and Discussions

 
GeneralMy vote of 5 PinpremiumVolynsky Alex28-Aug-14 10:57 
GeneralMy vote of 5 PinmemberHumayun Kabir Mamun28-Aug-14 2:47 
GeneralRe: My vote of 5 PinmemberMark_Shield28-Aug-14 3:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140905.1 | Last Updated 28 Aug 2014
Article Copyright 2014 by Mark_Shield
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid