Reinforced.Typings - More Details

  • Tutorial
Hello again.
Who about what, and I again about Reinforced.Typings - my library for generating TypeScript glue-code from C # -assemblies, a brief introduction to which I made in a previous post. After that I immediately received a number of questions and comments (not only on Habrahabr, by the way - many of those interested were simply not registered on it). For this, of course, many thanks to everyone, but based on the information analyzed, I realized that one short post is not enough to describe how and what is being implemented. It turns out that people ask questions “but is this supported?” And everyone has to explain the same thing over and over again. So in this article I will make a small cheatsheet on attributes, fluent configurations and talk about additional features. In general, welcome. Caution, Longrid and background information!


In the previous series


When I first talked about Reinforced.Typings, I casually mentioned that there are different attributes and they have a lot of different properties, there are some code generators and something about the TypeScript build process. Everything is somehow messy, superficial, without details. Let's put it on the shelves. I am as concise as possible, but I will tell you in detail about the various configuration options, the assembly script, how it all works together and what can be fashioned out of it all.
In general, this article is a kind of manual / training manual / cheatsheet by Reinforced.Typings, because I still do not have time and resources to write full documentation in English. Of course, not everything is so sad - I have already gathered my courage and wrote the whole documentation plan. However, for more (+ an article on Habrahabr), the "father of Russian democracy" was not enough. So let's go.
PS: It’s worth noting that I still counted a little and during the time that has passed since the last article was written, I added some features to the library, fixed bugs (without them). Accordingly, the material presented is relevant from version 1.0.7.

How does it work in general?


You are relaxed, satisfied, you have warm feet and you write in C # and ASP.NET MVC using TypeScript. You install Reinforced.Typings from NuGet with the command

PM > Install-Package Reinforced.Typings

Reinforced.Typings is immediately integrated into the build process of your application, starting with every build of the project. This moment is controlled from the Reinforced.Typings.settings.xml file, which is added to the root of your project - I call it the “build configuration”. Having started, Reinforced.Typings does its dark job, loading the assembly of your project and crawling into it through Reflection. The desired TypeScript glue-code (for example, TypeScript-interfaces corresponding to your server TransferObjects / View-models transmitted to the client) is generated based on two possible factors:
  • Attribute configuration - that is, types and their members are exported, marked with special attributes (their list and properties will be given below)
  • Fluent configurations - when, to find out how and what you want to generate, the method specified in the assembly config is called. This configuration option will also be covered later.

Attribute and fluent configurations can be combined - this is not forbidden. They basically provide equal opportunities, but vary in flexibility.
In general, all the secret knowledge necessary to use Reinforced.Typings is to navigate its configuration parameters. And there are only 3 settings points mentioned above - assembly configuration (Reinforced.Typings.settings.xml), attribute and fluent configuration. The assembly configuration determines the global parameters of the entire process, such as which files the generated code will be written to, whether to export documentation, which assemblies to connect, etc. Attribute and fluent configurations determine which code to generate and for which types.

Assembly configuration


Reinforced.Typings generates your taps every time you rebuild the project. As mentioned above, it is embedded in the project assembly process. This is done through the mechanism of embedding .targets and .props files from a package that appeared in version NuGet 2.5.
By the way
The generator itself is moved to the rtcli.exe tool, which is launched using the included RtCli MSBuild task. In a word, everything is in the best traditions of calling custom tools during a build. In principle, nothing stops you from brazenly tearing out rtcli.exe and Reinforced.Typings.dll from the / tools directory of the package itself and use it as you wish (as well as the RtCli task located in /build/Reinforced.Typings.Integrate.dll) , but let's be objective - few will really do it.

Some parameters of the generator launch are made in the Reinforced.Typings.settings.xml file, which is a piece of the MSBuild script that is connected in the .targets file of the package itself. Reinforced.Typings.settings.xml is added to the root of your project when you install the package. By default, it looks like this . I would not argue that it is well documented (mainly because of the broken English), so I bring a cheatsheet with a description of the configuration parameters:
Parameters in Reinforced.Typings.settings.xml
RtTargetFile (string) The
full path to the file in which all generated tippings will be written. In it (as in all other parameters), you can use MSBuild variables, including those specific to Microsoft.Common.CurrentVersion.targets. This parameter is the key and required when you just need to drop the tippings for your project into one file, but it is not used if the function of splitting the generated code into many files is enabled (see below). Do not forget to add this file to your project by hand!

