Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

Hubs.tt Will Save Your Life

5.00/5 (4 votes)
16 Apr 2014CPOL1 min read 14K  
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.

License

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