0 Comments

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

4 Comments

I have been playing around with TypeScript for a while and usually I just publish from my machine Embarrassed smile but today I decided to setup a CI build for the solution and found that I received the error

 <Path to file>.ts (1): Emit Error: Write to file failed..

When a build agent checks out code for building the primary source files are locked so obviously it wouldn't let me overwrite any as part of my build process.

It took me a couple of minutes to realize it but this error was basically due to me having checked in the .js and .jsmap file that is generated for .ts files when you build a TypeScript enabled project. Simple enough to fix:

1. Remove all .js and .jsmap files that are generated off .ts files from your project

2. Make sure those same files are deleted from source control

3. Kick off the build and smile because things should be running smoothly again Smile