ADO - Implementing the Repository Pattern with ADO.NET

The Repository Pattern is a software design pattern that acts as an intermediary between the application's business logic and the database. Instead of allowing different parts of the application to directly interact with ADO.NET classes such as SqlConnection, SqlCommand, and SqlDataReader, all database operations are managed through repository classes. This approach improves code organization, maintainability, scalability, and testability.

In applications that use ADO.NET, SQL queries are often written directly inside forms, controllers, or service classes. As the application grows, this leads to duplicated code, poor maintainability, and difficulty in modifying database logic. The Repository Pattern addresses these problems by placing all database-related operations into dedicated repository classes.

What is the Repository Pattern?

A repository is a class that provides methods for performing Create, Read, Update, and Delete (CRUD) operations on a specific database table or entity. It acts as a collection of objects, hiding the details of how data is stored or retrieved.

Instead of writing SQL statements throughout the application, the repository exposes simple methods such as:

  • GetAllEmployees()

  • GetEmployeeById()

  • AddEmployee()

  • UpdateEmployee()

  • DeleteEmployee()

The application simply calls these methods without worrying about the underlying SQL commands.

Why Use the Repository Pattern?

Using the Repository Pattern offers several advantages:

Better Code Organization

Database access code is separated from business logic. This creates a cleaner project structure where each class has a single responsibility.

Easier Maintenance

If a database query needs modification, the change is made only inside the repository instead of multiple files.

Improved Reusability

Repository methods can be reused by different parts of the application, reducing duplicate code.

Better Testability

Business logic can be tested independently by replacing the repository with mock or fake implementations during testing.

Scalability

As the application grows, additional repositories can be created for different entities without affecting the existing architecture.

Basic Project Structure

A typical application using the Repository Pattern may be organized as follows:

Project
│
├── Models
│     Employee.cs
│     Department.cs
│
├── Repositories
│     IEmployeeRepository.cs
│     EmployeeRepository.cs
│
├── Services
│     EmployeeService.cs
│
├── Database
│     DbConnection.cs
│
└── Program.cs

Each folder has a specific purpose:

  • Models represent database entities.

  • Repositories contain database access logic.

  • Services contain business rules.

  • Database folder manages database connections.

Creating the Model Class

Suppose there is an Employee table containing:

  • EmployeeID

  • Name

  • Department

  • Salary

The corresponding model class stores these properties.

public class Employee
{
    public int EmployeeID { get; set; }
    public string Name { get; set; }
    public string Department { get; set; }
    public decimal Salary { get; set; }
}

The model represents one record from the database.

Creating the Repository Interface

An interface defines the operations supported by the repository.

public interface IEmployeeRepository
{
    List<Employee> GetAllEmployees();
    Employee GetEmployeeById(int id);
    void AddEmployee(Employee employee);
    void UpdateEmployee(Employee employee);
    void DeleteEmployee(int id);
}

Using an interface allows different implementations without changing the business logic.

Implementing the Repository

The repository class implements the interface and performs actual database operations.

public class EmployeeRepository : IEmployeeRepository
{
    private readonly string connectionString;

    public EmployeeRepository(string connectionString)
    {
        this.connectionString = connectionString;
    }
}

The repository stores the connection string and uses it for database access.

Reading Records from the Database

The GetAllEmployees() method retrieves every employee.

The implementation generally follows these steps:

  1. Open a database connection.

  2. Create a SQL query.

  3. Execute the query.

  4. Read data using SqlDataReader.

  5. Convert rows into Employee objects.

  6. Return the collection.

Example:

public List<Employee> GetAllEmployees()
{
    List<Employee> employees = new List<Employee>();

    using(SqlConnection con = new SqlConnection(connectionString))
    {
        SqlCommand cmd = new SqlCommand(
            "SELECT * FROM Employee", con);

        con.Open();

        SqlDataReader reader = cmd.ExecuteReader();

        while(reader.Read())
        {
            Employee emp = new Employee();

            emp.EmployeeID = Convert.ToInt32(reader["EmployeeID"]);
            emp.Name = reader["Name"].ToString();
            emp.Department = reader["Department"].ToString();
            emp.Salary = Convert.ToDecimal(reader["Salary"]);

            employees.Add(emp);
        }
    }

    return employees;
}

The calling code receives a list of Employee objects instead of dealing with SQL statements.

