“Slow and Steady” No Longer Wins the Race: Using SignalR to Expedite Client/Server Communication

small-logo-icon2
Distillery
  • Date Published
  • Categories Blog
  • Reading Time 11-Minute Read

Modern applications – whether they’re web or mobile applications – offer a huge number of interactive features and allow users to receive results almost immediately after performing an action.

Modern applications – whether they’re web or mobile applications – offer a huge number of interactive features and allow users to receive results almost immediately after performing an action. But the race is always on to make apps yet more interactive and make the apps’ communication with servers happen even faster. Fortunately, there are several methods developers can use to make their apps more interactive. For example, the internet world is full of information about JavaScript frameworks which have recently become popular (e.g., React or AngularJS). In addition, a few years ago, new WebSocket technology was introduced that increases the overall level of efficiency of the communication between the server and the client.

There are various libraries available which can be used to make the process of WebSocket implementation less complex. The most popular libraries include Socket.IO and ASP.NET SignalR. SignalR was developed by Microsoft, so .NET developers are very aware of it. Today, I’m going to outline a set of operations which you can use to set up SignalR for your project.

Overview

When starting a new project, developers are always trying to answer the following questions:

  • “Where to start?”
  • “What are the basic steps for making things work?”
  • “How can I set up everything so that it fits my requirements?”

Recently, while working on an application and endeavoring to set up SignalR, I was asking myself these very same questions. In an effort to save time for other developers both now and in future, I decided to write this blog article to share the answers I’ve found. With that in mind, this primer includes the following:

  1.       Configure serialization of requests and responses
  2.       Standardized responses
  3.       Authorization
  4.       Validation
  5.       Exception handling
  6.       Dependency injection

(Note: the entire code can be viewed and downloaded on GitHub here.)

Configure Serialization of Requests and Responses

Description

When we need to change the form of the requests and responses, we have to set up serialization. Let’s look at the most popular settings: field naming style, and deleting empty values from responses to decrease the amount of traffic.

Implementation

Serialization settings can be changed using JsonSerializer. In order to change the field naming style, however, we have to create a custom ContractResolver. Let’s begin with that.

Our custom resolver will be inherited from the IContractResolver interface. We need to identify our field naming style inside the constructor. The interface insists on implementing the ResolveContract method; thus, we are going to do it inside.

public class SignalRContractResolver : IContractResolver
{
private readonly Assembly assembly;
private readonly IContractResolver camelCaseContractResolver;
private readonly IContractResolver defaultContractSerializer;

public SignalRContractResolver()

{
defaultContractSerializer = new DefaultContractResolver();
camelCaseContractResolver = new CamelCasePropertyNamesContractResolver();
assembly = typeof(Connection).Assembly;
}

public JsonContract ResolveContract(Type type)
{
if (type.Assembly.Equals(assembly))
return defaultContractSerializer.ResolveContract(type);

return camelCaseContractResolver.ResolveContract(type);
}
}

Now we have to create an object with JSON settings, informing our application that it has to use it. Ignoring empty fields will be configured not in the resolver, but rather in the setting object.

var settings = new JsonSerializerSettings
{
ContractResolver = new SignalRContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
var serializer = JsonSerializer.Create(settings);
GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => serializer);

This setting is going to be applied to the static GlobalHost object used by SignalR.

Standardized Responses

Description

During the process of developing the client, it may be useful to standardize all responses from the server, maintaining a strictly defined structure. By doing this, the client will always know which fields have to be addressed in specific cases.

Implementation

To implement standardized responses, we must create several classes that correspond with the various response types.

Base Response

The base response contains only one bool-type field Successful, which is used to identify the successful response.

public abstract class BaseHubResponse
{
public BaseHubResponse(Boolean successful)
{
Successful = successful;
}

public virtual Boolean Successful { get; private set; }
}

Successful Response

The successful response is inherited from the basic one. The successful response will always have the Successful field set to true. The successful response may be empty or may contain a generic Data field.

public class OkHubResponse : BaseHubResponse
{
public OkHubResponse()
: base(true)
{ }
}

public class OkHubResponse<T> : OkHubResponse
{
public OkHubResponse()
: base()
{ }

public OkHubResponse(T data)
: base()
{
Data = data;
}

public virtual T Data { get; set; }
}

Error Response

The error response is inherited from the basic one. The error response will always have the Successful field set to false.

The error response contains three additional fields: error code (to show the client the reason for the error), error text (in the event we want to show the error message to the user), and the collection of invalid fields (if the request is not valid).

public class ErrorHubResponse : BaseHubResponse
{
public ErrorHubResponse(OutboundError error)
: base(false)
{
ErrorCode = error.ErrorCode;
ErrorMessage = error.ErrorMessage;
}

public ErrorHubResponse(OutboundErrorException errorException)
: this(errorException.OutboundError)
{ }

public ErrorHubResponse(OutboundError error, IEnumerable<String> invalidFields = null)
: this(error)
{
InvalidFields = invalidFields;
}

public virtual Int32 ErrorCode { get; set; }
public virtual String ErrorMessage { get; set; }
public virtual IEnumerable<String> InvalidFields { get; set; }
}

Authorization

Description

One possible authorization option for hubs with SignalR is sending a token using the query string. In other words, we are going to add a token to the query string in each and every request to the hub methods. After that, we have to the token from the query string on the server and add it into the request context.

Implementation

To implement this, we have to create our own OAuthBearerAuthenticationProvider. As I mentioned above, we have to check the query string, find the token, and put it into the context. Our provider will have the following appearance:

public class ApplicationOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null) throw new ArgumentNullException(“context”);

var queryToken = context.OwinContext.Request.Query[“access_token”];
if (!String.IsNullOrEmpty(queryToken))
context.Token = queryToken;

return Task.FromResult<Object>(null);
}
}

Next we have to set up our application to use our custom provider. To do this, we have to call the UseOAuthBearerAuthentication method from the IAppBuilder object and define that we want to use our custom provider.

Note: We are using Owin specification on the project.

app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerProvider()
});

Validation

Description

ASP.NET MVC and Web API have allowed developers to get into the habit of performing request validation by using a special attribute for a method or a controller. Wouldn’t it be convenient to have such a tool when working with SignalR hubs? Fortunately, it’s possible!

Implementation

To make it happen, we have to write a module that will be included in the request processing flow. This module will check if our validation attribute is added to the method. If the attribute is added, we get the request validators and start the validation process.

We need to implement two classes: HubPipelineModule, which will be included in the request processing flow; and the attribute itself, which will be used to control the validation process.

The module contains a dictionary that is used to keep information about the existence of the validation attribute of the method. This feature saves a great deal of time. If the request is invalid, we need to generate the error response and send it to the client.

The validation module has the following appearance:

public class ValidationHubPipelineModule : HubPipelineModule
{
private readonly ConcurrentDictionary<MethodDescriptor, IEnumerable<IValidateHubMethodInvocation>> _methodInvocationCache = new ConcurrentDictionary<MethodDescriptor, IEnumerable<IValidateHubMethodInvocation>>();

public override Func<IHubIncomingInvokerContext, Task<object>> BuildIncoming(Func<IHubIncomingInvokerContext, Task<object>> invoke)
{
return base.BuildIncoming(context =>
{
// Get method attributes implementing IValidateHubMethodInvocation from the cache
// If the attributes do not exist in the cache, retrieve them from the MethodDescriptor and add them to the cache
var methodLevelValidator = _methodInvocationCache.GetOrAdd(context.MethodDescriptor,
methodDescriptor => methodDescriptor.Attributes.OfType<IValidateHubMethodInvocation>()).FirstOrDefault();

// no validator… keep going on with the rest of the pipeline
if (methodLevelValidator == null)
return invoke(context);

var invalidFields = methodLevelValidator.ValidateHubMethodInvocation(context);

// No errors… keep going on with the rest of the pipeline
if (!invalidFields.Any())
return invoke(context);

var response = new ErrorHubResponse(OutboundErrors.General.REQUEST_IS_INVALID, invalidFields);

// Send error model as a result back to the client
return SetResult<Object>(response);
});
}

private static Task<T> SetResult<T>(T resultModel)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetResult(resultModel);
return tcs.Task;
}
}

