Hubs.tt will save your life
Updates have been made, see the end of the post 🙂
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 hubsinterface 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#> hubinterface <#= 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 🙂. 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 hubsinterface SignalR { /** * The hub implemented by SignalR_TypeScript_BasicChat.hubs.ChatHub */ chatHub : ChatHub;}//#endregion available hubs///////////////////////// Service Contracts ///////////////////////////#region service contracts//#region ChatHub hubinterface 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 contractsAs 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 😁.
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 🙂
