Weaving .NET 5 into legacy apps while keeping users happy

Here's how the author replaced an old web service built with Net 2.0 with a shiny new ASP.NET Core API using .NET 5 and Entity Framework Core 5—without disrupting the user experience or client's happiness.

Author: Julie Lerman


In the book “Object Oriented Re-Engineering”, there is a discussion about a pattern called Conserve Familiarity. The stated intent for this pattern is to “avoid radical changes that may alienate users.” I was only recently introduced to this book and this pattern. However, its intent has been an instinctive part of how I have worked with my clients for decades.

I want to share a story of executing this pattern as I replaced part of a 17-year-old Windows Forms application originally built with .NET 2.0. The application is coupled with a set of web services written using what was bleeding-edge at the time—Web Service Enhancements (WSE), the precursor to Windows Communication Foundation (WCF). Some of the features of the app (built to maintain repair tickets for this company’s vast array of equipment) had been replaced by a web application. But this old app was still being used by managers to perform special tasks on tickets created by users of the newer web app.

First: Let’s bow our heads for a moment to a solution that has been performing perfectly well with no needed changes for 17 years! That’s no small feat.

The programmer in me thought it was a great opportunity to create a whole new application to replace the management app. However, the pragmatist was aware of the fact that only a few people used the app—and even though it was important to the business, the time and expense of a full rewrite was difficult to justify. Instead, I chose the path of lesser resistance, one that would be interesting to me and a good research investment for the client: I replaced the very old web service with a shiny new ASP.NET Core API using .NET 5 and Entity Framework Core 5.

The client continues to run it internally, but using Kestrel as its server. Running the API with dotnet run on its own would not provide restart protection for failures. Therefore, we are using IIS as a reverse proxy . In our case, there was no need to worry about load balancing, although the proxy solution helps with that as well.

Additionally, because I often spend this kind of time and effort on research projects for articles, Pluralsight courses and conference talks, I decided in advance that I would put a cap on the billable hours and chalk any additional hours to my own entertainment and educational value.

Replacing the existing service calls when the source code is lost

Before I started, there was one big elephant in the room: I could not find the 17-year-old code for the WSE services anywhere. Pretty embarrassing, I know, but it was many, many computers ago. I scoured old drives that I’ve held onto over the years. I checked every place I’ve ever backed up, including Dropbox, online backup solutions and my NAS. None of these locations existed back then. It was nowhere to be found. Perhaps it’s on a dusty CD somewhere? I decided to stop wasting my time on that problem.

Luckily, the existing service is still running, so I could access the WSDL to see the list of calls and what their expected input and any output might be. The Windows App uses XML Web Services via System.Web.Services to access the WSE services, and the old Visual Studio tooling for this generated proxy classes for invoking the services. These classes were part of the Windows Forms application and provided some helpful details of request and response expectations.

One more way to glean important clues was to run the app and watch what was happening in the database. This let me discover all of the stored procedures that were called by the various services. A bonus was that not all of the services were needed any more. Two key features—creating new items and reporting—had been relegated to the new web application. The managers working in the old Windows application no longer performed those tasks, so I could ignore the related service calls and delete some code from the Windows App.

The world of data access had changed quite a bit since I wrote the app. It uses ADO.NET DataTables and DataSets including typed DataSets in Visual Studio. I was never an ace with the typed DataSets, and they now confound me even more after all these years. Additionally, my code had custom-written serializers and deserializers for moving data between the app and the services.

There were a number of challenges (some just long-forgotten syntax and APIs) that I needed to overcome to create the new API and adapt the backend of the Windows application to work with that API. New stored procedures and APIs needed to return data shaped as closely as possible to the original services.

Interestingly, the back end of the Windows application, between the UI and the services, became what we refer to as an “anti-corruption layer” in Domain-Driven Design. It acts as a translator between the nice new API and the clunky old Windows App.

The new API, one method at a time

I worked on one method of the new API at a time to let me get a feel for challenges I may encounter and for what the full workflow would be—creating the new database objects, the API method, calling it from the WinForms app and finally, getting the results back to the UI in the same format it has relied on for 17 years.

