Hubs.tt will save your life

category: [ Visual Studio ] tags: [ GitHub Projects ] [ SignalR ] [ TypeScript ]
created: 24 Apr 2014 @ 15:58 modified: 24 Apr 2017 @ 13:57


Updates have been made, see the end of the post Smile

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.

UPDATE 22-Apr-2014: Also available on GitHub using the gist link https://gist.github.com/Gordon-Beeming/11166590 

Update 24-Apr-2014: This has been added to Web Essentials now as well and will be available in the next release and is currently available in the Web Essentials Nightly Build, https://github.com/madskristensen/WebEssentials2013/pull/926 Smile

image

ABOUT ME

Gordon Beeming works at Nologo Studios in the sunny city of Durban, South Africa. He is the Lead for the Data and Services Team and has a strong focus on Developer Efficiencies and R&D. When he's not hacking away at a keyboard in Visual Studio he'll generally be relaxing with his family or hitting the black top getting in some mileage. He is a Visual Studio ALM Ranger and Visual Studio ALM MVP.

Follow me on Strava

TOOLS

I plan on writing a bunch of online tools and sharing the code for how I made those tools. If you have any feedback you can ping me on Twitter (@GordonBeeming) or mail me [email protected].