RtConfigurationMethod (string)
Specifies the fully qualified name of the method that will be called to build the fluent configuration. For example My.Assembly.Configuration.ConfigureTypings. You don’t need to specify the assembly - Reinforced.Typings will find this method either in the assembly of the project itself or in its references. The method must be static and accept a single parameter of type Reinforce.Typings.Fluent.ConfigurationBuilder as an input (the parameter name is not important). It is worth noting that you still have the ability to use attributes, even if you use the fluent configuration. However, keep in mind that if there is a fluent and an attribute configuration for the same member, then fluent will be preferred.

RtWriteWarningComment (true / false)
Controls the writing to the output file of a warning that the file is generated automatically. If this parameter is set to true, then the caption "// This code was generated by a Reinforced.Typings tool." Blah blah blah will flaunt in the header of each generated file. To be honest, I do not know who this parameter can be useful for, but it is.

RtExportPureTypings (true / false)
Causes RT to generate TypeScript types (.d.ts) instead of regular TypeScript. Enabling this configuration parameter forces the library to justify its name (although it is disabled by default). And the thing is that the syntax of .d.ts and .ts is slightly different. Hence the need for the existence of this parameter.

RtDivideTypesAmongFiles (true / false)
When this parameter is false, then all generated tippings will be written to a single file specified in the RtTargetFile parameter. This is not always convenient, because it can lead, for example, to monstrous merges when using SCM. If you set this parameter to true, then the generated TypeScript will be scattered into different files (class-per-file). In this case, the RtTargetFile will be ignored. Just remember to add the generated files to your project with your hands as they appear!
You can configure what and where to put it using the [TsFile] / fluent .ExportTo attribute. It is worth noting that RT itself will perfectly understand the addition of /// directivesto neighboring types used in this case. But in case of difficulty, you can always help him with the [TsAddTypeReference] / fluent attribute .AddReference

RtTargetDirectory (string)
Used in conjunction with RtDivideTypesAmongFiles to specify the directory into which all generated files will be dumped. Be sure to specify this parameter if you are using RtDivideTypesAmongFiles.

RtRootNamespace (string)
Also used in conjunction with RtDivideTypesAmongFiles. The fact is that through Reflection it is impossible to determine the root namespace of the assembly. And without this, it will not be possible to correctly scatter the generated files into directories.

RtBypassTypeScriptCompilation (true / false)
TypeScript is built first when building the project. Sometimes a situation arises that you generated tippings, and they made the TypeScript-code of your project incomparable. And in order to fix this, you need to rebuild the project and regenerate the taiping, however you cannot do this, since the project is not going to because the time scripts are not going to. The way out of this vicious circle is setting RtBypassTypeScriptCompilation to true. This setting disables the assembly of time scripts before building the project and collects them after the project is built, which makes the .dll-file of your project calmly assemble, and RT - generate fresh taiping from it. Please remember to return this parameter to false when the problem is resolved. Otherwise, there may be problems with publishing of collected javascripts.

RtCamelCaseForMethods (true / false)(implemented feature request Tremor )
Forcibly converts the names of all methods to camelCase (instead of the traditional .NET PascalCase). Used by aesthetics from javascript. Also, camelCase-ing can be controlled separately for each method by using the ShouldBeCamelCased property of the TsFunction attribute.

RtCamelCaseForProperties (true / false) ( Tremor feature request implemented )
Same as RtCamelCaseForMethods, only for properties. ShouldBeCamelCased also has TsProperty.

RtGenerateDocumentation (true / false)
When set to true, as well as enabling documentation generation in XML in the settings of your project, Reinforced.Typings will extract the path to the XMLDOC file during the build process and convert it to jsdoc, which it will append to the generated files. Everything is fine with this approach, except that by default, after you include XML documentation in the export project, the compiler starts bombarding you with undocumented public classes / class members. Not that it somehow interfered technically, but visually infuriates.

RtDisable (true / false)
Disables Reinforced.Typings. While this parameter is true, the procedure for generating tipping will not be called. However, RtBypassTypeScriptCompilation will still be active.

Item group RtAdditionalAssembly
You can drop additional assemblies into this Item-group that Reinforced.Typings should take into account when exporting (read: export taipings from them too). RT has full paths to the References of your project, so in RtAdditionalAssembly you can simply include the name of the assembly (everything that comes before .dll).


Attributes


One assembly configuration is not enough for a successful export. You will also have to tell RT exactly what you want to see in the generated TypeScript files. This can be done, for example, by hanging the appropriate attributes over the exported types (classes, interfaces, enumerations) and their members. This I call “attribute configuration”. There is also a fluent configuration, but I will talk about it a little later, because it is completely based on the attribute configuration and repeats it in many ways, and therefore it will be more pedagogical to talk about the attribute configuration first.
All the Reinforced.Typings attributes lie, oddly enough, in the Reinforced.Typings.Attributes namespace, which can confuse the newcomer (sarcasm). All attributes can be inherited. All properties are overloaded. As one of the available work techniques, you can inherit from any of them and make your attribute so that you don’t drag a pack of parameters with you every time.
Below I give a cheatsheet for all available attributes. The expression “required attribute” in it means that if you do not set this attribute, then the corresponding entity will not be exported to TypeScript.

Attribute nameRequired?Result in tippingExportable (patient)
TsInterfaceYesTypeScript interfaceClass, interface, structure
TsClassYesTypeScript classClass structure
TsPropertyNotInterface / Class FieldProperty, field (class / structure)
TsfunctionNotInterface / class method (in the case of the class - the body is exported as return null; if the method is non-void and as empty in the case of void)Property, field (class / structure)
TsEnumYesTypeScript enumerationenum obviously
TsValueNotOne of the values ​​of a TypeScript enumerationThe value of enum is not obvious
TsParameterNotParameter (formal argument) of the TypeScript methodMethod parameter, oddly enough
TsGenericNotType parameter of the TypeScript method / class / whateverParameter type
TsIgnoreNotPatient will not be exported to TypeScriptProperty, constructor (!), Field, method, parameter

A little bit about exporting constructors
I would also like to mention the TsBaseParam attribute. RT can export classes, as well as their constructors. In the case when you inherit classes from each other and explicitly call the constructor of the ancestor in the constructor of the heir using: base () - no information about this can be obtained through Reflection. Therefore, for the correct export of such cases, you can put the TsBaseParam attribute on the constructor. It has one constructor and it takes an input of an array of strings in which you can write any TypeScript expressions. This will all be written in TypeScript super (...). In practice, I have little idea of ​​the usefulness of this attribute, but it is. It is believed that this attribute exists simply to show how cool Reinforced.Typings is.

Otherwise, if you enable the export of constructors by the corresponding property of the [TsClass] attribute, then they will be exported correctly. That is, no special attributes are provided for designers.


Attribute Properties


There are a lot of properties, so I turned their list into a spoiler, so as not to clog the article. There, in the list of properties, I specify the name of the property, in parentheses the default value of the property and describe what it controls. This is for the most part reference information, which is duplicated in XMLDOCs, only in English. According to this - open carefully.

Footcloth text
TsInterface

  • AutoI (true) - whether to automatically put the letter I in front of the patient's name (if it is not)
  • AutoExportMethods (true) - whether to automatically export all patient methods. If not, you will put [TsFunction] on the methods yourself
  • AutoExportProperties (true) - same as above, but for property and [TsProperty]
  • IncludeNamespace (true) - whether to put the patient in module when exporting
  • Name (null) - overrides the name of the patient
  • Namespace (null) - overrides the patient's namespace