I chose the simplest method: Retrieving a list of existing repair tickets from which the managers choose the ticket they need to work on. I discovered from the stored procedure that I was returning complete details of every ticket, although the application only used minimal display data and the ticket IDs. I must have intended to avoid a second call to the database.

I soon found out the app does go back to the database for the details. That was another embarrassing discovery, but we live and learn. So, I created a new database procedure that only returned the data needed to populate the tree. I chose a procedure rather than a view so I could continue to pass in parameters and let the procedure itself do the sorting and filtering. In my API, I created a simple class (TicketHighlight) to match the output of the new procedure. The controller’s GET method triggers logic which calls that stored procedure using EF Core’s FromSqlRaw, and returns the list of TicketHighlight data as the response from the controller method.

While performance was not a real concern, it was still important to follow good practices with EF Core, so I was sure to define the entire DbContext used by the API so it would not waste any resources tracking query results. That means setting the context’s ChangeTracker.QueryTrackingBehavior to QueryTrackingBehavior.NoTracking in the constructor. I also added Swagger into my API so I could perform some simple tests on the API. It was so nice to be using current, familiar technology to fix up this old application rather than having to tangle with the very old technology.

Calling the API from the Windows Forms app

The old wrapper method that invoked the service call to get all of the tickets returned a fully loaded (and bloated) dataset, which I then passed to the UI. The service itself returned a dataset. Yikes! The new API returned streamlined JSON data which I could use to create the DataSet that the UI was expecting.

At some point in the app’s history, I had updated it from .NET Framework 2.0 to 4.0. But in order to call my new APIs, I had to update the app to 4.72 so I could use HttpClient and other methods from System.Net.Http. That update was a simple change in the project properties and was 100% backward compatible with the existing code.

Remember that the original app is in VB.NET, which was a bit of a challenge for me because it’s been a while! The server address is set via an appsetting (apiURL) in app.config. Remember app.config? I did not want to add layers of asynchronous code, so I used the Result method of both GetAsync and ReadAsStringAsync to get the result synchronously in this GetAllTicketHighlightsFromAPI method. I short-circuited the original GetAllTickets method to call this new one so the UI doesn’t even know that it’s calling a totally different set of code.

Public Function GetAllTicketHighlightsFromAPI() As DataSet
    Dim httpClient As New HttpClient
    Dim response As HttpResponseMessage =
      httpClient.GetAsync(apiURL & "/api/RepairTicket/highlights").Result
    If response.IsSuccessStatusCode = True Then
        Dim json As String = response.Content.ReadAsStringAsync().Result
        Dim ds As New DataSet
        ds.Tables.Add(JsonConvert.DeserializeObject(Of DataTable)(json))
        ds.Tables(0).Columns("dateIn").ColumnName = "DateIn"
        Return ds
     Else
        Return Nothing
     End If
End Function

Once I have the response, I read its streamed content into a string and then deserialized that string into a DataTable, which gets added to a new DataSet. The UI is expecting a typed DataSet and was not happy with what I was sending it. Finally, I discovered that it was case-sensitive. As I had grown tired of modifying the API to ensure that the casing matched the typed DataSet when it came across, you can see that I simply changed the column name on the fly from dateIn to DateIn. The bonus of this hack: It made me recall this time-consuming problem to share with you. Given the nature of this app, I’m not worried about including this lazy little hack.

With all this in place, I added a unit test project to the old solution. The test project targets .NET Framework 4.7.2 to match the Windows app and its backend, but is written in C# because that’s where my comfort lies these days. The automated tests ensure that I can call GetAllTicketHighlightsFromAPI and get back relevant data. The real test was in running the application with the UI calling my new method, and visually witnessing that the tree with the list of items was populating in the same way it was when pointing to the original method that uses XML Web Services to interact with the old WSE services. And it worked!

Applying the lessons learned to more complicated methods

