We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Exploring ASP.NET Core Fundamentals - Repository Pattern and Usage in EF Core

ASP.NET Core Design Patterns  • Posted one year ago

In the previous article we discussed in detail about the Entity Framework Core ORM (Object Relational Mapper) developed for .NET Core and how we can inject the DatabaseContext or the DbContext instance extended by a custom class which holds the bridge between the .NET Core business layer and the back-end database by means of DbSets of entity classes which form the code-first approach of defining and mapping a database object to a class object. We also discussed that injecting a DbContext object directly into business logic is not a good practice and we would require a Repository pattern to implement the same. In this article we will look into what a Repository pattern is and how we can implement a Repository pattern onto an Entity Framework model and what advantages it provides over a non-pattern approach.

What is a Repository Pattern?

Let's start from the question of what really a design pattern is. A Design Pattern is a structural solution designed for a distinct set of problems which can hinder code structure, behaviour and finally performance. By using a particular design pattern while programming a solution, we ensure that the code structure and hierarchy we create is free of a particular problem that design pattern was brought in to solve. The first step in implementing a design pattern is in identifying the flaw in the existing code and selecting the respective pattern to implement to overcome it. Sometimes we use design patterns subconsciously, we don't know that we're actually implementing a pattern and yet we tend to code in that way. Having a basic idea of design patterns basically helps code better and thereby improves maintainability and scalability. In dotnet core, we already implement Singleton pattern (by defining the lifetimes of services or class objects which we induct into pipeline and later inject when necessary) and we use Factory patterns as well (for example the IHttpClientFactory we discussed earlier for handling HttpClient objects). Although we don't actually implement the behavior, by using the libraries that dotnet core provides for the respective functionalities, we actually try to mimic them in our structure. One of the 23 design patterns developed for various programming problems, the Repository pattern we are about to implement solves the basic problem of logical abstraction or logical partitioning. Let's use the same example we've discussed earlier,

public MyController : Controller 
{
	DatabaseContext context;
	
	public MyController(DatabaseContext ctx) {

		this.context = ctx;

	}

	public void Query() 
	{
		var records = context.Users.Where(X => x.Id == 1).FirstOrDefault(); 
	}

	public void Insert() 
	{
		var user = new User { Name = "Alan" };
	
		context.Users.Add(user); 
		context.SaveChanges(); 
	}

	public void Update() 
	{
		var record = context.Users.FirstOrDefault(x => x.Id == 1); 
		record.Name = "Bibo"; 
		context.SaveChanges(); 
	}

	public void Delete()
	{
		var record = context.Users.FirstOrDefault(x => x.Id == 1); 
		context.Users.Remove(record); 
		context.SaveChanges(); 
	}	
}

In the above example, we directly implement the logic of a CRUD operation (Create, Retrieve, Update and Delete) on the database using the injected DbContext object (context of type DatabaseContext) in the Controller class. We need to realize a point here that, the main purpose of a Controller class is to handle View related or View Model related logic for a given request. Which is the concern of the class MyController. And by writing down the logic for data handling which isn't the concern of a Controller, we defeat the purpose of a Logical abstraction or simply called Separation of Concerns (SoC). Also, let's assume that there's a data logic on the context entity Users which is used by three controllers OneController, TwoController and ThreeController. If we go by the above approach, we end up rewriting the same logic in three different places which defeats the purpose of code reuse and maintainability. This problem will be solved by Repository pattern which "Separates the Data Logic from the Business Logic and lets the Business Logic utilize an object of the implementor which implements the features stated by a Definition."

Implementing a Repository Pattern:

We start by identifying all the reusable Data Logic, which we shall define using an interface.

public interface IDataService {
	List<User> Query(string name);
	int Create(string name);
	void Update(string name);
	void Delete(int Id);
}

The above interface defines four basic data functionalities which are needed by all the controllers. Next, we define an implementor that holds an implementation of the above definition.

public class DataService : IDataService {
	
	DatabaseContext context;
	
	public DataService(DatabaseContext context) {
		this.context = context;
	}

	public List<User> Query(string name) {
		return context.Users.Where(x => x.Name == name).ToList();
	}

	public int Create(string name) {
		var user = new User { Name = name };
		context.Users.Add(user);
		context.SaveChanges();
		return user;
	}

	public void Update(string name) {
		var user = this.Query(name).FirstOrDefault();
		if(user != null)
			user.Name = name;
		context.SaveChanges();
	}

	public void Delete(int Id) {
		context.Users.Remove(context.Users.FirstOrDefault(x => x.Id == Id));
		context.SaveChanges();
	}
}

Now that we have an implementation of our DataService, we can simply use this DataService object in our controller class for handling data logic without the need for actually having any logic within the controller.

public class MyController : Controller {

	IDataService data;

	public MyController(DatabaseContext context) 
	{
		this.data = new DataService(context);
	}

	public void Query(string name) 
	{
		// returns list of matching users
		var users = data.Query(name);
	}

	public void Insert(string name) 
	{
		// returns the created user Id
		var userId = data.Create(name);
	}

	public void Update(string name) 
	{
		// updates the name of the user
		data.Update(name);
	}

	public void Delete(int id)
	{
		// deletes the user bearing the id
		data.Delete(id);
	}	
}

If we compare the controller before implementing pattern and after implementing the pattern, we see that the code is a lot more cleaner and understandable. One more advantage of this approach is that the consumer logic doesn't need to know the underlying service logic, which means that later if we decide to move over to another database logic or ORM, still the controller logic works in the same way; we just need to change the underlying implementation. The above code can be further more simplified by inducting DataService into pipeline as an injectable.

public class Startup {

	public void ConfigureServices(IServiceCollection services) {

		services.AddDbContext<DatabaseContext>(
			options => {
				//connection string logic 
			});
		services.AddScoped<IDataService, DataService>();
	}
  
  ...

}

It can be observed here that the service is defined with a Scoped lifetime, because as we discussed earlier the DbContext service is of SCOPED lifetime and CANNOT be injected in a SINGLETON service.

now the controller logic can be changed as:

public class MyController : Controller {

	IDataService data;

	public MyController(IDataService data) 
	{
		this.data = data;
	}
	
	...
}

This provides a decoupled code structure, since we can choose the implementation of the IDataService at runtime and the respective implementation will be used across the pipeline.