Cleaning up a .NET 6 Web API Converted from Visual Basic
The Road Assets Editor or RAE is a tool that enables employees to review all road assets built in King County. It was created to support the road crews that are tasked with inventorying and reviewing the status of these assets on a periodic basis.
A road asset is a service to the public and a responsibility of the county. They range from features like an ADA compliant ramp at the corner of an intersection to a speed bump or signage attached to a pole. Hundreds of thousands of assets are catalogued and maintained in the RoadsGIS database. Map queries against this data set are handled by ArcGIS Server which exposes feature services that can be queried using a REST API. For queries that don’t directly contain spatial data the RoadsGIS-Web-API was created.
Convert Sync to Async
The spatial and non-spatial queries made by the Silverlight app are made directly by that app. This architecture was broken out into the model I described above where the frontend web app queries spatial data using ArcGIS Server REST API and non-spatial data using the RoadsGIS-Web-API. To get the project started, code from the Silverlight app was used either directly in, or as inspiration for the new API. This worked well and over a period of a few months the team was able to get the new API up and running. Shortly after, the team began development of the Next.js-based frontend which handles the web map and uses this new API to gather data to assist the editing process.
Near the completion of the new frontend, we started to confront the variety of strategies used in this project to store are set configuration information. It was from this angle that I began a two-week refactoring of the RoadsGIS-Web-API project which resulted in 29 commits and a drastically simplified codebase. One of the big strengths of the .NET ecosystem is the pervasive use of asynchronous programming which enables improved scalability particularly in web apps. Unfortunately, the API project was completely synchronous.
To solve this problem, I started by rewriting the code that used the Entity Framework Core library to make database calls. Connecting to a database, making a query, and then serializing the response to an object is a fundamentally asynchronous process as we don’t know when the database will respond to the request. Because of the virality of the async and await keywords in C# I then worked up the stack from these now async database calls to the controller where the endpoints were converted to async.
Interestingly some of the API endpoints made call to the ArcGIS Server REST API before returning non-spatial data like an Id or Description to the frontend web app. HTTP calls are another fundamentally asynchronous process as we don’t know when the web server, we submitted the request to will respond. This code also used the depreciated System.Web library to make these API requests. To solve this issue, I used the Flurl library which is a comfortable and fully async wrapper around the newer System.Net.HTTP library.
Now the remote resources consumed by the web API are done so asynchronously and the endpoints exposed by the API handle incoming requests asynchronously too. The upside to this is that the API can now handle many more requests as the threads that would have been consumed by waiting for remote resources to respond can now be suspended, while other requests are handled, and then resumed when they are ready.
Exceptions are not your Enemy
The usage of null objects and try catch statements is another area that was ripe for improvement in the web API. Often code in the API would start by creating a null instance of an object and then at the end, the object would be returned. Somewhere in the middle, inside of a try catch statement, the null object would hopefully be replaced by real data. But if an exception were to occur, that exception would be suppressed, and the method would return null for no clear reason to the caller.
To solve this issue, we can remove the null object at the start of the method and the return of that object at the end. This creates a different issue as our method now lacks a return statement, but we can solve that by swapping in a return in place of assignments to the null object.
Then there is the issue of using or misusing try catch statements to suppress exceptions. A good rule of thumb here is to ask yourself if this method should fail for any reason, if the answer is no, then do not wrap it in a try catch statement. This is advantageous because when your code does break, the exception will bubble up through the stack to where you’re calling the code and then you will see specifically where things broke when you are debugging the issue. Under the old style nothing would appear to be wrong, but your code wouldn’t work correctly.
These two changes to the style of the code lead to a final product that is easier to read, does not return null under any circumstances, and is easier to troubleshoot and debug when it breaks.
Configured for Success
Configuration is another area where things were simplified. In an ASP.NET Core web app the primary source of truth for configuration is a file named appsettings.json. This is a JSON formatted file that contains key-value pairs that are read when the applications starts up. Inside of the app you can read the values of these keys as strings by asking the configuration object for a specific key.
You can also add additional sources of information to a configuration object. To take advantage of this we add the environmental variables present on the system. The idea here is that if the target system needs the app configured in specific way, that wasn't handled in the deployment pipeline, you can set an environmental variable on the Azure app service instance to accomplish that task. We also removed a dependency on the RoadsGIS database which is where the configuration data was stored in prior versions of this app. Those items are now in the appsettings.json file.
While directly converting code from a legacy Visual Basic project into a C# web API does work, there’s a lot to be gained from doing a second pass. Newer language features in C# can help you to make your code terser, more performant, and easier to reason about than a direct 1 to 1 conversion of Visual Basic concepts and style. If you can make time for a refactoring process like this, I highly recommend it.