Skip navigation

Category Archives: SignalR

In the following post, I’ll discuss a small sample show case how to broadcast WebAPI trace through SignalR.

  • SignalR is an async signaling library for .NET to help build real-time, multiuser interactive web applications. It can be used to push data from server to the client.
  • WebAPI tracing is the feature to track all internal activities in the server and log then to customized trace repository.

Scenario

User can connect to the server tracing page to get real-time trace updating. To access the information, they need to go through basic authentication.

Persisitent Connection

The core component is an persistent connection. Three actions are overrided:

  • When an connection is made, this connection is added to an unauthenticated group.
  • When an connection is released, it is removed from both authenticated and unauthenticated group. Just in case it is in either of them.
  • When a data is recieved from a connection, this data is expected to be a token string. The token string is used to authenticate the client. Details of the token will be discussed later.
public class TracePersistentConnection : PersistentConnection
{
    public static readonly string Authenticated = "authenticated";
    public static readonly string Unauthenticated = "unauthenticated";

    protected override Task OnConnectedAsync(IRequest request, string connectionId)
    {
        return Groups.Add(connectionId, Unauthenticated);
    }

protected override Task OnDisconnectAsync(string connectionId)
{
        return Groups.Remove(connectionId, Authenticated)
                     .ContinueWith(task => Groups.Remove(connectionId, Unauthenticated));
    }

    protected override Task OnReceivedAsync(IRequest request, string connectionId, string data)
    {
        if (ClientTokens.Instance.Exists(data))
        {
            return Groups.Add(connectionId, Authenticated)
                         .ContinueWith(task => Groups.Remove(connectionId, Unauthenticated));
        }
        else
        {
            return base.OnReceivedAsync(request, connectionId, data);
        }
    }
}

You must notice that there are not codes sending message to clients. That is because sending message is the responsibility of trace writer.

Trace Writer

A trace writer is replace the default implementation through WebAPI’s dependency resolver.

public class TraceWriter : ITraceWriter
{
    private ConcurrentQueue<TraceRecord> _traces;

    private TraceWriter()
    {
        _traces = new ConcurrentQueue<TraceRecord>();
    }

    private static TraceWriter s_instance = new TraceWriter();

    public static TraceWriter Instance
    {
        get { return s_instance; }
    }

    public void Trace(
        HttpRequestMessage request,
        string category,
        TraceLevel level,
        Action<TraceRecord> traceAction)
    {
        var trace = new TraceRecord(request, category, level);
        traceAction(trace);

        _traces.Enqueue(trace);

        var context =
                GlobalHost.ConnectionManager
                          .GetConnectionContext<TracePersistentConnection>();
        context.Groups.Send(
            TracePersistentConnection.Authenticated,
            new
            {
                trace.RequestId,
                trace.Request.RequestUri,
                Status = trace.Status.ToString(),
                Level = trace.Level.ToString(),
                trace.Message,
                trace.Category,
                TimeTicks = trace.Timestamp.Ticks,
                trace.Operator,
                trace.Operation,
                Exception = trace.Exception == null ? "" : trace.Exception.Message,
            });
    }
}

You may notice that when send the trace record to group, I construct a new object  rather than send out the trace it self and let the formatter take care of serialization. I actually tried to do so, but somehow SignalR hit some internal failure and that cause the message never make to the client. I guess it will be my next topic.

Server Setup

Everything is set up in global.asax.cs:

  • Add Route to the WebAPI controller
  • Add Route to the SignalR persistent connection
  • Replace trace writer in dependency resolver
  • Add authentication handler
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            var config = GlobalConfiguration.Configuration;

            config.MessageHandlers.Add(new AuthenticationHandler());

            config.Services.Replace(typeof(ITraceWriter), TraceWriter.Instance);

            config.Routes.MapHttpRoute(
                "default",
                "api/{controller}/{action}",
                new { action = RouteParameter.Optional });

            RouteTable.Routes.MapConnection<TracePersistentConnection>("trace", "trace/{*operatopn}");
        }
    }

Client

The client is a simple HTML + Javascript page.

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <link rel="stylesheet" type="text/css" href="Style/Default.css" />
    <title>WebAPI Tracing + SignalR</title>
