We recently worked with a client looking to replatform their legacy application. Their application had been running for years but was starting to show signs of aging, both in terms of the user interface as well as technical debt. At this point, they were ready to invest in an upgrade.
Their current application was built with Windows Forms and the goal was to bring it to the web with ASP.NET Web API and Angular. The unique challenge for this effort was that we had a tight timeline and budget, so a full rewrite of the backend was not feasible. To solve this, we had to come up with a design that not only allowed us to build a brand new web services API and Angular front end but also reuse their existing business logic to handle these timeline and budget constraints. The other challenge was performing the modernization in a way that would allow them to upgrade their backend as a separate effort at a future time.
In this post, I’m going to focus on the web services API side of the solution. Replacing the complex UI functionality of their Windows application with Angular was a big undertaking and would be the perfect subject for another blog entry in the future.
The Scope of the Effort
Their existing application was a sophisticated drawing tool, allowing users to design custom dashboards for business intelligence purposes. Written for the Windows desktop with Windows Forms, the application defined large classes, with web-unfriendly properties (Windows Forms controls, byte streams, etc). This made working with these existing types in Angular not possible. In addition, they had a lot of large static classes with complicated business logic, ruling out a possible quick backend rewrite.
Our solution was to have a web services API, including a small mapping layer, sit between the legacy code and our Angular client. This would allow us to map back and forth between their legacy services/types and the new types we would to expose to the Angular client. Not only would this allow for maximum reuse of the legacy code, but also would allow a clean separation of concerns, making a replacement of the legacy backend much easier in the future.
This image shows the high-level design for the solution. The gray box represents their existing Windows Forms code, with the green boxes being the new technology stack that would be replacing it. The orange boxes are what would be reused by the new stack. Their business logic was a combination of class libraries and WCF web services, calling into ADO.NET and/or EntityFramework, ultimately talking to a SQL Server database. The reuse of the legacy backend would ensure replacement of the most important pieces of the application while helping ensure delivery within our time and budget constraints.
With the solution design agreed upon, we started development. For ease of getting up and running quickly, we chose to use the Voyage platform (https://github.com/lssinc/voyage-api-dotnet) as our base. Voyage includes a lot of functionality out of the box, including AutoMapper for object mapping, allowing us to start implementing our design from day one.
One of our major challenges was to figure out how to map their existing objects to something we could use in a single page web application. Their existing desktop application directly manipulated Windows Forms controls, which would not work for us. We analyzed each object exposed by the legacy app and created new models that matched one to one for simple types. For complex types, we created custom AutoMapper configurations to map to a type that would work with our Angular client.
An example was their use of the C# MemoryStream. They utilized a few third-party Windows Forms libraries that worked with streams of bytes to display images on the screen. In order to reuse this code, we were able to take this MemoryStream, convert it to a PNG image, and convert that PNG to a base64 string, allowing us to display it in the browser.
(Conversion of stream to base64)
With the complex properties accounted for, we were able to build out the rest of our API controllers and services quickly.
As you can see, we made sure our controller actions were small. We wanted to push the logic into the service layer to maintain a clean separation of concerns. As far as the consumer of the API was concerned (the Angular app is this case), it was just talking to a brand new backend.
This shows an example of the pattern we used throughout the API. The pattern consisted of three simple steps.
1) Map the new type into the legacy type.
2) Call into the legacy services with that type.
3) Map the legacy result type into a new result type.
With this pattern in place, only the service layer knew about the legacy types and services, allowing a much simpler legacy backend rewrite in the future.
Legacy software modernization can be challenging, especially under time and budget constraints. With the approach we took for this modernization effort, we were able to deliver a modern web application that delivered all of the functionality that their legacy application did. With the constraints we had, large portions of their legacy backend are still in use, but our solution has created a clean separation between new and old, allowing a much easier rewrite to be done in the future.