Type-safety across .Net and TypeScript – Finding the types

This is part 3 of how we generate types from our .Net backend to be used in our TypeScript client.

  1. Why do we even bother?
  2. Generating TypeScript constants from .Net constants
  3. Finding the types used in communication between the .Net backend and the TypeScript client
  4. Generating TypeScript classes from .Net types
  5. Generating Angular Services from .Net WebApi Controllers
  6. TypeScript-friendly JSON serialization of F# types
  7. Testing JSON serialization and deserialization
  8. Putting all the parts together

Be prepared, a lot of reflection ahead!

Some context

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

Line 9: 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 types. The 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 (WebsiteApiController and 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.

Conclusion

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

About the author

Urs Enzler

5 comments

By Urs Enzler

Recent Posts