</head>
<body>
    <div id="auth">
        <h1>WebAPI Tracing + SignalR</h1>
        <table id="logTable">
            <tr>
                <td>Username</td>
                <td>
                    <input type="text" id="txtUsername" value="admin" /></td>
            </tr>
            <tr>
                <td>Password</td>
                <td>
                    <input type="password" id="txtPassword" value="password" /></td>
            </tr>
        </table>
        <input type="button" id="btnLogin" value="Log in" />
        <input type="button" id="btnLogout" value="Log out" />
    </div>

    <div id="trace">
        <h1>Trace Records</h1>

        <input type="button" id="btnClear" class="button" value="Clear" />

        <div id="message" class="output"></div>
        <div id="exception" class="output"></div>

        <table id="tracetable">
            <tr id="header">
                <th id="requrl">URL</th>
                <th id="level">Level</th>
                <th id="category">Category</th>
                <th id="operator">Operator</th>
                <th id="operation">Operation</th>
                <th id="status">Status</th>
            </tr>
        </table>
    </div>

    <script type="text/javascript" src="Scripts/jquery-1.6.4.js"></script>
    <script type="text/javascript" src="Scripts/jquery-ui-1.8.20.js"></script>
    <script type="text/javascript" src="Scripts/jquery.base64.js"></script>
    <script type="text/javascript" src="Scripts/jquery.signalR-0.5.2.js"></script>
    <script type="text/javascript" src="Scripts/Home.js"></script>
</body>
</html>

 


$(function () {
    function updateTraceTable(trace) {
        var row = $('<tr></tr>');
        row.append('<td>' + trace.RequestUri + '</td>');
        row.append('<td>' + trace.Level + '</td>');
        row.append('<td>' + trace.Category + '</td>');
        row.append('<td>' + trace.Operator + '</td>');
        row.append('<td>' + trace.Operation + '</td>');
        $('<td>' + trace.Status + '</td>').appendTo(row);

        row.insertAfter('#header');

        row.hover(
            function () {
                $('#message').text(trace.Message);
                $('#exception').text(trace.Exception);
                $(this).css("background-color", "lightgreen");
            },
            function () {
                $('#message').text("");
                $('#exception').text("");
                $(this).css("background-color", "white");
            });
    };

    function setupTraceConnection(token) {
        var connection = $.connection('/trace');

        connection.received(function (trace) {
            $('#debuginfo').text(trace);
            updateTraceTable(trace);
        });

        connection.start().done(
            function () {
                connection.send(token);
            });

        $('#btnClear').click(function () {
            $('#debuginfo').text("clear the trace records");
            $('td', '#tracetable').remove();
        });
    };

    $('#btnLogout').hide();
    $('div#trace').hide();

    $('#btnLogin').click(function (event) {
        var username = $('input#txtUsername').val();
        var password = $('input#txtPassword').val();

        var options = {
            url: 'api/TraceAuthentication/GetToken',
            type: 'GET',
            data: { 'username': username },
            beforeSend: function (xhr) {
                var raw = username + ":" + password;
                var encoded = jQuery.base64.encode(raw);
                xhr.setRequestHeader("Authorization", "Base " + encoded);
            },
            timeout: '60000',
            success: function (token) {
                $('div#trace').show();
                $('#logTable').hide();
                $('#btnLogin').hide();
                $('#btnLogout').show();
                setupTraceConnection(token);
            },
            error: function (data, status, err) {
                alert(err);
            }
        }
        $.ajax(options);
    });

});

Above are the html and the javascript used to update it. Line 26 to 33 of javascript code snippet actually set up the connection to persistent connection. You may wonder what’s the magic string “/trace”. It is the route set up to the PersistentConnection defined in global.asax.cs.

Till now the server is fully functional except one part: security. You don’t want users to see the trace just because they ‘accidentally’ know the endpoint. So here’s the last piece.

Authentication

Here’s the logic I adopted in this sample:

The client first visit a WebAPI controller to get a token. The controller is protected by basic authentication (or any security model you can image). The token is a security string combing client information and a dynamic server string. The server register the token to a global bag. The client uses the token to send out first message to Hub/PersisitentConnection after connected, which will make itself into an authenticated group, to which Trace information are broadcasted to.

Some of the logic have been already presented in above code snippets.

  • In the TracePersistentConnection, for a new created connection a token is returned.
  • In the TracePersistentConnection, it actively manages the groups.
  • In the client javascript, right after the connection is setup a token is sent to server to announce client’s identity.
  • In the trace writer, the information only writes to the authenticated group.

What missing is a global structure store the tokens.


    public class ClientTokens
    {
        private static ClientTokens s_instnace = new ClientTokens();
        private ConcurrentBag<string> _tokens = new ConcurrentBag<string>();

        private ClientTokens()
        {
        }

        public static ClientTokens Instance
        {
            get
            {
                return s_instnace;
            }
        }

        public bool Exists(string token)
        {
            return _tokens.Contains(token);
        }

        public void AddToken(string token)
        {
            _tokens.Add(token);
        }
    }

Summary

I tried to demo following features in this sample:

  • WebAPI and SignalR side by side
  • Broadcast trace in real-time
  • Authentication pattern

Some thing worth to dig deeper:

  • Serialization in SignalR
  • More authentication pattern

Link

Advertisements