This is part 3 of how we generate types from our .Net backend to be used in our TypeScript client.
- 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
Be prepared, a lot of reflection ahead!
We use ASP.NET WebApi Controllers to provide HTTP endpoints to our client. To find all the types that we need to generate in TypeScript for the client to consume, we look for all input parameter types and return types of the web methods.
The code you will see below is neither beautiful nor fast. It does not have to be. It is okay for us how it is. But of course, for the sake of learning F#, let me know what could be improved.
Finding the types used in communication between the .Net backend and the TypeScript client
I just throw some code at you, the explanation is below:
module W2w.TypeReflector open System open System.Collections.Generic open System.Reflection open <some of our namespaces> open Microsoft.FSharp.Reflection let getReferencedTypes (relevantAssemblies : Assembly) (t : Type) = if (t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<IEnumerable<_>>) || (t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<IReadOnlyCollection<_>>) || (t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<IReadOnlyList<_>>) then t.GetGenericArguments() elif t.IsArray then [| t.GetElementType() |] elif t.IsInterface then let implements (i : Type) (c : Type) = c.GetInterfaces() |> Array.map (fun x -> if i.IsGenericTypeDefinition && x.IsGenericType then x.GetGenericTypeDefinition() else x) |> Array.contains i if t.IsGenericType then let t' = t.GetGenericTypeDefinition() let implementations = relevantAssemblies |> Array.collect (fun a -> a.GetTypes()) |> Array.filter (implements t') implementations else let implementations = relevantAssemblies |> Array.collect (fun a -> a.GetTypes()) |> Array.filter (implements t) implementations //elif (t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<List<_>>) then elif not (t.FullName = null) && t.FullName.StartsWith("Microsoft.FSharp.Collections.FSharpList") then [| t.GetProperty("Head").PropertyType |] elif FSharpType.IsUnion t then FSharpType.GetUnionCases(t) |> Array.collect (fun case -> case.GetFields() |> Array.map (fun field -> field.PropertyType)) elif (t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Maybe<_>>) then t.GetGenericArguments() elif (FSharpType.IsTuple t) then FSharpType.GetTupleElements t else let referencedTypes = t.GetProperties() |> Array.map (fun property -> property.PropertyType) // we only scan properties (the only thing that is serialized to JSON |> Array.filter (fun typ -> typ <> t) // ignore self-reference (cycle) |> Array.filter (fun typ -> typ.IsGenericType || // ignore types from irrelevant assemblies except generic types, arrays and tuples typ.IsArray || FSharpType.IsTuple typ || relevantAssemblies |> Array.contains typ.Assembly) // because the generic type parameter could be of interest let interfaces = t.GetInterfaces() |> Array.filter (fun i -> relevantAssemblies |> Array.contains i.Assembly) referencedTypes |> Array.append interfaces let rec getTypesRecursively (relevantAssemblies : Assembly) (additionalTypes : Type list) (types : Type seq) = let q = Queue<Type>(types) let processedTypes = HashSet<Type>() let result = List<Type>() while (q.Count > 0) do //printfn "----" //printfn "queue length = %i" q.Count let t = q.Dequeue() if relevantAssemblies |> Array.contains t.Assembly || additionalTypes |> List.contains t then if not(t.IsArray) then // ignore Foo that is reported to be from the assembly where Foo is from if t.IsGenericParameter then () elif t.IsGenericType then // only include the generic type in the result, not the specific ones (Foo<A> -> Foo<>) result.Add(t.GetGenericTypeDefinition()) else result.Add(t) let newType = processedTypes.Add t if newType then let referencedTypes = getReferencedTypes relevantAssemblies t //printfn "type %s references:" t.FullName for r in referencedTypes do //printfn "- %s" r.FullName if not(processedTypes.Contains(r)) then q.Enqueue r result let getTypesRecursivelyUsedByControllers (controllersAssembly : Assembly) (relevantAssemblies : Assembly) (additionalTypes : Type list) (ignoredTypes : Type list) = controllersAssembly.ExportedTypes |> Seq.filter (fun t -> t.BaseType = typedefof<WebsiteApiController> || t.BaseType = typeof<DevicesApiController>) |> Seq.collect (fun controller -> controller.GetMethods(BindingFlags.Public ||| BindingFlags.Instance) |> Array.filter (fun m -> m.DeclaringType = controller)) |> Seq.collect (fun method -> method.GetParameters() |> Array.append [| method.ReturnParameter |]) |> Seq.map (fun parameter -> parameter.ParameterType) |> Seq.append additionalTypes |> Seq.distinct |> getTypesRecursively relevantAssemblies additionalTypes |> Seq.distinct |> Seq.filter (fun t -> not (ignoredTypes |> List.contains t)) |> Seq.sortBy (fun t -> t.Name) |> Seq.toList let getTypesRecursivelyUsedByControllersInputsOnly (controllersAssembly : Assembly) (relevantAssemblies : Assembly) (additionalTypes : Type list) (ignoredTypes : Type list) = controllersAssembly.ExportedTypes |> Seq.filter (fun t -> t.BaseType = typedefof<WebsiteApiController> || t.BaseType = typeof<DevicesApiController>) |> Seq.collect (fun controller -> controller.GetMethods(BindingFlags.Public ||| BindingFlags.Instance) |> Array.filter (fun m -> m.DeclaringType = controller)) |> Seq.collect (fun method -> method.GetParameters()) |> Seq.map (fun parameter -> parameter.ParameterType) |> Seq.distinct |> getTypesRecursively relevantAssemblies additionalTypes |> Seq.distinct |> Seq.filter (fun t -> not (ignoredTypes |> List.contains t)) |> Seq.sortBy (fun t -> t.Name) |> Seq.toList
getReferencedTypes returns all types that are used by the specified type
t. Depending on what
t is this is either a nested type (e.g. collections) are its properties (records, classes) or its union cases (discriminated unions).
So we go through some
ifs to find out what
t is. I would use active patterns if they could support the number of cases needed here.
Line 10-13: For generic collections, we return the generic parameters.
Line 15: For arrays we use the type of the elements.
Line 18-44: For interfaces, we go through all the assemblies that were specified and look for types that implement the interface. Generic interfaces like
IMyInterface<T> need some special handling. We return all implementations that we found.
Line 46-48: I had some trouble with F# lists because matching the generic type definition did not work. So I used a workaround to check the name of the type. F# lists have a property
Head that we can use to get the type of the elements.
Line 50-54: For discriminated unions, we get all the cases and all their fields. For example
type A = B of int | C of string*float results in
[| int ; string ; float |]
Line 56: In our C# code we have a type
Maybe<T> that is more or less equivalent to F#’s
Option. We just take the type of the element.
Line 59: For tuples, we get the types of the tuple elements.
Line 62-77: For all other cases (records, classes, structs), we get all the types of the properties, ignore any self-references and continue only with types that are from our assemblies (relevant assemblies). Generic types, arrays and tuples are special cases since they could contain further types from our assemblies that should be included.
We also need to get all the implemented interfaces from our code to be included. We don’t use class inheritance, so including the interfaces is enough.
In the end, we want all our own types, not the types from the .Net framework or some other third-party library. I’ll show in part 8 – putting all together, how to handle some special types that need to be included anyway.
Line 80: The
getTypesRecursively function returns all types that are referenced by the specified
relevantAssemblies are the assemblies from which we want to get the types. We only process referenced types that are part of one of these assemblies. The exception: The
additionalTypes are types that are not part of the
relevantAssemblies but still need to be processed. I’ll cover special cases in a later part of the blog post series.
Line 81-83: I use a queue to hold the not yet processed types and initially load all the specified types into it. I use a
HashSet to store the already processed types. And a mutable
List to hold the results. I find it easier to work with mutable data structures in this case.
Line 85-98: We process the types in the queue by first checking whether we want to process this type – it is from the
relevantAssemblies or in the
additionalTypes. An array is a special case we need to ignore because an array is reported to be from the assembly of its element. This is okay because we have already seen that for an array, we take the element type in
getReferencedTypes and in our case, we don’t have arrays directly as inputs or outputs in our web methods – yeah, a bug that probably will hunt us in the future. We also ignore generic type parameters – coming for example from
Foo<T>. If the type is a generic type, we add the generic type definition –
Foo<T> instead of
Foo<string>. Otherwise, we simply add the type.
Line 100: We add the type to the processed ones and get back whether it is newly added, meaning that we need to process it further.
Line 101-112: If it is a not yet process type, we get all the referenced types and add all the not yet processed types to the queue.
Finally, we return all the found types.
Line 115: The function
getTypesRecursivelyUsedByControllers finds all web controllers in the specified
controllersAssembly that are derived from our web controllers (
DevicesApiController). Then it looks for all public instance methods that are directly declared on the controller class, not on a super class. Then it gets all method parameters and the return type. And it adds the specified
additionalTypes. For all these types the
getTypesRecursively function is executed to get the whole tree of types used. With some distincs and sorts, we get the result.
We use the input and return types to generate TypeScript proxies and for testing whether they can be JSON serialized.
Line 140: The function
getTypesRecursivelyUsedByControllersInputOnly is the same as the function above but returns only the types that are used as inputs.
We use the input types for testing whether they can be JSON deserialized.
As we have seen, it is quite simple to find all the types that are sent over the wire between our backend and client by using a bit of reflection.
It the next post, I’ll show you how we generate TypeScript classes for all the types we found in this blog post. Stay tuned.
This blog post is made possible with the support of Time Rocket, the product this blog post is about. Take a look (German only).