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:
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; }
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;
}
}
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