TsClass

  • AutoExportProperties , AutoExportMethods , IncludeNamespace , Name , Namespace - similar to TsInterface properties. A plus:
  • AutoExportFields (true) - same as AutoExportProperties, but for fields
  • DefaultMethodCodeGenerator (null) - allows you to override the code generator immediately for all exported methods of the class

TsProperty

  • Type (null, the type name is substituted) - overrides the name of the patient type in TypeScript. Like a string. Well, that is, you can write anything at all. It helps when the type of the property is not displayed in TS (you get any), or you need to make a reference to a non-exported type. For example - in jQuery
  • StrongType (null, the type name is substituted) is the same as Type, but you can specify the .NET type. Convenient for delegates for example - wrote StrongType = typeof (Func) and order, no need to bathe with tons of brackets
  • Name (null) - overrides the name of the patient
  • ForceNullable (false) - says to force the field to be nullable. Well, that is, turn field: boolean into field?: Boolean
  • ShouldBeCamelCased (false) - whether the property name should be converted to camelCase (implemented feature request Tremor )

Tsfunction

  • Type , StrongType - similar to TsProperty properties, but overrides the return type of the method
  • Name (null) - overrides the name of the patient
  • ShouldBeCamelCased (false) - whether the method name should be converted to camelCase (the feature request Tremor is implemented )

TsEnum

  • IncludeNamespace , Name , Namespace - similar to TsInterface properties. It has no more parameters.

TsValue

  • Name - overrides the name of the patient (the specific value of the enum, that is)

TsParameter

  • Type , StrongType - similar to TsProperty properties, but overriding the type of the method argument
  • Name (null) - overrides the name of the patient
  • DefaultValue (null) - indicates the default value. To be substituted as parameter: boolean = false , for example. Caution, you can shoot yourself in the foot.
  • ShouldBeCamelCased (false) - whether the parameter name should be converted to camelCase ( Tremor feature request is implemented )

TsGeneric

  • Type , StrongType - similar to TsProperty properties, but overriding type parameter type

TsBaseParam

  • Values is an array of strings representing TypeScript expressions. The contents of which will be used to generate a call to super (...) in the case of exporting a TypeScript class

TsAddTypeReference

  • RawPath - path to the file to be written in the /// directiveto be added to the file with the exported type
  • Type - type, path to the file containing which will be added in the /// directive to file with export type


It is worth noting that the attributes have a small hierarchy of inheritance, so some properties are in several attributes and do roughly the same thing (say, Name, overrides the name of the exported class / interface, but it also works for method or property parameters).

Export TypeScript code to multiple files


This feature is enabled when RtDivideTypesAmongFiles is set to true in the build config. By default, without additional configuration, RT will scatter all your classes according to the good old tradition of OO languages ​​- class it in a separate file, and even put it in a subdirectory according to Namespace (in order not to spoil unnecessary directories in this regard, it is recommended to use assembly config parameter RtRootNamespace). There is also one nuance - the studio does not always see that any of the types used in the .ts file lies in the neighboring file and underlines how much in vain. To prevent this situation from occurring, add the /// directive to all .ts files. So - in most cases, RT manages to correctly place this directive by inspecting type dependencies by looking at property / field types, return values ​​of methods, parameter types, etc. And this is priceless. For the rest, there is a MasterCard attribute TsAddTypeReference. In the spoiler below, we will talk about it, as well as about some other attributes that affect the scattering of TypeScript code into different files.

Another foot of text
  • TsAddTypeReference - this attribute allows you to add the /// directiveto the file in which the generated code for the patient will be written. This is necessary when, for example, you redefine Type with a string for any exported members and inspect references via Reflection is not possible. You can specify the type as the constructor parameter of this attribute (for example, [TsAddTypeReference (typeof (AnotherExportedType))]). Then RT itself will determine in which file it lies and will generate the corresponding path for the directive. If an explicit path is specified (for example, [TsAddTypeReference ("../../ jquery.d.ts")]), it will be used to add the directive. This attribute is hung on classes, interfaces, and enums. It is allowed to use several times.
  • TsFile - Indicates in which file to put the generated code for the patient. This attribute is active only when RtDivideTypesAmongFiles in the assembly configuration is set to true. The path in this attribute is relative to RtTargetDirectory
  • TsReference - Since we started talking about processing references, we should mention this attribute. It indicates that the /// directive should be added to the generated file (or all files)to the specified path. Without reference to any particular type. The reference specified by this attribute will be added to each generated file. Useful for adding reference, for example, for tipping for jQuery. This attribute is hung on the assembly, therefore, it must be written as [assembly: TsReference ("~ / Scripts / typings / jquery.d.ts")]. It is also allowed to use it several times.




