Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Hubs.tt will save your life

, 16 Apr 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
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 "IClient", a TS int

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.

<#@ 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 simple copy it to ~/Scripts/typings/Hubs.tt and watch the magic happen Smile. Currently I have a simple hub like below

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.

// 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 changing things a lot or just want to play and not worry about the "boring" stuff like making sure you typing's 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)

Share

About the Author

Gordon W Beeming
Software Developer Derivco
South Africa South Africa
Gordon Beeming is a Software Developer at Derivco in the sunny city of Durban, South Africa. He spends most his time hacking away at the keyboard in Visual Studio or with his family relaxing. He is a Visual Studio ALM Rangers, Visual Studio ALM MVP and Friend of Red Gate. His blog is at 31og.com and you can follow him on Twitter at twitter.com/gordonbeeming
 
http://31og.com
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150129.1 | Last Updated 16 Apr 2014
Article Copyright 2014 by Gordon W Beeming
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid