Don't Repeat Yourself (DRY) in C#: Write Code Once, Use It Everywhere

Don't Repeat Yourself (DRY) in C#: Write Code Once, Use It Everywhere

The core idea behind the Don't-Repeat-Yourself (DRY) principle is straightforward: every piece of knowledge should have a single, unambiguous representation in your system.

When you find yourself copying and pasting code, that's a red flag. Let me walk you through practical scenarios where DRY can transform your C# codebase.

The Email Validation Problem

Imagine you're building a user management system. You need to validate email addresses in multiple places: registration, profile updates, and contact forms.

Here's what many developers write:

public void RegisterUser(string email, string password)
{
    if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
    {
        throw new Exception("Invalid email format");
    }
    // Registration logic...
}

public void UpdateProfile(string email, string phone)
{
    if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
    {
        throw new Exception("Invalid email format");
    }
    // Update logic...
}

public void SendNewsletter(string email, string content)
{
    if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
    {
        throw new Exception("Invalid email format");
    }
    // Send logic...
}        

What's the problem? The validation logic appears three times. Tomorrow, if your business requires more robust validation (checking domain, format, etc.), you'll need to update it everywhere.

Solution: Extract Validation Logic

Create a dedicated validator:

public static class EmailValidator
{
    private static readonly Regex EmailRegex = 
        new Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
    
    public static bool IsValid(string email)
    {
        return !string.IsNullOrWhiteSpace(email) && 
               EmailRegex.IsMatch(email);
    }
    
    public static void ValidateOrThrow(string email)
    {
        if (!IsValid(email))
        {
            throw new ArgumentException("Invalid email format");
        }
    }
}        

Now your methods become cleaner:

public void RegisterUser(string email, string password)
{
    EmailValidator.ValidateOrThrow(email);
    // Registration logic...
}

public void UpdateProfile(string email, string phone)
{
    EmailValidator.ValidateOrThrow(email);
    // Update logic...
}        

One change, propagated everywhere!


Logging Repetition Across Your Application

Consider this common pattern in enterprise applications:

public class OrderService
{
    public void CreateOrder(Order order)
    {
        try
        {
            // Order creation logic
            Console.WriteLine($"[INFO] Order {order.Id} created at {DateTime.Now}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ERROR] Failed to create order: {ex.Message} at {DateTime.Now}");
            throw;
        }
    }
}

public class PaymentService
{
    public void ProcessPayment(Payment payment)
    {
        try
        {
            // Payment logic
            Console.WriteLine($"[INFO] Payment {payment.Id} processed at {DateTime.Now}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ERROR] Failed to process payment: {ex.Message} at {DateTime.Now}");
            throw;
        }
    }
}        

Every service repeats the same logging pattern. What happens when you need to switch from Console to a file, or add structured logging with correlation IDs?

Solution: Centralize Logging Infrastructure

public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message, Exception ex);
}

public class ConsoleLogger : ILogger
{
    public void LogInfo(string message)
    {
        Console.WriteLine($"[INFO] {message} at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
    }
    
    public void LogError(string message, Exception ex)
    {
        Console.WriteLine($"[ERROR] {message}: {ex.Message} at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
    }
}

public class OrderService
{
    private readonly ILogger _logger;
    
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }
    
    public void CreateOrder(Order order)
    {
        try
        {
            // Order creation logic
            _logger.LogInfo($"Order {order.Id} created");
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to create order", ex);
            throw;
        }
    }
}        

Now you can swap ConsoleLogger for FileLogger or DatabaseLogger without touching your service classes.


Configuration Values Scattered Everywhere

Here's another common scenario:

public class EmailService
{
    public void SendWelcomeEmail(string to)
    {
        var client = new SmtpClient("smtp.company.com", 587);
        // Send email...
    }
}

public class NotificationService
{
    public void SendAlert(string to)
    {
        var client = new SmtpClient("smtp.company.com", 587);
        // Send notification...
    }
}

public class ReportService
{
    public void SendReport(string to)
    {
        var client = new SmtpClient("smtp.company.com", 587);
        // Send report...
    }
}        

The SMTP configuration is hardcoded three times. When you move from development to production, you'll hunt for every instance.

Solution: Configuration Class

public static class AppConfig
{
    public static class Email
    {
        public const string SmtpHost = "smtp.company.com";
        public const int SmtpPort = 587;
        public const int MaxRetries = 3;
        public const int TimeoutSeconds = 30;
    }
}

public class EmailService
{
    private readonly SmtpClient _client;
    
    public EmailService()
    {
        _client = new SmtpClient(AppConfig.Email.SmtpHost, 
                                 AppConfig.Email.SmtpPort)
        {
            Timeout = AppConfig.Email.TimeoutSeconds * 1000
        };
    }
}        

Even better, load from appsettings.json using IConfiguration for true external configuration.


Repeated Null Checks and Validation

Look at this pattern:

public class UserService
{
    public void UpdateUserName(User user, string newName)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(newName))
            throw new ArgumentException("Name cannot be empty");
        
        user.Name = newName;
    }
    
    public void UpdateUserEmail(User user, string newEmail)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(newEmail))
            throw new ArgumentException("Email cannot be empty");
        
        user.Email = newEmail;
    }
    
    public void UpdateUserPhone(User user, string newPhone)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrWhiteSpace(newPhone))
            throw new ArgumentException("Phone cannot be empty");
        
        user.Phone = newPhone;
    }
}        

The null-checking boilerplate is everywhere!

Solution: Guard Clauses Helper

public static class Guard
{
    public static void AgainstNull<T>(T value, string paramName) where T : class
    {
        if (value == null)
            throw new ArgumentNullException(paramName);
    }
    
    public static void AgainstNullOrEmpty(string value, string paramName)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException($"{paramName} cannot be empty", paramName);
    }
}

public class UserService
{
    public void UpdateUserName(User user, string newName)
    {
        Guard.AgainstNull(user, nameof(user));
        Guard.AgainstNullOrEmpty(newName, nameof(newName));
        
        user.Name = newName;
    }
    
    public void UpdateUserEmail(User user, string newEmail)
    {
        Guard.AgainstNull(user, nameof(user));
        Guard.AgainstNullOrEmpty(newEmail, nameof(newEmail));
        EmailValidator.ValidateOrThrow(newEmail);
        
        user.Email = newEmail;
    }
}        

Much cleaner, and you can extend Guard with more validation patterns.


Database Connection Strings

This anti-pattern is everywhere:

public class CustomerRepository
{
    public List<Customer> GetAll()
    {
        using (var conn = new SqlConnection("Server=localhost;Database=MyDb;User Id=sa;Password=secret"))
        {
            // Query logic...
        }
    }
}

public class OrderRepository
{
    public List<Order> GetAll()
    {
        using (var conn = new SqlConnection("Server=localhost;Database=MyDb;User Id=sa;Password=secret"))
        {
            // Query logic...
        }
    }
}        

Don't do this! Security risk, maintenance nightmare, and deployment headache.

Solution: Dependency Injection with Configuration

public interface IDbConnectionFactory
{
    SqlConnection CreateConnection();
}

public class SqlConnectionFactory : IDbConnectionFactory
{
    private readonly string _connectionString;
    
    public SqlConnectionFactory(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }
    
    public SqlConnection CreateConnection()
    {
        return new SqlConnection(_connectionString);
    }
}

public class CustomerRepository
{
    private readonly IDbConnectionFactory _connectionFactory;
    
    public CustomerRepository(IDbConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }
    
    public List<Customer> GetAll()
    {
        using (var conn = _connectionFactory.CreateConnection())
        {
            // Query logic...
        }
    }
}        

Now connection strings live in one place (appsettings.json), and switching databases becomes trivial.


Conditional Logic for User Permissions

Consider permission checks scattered everywhere:

public void DeleteOrder(int orderId, User currentUser)
{
    if (currentUser.Role != "Admin" && currentUser.Role != "Manager")
    {
        throw new UnauthorizedAccessException();
    }
    // Delete logic...
}

public void ApproveInvoice(int invoiceId, User currentUser)
{
    if (currentUser.Role != "Admin" && currentUser.Role != "Manager")
    {
        throw new UnauthorizedAccessException();
    }
    // Approve logic...
}        

Solution: Authorization Service

public interface IAuthorizationService
{
    bool CanManageOrders(User user);
    bool CanApproveInvoices(User user);
}

public class AuthorizationService : IAuthorizationService
{
    private static readonly string[] ManagerRoles = { "Admin", "Manager" };
    
    public bool CanManageOrders(User user)
    {
        return ManagerRoles.Contains(user.Role);
    }
    
    public bool CanApproveInvoices(User user)
    {
        return ManagerRoles.Contains(user.Role);
    }
}

public class OrderService
{
    private readonly IAuthorizationService _authService;
    
    public OrderService(IAuthorizationService authService)
    {
        _authService = authService;
    }
    
    public void DeleteOrder(int orderId, User currentUser)
    {
        if (!_authService.CanManageOrders(currentUser))
        {
            throw new UnauthorizedAccessException();
        }
        // Delete logic...
    }
}        

When permission rules change, you update one service instead of hunting through dozens of methods.


Key Takeaways

Extract validation into reusable helpers and validators ✅ Centralize configuration in constants or configuration files ✅ Use dependency injection to avoid hardcoded dependencies ✅ Create guard clauses for common argument validation ✅ Abstract infrastructure (logging, database) behind interfaces ✅ Consolidate business rules in dedicated services

Remember: DRY isn't about eliminating every line of duplicate code. It's about ensuring that each piece of business logic, each rule, and each configuration exists in exactly one place. When that knowledge needs to change, you change it once.

The real benefit? Maintainability. Six months from now, when requirements change, you'll thank yourself for following DRY.

What's the most painful duplication you've encountered in your codebase? Drop a comment! 👇

**#CSharp #CleanCode #DRY #SoftwareEngineering #DotNet #BestPractices #CodeQuality #ProgrammingSoftwareDevelopment #DesignPrinciples
































To view or add a comment, sign in

More articles by Naresh Kumar Katta

Others also viewed

Explore content categories