Motivation
One of the most diffucult challenges when working with Rest APIs are types, especially when developers on both the backend and frontend are making changes. One of the common ways to solve this is by using GraphQL, however it's slower and quite complex - not great for small to medium software projects.
Something quite interesting happening in the Typescript community is the adoption of tRPC which is a handle library to deal with Api requests between the front and backend, converting REST APIs into a RPC-like feel. Types between a client in a server are always defined the same and TS inference travels accross the client/server boundary. This typescript library allowed me to create, and consume all CRUD endpoints for a specific type in about 15 minutes. Once tRPC is setup, the developer speed is truly unmatched.
As I am working on a new DSL language service, I have been looking to find ways to speed up the developer type handling process without wasting too much time. If there was a way to take the shape of data from the database and replicate it to the frontend without any manual labour, that would be a big win in my book.
I am already handling database migrations by editing the EF Core classes and then pushing those changes to a SQLite database. If there was a way to push type changes to a '/src/global_types.ts', then I could reference those types throughout the project, with typechecking and inference built in.
Could we push migration classes as types to TS?
Because this strategy is already being used to handle database migrations, we could probably do the same to the frontend.
First Pass
The first thing I tried was by hammering out a quick scanner,parser, and converter which I got a lot more mileage than I expected. There were a few hiccups, first, a scanner, and parser are a lot of work to keep up with, because C# has a lot of features. For example I didn't think about generics, attributes, or inheritance until later. While these aren't to bad if one writes a good scanner and parser, it's still just a lot of work.
Converter.fs (snippet)
F# module that takes a list of types and exports them out the the right file
There were also a few bugs, and edge cases (of course). For example the Js JSON parser doesn't recognize the difference between "userType", and "UserType" when parsing JSON while C# will happily parse UserType to "userType". DateTime doesn't play nice either, so I ended up converting it to a string on the client side.
Conversion issues
While types and names did get converted with the correct upper and lowercase, ASP.Net didn't work that way. 'FooBar' would become 'fooBar' and JS would be in shambles.
When dog fooding this tool, I noticed something interesting. Not only was I using database types, but I was also stuffing API request, and response classes. I could create/edit a request body class then once satisfied, just run the application to update the frontend with the changes.
Using .NET Reflection
It works really well! But I had a problem, first the parser while really good, could be better. I should use reflection to do this instead of hand parsing out the file. I also needed something that could span accross a project not just a single file (like a parser) without creating crazy abstractions or manual matinence (like pushing all classes to list on the top level for conversion). The converter needs to be simple and un-intrusive to the development process.
So, I started tinkering with reflection, found a nice way to handle these type issues; and discovered and interesting architecural pattern along the way.
I created an abstract class, called which houses the reflection logic. The abstract class, called BaseTypeScriptConverter finds all subclasses and gets the nested EF core class data, close what the parser did:
GlobalData.cs
Subclass that holds all nested classes (or records) that we want to convert to TS. Notice that it inherits from BaseTypeScriptConverter
BaseTypeScriptConverter.cs
Abstract class that fetches all subclasses and the nested internal classes or records
One of the intersting parts about reflection is that it handles much of the analyser logic during compile time, not during the runtime. This breaks the traditional way of needing to instantiate everything. So as long as I can call BaseTypeScriptConverter.Run() , it will fetch all subclasses reguardless of their location in the codebase, and relationship to other objects. It's slick, and does not hinder any archiectural design.
But there is one tiny thing I didn't mention. You do have to insantiate something in order to call BaseTypeScriptConverter.Run() so, I built an empty subclass called TSConversionRunner that I could instantiate and use to boot the code analyzer process. From there all the other classes, instantiated or not, would get analized and converted
Putting it All Together
The serializer creates an empty subclass then calls BaseTypeScriptConverter.Run() which returns all assembly info to serialize the types to Typescript
There are a few things not mentioned in this post, like the serializer, or the [ConvertToTypeScript] attribute which is just an empty class attribute
Conclusion
I think there are a few things that could have been done better. First, the subclass, like GlobalData.cs should probably hold a property, or attribute property containing the file location of the type generation. That way types can be better organized on the frontend side. Next, the event trigger works fine here, but it's completely unnecessary and adds to the complexity of the project.
Despite its rough-and-dirty approach it works pretty well, and doesn't require a lot of maintaining to save time. I will probably keep working on this tool as I think more about scaffolding within the .NET/React framework.
Links