Skip to content

Recipes

Steve Leigh edited this page Dec 10, 2018 · 6 revisions

I'm using WebAPI and want all my DTOs to live on the client side

The basic idea is to add a console project to your application which does the following:

  1. Uses reflection to get all the API Controllers
  2. Uses reflection to find all the parameter and return values for the public methods in these controllers
  3. Does some filtering and transforming to ensure only relevant stuff is being included
  4. Uses TypeLite to grab the relevant inputs/outputs
  5. Uses EnumGenie to emit all the enums and helpers
    public class Program
    {
        public static void Main()
        {
            var apiTypes = typeof(FooController).Assembly.GetExportedTypes()
                .Where(t => t.IsSubclassOf(typeof(ApiController)))
                .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod))
                .SelectMany(ParameterAndReturnTypes)
                .SelectMany(Unwrap)
                .Where(t => !t.IsPrimitive && t != typeof(string))
                .Where(t => !t.Namespace.StartsWith("System.")) // Customize this bit to suit your app
                .Distinct()
                .OrderBy(t => t.FullName) // Makes output better for diff
                .ToList();

            var typeScriptFluent = TypeScript.Definitions()
                .WithIndentation("    ")
                .WithModuleNameFormatter(tsModule => "Api")
                .WithConvertor<DateTimeOffset>(obj => "string") // You may need to add more of these
                .WithConvertor<Guid>(obj => "string")
                .WithMemberFormatter(mf =>
                {
                    // Hack to ignore statics - limitation of TypeLite
                    if ((mf.MemberInfo as PropertyInfo)?.GetGetMethod().IsStatic ?? false)
                        return $"// Ignore static: {mf.Name}";

                    // Hack to mark nullables as such- limitation of TypeLite
                    var suffix = ((mf.MemberInfo as PropertyInfo)?.PropertyType.IsNullable() ?? false) ? "?" : "";
                    return $"{char.ToLower(mf.Name[0])}{mf.Name.Substring(1)}{suffix}";
                })
                .AsConstEnums(false);

            foreach (var type in apiTypes)
            {
                typeScriptFluent = typeScriptFluent.For(type);
            }

            var tsModel = typeScriptFluent.ModelBuilder.Build();

            File.WriteAllText("api.d.ts", typeScriptFluent.Generate());

            new EnumGenie.EnumGenie()
                .SourceFrom.List(tsModel.Enums.Select(e => e.Type))
                .WriteTo.File("enums.ts", cfg => cfg.TypeScript())
                .Write();
        }

        private static IEnumerable<Type> ParameterAndReturnTypes(MethodInfo method)
        {
            return method.GetParameters().Select(p => p.ParameterType)
                .Concat(new[] { method.ReturnType })
                .Distinct();
        }

        private static IEnumerable<Type> Unwrap(Type type)
        {
            return type.IsGenericType ? UnwrapGeneric(type)
                : type.IsArray ? UnwrapArray(type)
                    : new[] { type };
        }

        private static IEnumerable<Type> UnwrapArray(Type type)
        {
            return Unwrap(type.GetElementType());
        }

        private static IEnumerable<Type> UnwrapGeneric(Type type)
        {
            return type.GenericTypeArguments.SelectMany(Unwrap);
        }
    }

But I don't like TypeLite

Have you tried Typescriptr? This snippet has a little hack from the default configuration to write enum definitions to the same file as numbers, so the output is compatible with Enum Genie.

    public class Program
    {
        public static void Main()
        {
            var generator = TypeScriptGenerator.CreateDefault()
                .WithEnumFormatter(EnumFormatter.ValueNumberEnumFormatter, (type, style) => type.Name)
                .WithTypeMembers(MemberType.PropertiesAndFields);

            var typesToGenerate = 
                typeof(IAmWebApi).Assembly.ExportedTypes
                    .Where(type => type.GetCustomAttribute<ClientTypeAttribute>() != null);

            var result = generator.Generate(typesToGenerate);
            
            using(var fs = File.Create("types.d.ts"))
            using(var tw = new StreamWriter(fs)) {
                tw.Write(result.Enums.Replace("enum ", "declare enum ") + result.Types);
            }

            new EnumGenie.EnumGenie()
                .SourceFrom.Assembly(typeof(IAmWebApi).Assembly)
                .WriteTo.File("enums.ts", cfg => cfg.TypeScript())
                .Write();
        }
    }
Clone this wiki locally