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" #>
<# #>
<#@ assembly name=
"$(SolutionDir)\packages\Microsoft.AspNet.SignalR.Core.2.0.3\
lib\net45\Microsoft.AspNet.SignalR.Core.dll" #>
<# #>
<#@ 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());
#>
interface SignalR {
<#
foreach (var hub in hubmanager.GetHubs())
{
#>
<#= FirstCharLowered(hub.Name) #> : <#= hub.HubType.Name #>;
<#
}
#>
}
<#
foreach (var hub in hubmanager.GetHubs())
{
var hubType = hub.HubType;
string clientContractName = hubType.Namespace + ".I" + hubType.Name + "Client";
var clientType = hubType.Assembly.GetType(clientContractName);
#>
interface <#= hubType.Name #> {
server : <#= hubType.Name #>Server;
client : <#= clientType != null?(hubType.Name+"Client"):"any"#>;
}
<#
#>
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));
#>
<#= FirstCharLowered(method.Name) #>(<#=string.Join(", ", ps)#>) :
JQueryPromise<<#= GetTypeContractName(method.ReturnType)#>>;
<#
}
#>
}
<#
#>
<#
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);
#>
<#= FirstCharLowered(method.Name) #> : (<#=string.Join(", ", ps)#>) => void;
<#
}
#>
}
<#
}
#>
<#
}
#>
<#
while(viewTypes.Count!=0)
{
var type = viewTypes.Pop();
#>
interface <#= GenericSpecificName(type) #> {
<#
foreach (var property in type.GetProperties
(BindingFlags.Instance|BindingFlags.Public|BindingFlags.DeclaredOnly))
{
#>
<#= property.Name#> : <#= GetTypeContractName(property.PropertyType)#>;
<#
}
#>
}
<#
}
#>
<#+
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)
{
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 . 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);
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);
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.
interface SignalR {
chatHub : ChatHub;
}
interface ChatHub {
server : ChatHubServer;
client : ChatHubClient;
}
interface ChatHubServer {
connect(displayName : string) : JQueryPromise<void>;
disconnect() : JQueryPromise<void>;
getConnectedClients() : JQueryPromise<ConnectedClients[]>;
sendAll(message : ChatMessage) : JQueryPromise<void>;
sendTo(message : ChatMessage) : JQueryPromise<void>;
}
interface ChatHubClient
{
addNewMessageToPage : (msg : ChatMessage) => void;
}
interface ChatMessage {
Name : string;
Message : string;
ConnectionId : string;
}
interface ConnectedClients {
ConnectionId : string;
DisplayName : string;
LastPingTime : string;
}
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 .