Refactoring in C# — Step by Step Guide
Refactoring is not about changing what the code does. It is about improving how the code is written.
The goal is to make the code easier to read, easier to test, and easier to maintain.
In this tutorial, we will refactor a simple user registration method step by step.
The Original Code
Imagine we have a method like this:
public void RegisterUser(string username, string password, int age, string email)
{
if (username == null || username == "")
throw new Exception("Username is required");
if (password == null || password == "")
throw new Exception("Password is required");
if (password.Length < 8)
throw new Exception("Password must be at least 8 characters");
if (age < 18 || age > 120)
throw new Exception("Invalid age");
if (email == null || !email.Contains("@") || !email.Contains("."))
throw new Exception("Invalid email");
Console.WriteLine("User registered successfully!");
}
The code works, but it has some problems.
What Is Wrong With This Code?
The method has too many responsibilities:
- It validates input
- It performs business logic
- It mixes concerns
This makes it harder to read and test.
Other issues:
- Validation logic is mixed with business logic
- The method is too long
- Repeated checks
- Primitive obsession (string, int instead of domain types)
Step 1: Use Guard Clauses
public void RegisterUser(string username, string password, int age, string email)
{
ArgumentException.ThrowIfNullOrWhiteSpace(username);
ArgumentException.ThrowIfNullOrWhiteSpace(password);
if (password.Length < 8)
throw new ArgumentException("Password must be at least 8 characters.", nameof(password));
if (age is < 18 or > 120)
throw new ArgumentOutOfRangeException(nameof(age));
if (!IsValidEmail(email))
throw new FormatException("Invalid email format.");
Console.WriteLine("User registered successfully!");
}
private bool IsValidEmail(string email)
{
return !string.IsNullOrWhiteSpace(email)
&& email.Contains("@")
&& email.Contains(".");
}
Step 2: Use a Request Object (DTO)
public class UserRegistrationRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public int Age { get; set; }
public string Email { get; set; } = string.Empty;
}
Updated method:
public void RegisterUser(UserRegistrationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Username);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Password);
if (request.Password.Length < 8)
throw new ArgumentException("Password must be at least 8 characters.");
if (request.Age is < 18 or > 120)
throw new ArgumentOutOfRangeException(nameof(request.Age));
if (!IsValidEmail(request.Email))
throw new FormatException("Invalid email format.");
Console.WriteLine("User registered successfully!");
}
Step 3: Extract Validation Into a Validator
public class UserRegistrationValidator
{
public void Validate(UserRegistrationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Username);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Password);
if (request.Password.Length < 8)
throw new ArgumentException("Password must be at least 8 characters.");
if (request.Age is < 18 or > 120)
throw new ArgumentOutOfRangeException(nameof(request.Age));
if (!IsValidEmail(request.Email))
throw new FormatException("Invalid email format.");
}
private static bool IsValidEmail(string email)
{
return !string.IsNullOrWhiteSpace(email)
&& email.Contains("@")
&& email.Contains(".");
}
}
Service:
public void RegisterUser(UserRegistrationRequest request)
{
var validator = new UserRegistrationValidator();
validator.Validate(request);
Console.WriteLine("User registered successfully!");
}
Step 4: Inject the Validator
public interface IUserRegistrationValidator
{
void Validate(UserRegistrationRequest request);
}
public class UserRegistrationValidator : IUserRegistrationValidator
{
public void Validate(UserRegistrationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Username);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Password);
if (request.Password.Length < 8)
throw new ArgumentException("Password must be at least 8 characters.");
if (request.Age is < 18 or > 120)
throw new ArgumentOutOfRangeException(nameof(request.Age));
if (!IsValidEmail(request.Email))
throw new FormatException("Invalid email format.");
}
private static bool IsValidEmail(string email)
{
return !string.IsNullOrWhiteSpace(email)
&& email.Contains("@")
&& email.Contains(".");
}
}
public class UserRegistrationService
{
private readonly IUserRegistrationValidator _validator;
public UserRegistrationService(IUserRegistrationValidator validator)
{
_validator = validator;
}
public void RegisterUser(UserRegistrationRequest request)
{
_validator.Validate(request);
Console.WriteLine("User registered successfully!");
}
}
Step 5: Value Objects
public sealed record Email
{
public string Value { get; }
public Email(string value)
{
if (string.IsNullOrWhiteSpace(value) ||
!value.Contains("@") ||
!value.Contains("."))
{
throw new FormatException("Invalid email format.");
}
Value = value;
}
public override string ToString() => Value;
}
public sealed record Password
{
public string Value { get; }
public Password(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Password is required.");
if (value.Length < 8)
throw new ArgumentException("Password must be at least 8 characters.");
Value = value;
}
}
Step 6: Specification Pattern
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
}
public class AgeSpecification : ISpecification<int>
{
public bool IsSatisfiedBy(int age)
{
return age is >= 18 and <= 120;
}
}
Step 7: Result Pattern
public record Result(bool Success, string Error)
{
public static Result Ok() => new(true, string.Empty);
public static Result Fail(string error) => new(false, error);
}
public Result RegisterUser(UserRegistrationRequest request)
{
if (string.IsNullOrWhiteSpace(request.Username))
return Result.Fail("Username is required.");
if (string.IsNullOrWhiteSpace(request.Password))
return Result.Fail("Password is required.");
return Result.Ok();
}
Final Service
public class UserRegistrationService
{
private readonly IUserRegistrationValidator _validator;
public UserRegistrationService(IUserRegistrationValidator validator)
{
_validator = validator;
}
public void RegisterUser(UserRegistrationRequest request)
{
_validator.Validate(request);
Console.WriteLine("User registered successfully!");
}
}
Summary
| Technique | Benefit |
|---|---|
| Guard Clauses | Fail fast validation |
| DTO | Cleaner input structure |
| Validator | Separation of concerns |
| DI | Better testability |
| Value Objects | Stronger domain rules |
| Specification Pattern | Flexible business rules |
| Result Pattern | Better API/UI flow |
Bonus: State Pattern Example
Initial code :
foreach (char c in input)
{
if (c == '[')
{
insideBrackets = true;
currentValue.Clear();
}
else if (c == ']' && insideBrackets)
{
results.Add(currentValue.ToString());
insideBrackets = false;
}
else if (insideBrackets)
{
currentValue.Append(c);
}
}
return results;
public class ParserContext
{
public List<string> Results { get; } = new();
public StringBuilder Current { get; } = new();
public IParserState State { get; set; } = new OutsideState();
public void Process(char c) => State.Handle(this, c);
}
public interface IParserState
{
void Handle(ParserContext ctx, char c);
}
public class OutsideState : IParserState
{
public void Handle(ParserContext ctx, char c)
{
if (c == '[')
{
ctx.Current.Clear();
ctx.State = new InsideState();
}
}
}
public class InsideState : IParserState
{
public void Handle(ParserContext ctx, char c)
{
if (c == ']')
{
ctx.Results.Add(ctx.Current.ToString());
ctx.State = new OutsideState();
}
else
{
ctx.Current.Append(c);
}
}
}
Usage :
var input = "Hello [John] and [Mary]";
var context = new ParserContext();
foreach (char c in input)
{
context.Process(c);
}
foreach (var result in context.Results)
{
Console.WriteLine(result);
}
Conclusion
Start simple:
- Guard clauses first
- Extract methods next
- Then introduce DTOs
- Then validators
- Then value objects
- Then patterns (Specification, State, etc.)
Good refactoring makes code simpler, not more complicated.
Top comments (0)