With this workflow now successful, I was ready to tackle the next method which was a little more complicated: Retrieving a repair ticket along with its child data, a list of repairs for that particular equipment and their status. The original WSE method returned a graph that I was easily able to deserialize into the parent object with its children in a DataTable.

To keep things simple in my new API, I broke the logic into two reads—one for the ticket and one for the list of repairs. That meant a pair of new stored procedures and a pair of GET methods in the API following the same path I’d used to create the previous method. These new methods were exposed at [server]/api/tickets and [server]/api/tickets/repairs, with each method taking a single ID parameter representing the ticket and returning data structured to match what the original service returned.

In the back end of the Windows application, I created a single method that would first call the GET for the ticket to populate the properties of the existing RepairTicketDetails type, and another which would call the GET method to retrieve the repairs. I deserialized the JSON ticket results into a RepairTicket object, and the JSON repairs results into another DataTable.

I encountered two conflicts when deserializing the JSON formatted Repairs into the typed DataTable that the UI expected. I imagine they are not uncommon, so I want to share the problems and solution with you.

The first was with a property name. There are a number of possible repairs that could be performed on a given type of equipment. The repair list returns all of those possibilities, but only some of them are necessary, and in the UI each repair type displays a checkbox which is checked if the repair is being performed. Doing this 17 years ago, there was no problem naming the relevant DataTable column “checked.” But when I was building the API in C# 9, “checked” is a reserved keyword, so I named that property IsChecked. This created the first conflict. The second conflict is that the streamed results interpret that same property as Int64 but the typed DataTable expects Int32.

You already saw that I can simply change a column name on the fly before deserializing, but you can’t alter a column’s DataType if there is already data. There were two paths to solving the problem, and neither required changing the DataType. One path is to deserialize the results into a DataTable, change the column name, serialize the DataTable back into string and then deserialize it into the typed DataTable. The other, simpler, path (the one I chose, which you can see below) is to deserialize the results into a DataTable, change the column’s name, instantiate a new typed DataTable and then use its Merge method to pull in the DataTable rows. This will still throw an error because of the conflicting DataType; however, adding in the MissingSchemaAction.Ignore parameter will allow the method to ignore the conflicting types.

Dim json As String = response.Content.ReadAsStringAsync.Result
Dim t As DataTable = JsonConvert.DeserializeObject(Of DataTable)(json)
t.Columns("ischecked").ColumnName = "checked"
Dim typedDT As New dsTicketDetails.Table1DataTable
typedDT.Merge(t, True, MissingSchemaAction.Ignore)
Return typedDT

For updates, I also needed to make sure that I took the differing column names into account.

Great research for me and happy results for the client

There were a few more methods I needed to handle, but by the time I had gotten through these, I’d learned enough about the problems I would encounter. Thanks to that, the rest of the methods, even updates and deletes, were not terribly challenging.

Now I’m able to add in the new data fields that the client has requested because I have an API written using modern technology that I am very comfortable with. The frontend is up to date enough that once I remind myself of some of the VB.NET syntax I’ve forgotten, I can make the very rarely requested changes. The users can just continue using the application while we focus on more important software challenges at the company, and the client is very happy that the critical part of the application (which truly lies in the APIs) is maintainable and can be used in any future UI that is developed. Additionally, we are now aware that we can rely on this type of transition for other old apps that are still in use if needed.



Related tags:

programming   dotnet  
About the author

Julie Lerman is a software coach with 30 years of experience as a developer. She is a long-time Pluralsight author, Microsoft MVP, Microsoft Regional Director and Docker Captain who lives in the hills of Vermont and continues to be known as the world’s top expert in Entity Framework. You can find Julie giving keynotes and hard-core coding sessions on Domain-Driven Design, Azure, Entity Framework and other topics at software conferences around the world. As a coach, Julie provides support for DDD and architecture implementation, facilitates Event Storming workshops and provides deep dives into Entity Framework for her clients. Julie has authored the highly acclaimed "Programming Entity Framework" books from O’Reilly, the MSDN Magazine Data Points column and numerous articles in your favorite programming resources.

10-day free trial

Sign Up Now