ASP.NET Core  

Step by Step Practical: Using Dependency Injection with Repository Pattern and ASP.NET Core Web API

Introduction

In modern ASP.NET Core applications, Dependency Injection (DI) and the Repository Pattern are often used together to create clean, maintainable, and testable code.

  • Repository Pattern abstracts data access logic.

  • Dependency Injection provides dependencies (like repositories) to controllers or services automatically.

Using both together ensures your Web API is scalable, loosely coupled, and easier to maintain.

This article explains step by step how to implement DI with the Repository Pattern in an ASP.NET Core Web API project.

Step 1: Create a New ASP.NET Core Web API Project

Open a terminal and run:

dotnet new webapi -n DIRepoApi
cd DIRepoApi
dotnet run

This creates a new Web API project that runs at https://localhost:5001.

Step 2: Create the Data Model

Create a Models/User.cs file:

namespace DIRepoApi.Models
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
}

This model represents a User entity in the application.

Step 3: Create the Repository Interface

Create a Repositories/IUserRepository.cs file:

using DIRepoApi.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DIRepoApi.Repositories
{
    public interface IUserRepository
    {
        Task<IEnumerable<User>> GetAllUsersAsync();
        Task<User> GetUserByIdAsync(int id);
        Task AddUserAsync(User user);
        Task UpdateUserAsync(User user);
        Task DeleteUserAsync(int id);
    }
}
  • This defines all methods to interact with users.

  • Notice that there is no database code here, just method signatures.

Step 4: Implement the Repository

Create Repositories/UserRepository.cs:

using DIRepoApi.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DIRepoApi.Repositories
{
    public class UserRepository : IUserRepository
    {
        private readonly List<User> _users = new List<User>();

        public Task<IEnumerable<User>> GetAllUsersAsync() => Task.FromResult(_users.AsEnumerable());

        public Task<User> GetUserByIdAsync(int id) => Task.FromResult(_users.FirstOrDefault(u => u.Id == id));

        public Task AddUserAsync(User user)
        {
            user.Id = _users.Count + 1;
            _users.Add(user);
            return Task.CompletedTask;
        }

        public Task UpdateUserAsync(User user)
        {
            var existing = _users.FirstOrDefault(u => u.Id == user.Id);
            if (existing != null)
            {
                existing.Name = user.Name;
                existing.Email = user.Email;
            }
            return Task.CompletedTask;
        }

        public Task DeleteUserAsync(int id)
        {
            var user = _users.FirstOrDefault(u => u.Id == id);
            if (user != null) _users.Remove(user);
            return Task.CompletedTask;
        }
    }
}

For simplicity, this example uses an in-memory list instead of a database.
You can replace it with EF Core and SQL Server later.

Step 5: Register Repository in Dependency Injection

In Program.cs, add the repository to the DI container:

using DIRepoApi.Repositories;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Register repository using Scoped lifetime
builder.Services.AddScoped<IUserRepository, UserRepository>();

var app = builder.Build();

app.MapControllers();

app.Run();
  • AddScoped means one instance of UserRepository per HTTP request.

  • DI will automatically provide IUserRepository to any controller that needs it.

Step 6: Create the Controller

Create Controllers/UsersController.cs:

using DIRepoApi.Models;
using DIRepoApi.Repositories;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DIRepoApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        private readonly IUserRepository _userRepository;

        // Dependency injected via constructor
        public UsersController(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        [HttpGet]
        public async Task<IEnumerable<User>> GetUsers()
        {
            return await _userRepository.GetAllUsersAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<User>> GetUser(int id)
        {
            var user = await _userRepository.GetUserByIdAsync(id);
            if (user == null) return NotFound();
            return user;
        }

        [HttpPost]
        public async Task<ActionResult> AddUser(User user)
        {
            await _userRepository.AddUserAsync(user);
            return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
        }

        [HttpPut("{id}")]
        public async Task<ActionResult> UpdateUser(int id, User user)
        {
            if (id != user.Id) return BadRequest();
            await _userRepository.UpdateUserAsync(user);
            return NoContent();
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteUser(int id)
        {
            await _userRepository.DeleteUserAsync(id);
            return NoContent();
        }
    }
}

Key Points

  • The controller never creates the repository manually.

  • DI provides the IUserRepository instance automatically.

  • Clean separation between controller logic and data access logic.

Step 7: Testing the API

Run the project and use Postman or Swagger:

  1. GET /api/users – Retrieve all users.

  2. POST /api/users – Add a new user. Example JSON:

{
    "name": "Rahul",
    "email": "[email protected]"
}
  1. PUT /api/users/1 – Update user.

  2. DELETE /api/users/1 – Delete user.

Everything works without the controller knowing the implementation details of the repository.

Advantages of Using DI with Repository Pattern

  1. Loose Coupling: Controllers don’t depend on concrete repository classes.

  2. Easier Unit Testing: Mock IUserRepository for tests.

  3. Scalability: Easily replace UserRepository with EF Core or any other database provider.

  4. Maintainability: Data access logic is separated and easier to update.

Example: Unit Test Using DI

using DIRepoApi.Controllers;
using DIRepoApi.Models;
using DIRepoApi.Repositories;
using Moq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

public class UsersControllerTests
{
    [Fact]
    public async Task GetUsers_ReturnsAllUsers()
    {
        var mockRepo = new Mock<IUserRepository>();
        mockRepo.Setup(repo => repo.GetAllUsersAsync())
                .ReturnsAsync(new List<User> { new User { Id = 1, Name = "Test" } });

        var controller = new UsersController(mockRepo.Object);
        var result = await controller.GetUsers();

        Assert.Single(result);
    }
}

This test works without any real database, thanks to dependency injection.

Conclusion

Using Dependency Injection with Repository Pattern in ASP.NET Core Web API provides a clean, modular, and testable architecture.

  • DI allows the controller to receive dependencies automatically.

  • Repository Pattern encapsulates data access logic.

  • Together, they make applications scalable, maintainable, and unit-testable.

This is a professional approach for building production-ready ASP.NET Core Web APIs.

Once you master this, you can combine it with Entity Framework Core, Angular, or React to create a full-stack application.