CodeGenerator


Almost all attributes are inherited from TsAttributeBase, which defines just one property - CodeGeneratorType of type Type. What it is? And this is the ability to specify the type of code generator for any member of the class or type. A code generator is a special class that must implement the Reinforced.Typings.Generators.ITsCodeGenerator <> interface, parameterized:
  • System.Type in case of code generator for type (used with TsEnum, TsClass, TsInterface)
  • System.Reflection.PropertyInfo in case of code generator for property (used with TsProperty)
  • System.Reflection.FieldInfo in case of code generator for a field (used with TsField)
  • System.Reflection.MethodInfo in case of code generator for a method (used with TsFunction)
  • System.Reflection.ParameterInfo in case of a code generator for a method parameter (used with TsParameter)

ITsCodeGenerator <> defines just one property (Settings), which is enough to implement as an auto-property and just one method - Generate, which takes an input:
  • TElement, with which the generator is parameterized - the entity for which TypeScript is to be generated - code
  • TypeResolver is a class whose most valuable method is the ResolveTypeName method, which takes a type as input. It returns a TypeScript-friendly type name that can be written into the generated code.
  • WriterWrapper - a small wrapper over the TextWrapper in which you want to write the generated code
  • You can also use the Settings property inside the Generate method to, for example, view (or write to the output stream) the documentation for the generated entity

RT has several ready-made generators for classes, interfaces, enums, field parameters, methods and property - they all lie in the Reinforced.Typings.Generators namespace and can be inherited from them. Of course, nothing prevents you from making your code generator by directly implementing the ITsCodeGenerator <> interface.
When you have made your code generator, you need to specify that you want to use it by setting the CodeGeneratorType property of the corresponding attribute in typeof from your generator. Unfortunately, there is no type safety control here. Well, that is, if you specify CodeGeneratorType = typeof (string), then IntelliSense will not say that something is wrong. However, the same functionality is provided in the fluent configuration (.WithCodeGenerator <> method), and they will not let you specify the wrong code generator there.

А вот например
..., в моем текущем проекте, я унаследовался от Reinforced.Typings.Generators.MethodCodeGenerator, сделав свой генератор ActionInvokeGenerator, который пишет glue-code для вызова метода MVC WebAPI и возврата promise. После этого пометил нужные контроллеры, методы которых надо пробросить в TypeScript атрибутом [TsClass(AutoExportMethods = false)], а сами методы — с [TsFunction(CodeGeneratorType = typeof(ActionInvokeGenerator))]. В результате получил обертку для интересующих меня методов на клиенте. Об этом замечательно опыте я расскажу в следующей статье.


Fluent-конфигурация


Placing attributes on exported types is not always convenient. For example, in the case when you need to export a hundred types from a specific namespace, while tearing certain properties out of them and prescribing a specific export configuration for them, placing attributes becomes a little boring and monotonous. In addition, you cannot place export attributes over entities whose source code you do not have access to. Just to overcome this circumstance, in version 1.0.5 the ability of fluent-configuration was added. You can start using it along with attributes in 2 simple steps:
  1. Create a separate class, declare a public static method in it with a single parameter of type Reinforced.Typings.Fluent.ConfigurationBuilder. For example, like this:
    using Reinforced.Typings.Fluent;
    namespace TestProject.App_Start
    {
        public class TypingsConfiguration
        {
            public static void ConfigureTypings(ConfigurationBuilder builder)
            {
                // тут будет ваша fluent-конфигурация
            }
        }
    }
    

  2. In the build configuration, specify the fluent method to use. For example, like this:
    TestProject.App_Start.TypingsConfiguration.ConfigureTypings