This module works with the IValidateHubMethodInvocation interface. The interface has only one method that calls validation and returns the collection of invalid field names:

public interface IValidateHubMethodInvocation
{
IEnumerable<String> ValidateHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext);
}

Now, we need to implement the attribute itself. Keeping in mind that it’s inherited from the interface described above, we know that the attribute has only one method inside. We are going to use this method to go through all input parameters, initialize the validator for each of them, and use it to create the collection of invalid fields.

[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public sealed class ValidateHubMethodAttribute : Attribute, IValidateHubMethodInvocation
{
public IEnumerable<String> ValidateHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext)
{
var errors = new List<String>();

for (var i = 0; i < hubIncomingInvokerContext.Args.Count; i++)
{
var arg = hubIncomingInvokerContext.Args[i];

var validatorInstance = FluentValidationHelper.GetValidator(arg);
if (validatorInstance != null)
{
var validationResult = validatorInstance.Validate(arg);
if (!validationResult.IsValid)
{
var paramName = hubIncomingInvokerContext.MethodDescriptor.Parameters[i].Name;
errors.AddRange(validationResult.Errors.Select(error => $”{paramName}.{error.PropertyName}”));
}
}
}

return errors;
}
}

Finally, we need to include the module into the app pipeline:

GlobalHost.HubPipeline.AddModule(new ValidationHubPipelineModule());

Exception Handling

Description

All of us have been faced with the case when there is an unhandled server exception in the app. And sharing information about unhandled exceptions is a considered bad form. In order to avoid unwanted surprises on the client side in the event of exception, it could be useful to catch such exceptions on the server side and send a standardized response to the client.

Implementation

To catch all exceptions and send a standardized response to the client, we have to wedge in the flow. To do so, we need another module. The HubPipelineModule abstract class offers the OnIncomingError method, which can be useful for us in this situation. This method is called when the system receives an error response. We will redefine this method to return a suitable response.

public class ErrorHandlingPipelineModule : HubPipelineModule
{
protected override void OnIncomingError(ExceptionContext exceptionContext, IHubIncomingInvokerContext invokerContext)
{
var errorFromContext = exceptionContext.Error;
if (errorFromContext != null)
{
OutboundError error;

if (errorFromContext is OutboundErrorException)
error = ((OutboundErrorException)errorFromContext).OutboundError;
else
error = OutboundErrors.General.UNDEFINED_ERROR;

exceptionContext.Result = new ErrorHubResponse(error);
}

base.OnIncomingError(exceptionContext, invokerContext);
}
}

When the method is implemented, all we have to do is wedge it to the application flow:

GlobalHost.HubPipeline.AddModule(new ErrorHandlingPipelineModule());

Dependency Injection

Description

We are not going to spend a lot of time talking about the idea of dependency injection, because it’s pretty difficult to find a project that doesn’t have this pattern. No doubt we really want to use DI in our SignalR hubs.

Implementation

To use dependency injection in our hubs, we have to implement the IHubActivator interface. This will be used to initialize dependent services. The logic of this activator is very simple: we are going to return the initialized hub object by calling the factory method.

In our example, we implemented the ComponentFactory factory that involves StructureMap to initialize dependent services. We are not going to talk about its implementation, because it’s not included in the focus of this blog article. However, if it’s needed, it’s possible to find the factory code on GitHub.

public class HubActivator : IHubActivator
{
public IHub Create(HubDescriptor descriptor)
{
return (IHub)ComponentFactory.GetInstance(descriptor.HubType);
}
}

Finally, we need to register the activator in the app. To do so, we need to add a new resolver to GlobalHost.

GlobalHost.DependencyResolver.Register(typeof(IHubActivator), () => new HubActivator());

Conclusion

Modern technologies save us a tremendous amount of time, allowing us to perform various operations with incredible speed. In particular, by using WebSockets, we are able to create interactive client/server applications. If we need to wait for completion of a continuous operation or display new data, we don’t need to send repetitive requests to the server or – even worse – reload the webpage. WebSockets make it possible for the server to take the initiative and send the data to the client, thereby expediting and optimizing the entire communication process. And as we all know, faster communication makes for happier customers!