Introduction

Each technology becomes obsolete after some time. It is no different with the .NET Framework – it can be safely said that after the appearance of the .NET Core platform, the old Framework is slowly disappearing. Few people write about this platform, at conferences it has not been heard for a long time in this topic. Nobody starts a project in this technology. Only the .NET Core everywhere… except for a beloved legacy systems!

Well, despite the fact that there is .NET Core in the new solutions, a huge number of systems stand on the old .NET Framework. If you are not participating in a greenfield project but more in maintenance, then you are very likely to be sitting in an old framework. Who likes to play with old toys? Nobody. Everyone would like to use new technology, especially if it is better, more efficient and better designed.

Is the only solution to change the project and even the employer to use the new technology? Certainly not. We have 2 other options: “Big Bang Rewrite” or the use of “Strangler Pattern”

Big Bang Rewrite

Big Bank Rewrite means rewriting the whole application to new technology/system. This is usually the first thought that comes to our minds. After rewriting the entire system, turn off the old system and start the new one. It sounds easy but is not.

First of all, users do not use the new system until we have the whole system rewritten. This approach resembles the methodology of running a Waterfall project with all its drawbacks.

Secondly, it is an “all or nothing” approach. If during the project it turns out that we have run out of budget, time or resources, the End User is left with nothing – we have not delivered any business value. Users use the old system, which is still difficult to maintain. We can put our unfinished product on the shelf.

Can we do it better? Yes, and here comes the so-called “Strangler Pattern”.

Strangler Pattern

The origin of this pattern comes from an article written by Martin Fowler called Strangler Fig Application. The general point is that in tropical forests live figs that live on trees and are slowly “strangling” them.

Apart from the issues of nature, the metaphor is quite interesting and transferring it to the context of rewriting the system is described by Martin Fowler as follows:

gradually create a new system around the edges of the old, letting it grow slowly over several years until the old system is strangled

The keyword here is the word “gradually”. So, unlike the “Big Bang Rewrite”, we rewrite our system piece by piece . The functionalities implemented in the new system immediately replace the old ones. This approach has a great advantage because we immediately provide value and get immediate feedback. It is definitely a more “agile” approach with all the advantages.

Motivation

Before proceeding to implement this approach, I would like to present the motivations for rewriting the system into new technology. Because as with everything – “there is no such thing as a free lunch” – rewriting the system, even in an incremental approach, will cost money, time and resources anyway.

So below is a list of Architectural Drivers that may affect this decision:

Development will be easier, more productive

We can use new techniques, methods, patterns, approaches

System will be more maitainable

New technology gives us more tools, libraries, frameworks

New technology is more efficient and we want to increase performance of our solution

New technology has more support from vendor, community

Support of old technology is ended, no patches and upgrades are available

Old technology can have security vulnerabilities

Developers will be happy

It will be easier to hire new developers

We want to be innovative, increase Employer branding

We must meet some standards and old technology doesn’t make it possible for us

As you can see the list is long and you can probably mention other things. So if you think it’s worth it, here’s one way to do it in the Microsoft ecosystem – rewriting the .NET Framework application to .NET Core using “Strangler Pattern”.

Design

The main element of this pattern is the Proxy / Facade, which redirects requests to either the legacy system or the new one. The most common example is the use of a load balancer that knows where to direct the request.

If we have to rewrite the API, e.g. Web API, the matter seems to be simple. We rewrite each Web API endpoint and implement the appropriate routing. There is nothing to describe here.

However, in .NET, many systems are written combining both Backend and Frontend. I am thinking of all applications like WebForms, ASP.NET MVC with server-side rendering, WPF, Silverlight etc. In this case, placing the facade in front of the entire application would require generating FrontEnd by a new application as well.

Of course, this can be done but I propose a different solution – to rewrite only Backend.

If do not want to put a proxy in front of our old application, we can use the same old application as a proxy. In this way, all requests will go through the old application, while some of them will be processed on the side of the new system. This approach has the following implications:

Even if all logic is rewritten in the new system, the Frontend / Proxy will remain in the old one.

We need to implement the delegation logic of request processing on the old system

We move from monolithic architecture to a distributed system with all its drawbacks (see Fallacies of distributed computing)

Let’s see how the architecture of such a solution looks like:

The request process is as follows:

