Then we will create the other class libraries to make the rest of the layers with the following commands,
- dotnet new classlib -o RP.Core
- dotnet new classlib -o RP.BL
- dotnet new classlib -o RP.DAL
The project structure is created as we can see below:
STEP 2 - Creating some models to persist
Inside the RP.Core layer, create a new folder named Models and two classes to make this example Category and Product:
- using System.ComponentModel.DataAnnotations;
-
- namespace RP.Core.Models
- {
- public class Category
- {
- [Key]
- public int Id { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- }
- }
- using System.ComponentModel.DataAnnotations;
-
- namespace RP.Core.Models
- {
- public class Product
- {
- [Key]
- public int Id { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- public decimal Price { get; set; }
- public int CategoryId { get; set; }
- public Category Category{get;set;}
- }
- }
If you get some errors with DataAnnotations, run:
- dotnet add package System.ComponentModel.Annotations
The database will be created automatically, don't worry about it.
STEP 3 - Create the Repositories
Inside the RP.DAL layer we will create the following folder structure
Then add the package of EntityCore to manage the persistence. In order to do that run the following command in the Terminal Console,
- dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Inside the Infrastructure folder, create a class named AppDbContext that will manage the context from EF Core, and it creates data related to Categories (because I only show the CRUD of Products),
- using Microsoft.EntityFrameworkCore;
- using RP.Core.Models;
-
- namespace RP.DAL.Infrastructure
- {
- public class AppDbContext: DbContext
- {
- public AppDbContext(DbContextOptions<AppDbContext> options)
- :base(options)
- {
- }
- public DbSet<Product> Products{ get; set; }
- public DbSet<Category> Categories { get; set; }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Category>().HasData(
- new Category()
- {
- Id = 1,
- Name = "Electronics",
- Description = "Electronic Items"
- },
- new Category()
- {
- Id = 2,
- Name = "Clothes",
- Description = "Dresses"
- },
- new Category()
- {
- Id = 3,
- Name = "Grocery",
- Description = "Grocery Items"
- });
- }
- }
- }
If you get an error in Product and Category, remember to add the reference from RP.DAL toRP.Core classLib.
Inside the IRepositories folder we will create the interface of the generic repository base to manage the simple CRUD of every repository. So, create an interface called IBaseRepository.cs as we can see:
- using System;
- using System.Collections.Generic;
- using System.Linq.Expressions;
-
- namespace RP.DAL.IRepositories
- {
- public interface IBaseRepository<T> where T : class
- {
- void Add(T entity);
- void Update(T entity);
- void Delete(T entity);
- IEnumerable<T> GetAll(params Expression<Func<T, object>[] includes );
- IEnumerable<T> Filter(Expression<Func<T, bool>> where, params Expression<Func<T, object>>[] includes);
- int SaveChanges();
- }
The last thing we have to do to finish our Repository Layer is create a generic base class that implements all the methods of the Base interface.
So inside the Repositories folder, create a new class called BaseRepository.cs,
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Linq.Expressions;
- using Microsoft.EntityFrameworkCore;
- using RP.DAL.Infrastructure;
- using RP.DAL.IRepositories;
-
- namespace Base.Dal.Repositories
- {
- public class BaseRepository<T> : IBaseRepository<T> where T : class
- {
- internal AppDbContext _context;
- public BaseRepository(AppDbContext context)
- {
- this._context = context;
- }
- public virtual void Add(T entity) => _context.Set<T>().Add(entity);
-
- public virtual void Update(T entity) => _context.Entry(entity).State = EntityState.Modified;
-
- public virtual void Delete(T entity) => _context.Remove(entity);
-
- public IEnumerable<T> GetAll(params Expression<Func<T, object>>[] includes)
- {
- var query = _context.Set<T>().AsQueryable();
- foreach (Expression<Func<T, object>> i in includes)
- {
- query = query.Include(i);
- }
- return query.ToList();
- }
-
- public IEnumerable<T> Filter(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includes)
- {
- var query = _context.Set<T>().Where(predicate);
- foreach (Expression<Func<T, object>> i in includes)
- {
- query = query.Include(i);
- }
- return query.ToList();
- }
- public int SaveChanges() => _context.SaveChanges();
- }
- }
This class manages the CRUD operations of every type passed as generic. This class allows me to call a GetAll (the “includes” parameter represents all the lazy loading you want to load), find by the specific filter, create, update or delete.
STEP 4 - Create the Services inside the Bussiness Layer
Inside the RP.BL layer we will create the following folder structure:
There are so many models that don't have buissness logic. In order not to create a Service class for those, with the great help of generics, we can define only one Base service to manage the way to the repository and persist the data. So the interface Base Service is,
- using System;
- using System.Collections.Generic;
- using System.Linq.Expressions;
-
- namespace RP.BL.IServices
- {
- public interface IBaseService<T> where T : class
- {
- IEnumerable<T> Get(Expression<Func<T, bool>> where = null,params Expression<Func<T, object>>[] includes);
- IEnumerable<T> GetAll(params Expression<Func<T, object>>[] includes);
- int Create(T t);
- int Update(T t);
- void Delete(T t);
- }
- }
And the Base Service that implements the interface is the following,
- using System;
- using System.Collections.Generic;
- using System.Linq.Expressions;
- using RP.BL.IServices;
- using RP.DAL.IRepositories;
-
- namespace RP.BL.Services
- {
- public class BaseService<T> : IBaseService<T> where T : class
- {
- public readonly IBaseRepository<T> _TRepository;
- public BaseService(IBaseRepository<T> TRepository)
- {
- this._TRepository = TRepository;
- }
-
- public int Create(T T)
- {
- _TRepository.Add(T);
- int result = _TRepository.SaveChanges();
- return result;
- }
-
- public int Update(T T)
- {
- _TRepository.Update(T);
- int result = _TRepository.SaveChanges();
- return result;
- }
-
- public void Delete(T T)
- {
- _TRepository.Delete(T);
- _TRepository.SaveChanges();
- }
-
- public IEnumerable<T> Get(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includes)
- {
- return _TRepository.Filter(predicate, includes);
- }
-
- public IEnumerable<T> GetAll(params Expression<Func<T, object>>[] includes)
- {
- return _TRepository.GetAll(includes);
- }
- }
- }
And we finished the Business Layer (remember that if you have a model with its own business logic, you have to create an interface service, and a service implementation for that model).
STEP 5 - Making our Web Api
First of all, move your terminal inside the RP.Api and add the dependency to EF Core with the command:
- dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Add the dependency to the other class libraries (RP. Core, RP. DAL and RP.BL)
Let's install swagger with the following command:
- dotnet add RP.Api.csproj package Swashbuckle.AspNetCore -v 5.0.0
Let's open the Startup.cs and check that your ConfigureServices method is the same as mine:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- services.AddDbContext<AppDbContext>(o => o.UseSqlServer(Configuration.GetConnectionString("ProductDB")));
- services.AddScoped(typeof(IBaseRepository<>), typeof(BaseRepository<>));
- services.AddScoped(typeof(IBaseService<>), typeof(BaseService<>));
- services.AddSwaggerGen(c =>
- {
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
- });
- }
The “services.AddDbContext” line adds the context related to our connection string defined in the appsettings.json.
The “services.AddScoped” is necesary to manage our Dependency Injection inside the controllers and services.
The “services.AddSwaggerGen” is necesary to use swagger and test our application.
Now check the method below called public void Configure,
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
-
- using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
- {
- var context = serviceScope.ServiceProvider.GetRequiredService<AppDbContext>();
- context.Database.EnsureCreated();
- }
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- app.UseHttpsRedirection();
- app.UseRouting();
- app.UseAuthorization();
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
-
- app.UseSwagger();
-
-
- app.UseSwaggerUI(c =>
- {
- c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
- c.RoutePrefix = string.Empty;
- });
- }
The first line is necesary to create the database and the schema (you are welcome).
And the other lines I added are related to make swagger work (app.UseSwagger and app.UseSwaggerUI).
The last thing we have to do is create inside the Controllers folder the controller that will expose the endpoints of our product, ProductsController.cs,
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Microsoft.AspNetCore.Mvc;
- using RP.BL.IServices;
- using RP.Core.Models;
-
- namespace RP.Api.Controllers
- {
- [ApiController]
- [Route("api/[controller]")]
- public class ProductsController : ControllerBase
- {
- private readonly IBaseService<Product> _productService;
- public ProductsController(IBaseService<Product> _productService)
- {
- this._productService = _productService;
- }
-
- [HttpGet]
- public IActionResult Get()
- {
- var products = _productService.GetAll();
- return new OkObjectResult(products);
- }
-
- [HttpGet,Route("{Id}")]
- public IActionResult Get(int Id)
- {
-
- var products = _productService.Get(x=> x.Id == Id, i=>i.Category);
- if(!products.Any()){
- return new NoContentResult();
- }
- return new OkObjectResult(products.First());
- }
-
- [HttpPost]
- public IActionResult Post([FromBody] Product product)
- {
- _productService.Create(product);
- return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
- }
-
-
- [HttpPut("{id}")]
- public IActionResult Put(int id,[FromBody] Product product)
- {
- if(product!= null){
- _productService.Update(product);
- return new OkResult();
- }
- return new NoContentResult();
- }
-
- [HttpDelete("{id}")]
- public IActionResult Delete(int id)
- {
- var products = _productService.Get(x=> x.Id == id);
- if(!products.Any()){
- return new NoContentResult();
- }
- _productService.Delete(products.First());
- return new OkResult();
- }
- }
- }
Now our API is finally ready to be tested. Press F5 and test it with swagger (Remember to have defined the connection string inside the appsettings.json).
The Endpoint /api/Products get all the products stored without loading the category object.
The Endpoint /api/Products/{id} Load the specific product with the category object where you can see that our lazy loading works well.
Summary
The repository pattern has been implemented, with generics, DI, Swagger and more. Sorry for not making a long explanation of every concept because then the blog would be longer.
It is easy to implement and not too hard to understand. Please feel free to comment and give me your opinions about this pattern and my implementation.