Skip to content

Instantly share code, notes, and snippets.

@robfe
Last active March 18, 2020 16:17
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save robfe/4583549 to your computer and use it in GitHub Desktop.
Save robfe/4583549 to your computer and use it in GitHub Desktop.
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
<#@ 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.2.0\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.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.NameSpecified?hub.Name:FirstCharLowered(hub.Name) #> : <#= hub.HubType.Name #>;
<#
}
#>
}
//#endregion available hubs
///////////////////////
// Service Contracts //
///////////////////////
//#region service contracts
<#
foreach (var hub in hubmanager.GetHubs())
{
var hubType = hub.HubType;
Type clientType = ClientType(hubType);
#>
//#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?clientType.Name:"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 <#= clientType.Name #>
{
<#
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)
{
string name = type.Name.Split('`').First();
if (type.IsGenericType)
{
name += "<"+string.Join(", ", 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 Type ClientType(Type hubType)
{
while (hubType != null && hubType != typeof(Hub))
{
if (hubType.IsGenericType && hubType.GetGenericTypeDefinition() == typeof (Hub<>))
{
return hubType.GetGenericArguments().Single();
}
hubType = hubType.BaseType;
}
return null;
}
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 "";
}
}
#>
@smithkl42
Copy link

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.

@robfe
Copy link
Author

robfe commented Jan 21, 2013

  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...

@robfe
Copy link
Author

robfe commented Jan 22, 2013

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...

@smithkl42
Copy link

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!

@robfe
Copy link
Author

robfe commented Jan 23, 2013

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.

@upta
Copy link

upta commented Apr 9, 2013

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

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

@plasmaboyer
Copy link

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

@yetanotherchris
Copy link

3 years later...if anyone wants an alternative to T4 templates, I wrote a command line tool based off the code in this gist:

You can add it as a post-build step which makes build server integration a little easier (also we were having problems with conflicting version of JSON.NET).

@Jacknq
Copy link

Jacknq commented Sep 27, 2017

webapi vesion of this template?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment