RichardSoeteman.NET

Refactoring a large codebase with refactoring attributes

Published 21 February 2024

We've all been in this situation where we inherited a large project that is a complete mess and needed to be migrated to the latest version of Umbraco. Where to start when your first thought is let's start over? In this blog post I show you a technique I have quite some success with refactoring large codebases without breaking the whole solution or project scope.

Attributes as post-it notes

When migrating a project to the latest version of Umbraco, the main goal is the conversion, since we need to maintain the solution our other goal is to make sure we can maintain the code. To avoid we lose focus on the migration task itself I use attributes for refactoring.

Think of them as post-it notes you can add on top of Interfaces/Classes/Methods whatever to make notes for yourself while refactoring and remove them after you are done with refactoring. Adding these post-it's directly in the code forces you not to forget later on but still can continue on the initial migration task. 

Usually I end up with these 3 attributes:

  • Refactor, this attribute describes that something would need a refactoring that is not possible yet because it would break the solution.
  • Question, this attribute describes that I have a question about the code that needs to be answered (by the customer/ other team whatever).
  • DeleteLater  this attribute describes that the code is marked for delete.

To make sure we can say something meaningful about our intent we add a string parameter to the constructor of the attribute class so we can use that to describe our intent.

Why not just use ToDo comments and Obsolete attributes?

The reason why I prefer these custom attributes instead of ToDo comments or Obsolete attributes is simple. Open a random codebase and you see those comments and obsolete items already before starting to refactor the code and would just be noise for our task which we try to avoid.

Reference app

As much as I want to sometimes I don't share code from clients so I made a small reference app for this blog post that can be found on my GitHub page. Basically it is a small Umbraco site with two projects that contain some logic"

First we need to add the attributes to our solution. In our reference app you see all projects reference the core project so we add our attributes in there so we can use them everywhere in the solution.

Question attribute sample

We use the Question attribute for items we have questions about. 

Our reference app uses a Fax service for people that don't have email. In 2024 it might be a good idea to remove this, still our customer needs to agree on that and we need to ask them.

/// <summary>
/// Service for sending faxes to customers who don't have email
/// </summary>
[Question("It's 2024 do we really need a fax service?")]
public class FaxService :IFaxService
{
    /// <summary>
    /// Sends a Fax to the given fax number with the given document and returns true if the fax was sent successfully.
    /// </summary>
    public bool SendFax(string faxNumber, string document)
    {
        // This is a reference app just for outlining the refactoring attributes don't expect sending faxes here.
        return true;
    }
}
Refactor attribute sample

We use the Refactor attribute for items we will need to refactor later on. 

The ProductService from our reference app uses a legacy Product Service for the old product. Removing the legacy ProductService immediately would require that we first need to complete the task of removing the legacy service before we can continue. So by adding the refactor attribute we are simply making sure we don't forget to remove the legacy ProductService later on and still can continue with the migration.

public class ProductService(ILegacyProductService _legacyProductService) :IProductService
{
    [Refactor("Ensure the legacy products get in our new database and have a proper SKU then remove legacy productService")]
    public Product GetProduct(string sku)
    {
        if (sku.StartsWith("std-"))
        {
            //old product get from the legacy productService.
            return  _legacyProductService.GetProductById(sku.Replace("std-","").ToInt());
        }
        
        //Reference app so we don't get a real product from the database.
        return new Product();
    }
}
DeleteLater attribute sample

We use the DeleteLater attribute for items we will remove later in the project. 

As you could see in the previous sample. Our reference app uses a legacy service we want to remove. By adding the DeleteLater attribute we make sure we don't forget to delete this later on. 

/// <summary>
/// Legacy productService only used for the old product system.
/// </summary>
[DeleteLater("Ensure we delete all legacy before going live with the new product")]
public class LegacyProductService :ILegacyProductService
{
    /// <summary>
    /// Gets the product based on the given id.
    /// </summary>
    public Product GetProductById(int id)
    {
        //Reference app so we don't get a real product from the database.
        return new Product();
    }
}
Overview of all refactoring items

By using the show all references option in Visual studio on the attributes you can get a complete overview of all your little post-it's. Since we didn't do the actual refactoring itself at this point it's also a nice overview you can give to your customer or manager to give them some insights about the state of the code and what needs to be done to fix this. 

Apply the necessary changes and when you are done remove the attributes from the codebase again. This should be done in a short time frame otherwise this technique will not help you then it will be just as powerful as a ToDo statement that you find in every codebase. 

Hope this post helps you as much as it helped me.