1. Client sends a request

2. Old Frontend receives the request and a forwards it to the old Backend (the same application) or new Backend (new .NET Core application).

3. Both backends communicate with the same database

Communication with the new backend takes place only through the Gateway (see Gateway pattern). This is very important because it highlights the synchronous API call (see Remote Procedure Call). Under no circumstances don’t hide it! On the contrary – by using the Gateway we show clearly what is happening in our code.

The main goal is that our proxy should be as thin as possible. It should know as little as possible and have the least responsibility. To do this, when communicating with the new system, we can use the CQRS architectural style using the fact that queries and commands can be serialized. In this way, our proxy is only responsible for invoking the appropriate query or command – nothing more. Let’s see how it can be implemented.

Implementation

Backend API

Let’s start by defining the API of the new system – the endpoints responsible for receiving queries and commands. Thanks to the Mediatr and JSON.NET library it is very easy:

.NET Core API - Queries Controller [ApiController] [Route("api")] public class QueriesController : ControllerBase { private readonly IMediator _mediator; public QueriesController(IMediator mediator) { _mediator = mediator; } [AllowAnonymous] [HttpPost("queries")] public async Task<IActionResult> ExecuteQuery(RequestDto requestDto) { Type type = GetQueriesAssembly().GetType(requestDto.Type); dynamic query = JsonConvert.DeserializeObject(requestDto.Data, type); var result = await _mediator.Send(query); return Ok(result); } private Assembly GetQueriesAssembly() { return typeof(GetProductsQuery).Assembly; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [ ApiController ] [ Route ( "api" ) ] public class QueriesController : ControllerBase { private readonly IMediator _mediator ; public QueriesController ( IMediator mediator ) { _mediator = mediator ; } [ AllowAnonymous ] [ HttpPost ( "queries" ) ] public async Task < IActionResult > ExecuteQuery ( RequestDto requestDto ) { Type type = GetQueriesAssembly ( ) . GetType ( requestDto . Type ) ; dynamic query = JsonConvert . DeserializeObject ( requestDto . Data , type ) ; var result = await _mediator . Send ( query ) ; return Ok ( result ) ; } private Assembly GetQueriesAssembly ( ) { return typeof ( GetProductsQuery ) . Assembly ; } }

.NET Core API - Commands Controller [ApiController] [Route("api")] public class CommandsController : ControllerBase { private readonly IMediator _mediator; public CommandsController(IMediator mediator) { _mediator = mediator; } [AllowAnonymous] [HttpPost("commands")] public async Task<IActionResult> ExecuteCommand(RequestDto requestDto) { Type type = GetCommandsAssembly().GetType(requestDto.Type); dynamic command = JsonConvert.DeserializeObject(requestDto.Data, type); await _mediator.Send(command); return Ok(); } [AllowAnonymous] [HttpPost("commands-with-result")] public async Task<IActionResult> ExecuteCommandWithResult(RequestDto requestDto) { Type type = GetCommandsAssembly().GetType(requestDto.Type); dynamic command = JsonConvert.DeserializeObject(requestDto.Data, type); var result = await _mediator.Send(command); return Ok(result); } private Assembly GetCommandsAssembly() { return typeof(AddProductCommand).Assembly; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 [ ApiController ] [ Route ( "api" ) ] public class CommandsController : ControllerBase { private readonly IMediator _mediator ; public CommandsController ( IMediator mediator ) { _mediator = mediator ; } [ AllowAnonymous ] [ HttpPost ( "commands" ) ] public async Task < IActionResult > ExecuteCommand ( RequestDto requestDto ) { Type type = GetCommandsAssembly ( ) . GetType ( requestDto . Type ) ; dynamic command = JsonConvert . DeserializeObject ( requestDto . Data , type ) ; await _mediator . Send ( command ) ; return Ok ( ) ; } [ AllowAnonymous ] [ HttpPost ( "commands-with-result" ) ] public async Task < IActionResult > ExecuteCommandWithResult ( RequestDto requestDto ) { Type type = GetCommandsAssembly ( ) . GetType ( requestDto . Type ) ; dynamic command = JsonConvert . DeserializeObject ( requestDto . Data , type ) ; var result = await _mediator . Send ( command ) ; return Ok ( result ) ; } private Assembly GetCommandsAssembly ( ) { return typeof ( AddProductCommand ) . Assembly ; } }

We define a simple Data Transfer Object for these endpoints. It holds the type of object and data in serialized form (JSON):

RequestDto - serialized data for queries/commands public class RequestDto { public string Type { get; set; } public string Data { get; set; } } 1 2 3 4 5 6 public class RequestDto { public string Type { get ; set ; } public string Data { get ; set ; } }

At the moment, our API contract is just a list of queries and commands. For our old .NET Framework application to benefit from this contract using strong typing, we must put all of these objects to separate assembly. We want both .NET frameworks and .NET Core to be able to refer to it, so it must be set to target .NET Standard

The Gateway

As I wrote earlier, communication and integration with the new Backend must go through the gateway. The gateway responsibility is:

1. Serialization of the request object

2. Providing additional metadata for a request like the context of the user. This is important because the new Backend should be stateless .

3. Send a request to the appropriate address

4. Deserialization of response / error handling

However, for the client of our Gateway, we want to hide all these responsibilities behind the interface:

Gateway interface public interface INewAppGateway { void ExecuteCommand(ICommand command); T ExecuteCommand<T>(ICommand<T> command) where T : class, new(); T ExecuteQuery<T>(IQuery<T> query) where T : class, new(); } 1 2 3 4 5 6 7 8 public interface INewAppGateway { void ExecuteCommand ( ICommand command ) ; T ExecuteCommand < T > ( ICommand < T > command ) where T : class , new ( ) ; T ExecuteQuery < T > ( IQuery < T > query ) where T : class , new ( ) ; }

The implementation of Gateway looks like this:

Gateway implementation public class NewAppGateway : INewAppGateway { private readonly string _backendBaseUrl; private readonly IUserContext _userContext; public NewAppGateway(string backendBaseUrl, IUserContext userContext) { _backendBaseUrl = backendBaseUrl; _userContext = userContext; } public void ExecuteCommand(ICommand command) { RestClient client = new RestClient(_backendBaseUrl); var type = command.GetType().FullName; var data = JsonConvert.SerializeObject(command); var request = CreateRequest("api/commands", type, data); client.Execute(request); } public T ExecuteCommand<T>(ICommand<T> command) where T : class, new() { RestClient client = new RestClient(_backendBaseUrl); var type = command.GetType().FullName; var data = JsonConvert.SerializeObject(command); var request = CreateRequest("api/commands-with-result", type, data); var response = client.Execute<T>(request); return response.Data; } public T ExecuteQuery<T>(IQuery<T> query) where T : class, new() { var client = new RestClient(_backendBaseUrl); var type = query.GetType().FullName; var data = JsonConvert.SerializeObject(query); var request = CreateRequest("api/queries", type, data); var response = client.Execute<T>(request); return response.Data; } private IRestRequest CreateRequest(string resource, string type, string data) { IRestRequest request = new RestRequest(resource, Method.POST); request.RequestFormat = DataFormat.Json; request.JsonSerializer = new CustomJsonSerializer(); if (_userContext.UserId.HasValue) { request.AddHeader(HttpHeaderKeys.AuthorizationUserIdKey, _userContext.UserId.ToString()); } var requestDto = new RequestDto(type, data); request.AddBody(requestDto); return request; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class NewAppGateway : INewAppGateway { private readonly string _backendBaseUrl ; private readonly IUserContext _userContext ; public NewAppGateway ( string backendBaseUrl , IUserContext userContext ) { _backendBaseUrl = backendBaseUrl ; _userContext = userContext ; } public void ExecuteCommand ( ICommand command ) { RestClient client = new RestClient ( _backendBaseUrl ) ; var type = command . GetType ( ) . FullName ; var data = JsonConvert . SerializeObject ( command ) ; var request = CreateRequest ( "api/commands" , type , data ) ; client . Execute ( request ) ; } public T ExecuteCommand < T > ( ICommand < T > command ) where T : class , new ( ) { RestClient client = new RestClient ( _backendBaseUrl ) ; var type = command . GetType ( ) . FullName ; var data = JsonConvert . SerializeObject ( command ) ; var request = CreateRequest ( "api/commands-with-result" , type , data ) ; var response = client . Execute < T > ( request ) ; return response . Data ; } public T ExecuteQuery < T > ( IQuery < T > query ) where T : class , new ( ) { var client = new RestClient ( _backendBaseUrl ) ; var type = query . GetType ( ) . FullName ; var data = JsonConvert . SerializeObject ( query ) ; var request = CreateRequest ( "api/queries" , type , data ) ; var response = client . Execute < T > ( request ) ; return response . Data ; } private IRestRequest CreateRequest ( string resource , string type , string data ) { IRestRequest request = new RestRequest ( resource , Method . POST ) ; request . RequestFormat = DataFormat . Json ; request . JsonSerializer = new CustomJsonSerializer ( ) ; if ( _userContext . UserId . HasValue ) { request . AddHeader ( HttpHeaderKeys . AuthorizationUserIdKey , _userContext . UserId . ToString ( ) ) ; } var requestDto = new RequestDto ( type , data ) ; request . AddBody ( requestDto ) ; return request ; } }

Finally, we come to usage of our Gateway. Most often it will be a Controller (in the GRASP terms):

Represents the overall “system”, “root object”, device that the software is running within, or a major subsystem (these are all variations of a facade controller)

In other words, it will be a place in the old application that starts processing the request. In ASP.NET MVC applications this will be the MVC Controller, in WebForms application – the code behind class and in WPF application – ViewModel (from MVVM pattern).

For the purposes of this article, I have prepared a console application to make the example as simple as possible:

Sample application static void Main(string[] args) { INewAppGateway gateway = new NewAppGateway("http://localhost:5000", new UserContextMock { UserId = Guid.Parse("cd5c2ae8-6939-45b1-b07d-fdfb483b337d") }); Console.WriteLine("Adding first product"); gateway.ExecuteCommand(new AddProductCommand("Product 1")); Console.WriteLine("Adding second product"); gateway.ExecuteCommand(new AddProductCommand("Product 2")); Console.WriteLine("Adding third product"); gateway.ExecuteCommand(new AddProductCommand("Product 3")); Console.WriteLine("Get all products"); var products = gateway.ExecuteQuery(new GetProductsQuery()); foreach (var productDto in products) { Console.WriteLine($"Product name : {productDto.Name} for user: {productDto.UserId}"); } Console.ReadKey(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void Main ( string [ ] args ) { INewAppGateway gateway = new NewAppGateway ( "http://localhost:5000" , new UserContextMock { UserId = Guid . Parse ( "cd5c2ae8-6939-45b1-b07d-fdfb483b337d" ) } ) ; Console . WriteLine ( "Adding first product" ) ; gateway . ExecuteCommand ( new AddProductCommand ( "Product 1" ) ) ; Console . WriteLine ( "Adding second product" ) ; gateway . ExecuteCommand ( new AddProductCommand ( "Product 2" ) ) ; Console . WriteLine ( "Adding third product" ) ; gateway . ExecuteCommand ( new AddProductCommand ( "Product 3" ) ) ; Console . WriteLine ( "Get all products" ) ; var products = gateway . ExecuteQuery ( new GetProductsQuery ( ) ) ; foreach ( var productDto in products ) { Console . WriteLine ( $ "Product name : {productDto.Name} for user: {productDto.UserId}" ) ; } Console . ReadKey ( ) ; }

This sample application adds 3 products by executing 3 commands and finally gets the list of added products by executing the query. Of course, in a real application Gateway interface is injected in accordance with the principle of Dependency Inversion Principle.

Example repository

The entire implementation can be found on my specially prepared GitHub repository: https://github.com/kgrzybek/old-dotnet-to-core-sample

Summary

In this article I discussed the following topics:

Approaches and motivations for rewriting the system

Strangler Pattern

Designing the system for rewriting

Implementation of incremental migration from .NET Framework applications to .NET Core

The decision to rewrite the system is often not easy because it is a long-term investment. At the very beginning, the development of a new system rarely brings any business value – only costs.

That is why it is always important to clearly explain to all stakeholders what benefits the rewriting of the system will bring and what risks the lack of such rewriting may result. In this way, it will be much easier to convince anyone to rewrite the system and we will not look like people who only care about playing with new toys. Good luck!

Related Posts

1. Simple CQRS implementation with raw SQL and DDD

2. GRASP – General Responsibility Assignment Software Patterns Explained

3. Processing commands with Hangfire and MediatR