次の方法で共有


Automatic camel casing of properties with SignalR hubs (SignalR v2)

The web is built on HTTP which works on a request, response, request, response, … basis.  SignalR is a great technology that breaks that down and allows two-way communication between client and server. It will work with web sockets and fall back through a number of approaches right the way back to long-polling. One of the beauties of SignalR is that it abstracts all of this from you.

Because SignalR bridges the client and server, this often brings a clash between conventions: server-side we generally favour PascalCasing (e.g. SomeProperty), whereas client-side normally prefers camelCasing (someProperty). . A while back I posted about automatically converting between these conventions. Since then, the ASP.NET team have released SignalR v2 and the refactorings mean that the previous solution no longer works. in this post I’ll walk you through how to achieve the same goal in SignalR v2. There is a link to a downloadable version of the code at the end of the post)

Getting Started

I started from a similar point and created an empty ASP.NET application in Visual Studio 2013. I then added the reference to SignalR via nuget. I like the Package Manager Console, so I used the following command (but you can also use the Add Package Reference window in Visual Studio)

Install-Package Microsoft.AspNet.SignalR

This brings up a readme for how to configure SignalR. Once I’d done that I added the same Chat hub as we had at the end of the previous post:

 public class Chat : Hub
{
     public void Send(string message)
     {
         Clients.All.AddMessage(
 new Message 
 { 
 Body = "SENT: " + message, Timestamp = DateTime.UtcNow 
 });
      }
}   

This uses the Message class. We could annotate that with the JsonProperty attribute to customise how JSON serialisation as shown below

 public class Message 
{
     [JsonProperty("body")]
     public string Body { get; set; }
     [JsonProperty("timestamp")]
     public DateTime Timestamp { get; set; } 
}   

Whilst this achieves the right serialisation it is a manual step and we want to avoid it so that the Message class is nice and simple:

 public class Message 
{
     public string Body { get; set; }
     public DateTime Timestamp { get; set; } 
}   

Before customising the serialisation, let’s add the HTML page (Index.html) with the following content:

 <!DOCTYPE html> 
<html xmlns="https://www.w3.org/1999/xhtml"> 
<head>
     <title>SignalR Camel Casing</title>
     <link href="~/Styles/Site.css" rel="stylesheet" type="text/css" /> 
</head> 
<body>
     <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
     <script src="Scripts/jquery.signalR-2.0.0.min.js"></script>
     <script src="https://blogs.msdn.com/signalr/hubs" type="text/javascript"></script>
     <script type="text/javascript">
         $(function () {
             // Proxy created on the fly
             var chat = $.connection.chat;
             // Declare a function on the chat hub for the server to invoke
             chat.client.addMessage = function (message) {
                 $('#messages').append($('<li>').text(message.timestamp + ' ' + message.body));
             };
             // Start the connection
             $.connection.hub.start()
                 .done(function () {
                     $("#broadcast")
                         .removeAttr('disabled')
                         .click(function() {
                             // Handle button click and call the chat method on the server
                             chat.server.send($('#msg').val());
                         });
                 });
         });
     </script>
     <h2>SignalR camelCasing</h2>
     <input type="text" id="msg" />
     <input type="button" id="broadcast" value="Send" disabled="disabled" />
     <ul id="messages"></ul> 
</body> 
</html>   

As you can see from the client-side code, we are accessing message.tempstamp and message.body, as camelCased properties (i.e. not message.Timestamp etc that we would use server-side). To achieve this we need to plug in to the serialiser that SignalR uses.

Adding camelCasing behaviour

SignalR uses the JSON.Net serialiser and it includes a CamelCasePropertyNamesContractResolver. However, if we use this then we break SignalR itself as it uses PascalCasing for its client-side code. Instead we can re-use the FilteredCamelCasePropertyNamesContractResolver class from the previous post.

 public class FilteredCamelCasePropertyNamesContractResolver : DefaultContractResolver 
{
     public FilteredCamelCasePropertyNamesContractResolver()
     {
         AssembliesToInclude = new HashSet<Assembly>();
         TypesToInclude = new HashSet<Type>();
     }
     // Identifies assemblies to include in camel-casing
 public HashSet<Assembly> AssembliesToInclude { get; set; }
     // Identifies types to include in camel-casing
 public HashSet<Type> TypesToInclude { get; set; } 
     protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
     {
         var jsonProperty = base.CreateProperty(member, memberSerialization);
         Type declaringType = member.DeclaringType;
         if (
             TypesToInclude.Contains(declaringType)
             || AssembliesToInclude.Contains(declaringType.Assembly)) 
        {
             jsonProperty.PropertyName = jsonProperty.PropertyName.ToCamelCase();
         }
         return jsonProperty;
     }
 }   

This class only applies the camelCasing to types that are in the TypesToInclude list, or that are contained in assemblies in the AssembliesToInclude list.

Finally, we need to tell SignalR to use a JSON serialiser with this contract resolver. To do this we register a serialiser with the SignalR dependency resolver. SignalR uses the dependency resolver when it looks up services (one of which is the JSON serialiser). Since SignalR 2 is built on top of OWIN, we add the registration code in the OWIN Startup class:

 public class Startup 
{
     private static readonly Lazy<JsonSerializer> JsonSerializerFactory = new Lazy<JsonSerializer>(GetJsonSerializer);
     private static JsonSerializer GetJsonSerializer()
     {
         return new JsonSerializer
         {
             ContractResolver = new FilteredCamelCasePropertyNamesContractResolver
             {
                 // 1) Register all types in specified assemblies:
                 AssembliesToInclude =
                 {
                     typeof (Startup).Assembly
                 },
                 // 2) Register individual types:
                 //TypesToInclude =
                 //                {
                 //                    typeof(Hubs.Message),
                 //                }
             }
         };
     }
     public void Configuration(IAppBuilder app)
     {
         app.MapSignalR();
         GlobalHost.DependencyResolver.Register(
 typeof(JsonSerializer), 
 ()=>JsonSerializerFactory.Value);
      } 
}   

In this code we create a JsonSerializer which uses our custom contract resolver. We then register a resolver for the JsonSerializer type with GlobalHost.DependencyResolver that returns our JsonSerialiser. With this in place our job is done!

Summary

In this post we’ve seen how we can customise the JSON serialisation for our own code that builds on SignalR, without breaking SignalR itself. By doing this we can keep the conventions we want for property naming on both client- and server-side, without having to manually override the naming for each individual property.

The accompanying code for this post can be downloaded from: https://code.msdn.microsoft.com/Automatic-camel-casing-of-cb3d259e

Comments

  • Anonymous
    January 25, 2014
    Good article.

  • Anonymous
    August 06, 2014
    Hi Stuart, I'm really glad I finally stumbled across your post here. I'm convinced that SignalR should have built-in support for what, I can only assume, is a highly common requirement in client-server communications. Thanks to your contract resolver here it is! So, many thanks! Cheers, Zac

  • Anonymous
    November 09, 2015
    Hi Stuart, Would you be kind enough to share your reasoning for not using Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver and rather your own FilteredCamelCasePropertyNamesContractResolver? TIA, Afzal

  • Anonymous
    November 09, 2015
    Sorry, I seems to have skimmed over your paragraph that explains your reasoning :-| I understand the reasoning now.