Welcome, dear readers, to the 95th edition of The Catch Block!
In this edition: I spent a lot of time this last week refactoring and paying down technical debt, and share some of the tips I learned from doing so.
Plus: .NET 7 Preview 1; impostor syndrome; accountability; 20 years of .NET; and thinking big.
Tips for Refactoring Large Projects Slowly and Deliberately
I spent most of this past week paying down tech debt for my team's major internal project. This project is our team's killer app, one that needs to be up 24/7, and it was drowning in about 10 years worth of cruft and crap. It has needed a good refactoring for quite a while.
Case in point: the majority of the app was using a LINQ-to-SQL DBML file, even though LINQ-to-SQL was deprecated as far back as 2014. Just because the .NET Framework still includes it doesn't mean we should be using it.
My changes largely consisted of deleting the DBML file and then seeing what broke, and refactoring those things to use Dapper and SQL queries, with simple data-transfer objects (DTOs). In fact, Dear Readers, you might remember that I wrote about doing just that in Issue #85:
But this story won't be a rehash of that one. Instead, what I want to focus on here are some tips that I've picked up for in-place refactoring. Tips that have helped make refactoring this project easier than it would've been.
Tip #1: Refactor One Small Thing at a Time
The primary goal during any major refactoring should be to ensure that at any point during the refactoring, the functionality should still work.
Let's say I have the following C# code which uses an EntityFramework context, and I want to refactor it to use Dapper.
public void AddUser(string firstName, string lastName, DateTime dateOfBirth, string phoneNumber)
{
MyEntities myEntities = new MyEntities(); //EF context
var newUser = new Entities.User
{
FirstName = firstName,
LastName = lastName,
DateOfBirth = dateOfBirth,
PhoneNumber = phoneNumber
};
myEntities.Users.Add(newUser);
myEntities.SaveChanges();
}
If we wanted to rewrite this method using Dapper, it might look like this:
public void AddUser(string firstName, string lastName, DateTime dateOfBirth, string phoneNumber)
{
using (IDbConnection conn = new SqlConnection("connection string"))
{
conn.Open();
var sql = "INSERT INTO dbo.Users (FirstName, LastName, DateOfBirth, PhoneNumber) VALUES (@firstName, @lastName, @dateOfBirth, @phoneNumber)";
conn.Execute(sql, new{ firstName, lastName, dateOfBirth, phoneNumber });
}
}
At this point, you should be able to deploy your code all the way up to production and not impact your users at all. That is the ultimate goal of any refactoring: break the issue down into small enough problems that, after you fix each of them, you could deploy to prod and not impact anything.
But why is this true in this case? Because neither the parameters (inputs) to the AddUser()
method nor the return type (output) from it changed. Since neither inputs nor outputs changed, this is a simple refactoring; nothing else should need to be modified.
What if that wasn't true? How should refactoring proceed in situations where the inputs and/or outputs from a block DO need to change?
Tip #2: Refactor Everything That Small Thing Touches
Before refactoring a different small thing, that is.
Say we have a different method, GetUserByID()
:
public Entities.User GetUserByID(int id)
{
MyEntities myEntities = new MyEntities(); //EF context
return myEntities.Users.First(x => x.ID == id);
}
GetUserByID()
uses the Entities.User
class, which we may want to get rid of in favor of a DTO class (this is precisely what my team has been doing in our big 24/7 project).
Let's first create that DTO class:
namespace DTOs
{
public class User //Data-transfer object
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string PhoneNumber { get; set; }
}
}
Merely coding up this class actually keeps us in the same situation we had before: we could deploy to prod and not impact anything, since nothing actually uses the DTOs.User
class yet.
The next step is to change the method itself. Changing the method is straightforward:
public DTOs.User GetUserByID(int id)
{
using(IDbConnection conn = new SqlConnection("connection string")
{
conn.Open();
string sql = "SELECT * FROM Users WHERE ID = @id";
return conn.QueryFirst<DTOs.User>(sql, new { id } );
}
}
At this point, we should be looking to find any piece of code that calls the GetUserByID()
method, because the return type for it changed.
Let's pretend this is an MVC project, and this method is in a class called UserRepository
. UserRepository
is invoked by a controller, appropriately named UserController
. It might look like this:
The problem that we have now is that we have to change the UserDetails()
action AND the UserViewModel
class to use the new DTOs.User
object instead of the Entities.User
class.
You have probably guessed by now that this could go on for a while. For example, it's possible that UserViewModel
could call another class that also uses Entities.User
, or that the UserDetails()
action is invoked by other actions. But this is the price of refactoring! Without taking the time to slowly fix everything the refactored module touches, you are far more likely to introduce bugs when you are "done" refactoring.
The key is to move slowly, and deliberately. Take the time to find everything touched by the refactored module, and change them to use the new architecture before moving on to another module.
Refactoring takes time! But, as my team and I can attest to now, it also works wonders.
.NET 7 Preview 1 Now Available!
.NET 7 Preview 1 is now available! Check it out here:
As far as I can tell, the major inclusion in .NET 7 is the MAUI (Multi-platform App UI) framework, which also recently released a new preview. Other than that, I don't see any big changes in this preview.
We should also note that .NET 7 is a "Current" release, meaning it's not long-term support and will get updates for only 18 months after release.
To go along with this, ASP.NET and Entity Framework also released their previews for .NET 7:
I'm looking forward to more .NET goodness in the next several previews.
Other Neat Reads
Catch Up with the Latest Issue!
Check Out All Issues of The Catch Block!
If you're interested to see what other kinds of topics we cover in this newsletter, you can check out the entire backlog on this page:
If you're ready to get this .NET, web tech, and story goodness in your inbox every Wednesday, click the button below to sign up as a subscriber to The Catch Block!
Thanks for reading, and we'll see you next week!