This is part 2 of the series about how we generate code from .Net for TypeScript:
- Why do we even bother?
- Generating TypeScript constants from .Net constants
- Finding the types used in communication between the .Net backend and the TypeScript client
- Generating TypeScript classes from .Net types
- Generating Angular Services from .Net WebApi Controllers
- TypeScript-friendly JSON serialization of F# types
- Testing JSON serialization and deserialization
- Putting all the parts together
We start with the simplest part, generating constants.
Generating TypeScript Constants from .Net Constants
We need to share constants defined in our .Net backend with our TypeScript client. We have constants that are string
s, GUID
s, or wrappers around string
s or GUID
s.
We do this with this code (explanation below):
module W2w.ConstantsWriter
open System
open System.Reflection
open System.IO
type ConstantInfo = { Name : string ; Value : string }
type ConstantClassInfo =
{
ClassName : string
Constants : ConstantInfo []
}
let (|String|StringWrapper|Guid|GuidWrapper|Unknown|) (value : obj) =
match value with
| null -> Unknown
| :? string as s -> String s
| :? Guid as g -> Guid g
| _ ->
let valueProperty = value.GetType().GetProperty("Value")
match valueProperty with
| null -> Unknown
| _ ->
match valueProperty.PropertyType with
| x when x = typeof<Guid> ->
let g : Guid = downcast (valueProperty.GetValue(value))
GuidWrapper g
| x when x = typeof<String> ->
let s : string = downcast (valueProperty.GetValue(value))
StringWrapper s
| _ -> Unknown
let writeConstants (assemblies : Assembly []) (outputPath : string) =
let types =
assemblies
|> Array.collect (fun a -> a.GetTypes())
|> Array.sortBy (fun t -> t.Name)
let staticClasses =
types
|> Array.filter (fun t -> t.IsClass && t.IsAbstract && t.IsSealed)
let constantsPerClass =
let getConstantsFromType (c : Type) =
c.GetFields(BindingFlags.Public ||| BindingFlags.Static)
|> Array.choose (fun field ->
match field.GetValue(null) with
| String s -> s |> Some
| StringWrapper s -> s |> Some
| Guid g -> g.ToString().ToUpper() |> Some
| GuidWrapper g -> g.ToString().ToUpper() |> Some
| Unknown -> None
|> Option.map (fun v ->
{
ConstantInfo.Name = field.Name
Value = v
}))
staticClasses
|> Array.map (fun c ->
{
ConstantClassInfo.ClassName = c.Name
Constants = getConstantsFromType c
})
let infos =
constantsPerClass
|> Array.filter (fun c -> c.Constants |> Array.isEmpty |> not)
|> Array.map (fun c ->
let constants =
c.Constants
|> Array.map (fun c ->
$""" static {c.Name |> TypeWriter.toSmallCamelCase} = "{c.Value}" """)
sprintf
$"""
export class {c.ClassName}
{{
{constants |> String.concat Environment.NewLine}
}}""")
let content =
infos |> String.concat Environment.NewLine
File.WriteAllText(outputPath, content, System.Text.UTF8Encoding())
F#
Line 7: ConstantInfo
holds the name and the value of a constant.
Line 9: ConstantClassInfo
holds the name of the class that contains constants and an array ConstantInfo
s.
Line 15: I use this active pattern to make finding constants a bit easier. Our constants come in different variants. They can be one of these:
- A simple string constant:
public static const MyConstant = "my constant value";
- A simple GUID:
public static readonly Guid MyConstant = Guid.Parse("...")
- A wrapped string constant. We wrap identifiers in C# with classes or records so that we get better type safety. These wrappers always have a property with the name
Value
of the typestring
. This is a strict code convention we follow. - A wrapped GUID constant, analogue to the wrapped string constant.
The active pattern tries to match the specified value with one of the cases explained above.
At the moment, we only have to parse constants from C# code. When we add constants in F# code, I’ll extend the active pattern accordingly to find [<Literal<]
s. Additionally, it doesn’t matter for us when we find some false positives and generate some “constants” that are not used by the client.
Line 35: The writeConstants
function takes the assemblies it has to parse for constants and the path of the file to create. I’ll show how we get the assemblies in Part 6.
Line 36: We get all types from the specified assemblies and sort the types alphabetically. Sorting helps reduce noise when diffing in git and makes it easier to check the generated code when new constants are added.
Line 41: We get all the static classes. This is another convention that all constants that have to be shared have to be inside a static class.
Line 46: We get all public static fields and try to match them with one of the constant variants shown above.
Line 59: I’m not proud of using a null
here, but because of the nesting, it is easier than using an Option
. If you know a better solution, please let me know in the comments or on Twitter (@ursenzler).
Line 61: Filter out all fields that we didn’t recognise as a constant.
Line 63: we build the data that contains all classes with their contained constants.
Line 73: We filter out all classes that do not contain any constants.
Line 74-88: We write the TypeScript code for each class and its constants. We handle all constants as strings. That is good enough at the moment.
Line 90: Let’s write the file to disk.
The result looks like this (only a small fragment shown):
export class Activities
{
static createdType = "cre"
static deletedType = "del"
static rejectedType = "rej"
static defaultValueSetType = "def"
}
export class ColorSchemeConstants
{
static defaultSchemeId = "1246972D-B526-4CF8-A5A3-FAA8F2470A78"
}
In the next post, I’ll show you how we find the .Net types for which we need to generate their TypeScript equivalents.
This blog post is made possible with the support of Time Rocket, the product this blog post is about. Take a look (German only).
[…] the next post, I’ll show you how we generate constants that need to be […]
[…] Generating TypeScript constants from .Net constants […]
[…] Generating TypeScript constants from .Net constants […]
[…] Generating TypeScript constants from .Net constants […]
I would avoid using the null with by using Array.choose and Option.map like this:
let getConstantsFromType (c : Type) =
c.GetFields(BindingFlags.Public ||| BindingFlags.Static)
|> Array.choose (fun field ->
let v = field.GetValue(null)
match v with
| String s -> s |> Some
| StringWrapper s -> s |> Some
| Guid g -> g.ToString().ToUpper() |> Some
| GuidWrapper g -> g.ToString().ToUpper() |> Some
| Unknown -> None
|> Option.map (fun v ->
{
ConstantInfo.Name = field.Name
Value = v
}
)
)
Yes, the null was a hack. Thanks for the better solution!