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.

The word change illuminated in white and reflected on a tiled floor.
Yes! But slowly, and deliberately. Photo by Nick Fewings / Unsplash

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:

The Catch Block #85 - Adventures in Refactoring to Dapper
Refactoring is fun! Maybe. Plus: VS 2022 Razor editor, source generators, clean architecture, and a smelly end to the Christmas tree saga.

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:

public class UserController : Controller
{
    private readonly UserRepository _userRepo;

    //UserRepository is injected into UserController 
    //using Dependency Injection
    //Exactly how this is done is left to the reader

    [HttpGet]
    public UserViewModel UserDetails(int id)
    {
        Entities.User userDetails = _userRepo.GetUserByID(id);
        
        UserViewModel model = new UserViewModel();
        model.User = userDetails;
        
        return View(model);
    }
}
This code assumes that the view UserDetails.cshtml exists in the Views/User folder

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:

Announcing .NET 7 Preview 1
Announcing .NET 7 Preview 1, the first preview of a major .NET version that will focus on app modernization, cloud native, containers and more.

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:

ASP.NET Core updates in .NET 7 Preview 1
.NET 7 Preview 1 is now available!. This is the first preview of the next major version of .NET, which will include the next wave of innovations for web development with ASP.NET Core. In .NET 7 we plan to make broad investments across ASP.NET Core.
Announcing Entity Framework 7 Preview 1
EF7 will focus on JSON column support, bulk updates, performance, and enabling EF6 upgrades plus much more. Preview 1 lays the foundation.

I'm looking forward to more .NET goodness in the next several previews.

Other Neat Reads

20 years of .NET
.NET turns 20 years today and it is just kind of ... wow! 20 years of .NET also means to me to be a software engineer for more than 20 years.

Imposter syndrome - a coin with 2 sides?
Lots have been written about Imposter Syndrome , especially with how it relates to the world of software development. I wouldn’t normally wr...

#OpenCV – Detect and blur faces 😁 using haar cascades in C#
Hi ! NET is in anniversary mode, so I think it will be a good idea to share some camera samples in...

Think big, start small
Big feels overwhelming so start small and go from there.

How Everything We’re Told About Website Identity Assurance is Wrong
I have a vehement dislike for misleading advertising. We see it every day; weight loss pills, make money fast schemes and if you travel in the same circles I do, claims that extended validation (EV) certificates actually do something useful: Why are you still claiming this @digicert? This is extreme…

AutoMapper, Nullable DateTime, and Selecting the Right Constructor
I learned a pretty hard lesson while trying to map nullable DateTimeOffset to nullable DateTime and thought I’d share what I found.

Can teams be accountable for delivery of features?
Including delivery/deployment in the definition of done I’m told is unfair because teams aren’t in control of what gets delivered or whe...

Catch Up with the Latest Issue!

The Catch Block #94: C#’s New !! (Bang-Bang) Operator
C#’s new !! operator is surprisingly controversial. Plus: .NET’s 20th; immutability; the CUPID principles; CancellationToken; and a practical guide to Dapper.

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:

The Catch Block - Exception Not Found
A premium .NET, C#, and web tech newsletter. Issues come out every Wednesday!

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!