"тнιѕ вℓσg ¢συℓ∂ ѕανє уσυя мσηєу ιƒ тιмє = мσηєу" - ∂.мαηנαℓу

Saturday 28 November 2020

Generic Repository Design Pattern with Entity Framework Core

Generic Repository pattern is a design pattern available in C#. Found it very cool!. Generic repository design pattern is very useful when we have more than one entity to deal with in the Entity Framework Core ( EF Core Basics are explained here -https://crmdm.blogspot.com/2020/05/how-to-write-to-sql-table-using-entity.html ). This  design pattern avoids a lot of repetitive code.

For instance, consider two entities used in the entity framework core.

  • Contact
  • Loan

Now these two entites have common operations like inserting to a sql table, read from sql table, removing from the sql table. So we could utilise a generic repository for this. This would avoid repetitive code throughout. Code looks clean too.

In this post I have followed TDD approach. Entity Framework Core unit testing is done using Xunit and Sqlite

 There are 3 options for Entity Framework Core unit testing. Had discussion with my colleague on this specific part as well. It turned out that the sqlite is the better option as other ones have limiations.Hence it is used here. ( ref :https://docs.microsoft.com/en-us/ef/core/testing/ )

First things First. Model is one of our first bricks.

Models: These are a couple of sample models. Feel free to define yours. Please note that Key is a must. Key could be of any data type.

Contact Model.

using System.ComponentModel.DataAnnotations;

namespace RepositoryDesignPattern.Domain
{
    public class Contact
    {
        [Key]
        public int CustomerId { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}

Loan Model

using System.ComponentModel.DataAnnotations;

namespace RepositoryDesignPattern.Domain.Model
{
    public class Loan
    {
        [Key]
        public int LoanId { get; set; }

        public decimal Balance { get; set; }

        public string Description { get; set; }

    }
}

Next is our generic interface- IRepository - feel free to name as per your preference. Also more generic methods can be included as per the requirements.

using System.Collections.Generic;

namespace RepositoryDesignPattern.Application.Repositories
{
    public interface IRepository<T> where T : class
    {
        void Add(T entity);

        IEnumerable<T> All();

        void SaveChanges();

        void Remove(T entity);
    }
}

Now the generic repository class which is implemented from the above interface.

using Microsoft.EntityFrameworkCore;
using RepositoryDesignPattern.Application.Repositories;
using System.Collections.Generic;
using System.Linq;

namespace RepositoryDesignPattern.Infrastructure.Repositories
{
    public class Repository<T> : IRepository<T> where T : class
    {
        protected readonly DbContext _context;
        protected readonly DbSet<T> _entities;

        public Repository(DbContext context)
        {
            _context = context;
            _entities = context.Set<T>();
        }

        public void Add(T entity) => _entities.Add(entity);

        public  IEnumerable<T> All() => _context.Set<T>().ToList();

        public  void SaveChanges() => _context.SaveChanges();

        public  void Remove(T entity) => _context.Remove(entity);
    }
}

-----------------------------------------------------------------------------------------------------

Optional Note: This part is useful when the repos get expanded. Not required for demoing generic repository.
It is possible to override these methods from another repository if needed. In that case we need to add virtual keyword for the method in the generic repo.

Also generic repository can be marked as an abstract class at a later stage when other repos are defined properly.

For instance, 

  public virtual void Add(T entity) => _entities.Add(entity);

Overriding from another repo. For instance if we go with a new loan repo,

using RepositoryDesignPattern.Domain.Model;

namespace RepositoryDesignPattern.Infrastructure.Repositories
{
    public class LoanRepository : Repository<Loan>
    {
        public LoanRepository(SqlDbContext dbContext) : base(dbContext)
        {

        }

        public override void Add(Loan loan)
        {
            loan.Description = "Override";
            base.Add(loan);
        }
    }
}

----------------------------------------------------------------------------------------------------

So we are mainly interested in Adding a new record, Retrieving all records from a specific table, Save changes to the actual table, Remove a record. Add or remove operations get completed after applying savechanges. T represents a type parameter in C#. In this scenario it could be Loan or Contact entity.

Next part is to define a custom Dbcontext. This is to handle our own Sql DB and tables. DbContextOptions is an important part. This could be an actual sql db or in memory db or any other supported db by Entity Framework Core. You could see two tables Contact and Loan. DbContextOptions value is passed in the constructor.

using Microsoft.EntityFrameworkCore;
using RepositoryDesignPattern.Domain;
using RepositoryDesignPattern.Domain.Model;

namespace RepositoryDesignPattern.Infrastructure
{
    public class SqlDbContext : DbContext
    {
        public SqlDbContext(DbContextOptions options) : base(options) { }

        public DbSet<Contact> Contacts { get; set; }

        public DbSet<Loan> Loans { get; set; }
    }
}

As per the TDD approach we use the unit tests to provide in memory db and test our code.

Arrange part is set up using Sqlite ( Please note the part .UseSqlite. In a real scenario this part would be .UseSqlServer ). Act part is calling the actual code. And assert part evaluates expected result and output.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using RepositoryDesignPattern.Domain.Model;
using RepositoryDesignPattern.Infrastructure;
using RepositoryDesignPattern.Infrastructure.Repositories;
using System.Data.Common;
using System.Linq;
using Xunit;

namespace RepositoryDesignPattern.Unit.Tests
{
    public class SqlRepositoryTests
    {
        [Fact]
        public void Insert_New_LoanRecord_And_Read_Successfully()
        {
            //Arrange
            var dbContextOptions = new DbContextOptionsBuilder<SqlDbContext>()
                .UseSqlite(CreateInMemoryDatabase())
                .Options;
            var context = new SqlDbContext(dbContextOptions);
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            Loan loanRecord = GetSampleLoanRecord();
            var expectedCount = 1;

            //Act
            var repository = new Repository<Loan>(context);
            repository.Add(loanRecord);
            repository.SaveChanges();
            var result = repository.All();

            //Assert
            Assert.Equal(expectedCount, result.Count());
            Assert.Equal(loanRecord.LoanId, result.FirstOrDefault().LoanId);
            Assert.Equal(loanRecord.Description, result.FirstOrDefault().Description);
            Assert.Equal(loanRecord.Balance, result.FirstOrDefault().Balance);
        }

        [Fact]
        public void Insert_New_LoanRecord_And_Removes_Successfully()
        {
            //Arrange
            var dbContextOptions = new DbContextOptionsBuilder<SqlDbContext>()
                .UseSqlite(CreateInMemoryDatabase())
                .Options;
            var context = new SqlDbContext(dbContextOptions);
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            Loan loanRecord = GetSampleLoanRecord();
            var expectedCount = 0;

            //Act
            var repository = new Repository<Loan>(context);
            repository.Add(loanRecord);
            repository.SaveChanges();
            repository.Remove(loanRecord);
            repository.SaveChanges();
            var result = repository.All();

            //Assert
            Assert.Equal(expectedCount, result.Count());
        }

        private DbConnection CreateInMemoryDatabase()
        {
            var connection = new SqliteConnection("Filename=:memory:");
            connection.Open();

            return connection;
        }

        private Loan GetSampleLoanRecord()
        {
            Loan loanRecord = new Loan();
            loanRecord.LoanId = 22;
            loanRecord.Description = "unit test";
            loanRecord.Balance = 1000.00m;

            return loanRecord;
        }
    }
}
 

Green is a good sign : )







No comments:

Post a Comment