SignalR, ActionFilters and ASP.NET Web API

Β· 1048 words Β· 5 minutes to read

There have been quite a few examples circulating on the web on how one would use SignalR together with Web API. It all started after Brad Wilson had a great example on calling SignalR from API controllers at NDC Oslo 2012.

Even on this blog we looked at calling SignalR from Web API ITraceWriter to provide realtime tracing capabilities.

How about, to avoid any controller-level noise, messaging the connected SignalR subscribers from ActionFilters? While this approach might not be applicable in all scenarios, when it is, I think it could provide a nice layer of separation.

More after the jump.

SignalR and attributes πŸ”—

Traditionally, we need to start with something, so let’s create a Web API web project & SignalR.

PM> Install-Package Microsoft.AspNet.SignalR  

We’d have the following simple, default hub:

[HubName("servicelog")]  
public class LogHub : Hub  
{}  

And a simple controller:

public class Message  
{  
public int Id { get; set; }  
public string Name { get; set; }  
}

public class ValuesController : ApiController  
{  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2" };  
}

public Message Get(int id)  
{  
return new Message { Id = id, Name = "test" };  
}

public void Post(Message message)  
{  
//do stuff  
}  
}  

Suppose we have a following scenario. We’d like to notify a connected hub client (i.e. admin) with all incoming API requests & all outgoing objects sent out from our API.

It would be quite convenient to be able to decorate the Action with an ActionFilter attribute, indicating how SignalR should behave for this action:

public class ValuesController : ApiController  
{  
[Hub(Name = "servicelog", IncomingMethod = "log", OutgoingMethod = "logArray")]  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2" };  
}

[Hub(Name = "servicelog", IncomingMethod = "log", OutgoingMethod = "logMessage")]  
public Message Get(int id)  
{  
return new Message { Id = id, Name = "test"};  
}

[Hub(Name = "servicelog", IncomingMethod = "log")]  
public void Post(Message message)  
{  
//do stuff  
}  
}  

So effecitvely, to tell each action that whenever it’s called, notify the client using IncomingMethod on a specific hub, and that its output should also be passed over to same hub and invoked on the client using another method (OutgoingMethod). We have to address hub by string name, because SignalR currently does not expose a way to address hub by Type (only via a strongly typed generic, which is not allowed in Attributes).

The ActionFilter attribute looks like this:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]  
public class HubAttribute : ActionFilterAttribute  
{  
public string Name { get; set; }  
public string IncomingMethod { get; set; }  
public string OutgoingMethod { get; set; }

public override async void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)  
{  
if (string.IsNullOrWhiteSpace(Name) || string.IsNullOrWhiteSpace(IncomingMethod)) return;  
var hub = GlobalHost.ConnectionManager.GetHubContext(Name);

if (hub != null)  
{  
var text = string.Empty;  
var stream = await actionContext.Request.Content.ReadAsStreamAsync();  
if (stream.CanSeek)  
{  
stream.Position = 0;  
using (stream)  
{  
var reader = new StreamReader(stream);  
text = reader.ReadToEnd();  
}  
}

hub.Clients.All.Invoke(IncomingMethod,  
new  
{  
Time = DateTime.Now.ToString("G"),  
Data = actionContext.Request.Method + " " + actionContext.Request.RequestUri +  
" " + text  
});  
}  
}

public override async void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
if (string.IsNullOrWhiteSpace(Name) || string.IsNullOrWhiteSpace(OutgoingMethod)) return;  
var hub = GlobalHost.ConnectionManager.GetHubContext(Name);

if (hub != null)  
{  
var response =  
await  
actionExecutedContext.Response.Content.ReadAsAsync(  
actionExecutedContext.ActionContext.ActionDescriptor.ReturnType);  
hub.Clients.All.Invoke(OutgoingMethod, new { Time = DateTime.Now.ToString("G"), Data = response });  
}  
}  
}  

The construct is a bit unusual, but that’s due to the nature of Attributes. Named parameters are not allowed there, and since I want to avoid a constructor with two strings (which is not very intuitive), we use public properties to fake the named paramaters (a common approach when working with attributes).

On the incoming side, we read the request body stream (which needs resetting) into a string an log it (with some more effort we could extract specific type explicitly), to the SignalR clients together with timestamp and HTTP method.

On the outgoing side, we extract the response object (based on action’s return type) from the HTTP response, and send that to the client (notice, there is no specific reason why I’d do that - this is just to illustrate that we don’t *have to* deal with pure HTTP, we could access our CLR types as well and send those down to JS, which I think is pretty cool). We extract the response object by inspecting the action’s return Type, to which we have access. Of course we are not limited to doing that - we might add more info. But, for the purpose of this demo - we send the exact same response content to SignalR clients as the API responds with.

In both cases, we are using a not widely known Invoke method on SignalR’s ClientProxy object. This allows us to invoke a function by string name, rather than using the conventional dynamic object.

Trying this out πŸ”—

We just need to add some HTML and JavaScript to handle the incoming SignalR messages. Notice we expose two methods, corresponding to the methods we specified on our attributes.

  
  
</p> 

<ul id="messages">
</ul>

</body>

  
  


  
</html>  

Note: I’m using here SignalR alpha2 since there were some OWIN issues preventing the Nuget installation of the RC2 at the time of writing this - but everything should be the same in RC2, except the JS reference would need to be updated.

We also need to make sure that SignalR hub routes are registered:

RouteTable.Routes.MapHubs();  

Finally, we can now run this solution.

If I navigate to:

  • /api/values

  • /api/values/{id}

We’ll see the log filling with the responses. Here’s how it might look after a few requests:

Of course, this can be used for other scenarios than just a dummy log like this - but I hope this illustrates well the idea. You might consider notifying specific connection ids, injecting some identity context into the ActionFilter and so on.

All in all, while it’s not always suitable (if you watch Brad’s talk from the beginning, he notifies SignalR client from the controller’s action when resource gets created - obviously such scenarios must be handled inside of an action since that’s dealing with local variables inside an action), IMHO, this is still quite an interesting approach to SignalR presence in your API application, since it would help you clean up the controllers and isolate the logic responsible for communicating with the hubs into a reusable attribute.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP