Click here to Skip to main content
15,886,101 members
Articles / Programming Languages / Typescript

Hubs.tt Will Save Your Life

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
16 Apr 2014CPOL1 min read 13.7K   4  
Hubs.tt will save your life

Recently, I started playing with SignalR using TypeScript, one of the things that very quickly made it's way into my project is the Hubs.tt T4 template file.

Hubs.tt is a "T4 template that creates Typescript type definitions for all your Signalr hubs. If you have C# interface named "I<hubName>Client", a TS interface will be generated for the hub's client too. If you turn on XML documentation in your build, XMLDoc comments will be picked up. Licensed with http://www.apache.org/licenses/LICENSE-2.0".

You can find a copy of it on GitHub using the link https://gist.github.com/htuomola/7565357. I have also placed a modified version below that updates for SignalR.Core.2.0.3.

C#
<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension=".d.ts" #>
<# /* Update this line to match your version of SignalR */ #>
<#@ assembly name=
"$(SolutionDir)\packages\Microsoft.AspNet.SignalR.Core.2.0.3\
lib\net45\Microsoft.AspNet.SignalR.Core.dll" #>
<# /* Load the current project's DLL to make sure the DefaultHubManager can find things */ #>
<#@ assembly name="$(TargetPath)" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Web" #>
<#@ assembly name="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" #>
<#@ assembly name="System.Xml.Linq, 
 Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="Microsoft.AspNet.SignalR" #>
<#@ import namespace="Microsoft.AspNet.SignalR.Hubs" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml.Linq" #>
<#
    var hubmanager = new DefaultHubManager(new DefaultDependencyResolver());
#>
// Get signalr.d.ts.ts from https://github.com/borisyankov/DefinitelyTyped (or delete the reference)
/// <reference path="signalr/signalr.d.ts" />
/// <reference path="jquery/jquery.d.ts" />

////////////////////
// available hubs //
////////////////////
//#region available hubs

interface SignalR {
<#
foreach (var hub in hubmanager.GetHubs())
{
#>

    /**
      * The hub implemented by <#=hub.HubType.FullName#>
      */
    <#= FirstCharLowered(hub.Name) #> : <#= hub.HubType.Name #>;
<#
}
#>
}
//#endregion available hubs

///////////////////////
// Service Contracts //
///////////////////////
//#region service contracts
<#
foreach (var hub in hubmanager.GetHubs())
{
    var hubType = hub.HubType;
    string clientContractName = hubType.Namespace + ".I" + hubType.Name + "Client";
    var clientType = hubType.Assembly.GetType(clientContractName);
#>

//#region <#= hub.Name#> hub

interface <#= hubType.Name #> {
    
    /**
      * This property lets you send messages to the <#= hub.Name#> hub.
      */
    server : <#= hubType.Name #>Server;

    /**
      * The functions on this property should be replaced 
      * if you want to receive messages from the <#= hub.Name#> hub.
      */
    client : <#= clientType != null?(hubType.Name+"Client"):"any"#>;
}

<#
/* Server type definition */
#>
interface <#= hubType.Name #>Server {
<#
    foreach (var method in hubmanager.GetHubMethods(hub.Name ))
    {
        var ps = method.Parameters.Select(x => x.Name+ " : "+GetTypeContractName(x.ParameterType));
        var docs = GetXmlDocForMethod(hubType.GetMethod(method.Name));

#>

    /** 
      * Sends a "<#= FirstCharLowered(method.Name) #>" message to the <#= hub.Name#> hub.
      * Contract Documentation: <#= docs.Summary #>
<#
    foreach (var p in method.Parameters)
    {
#>
      * @param <#=p.Name#> {<#=GetTypeContractName
                            (p.ParameterType)#>} <#=docs.ParameterSummary(p.Name)#>
<#
    }
#>
      * @return {JQueryPromise of <#= GetTypeContractName(method.ReturnType)#>}
      */
    <#= FirstCharLowered(method.Name) #>(<#=string.Join(", ", ps)#>) : 
     JQueryPromise<<#= GetTypeContractName(method.ReturnType)#>>;
<#
    }
#>
}

<#
/* Client type definition */
#>
<# 
    if (clientType != null)
    {
#>
interface <#= hubType.Name #>Client
{
<#
    foreach (var method in clientType.GetMethods())
    {
        var ps = method.GetParameters().Select
                 (x => x.Name+ " : "+GetTypeContractName(x.ParameterType));
        var docs = GetXmlDocForMethod(method);

#>

    /**
      * Set this function with a "function(<#=string.Join(", ", ps)#>){}" 
      * to receive the "<#= FirstCharLowered(method.Name) #>" message from the <#= hub.Name#> hub.
      * Contract Documentation: <#= docs.Summary #>
<#
    foreach (var p in method.GetParameters())
    {
#>
      * @param <#=p.Name#> 
        {<#=GetTypeContractName(p.ParameterType)#>} <#=docs.ParameterSummary(p.Name)#>
<#
    }
#>
      * @return {void}
      */
    <#= FirstCharLowered(method.Name) #> : (<#=string.Join(", ", ps)#>) => void;
<#
    }
#>
}

<#
    }
#>
//#endregion <#= hub.Name#> hub

<#
}
#>
//#endregion service contracts

////////////////////
// Data Contracts //
////////////////////
//#region data contracts
<#
while(viewTypes.Count!=0)
{
    var type = viewTypes.Pop();
#>


/**
  * Data contract for <#= type.FullName#>
  */
interface <#= GenericSpecificName(type) #> {
<#
    foreach (var property in type.GetProperties
            (BindingFlags.Instance|BindingFlags.Public|BindingFlags.DeclaredOnly))
    {
#>
    <#= property.Name#> : <#= GetTypeContractName(property.PropertyType)#>;
<#
    }
#>
}
<#
}
#>

//#endregion data contracts

<#+

    private Stack<Type> viewTypes = new Stack<Type>();
    private HashSet<Type> doneTypes = new HashSet<Type>();

    private string GetTypeContractName(Type type)
    {
        if (type == typeof (Task))
        {
            return "void /*task*/";
        }

        if (type.IsArray)
        {
            return GetTypeContractName(type.GetElementType())+"[]";
        }

        if (type.IsGenericType && typeof(Task<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
        {
            return GetTypeContractName(type.GetGenericArguments()[0]);
        }

        if (type.IsGenericType && 
            typeof(Nullable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
        {
            return GetTypeContractName(type.GetGenericArguments()[0]);
        }

        if (type.IsGenericType && typeof(List<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
        {
            return GetTypeContractName(type.GetGenericArguments()[0])+"[]";
        }    

        switch (type.Name.ToLowerInvariant())
        {
            case "datetime":
                return "string";
            case "int16":
            case "int32":
            case "int64":
            case "single":
            case "double":
                return "number";
            case "boolean":
                return "bool";
            case "void":
            case "string":
                return type.Name.ToLowerInvariant();
        }

        if (!doneTypes.Contains(type))
        {
            doneTypes.Add(type);
            viewTypes.Push(type);
        }
        return GenericSpecificName(type);
    }

    private string GenericSpecificName(Type type)
    {
        //todo: update for Typescript's generic syntax once invented
        string name = type.Name;
        int index = name.IndexOf('`');
        name = index == -1 ? name : name.Substring(0, index);
        if (type.IsGenericType)
        {
            name += "Of"+string.Join
            ("And", type.GenericTypeArguments.Select(GenericSpecificName));
        }
        return name;
    }

    private string FirstCharLowered(string s)
    {
        return Regex.Replace(s, "^.", x => x.Value.ToLowerInvariant());
    }

    Dictionary<Assembly, XDocument> xmlDocs = new Dictionary<Assembly, XDocument>(); 

    private XDocument XmlDocForAssembly(Assembly a)
    {
        XDocument value;
        if (!xmlDocs.TryGetValue(a, out value))
        {
            var path = new Uri(a.CodeBase.Replace(".dll", ".xml")).LocalPath;
            xmlDocs[a] = value = File.Exists(path) ? XDocument.Load(path) : null;
        }
        return value;
    }

    private MethodDocs GetXmlDocForMethod(MethodInfo method)
    {
        var xmlDocForHub = XmlDocForAssembly(method.DeclaringType.Assembly);
        if (xmlDocForHub == null)
        {
            return new MethodDocs();
        }

        var methodName = string.Format("M:{0}.{1}({2})", method.DeclaringType.FullName, 
        method.Name, string.Join(",", 
        method.GetParameters().Select(x => x.ParameterType.FullName)));
        var xElement = xmlDocForHub.Descendants("member").SingleOrDefault
                       (x => (string) x.Attribute("name") == methodName);
        return xElement==null?new MethodDocs():new MethodDocs(xElement);
    }

    private class MethodDocs
    {
        public MethodDocs()
        {
            Summary = "---";
            Parameters = new Dictionary<string, string>();
        }

        public MethodDocs(XElement xElement)
        {
            Summary = ((string) xElement.Element("summary") ?? "").Trim();
            Parameters = xElement.Elements("param").ToDictionary(x => 
                        (string) x.Attribute("name"), x=>x.Value);
        }

        public string Summary { get; set; }
        public Dictionary<string, string> Parameters { get; set; }
    
        public string ParameterSummary(string name)
        {
            if (Parameters.ContainsKey(name))
            {
                return Parameters[name];
            }
            return "";
        }
    }
#>

The way to use this file is to simply copy it to ~/Scripts/typings/Hubs.tt and watch the magic happen Smile. Currently I have a simple hub like below:

C#
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SignalR_TypeScript_BasicChat.hubs
{
    public class ChatHub : Hub
    {
        private static List<ConnectedClients> connections = new List<ConnectedClients>();

        public void Connect(string displayName)
        {
            if (!connections.Exists(o => o.ConnectionId == Context.ConnectionId))
            {
                connections.Add(new ConnectedClients 
                               { ConnectionId = Context.ConnectionId, 
                                 DisplayName = string.IsNullOrEmpty(displayName) ? 
                                 Context.ConnectionId : displayName });
            }
            if (!string.IsNullOrEmpty(displayName))
            {
                connections.First(o => o.ConnectionId == 
                      Context.ConnectionId).DisplayName = displayName;
            }
            connections.First(o => o.ConnectionId == Context.ConnectionId).LastPingTime = DateTime.Now;
        }

        public void Disconnect()
        {
            if (connections.Exists(o => o.ConnectionId == Context.ConnectionId))
            {
                connections.Remove(connections.First(o => o.ConnectionId == Context.ConnectionId));
            }
        }

        public ConnectedClients[] GetConnectedClients()
        {
            Connect(null);
            return connections.Where(o => DateTime.Now.Subtract
            (o.LastPingTime).TotalSeconds < 15 && o.ConnectionId != Context.ConnectionId).ToArray();
        }

        public void SendAll(ChatMessage message)
        {
            Connect(message.Name);
            // Call the addNewMessageToPage method to update clients.
            Clients.All.addNewMessageToPage(message);
        }

        public void SendTo(ChatMessage message)
        {
            if (string.IsNullOrEmpty(message.ConnectionId) || 
                message.ConnectionId == "everyone" || message.ConnectionId == "null")
            {
                SendAll(message);
            }
            else
            {
                Connect(message.Name);
                // Call the addNewMessageToPage method to update clients.
                Clients.Caller.addNewMessageToPage(message);
                Clients.Client(message.ConnectionId).addNewMessageToPage(message);
            }
        }
    }

    public class ConnectedClients
    {
        public string ConnectionId { get; internal set; }
        public string DisplayName { get; internal set; }
        public DateTime LastPingTime { get; internal set; }
    }

    public interface IChatHubClient
    {
        void addNewMessageToPage(ChatMessage msg);
    }

    public class ChatMessage
    {
        public string Name { get; set; }
        public string Message { get; set; }
        public string ConnectionId { get; set; }
    }
}

Having the Hubs.tt file stopped me from having to type all the code below to allow for TypeScript to build and also give me the correct schema of the hub.

C#
// Get signalr.d.ts.ts from https://github.com/borisyankov/DefinitelyTyped (or delete the reference)
/// <reference path="signalr/signalr.d.ts" />
/// <reference path="jquery/jquery.d.ts" />

////////////////////
// available hubs //
////////////////////
//#region available hubs

interface SignalR {

    /**
      * The hub implemented by SignalR_TypeScript_BasicChat.hubs.ChatHub
      */
    chatHub : ChatHub;
}
//#endregion available hubs

///////////////////////
// Service Contracts //
///////////////////////
//#region service contracts


//#region ChatHub hub

interface ChatHub {
    
    /**
      * This property lets you send messages to the ChatHub hub.
      */
    server : ChatHubServer;

    /**
      * The functions on this property should be replaced if you want to 
      * receive messages from the ChatHub hub.
      */
    client : ChatHubClient;
}

interface ChatHubServer {

    /** 
      * Sends a "connect" message to the ChatHub hub.
      * Contract Documentation: ---

      * @param displayName {string} 

      * @return {JQueryPromise of void}
      */
    connect(displayName : string) : JQueryPromise<void>;

    /** 
      * Sends a "disconnect" message to the ChatHub hub.
      * Contract Documentation: ---

      * @return {JQueryPromise of void}
      */
    disconnect() : JQueryPromise<void>;

    /** 
      * Sends a "getConnectedClients" message to the ChatHub hub.
      * Contract Documentation: ---

      * @return {JQueryPromise of ConnectedClients[]}
      */
    getConnectedClients() : JQueryPromise<ConnectedClients[]>;

    /** 
      * Sends a "sendAll" message to the ChatHub hub.
      * Contract Documentation: ---

      * @param message {ChatMessage} 

      * @return {JQueryPromise of void}
      */
    sendAll(message : ChatMessage) : JQueryPromise<void>;

    /** 
      * Sends a "sendTo" message to the ChatHub hub.
      * Contract Documentation: ---

      * @param message {ChatMessage} 

      * @return {JQueryPromise of void}
      */
    sendTo(message : ChatMessage) : JQueryPromise<void>;
}

interface ChatHubClient
{
    /**
      * Set this function with a "function(msg : ChatMessage){}" 
      * to receive the "addNewMessageToPage" message from the ChatHub hub.
      * Contract Documentation: ---

      * @param msg {ChatMessage} 

      * @return {void}
      */
    addNewMessageToPage : (msg : ChatMessage) => void;
}

//#endregion ChatHub hub
//#endregion service contracts

////////////////////
// Data Contracts //
////////////////////
//#region data contracts

/**
  * Data contract for SignalR_TypeScript_BasicChat.hubs.ChatMessage
  */
interface ChatMessage {
    Name : string;
    Message : string;
    ConnectionId : string;
}

/**
  * Data contract for SignalR_TypeScript_BasicChat.hubs.ConnectedClients
  */
interface ConnectedClients {
    ConnectionId : string;
    DisplayName : string;
    LastPingTime : string;
}

//#endregion data contracts

As you can see, this can be a huge time saver, especially if you are changing things a lot or just want to play and not worry about the "boring" stuff like making sure you typings match your C# code Open-mouthed smile.

This article was originally posted at http://31og.com/post/hubstt-will-save-your-life

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect SSW
South Africa South Africa

Comments and Discussions

 
-- There are no messages in this forum --