public
Last active

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

  • Download Gist
Hubs.tt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
<#@ 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.1.0.0-rc1\lib\net40\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.d.ts" />
/// <reference path="jquery.d.ts" />
 
////////////////////
// available hubs //
////////////////////
//#region available hubs
 
interface SignalR {
<#
foreach (var hub in hubmanager.GetHubs())
{
#>
 
/**
* The hub implemented by <#=hub.HubType.FullName#>
*/
<#= 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; // 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 "";
}
}
 
#>

Awesome! Except I can't get it to work :-). I threw it into my Hubs project, and updated it to point to my RC2 instance, but I'm having two problems:

(1) I'm referencing the RC2 SignalR libraries like so:

<#@ assembly name="$(SolutionDir)packages\Microsoft.AspNet.SignalR.Core.1.0.0-rc2\lib\net40\Microsoft.AspNet.SignalR.Core.dll" #>

But when I try to debug the template, it tells me that the given assembly name or codebase was invalid.

Oddly enough, giving it a fully qualified name does work, at least, it doesn't throw any errors:

<#@ assembly name="C:\source\Alanta\AlantaClient\packages\Microsoft.AspNet.SignalR.Core.1.0.0-rc2\lib\net40\Microsoft.AspNet.SignalR.Core.dll" #>

(2) When I reference the full-qualified file name, it runs, but gives me a mostly empty .d.ts file. When I debug it, it looks as if hubmanager.GetHubs() is returning empty.

Any thoughts?

@smithkl42, are you using vs 2012 or 2010? I know there's big difference between the two in terms of how it handles dependent files (but i haven't found any good documentation). And have you built your project successfully?

VS2012. My solution builds successfully.

Two thoughts/points of clarification:

(1) I'm pretty unfamiliar with T4 templates, never having written one myself, and only having used them on a few prior occasions. I presume that the way you intended this to work is to drop this into your SignalR project, save it, and then you get your TS definitions file "sitting behind" the .tt file? (As opposed, for instance, to writing code to get the template to generate the definitions at run-time?)

(2) I have my Hubs project sitting in a separate library from my web project. I've tried placing the T4 template both in the Hubs project as well as in my web project, with similar results. I also tried putting it in a fresh web app with a SignalR hub in it, with similar results.

1) Yep, that's correct.
2) My hubs are in my web project, so that could be part of the problem, but i'm surprised that it didn't get fixed when you tried the fresh web app

Maybe the issue is simply the timing of the t4 generation? It doesn't trigger the regeneration until you either save the t4 file or pick "run all templates" from the build menu (which has to be done after the web project is built).

Could you send me your fresh web app for me to test myself? robert dot ensor at gmail...

Actually, I've managed to replicate & fix the problem and have updated this Gist (see line 14), let me know if it works for you now...

I think that fixed it! Many thanks.

For what it's worth, using the Tangible T4 editor, I do get a blue squiggly on this line:

<#@ assembly name="$(TargetPath)" #>

And the associated error says:

The assembly $(TargetPath) could not be loaded. There was an exceptiong during load: A dependency could not be found!

But it does in fact generate the appropriate TS file, which is very helpful. Many thanks!

Yeah I get a squiggly from ForTea as well - but t4 itself can handle expanding the variable - it replaces it with the output dll of the current project.

Awesome template, great work! I made a slightly modified version that takes into account JsonProperty attributes :)

https://gist.github.com/upta/5347739

You must use firstLowerCase for hub property name:

//#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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.