Retrieving a Single Record

The repository can provide a method to retrieve one employee.

GetEmployeeById(int id)

Internally, this method uses a parameterized SQL query.

SELECT * FROM Employee
WHERE EmployeeID=@EmployeeID

Using parameters improves security and prevents SQL injection attacks.

Inserting Data

The AddEmployee() method inserts new records.

Typical SQL statement:

INSERT INTO Employee
(Name, Department, Salary)
VALUES
(@Name, @Department, @Salary)

The repository receives an Employee object, extracts its values, and executes the SQL command.

Updating Existing Records

The UpdateEmployee() method modifies existing information.

Example SQL:

UPDATE Employee
SET
Name=@Name,
Department=@Department,
Salary=@Salary
WHERE EmployeeID=@EmployeeID

The repository updates only the required record.

Deleting Records

The DeleteEmployee() method removes data.

SQL statement:

DELETE FROM Employee
WHERE EmployeeID=@EmployeeID

Again, parameters are used for security.

Using the Repository in Business Logic

Instead of writing SQL commands, business logic simply calls repository methods.

Example:

IEmployeeRepository repository =
new EmployeeRepository(connectionString);

List<Employee> employees =
repository.GetAllEmployees();

The business layer remains independent of database implementation details.

Using Services Alongside Repositories

A service layer often sits between the user interface and the repository.

User Interface
       │
       ▼
Service Layer
       │
       ▼
Repository
       │
       ▼
Database

For example:

  • Repository retrieves employee records.

  • Service calculates employee bonuses.

  • User interface displays the results.

Each layer performs its own responsibility.

Benefits of Using Interfaces

Interfaces provide flexibility.

For example:

IEmployeeRepository repository;

Later, the implementation can be changed to:

  • SQL Server

  • Oracle

  • MySQL

  • SQLite

  • Mock Repository for testing

without modifying business logic.

Exception Handling

Repository methods should handle database errors properly.

try
{
    // Database operation
}
catch(SqlException ex)
{
    Console.WriteLine(ex.Message);
}

Proper exception handling prevents unexpected application crashes.

Managing Database Connections

Repositories should always close connections automatically.

The using statement ensures:

  • Connections are released.

  • Resources are cleaned up.

  • Memory leaks are avoided.

Example:

using(SqlConnection con = new SqlConnection(connectionString))
{
    // Database code
}

This is considered the recommended practice in ADO.NET.

Using Transactions

Repositories can execute multiple SQL statements inside a transaction.

Example:

  • Insert Employee

  • Insert Payroll

  • Insert Attendance

If one operation fails, all changes are rolled back.

This maintains data consistency.

Dependency Injection

Instead of creating repositories manually, modern applications often use Dependency Injection.

Example:

public EmployeeService(IEmployeeRepository repository)
{
    this.repository = repository;
}

This approach reduces tight coupling and makes the application easier to test and maintain.

Repository Pattern vs Direct ADO.NET Access

Direct ADO.NET Repository Pattern
SQL code scattered across the project SQL code centralized in repositories
Difficult to maintain Easy to maintain
High code duplication Reusable methods
Hard to test Easy to mock and test
Business logic mixed with data access Clear separation of concerns
Difficult to scale Easier to extend

Best Practices

When implementing the Repository Pattern with ADO.NET, follow these best practices:

  • Keep SQL queries inside repository classes only.

  • Use parameterized queries to prevent SQL injection.

  • Create separate repositories for different entities.

  • Use interfaces for all repositories.

  • Manage database connections with the using statement.

  • Handle exceptions appropriately.

  • Keep repositories focused solely on data access and move business rules to the service layer.

  • Use transactions when multiple related database operations must succeed or fail together.

  • Adopt Dependency Injection to reduce coupling and improve testability.

  • Write clean, reusable methods with descriptive names.

Conclusion

Implementing the Repository Pattern with ADO.NET is an effective way to build structured, maintainable, and scalable database-driven applications. By encapsulating all database operations within repository classes, developers achieve a clear separation between data access and business logic. This results in cleaner code, easier maintenance, improved reusability, stronger testability, and greater flexibility as the application evolves. Whether developing a small business application or a large enterprise system, combining ADO.NET with the Repository Pattern provides a robust foundation for managing database interactions efficiently while following sound software design principles.