post-thumb

Why and how generate models from C# to Typescript

I don’t understand why developers doesn’t automate their job. They can save a huge amount of time with very low effort. How? For example, by generating models from backend to frontend. We will talk about it today.

How the generation can look like?

The main idea is to keep backend and frontend models synchronized automatically. In most cases frontend asks backend about data, so the main source of truth should be the backend site. I see it in that way. When model is changed on backend then frontend models should be updated automatically or by executing simple command, like running a console app.

Why generating is good

Before we go to details we should talk why it is soo good:

  • Focus on important things
    • Not on coping names from one place to another
    • We are developers. We should solve problems, not copy labels. Especially when we can avoid stupid work easily
  • Less work in the future
    • To not do the same work twice, so you will save your time
  • No risk of typo
    • We are humans. We make errors, like typos. We can reduce them to minimal by generating code.
  • Errors on build
    • When you map manually then you can be not synchronized and you can miss something.
    • But when you do it automatically, then TypeScript compiler fail on build and it always better to know about an error earlier
  • Better performance
    • Sometimes you need to check some data, some types, some structures, because you are not sure about it. When you generate it, they are sync. So some checking can be ignored, because compiler does it.

Why people don’t do this often?

If they are some many benefits, why this approach is so rare?

  • I have no idea :)
  • People don’t know how to achieve it
  • People believe that front/and and backend are independent (then I suggest to use BFF (Backend For Frontend) pattern)

How to do this

A real example

Here you can see a sample implementation. My approach is to create a simple version at the beginning and improve it when it is required. So that solution is not perfect, but it is simple and it will give you benefits form the 1st day of usage.

I will split into three fragments:

  1. How to get metadata
  2. How to generate code
  3. How to run it

So let’s get started

Getting metadata

At the beginning we need a model to keep fields in a simple flat format.

namespace CodePruner.Examples.TypeScriptCodeGenerators
{
    internal struct  BackendField
    {
        public string Name { get; set; }
        public string Type { get; set; }
    }
}

When we have model we can get it from metadata. We will use reflection for this:

using System;
using System.Collections.Generic;

namespace CodePruner.Examples.TypeScriptCodeGenerators
{
    internal class BackendFieldGetter
    {
        internal IEnumerable<BackendField> GetBackendField(Type sourceType)
        {
            var properties = sourceType.GetProperties();
            foreach (var property in properties)
            {
                var propertyType = property.PropertyType.Name;
                var propertyName = property.Name;
                var backendField = new BackendField
                {
                    Name = propertyName,
                    Type = propertyType
                };

                yield return backendField;
            }
        }
    }
}

Ok. It is done. Not we need to consume that data and create file content

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodePruner.Examples.TypeScriptCodeGenerators
{
    internal class TypeScriptContentGenerator
    {
        internal string GenerateModel(string className, IEnumerable<BackendField> backendFields)
        {
            var sb = new StringBuilder();
            sb.AppendLine($"export type {className} {{");
            foreach (var backendField in backendFields)
            {
                var frontendType = GetFrontendType(backendField);
                sb.AppendLine($"  {backendField.Name}: {frontendType};");
            }

            sb.AppendLine("}");

            return sb.ToString();
        }

        private string GetFrontendType(BackendField backendField)
        {
            switch (backendField.Type)
            {
                case "Int32":
                case "Single":
                    return "number";
                case "String":
                    return "string";
                case "DateTime":
                    return "Date";
            }

            return backendField.Type;
        }
    }
}

And join it as a one class to simplify the usage:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CodePruner.Examples.TypeScriptCodeGenerators
{
    public class TypeScriptModelGenerator
    {
        private BackendFieldGetter backendFieldGetter;
        private TypeScriptContentGenerator typeScriptContentGenerator;

        public TypeScriptModelGenerator()
        {
            backendFieldGetter = new BackendFieldGetter();
            typeScriptContentGenerator = new TypeScriptContentGenerator();
        }

        public string GenerateTypeScriptModel(Type type)
        {
            string typeName = type.Name;
            var backendFields = backendFieldGetter.GetBackendField(type);
            var content = typeScriptContentGenerator.GenerateModel(typeName, backendFields);
            return content;
        }
    }
}

OK. Before we start it we should write a unit test to be sure that the code is working properly"

using System;
using Xunit;
using Shouldly;
using System.Linq;

namespace CodePruner.Examples.TypeScriptCodeGenerators.UnitTests
{
    public class TypeScriptModelGeneratorTests
    {
        [Fact]
        public void it_should_generate_SampleClass_in_ts()
        {
            var typeScriptModelGenerator = new TypeScriptModelGenerator();
            var result = typeScriptModelGenerator.GenerateTypeScriptModel(typeof(SampleClass));

            var expectedClass =
@"export type SampleClass {
  AnInt: number;
  AString: string;
  AFloat: number;
  ADateTime: Date;
}
";

            result.ShouldBe(expectedClass);
        }
    }

    internal class SampleClass
    {
        public int AnInt { get; set; }
        public string AString{ get; set; }
        public float AFloat{ get; set; }
        public DateTime ADateTime { get; set; }
    }
}

And finally we can write a Runner, I mean a console app that combines all it together and we will be able to regenerate models as often as we wish:

using System;
using System.IO;

namespace CodePruner.Examples.TypeScriptCodeGenerators.Runner
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start: CodePruner.Examples.TypeScriptCodeGenerators");
            var typeScriptModelGenerator = new TypeScriptModelGenerator();
            var content = typeScriptModelGenerator.GenerateTypeScriptModel(typeof(AccountDto));
            File.WriteAllText("AccountDto.generated.ts", content);
        }
    }

    public class AccountDto
    {
        public int AccountId { get; set; }
        public string DisplayName { get; set; }
        public DateTime BirthDate { get; set; }
    }
}

Of course it is not a mature solution. There are multiple things to improve like:

  • Generating enums
  • Importing different types
  • Resolving paths
  • Generating arrays

But current version is saving a huge amount of my teams’ time.

Update

After a bit of time, I have found a ncie tool to generate the code. If you want to read about it go [here .

Is it good?

  1. Leave a comment below. We can discuss about.
  2. Create a PullRequest to that repo to improve that place or that code.
comments powered by Disqus

Are you still here? Subscribe for more content!