Contents
In this post, I will talking about the Repository
pattern and why should we use it.
1. Introduction
The Repository design pattern is a widely-used software design pattern that provides an abstraction layer between the business logic and the persistence layer of an application. It is used to manage the persistence of objects in a database or other data store. It separates the logic that retrieves the data and maps it to the domain model from the business logic that acts on the model. This separation of concerns allows for a more modular and flexible architecture that is easier to maintain and test.
The Repository pattern typically involves defining a set of interfaces for data access operations, such as create, read, update, and delete (CRUD) operations. These interfaces are then implemented by concrete repository classes that interact with the underlying data store.
By abstracting away the details of the data store, the Repository pattern allows the business logic to interact with the data using a uniform interface, regardless of the specific implementation of the data store. This makes it easier to switch between different data stores or to change the schema of the data store without affecting the rest of the application.
And Repository pattern is commonly used in object-oriented programming, particularly in applications that use an object-relational mapping (ORM) framework to interact with a database. However, it can also be used in other contexts, such as web services or file-based data stores.
Ok, let’s starting to try it!
2. Create Repository
From the last article, we have created an User
model and connect to database, so we can create the Repository
now.
For separate the logic between data access and business, it will use interface in Repository
pattern, so we will create the below interface for User
model, it should include the base CRUD
operations.
Create a folder name Repositories
under MyDemo.Core
, and create an Interfaces
folder
// MyDemo.Core/Repositories/Interfaces/IUserRepository.cs
public interface IUserRepository
{
IQueryable<User> GetAll();
IQueryable<User> GetWithCondition(Expression<Func<User, bool>> expression);
void Create(User entity);
void Update(User entity);
void Delete(User entity);
}
And then we can create the UserRepository.cs
file to implement the interface
// MyDemo.Core/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
protected ApplicationDbContext _context { get; set; }
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<User> GetAll() => _context.Set<User>().AsNoTracking();
public IQueryable<User> GetWithCondition(Expression<Func<User, bool>> expression) =>
_context.Set<User>().Where(expression).AsNoTracking();
public void Create(User entity) => _context.Set<User>().Add(entity);
public void Update(User entity) => _context.Set<User>().Update(entity);
public void Delete(User entity) => _context.Set<User>().Remove(entity);
}
The above codes will run well, but there is a problem in this way, what if there are many modes need to be implement the pattern? and we need to copy these almost same codes again and again? and if we want to change a method, we also need to update many of modes! So the other better way is use the Base Class
for avoid the duplicate coding.
We can create an IBaseRepository
for handle the common CRUD
methods
// MyDemo.Core/Repositories/Interfaces/IBaseRepository.cs
public interface IBaseRepository<T>
{
IQueryable<T> GetAll();
IQueryable<T> GetWithCondition(Expression<Func<T, bool>> expression);
void Create(T entity);
void Update(T entity);
void Delete(T entity);
}
As you can see, we use the generic in the base interface, so that can be handle difference models, and we need to implement it :
// MyDemo.Core/Repositories/BaseRepository.cs
public class BaseRepository<T> : IBaseRepository<T> where T : class
{
protected ApplicationDbContext _context { get; set; }
public BaseRepository(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<T> GetAll() => _context.Set<T>().AsNoTracking();
public IQueryable<T> GetWithCondition(Expression<Func<T, bool>> expression) =>
_context.Set<T>().Where(expression).AsNoTracking();
public void Create(T entity) => _context.Set<T>().Add(entity);
public void Update(T entity) => _context.Set<T>().Update(entity);
public void Delete(T entity) => _context.Set<T>().Remove(entity);
}
Ok, then we need to update the user repository interface
public interface IUserRepository : IBaseRepository<User>
{
}
In the end, we also need to update the implementation
public class UserRepository : BaseRepository<User>, IUserRepository
{
public UserRepository(ApplicationDbContext context) : base(context) {
}
}
Just so simple, right?
We only need to inherit the base repository and will be ok! But please hold on, why we need to create the empty repository class? Why not just use one base class for handle all of the models?
There are two reasons for do that:
- We need to identity what model will be use and pass the instance class to the base class, although you also can pass the instance to base class directly, but we suggest create a model class and that will be easy to maintain
Another important reason is maybe there will be other custom methods in a model. For example handle the user access right, this should be just for user model, it’s not a common CRUD method, so we need an user model for handle it, we can add more custom methods
3. Using the Async Programming
3.1. What’s Async Programming
In traditional synchronous programming, each operation is executed sequentially, one after the other. This means that if a long-running operation is encountered, the program will be blocked until the operation completes, which can lead to unresponsive or slow applications. Async programming is a programming paradigm that allows for non-blocking, asynchronous execution of code, enabling programs to perform multiple tasks concurrently without blocking the main thread or waiting for long-running operations to complete.
With async programming, long-running operations are executed in the background, allowing the program to continue executing other operations without waiting for the long-running operation to complete. This is achieved using asynchronous functions and callbacks, which enable non-blocking execution of code.
Async programming is particularly useful in scenarios where programs need to perform I/O operations, such as reading and writing to a file or making network requests. These operations can take a long time to complete, during which the program would be blocked in synchronous programming. With async programming, the program can continue executing other operations while the I/O operation is being performed in the background.
By using async programming, we can avoid performance bottlenecks and enhance the responsiveness of our application. So let’s try to use it!
3.2. Create Async Method
We can create the two async method in base interface
// MyDemo.Core/Repositories/Interfaces/IBaseRepository.cs
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> GetWithConditionAsync(Expression<Func<T, bool>> expression);
Task<T?> GetItemWithConditionAsync(Expression<Func<T, bool>> expression);
and implement it
// MyDemo.Core/Repositories/BaseRepository.cs
public async Task<IEnumerable<T>> GetAllAsync() => await GetAll().ToListAsync();
public async Task<IEnumerable<T>> GetWithConditionAsync(Expression<Func<T, bool>> expression) =>
await GetWithCondition(expression).ToListAsync();
4. Using the Codes
Let’s create an API controller to show how to use the repository. As we need to handle the API result, so it should create a common result object for return it fo client will be better.
4.1. Create Api Result Object
First of all, the API should return the status for determine the process success or not, and then should be include the message for describe the result, in the end we need a data object for return the query data from database, so there are at least 3 fields in the ApiResult
object:
// MyDemo.Core/Data/ApiResult.cs
public class ApiResult<T>
{
public ApiResult()
{
this.Success = true;
}
/// <summary>
/// TRUE if the Api attempt is successful, FALSE otherwise.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Api return result message
/// </summary>
public string Message { get; set; } = null!;
/// <summary>
/// Return the data for Api
/// </summary>
public T? Data { get; set; }
}
We use the generic for handle any of models.
4.2. Cerate Api Controller
Create an user Api controller, we can pass the repository object through constructor
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserRepository _userRepository;
public UserController(IUserRepository userRepository)
{
this._userRepository = userRepository;
}
}
And then create a Get
method for get user data
[HttpGet("users")]
public async Task<ActionResult<IEnumerable<User>>> GetUsers()
{
var apiResult = new ApiResult<IEnumerable<User>>();
try
{
//here is how to use the repository method
apiResult.Data = await _userRepository.GetAllAsync();
//return the result to client side
return Ok(apiResult);
}
catch (Exception ex)
{
//handle the exception
apiResult.Success = false;
apiResult.Message = ex.Message;
return StatusCode(500, apiResult);
}
}
The above Api controller sample only for demo how to use the Repository, so it’s simple and not complete, I will show you next time for how to create CRUD in the Api controller!
The last import thing, you need to register the repository to service before you use it, add below in
program.cs
builder.Services.AddScoped<IUserRepository, UserRepository>();
5. Summarize
We now know what’s Repository pattern and how to create it, and we should use the base class for avoid the duplicate codes, also, we know how to create the Async method, that can be help to improve the performance. In the end, we also created an Api controller to try to get the data with Repository.
That’s it, please let me know if you still have any problems 🙂