All. Now this method will be called every time when regenerating taipings (read: when rebuilding projects) and prepare the export configuration. Through the fluent configuration, it is quite easy to export bundles of the same type of interfaces and classes with the same type of configuration (the Tremor feature request is implemented , albeit in a slightly different form), which makes the task easier in case of a large project. Also a definite plus, in my opinion, is a strongly typed code generator indication, thanks to which you will never mistakenly use a code generator for a method on properties and vice versa.

With your permission, I will not describe in detail all the methods of ConfigurationBuilder (and all nested builders), because they are intuitively clear and also have good XMLDOC. Here is just a small piece of fluent configuration code to create an idea of ​​what it looks like:
Piece of code
using Reinforced.Typings.Fluent;
namespace TestProject.App_Start
{
    public class TypingsConfiguration
    {
        public static void ConfigureTypings(ConfigurationBuilder builder)
        {
            builder.ExportAsInterface()
                .WithPublicProperties()
                .WithProperty(c => c.Email).Type();
            builder.ExportAsInterface()
                .WithPublicProperties()
                .WithPublicMethods()
                .WithMethod(c => c.FillIn(Ts.Parameter(o => o.Type())))
                .Returns();
            builder.ExportAsInterface()
                .WithMethod(c => c.AddOrders(Ts.Parameter()));
            builder.ExportAsInterfaces(
                new[] {
                    typeof (ILoginPage), 
                    typeof (ILoginInformation),
                    typeof(IOrder)
                },
                c => c.ExportTo("login.ts").WithAllMethods(m => m.CamelCase()));
        }
    }
}


I think that the reader will easily understand what this configuration does. However, I can not resist a couple of remarks.
Remark times: TsBaseParam and TsGeneric remain available only as attributes. I was not able to come up with a beautiful fluent approach to configure these points. On the other hand, I rightly consider the removal of this into the fluent configuration to be somewhat overkill. In a word - use attributes for such bottlenecks.
There are two remarks: Ts.Parameter and the TryLookupDocumentationForAssembly method need a separate explanation.
Ts.Parameter is a special static method that can take the fluent-configuration builder for method parameters as input. That is, when you want to set the configuration for a method parameter, you just say something like
builder.ExportAsInterface()
    .WithMethod(m => m.MyMethod(
            Ts.Parameter(p => p.OverrideName("apple").Type())
              ))
    .Returns();

чтобы получить в TypeScript-е
export IMyClass {
	MyMethod(apple:string):any;
}
/*
При исходных данных
class MyClass
{
	public int MyMethod(int a)
	{
		return 0;
	}
}
*/

На мой взгляд, удобный и элегантный способ.
TryLookupDocumentationForAssembly — это метод самого ConfigurationBuilder-а, который говорит RT поискать файл с xml-документацией для указанной сборки рядом с самой сборкой. Имеет одну перегрузку, которая позволяет указать имя файла непосредственно (таким образом сборка будет использована только для определения директории, в которой надо искать). Я пришел к необходимости добавить этот метод, когда встала необходимость экспортировать документацию из другой сборки.
Вот. Теперь точно всё.

Заключение


Статья вышла довольно крупной, что, в принципе, соответствовало моей цели сделать более-менее подробный мануал при полном отсутствии документации по проекту и категорическом недостатке свободного времени для авторства. Еще меня не хватило на примеры использования, но им я посвящу следующую статью и постараюсь дописать и докоммитить примеры на github. Не стреляйте в пианиста — он пишет когда есть время.
Как обычно, я открыт к пожеланиям-предложениям и прочему feedback-у. Если в ходе использования вы найдете баги — не стесняйтесь отправлять issues на github проекта. Неожиданным pull-реквестам я так же очень рад — писать в одного, совмещая с fulltime-работой становится тяжеловато.
NuGet-пакет проекта лежит там же, где и лежал.

Also popular now: