Monday 6 June 2022

Repository pattern in Entity Framework Core

I have been looking into repository pattern for EF Core today. This is what I got :
 
 
using SomeAcme.Interfaces;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace SomeAcme.DAL.Pattern
{
    public class Repository<T> : IRepository<T> where T : class
    {

        private System.Data.Entity.DbContext GetContext()
        {
            return new SomeAcmeDbContext as System.Data.Entity.DbContext;
        }

        private T ExecuteQuery(Func<T, System.Data.Entity.DbContext, T> query, T entity)
        {
            using (var context = GetContext())
            {
                T result = query(entity, context);
                return result;
            }
        }

        public T Add(T entity, bool saveImmediate = false)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                var entityDb = dbContext.Set<T>().Add(entity);                
                if (saveImmediate)
                    SaveChanges(dbContext);
                return entityDb;

            }, entity);
        }

        public T Add(T entity, bool saveImmediate = false, params object[] keys)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                dbContext.Entry(obj).State = EntityState.Added;
                if (saveImmediate)
                    SaveChanges(dbContext);
                var entityInDb = dbContext.Set<T>().Find(keys);
                return entityInDb;
            }, entity);
        }

        public IEnumerable<T> AddRange(IEnumerable<T> entities, bool saveImmediate)
        {
            using (var dbContext = GetContext())
            {
                var entitites = dbContext.Set<T>().AddRange(entities);
                if (saveImmediate)
                    SaveChanges(dbContext);
                return entitites;
            }
        }

        public T Delete(bool saveImmediate = false, params object[] keyValues)
        {
            using (var dbContext = GetContext())
            {
                var entity = dbContext.Set<T>().Find(keyValues);
                if (entity == null)
                    return null;
                var entry = dbContext.Entry(entity);
                if (entry == null)
                    return null; 
                entry.State = EntityState.Deleted;
                if (saveImmediate)
                    SaveChanges(dbContext); 
                return entity; 
            }        
        }

        /// <summary>
        /// Note - requiring here that we have defined primary key(s) on the target tables ! 
        /// </summary>
        /// <param name="keyValues"></param>
        /// <returns></returns>
        public T Get(params object[] keyValues)
        {
            using (var dbContext = GetContext())
            {
                var entity = dbContext.Set<T>().Find(keyValues);
                dbContext.Entry(entity).State = EntityState.Detached; 
                return entity; 
            }      
        }

        public IList<T> GetAll(bool asNoTracking = true)
        {
            using (var dbContext = GetContext())
            {
                return asNoTracking ? dbContext.Set<T>().AsNoTracking().ToList() : dbContext.Set<T>().ToList();
            }           
        }

        public IList<T> GetAllByCondition(Expression<Func<T, bool>> condition, bool asNoTracking = true)
        {
            using (var dbContext = GetContext())
            {
                IQueryable<T> query = asNoTracking ? dbContext.Set<T>().AsNoTracking() : dbContext.Set<T>();
                var entities = query.Where(condition);
                return entities.ToList();
            }          
        }

        public T GetFirstByCondition(Expression<Func<T, bool>> condition)
        {
            return GetAllByCondition(condition).FirstOrDefault(); 
        }

        public T GetByKeyValues(bool asNoTracking, params object[] keyValues)
        {
            using (var dbContext = GetContext())
            {
                var entity = asNoTracking ? dbContext.Set<T>().AsNoTracking().FirstOrDefault() : dbContext.Set<T>().Find(keyValues);
                return entity;
            }
        }

        public void SaveChanges(object context)
        {
            var dbContext = context as System.Data.Entity.DbContext;
            if (dbContext == null)
            {
                throw new ArgumentException("Must be of type System.Data.Entity.DbContext", nameof(context));
            }
            dbContext.SaveChanges(); 
        }

        public T Update(T entity, bool saveImmediate = false, params object[] keyValues)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                var entityInDb = dbContext.Set<T>().Find(keyValues);
                if (entityInDb == null)
                    return null; 
                dbContext.Entry(entityInDb).CurrentValues.SetValues(obj);                
                  if (saveImmediate)
                {
                    SaveChanges(dbContext);
                }
                return obj;
            }, entity);            
        }

    }
}

 
 
 
And here are some unit tests against a database of mine (containing some integration tests)
 
using AutoMapper;
using FluentAssertions;
using SomeAcme.Common;
using SomeAcme.Common.DataContract;
using SomeAcme.Data.EntityFramework.Managers;
using SomeAcme.Data.EntityFramework.Models;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;

namespace SomeAcme.Service.Implementation.Test.Pattern
{

    [TestFixture]
    public class RepositoryTest
    {
        private IMapper _mapper; 

        [SetUp]
        public void TestInitialize()
        {
            IntegrationTestBootstrapper.Run();
            System.Threading.Thread.CurrentPrincipal = new ConcreteClaimsPrincipal(SomeAcme.Administrator, "107455", "Testutvikler, Ivrig");

            var configuration = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap>PasSystem, PasSystemDataContract>();
            });

#if DEBUG
            // only during development, validate your mappings; remove it before release
            // configuration.AssertConfigurationIsValid();
#endif
            _mapper = configuration.CreateMapper(); 

        }

        [Test]
        public void GetAllReturnsExpected()
        {
            var pasSystemRepository = new Repository>PasSystem>();
            var allPasSystem = pasSystemRepository.GetAll()?.Select(x => _mapper.Map>PasSystemDataContract>(x)).ToList();
            allPasSystem?.Count().Should().BePositive();
        }

        [Test]
        public void GetReturnsExpected()
        {
            var jobTitleRepo = new Repository>Title>();
            var jobTitle = jobTitleRepo.Get(6);
            Assert.IsNotNull(jobTitle);
            jobTitle.Text.Should().Be("Anestesisykepleier"); 
        }

        [Test] 
        public void AddRangeDoesNotThrow()
        {
            var postponementCauseRepo = new Repository>PostponementCause>();
            var postponementCauses = new List>PostponementCause>()
            {
                new PostponementCause{ FreshOrganizationalUnitId = 107455, IsActive = true, Text = "Personel not available"},
                new PostponementCause{ FreshOrganizationalUnitId = 107455, IsActive = true, Text = "Personel already busy"},
            };
            postponementCauseRepo.AddRange(postponementCauses, true);
        }

        [Test]
        public void GetByConditionReturnsNonEmpty()
        {
            var timeMatrixRepository = new Repository>TimeMatrix>();
            var timematrices = timeMatrixRepository.GetAllByCondition(t => t.PreOperation > 80).ToList();
            timematrices.Count().Should().BePositive(); 
        }

        [Test]
        public void AddTimeMatrixToDbViaRepositorySucceeds()
        {
            var timeMatrixRepository = new Repository>TimeMatrix>();

            var timeMatrix = new TimeMatrix
            {
                Code = "T100",
                IsActive = false,
                PostOperation = 11,
                PreOperation = 11,
            };
            var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);

            timeMatrixSavedToDb.TimeMatrixId.Should().BePositive(); 
        }

        [Test]
        public void AddAndUpdateTimeMatrixToDbViaRepositorySucceeds()
        {
            var timeMatrixRepository = new Repository>TimeMatrix>();

            var timeMatrix = new TimeMatrix
            {
                Code = "T100",
                IsActive = false,
                PostOperation = 11,
                PreOperation = 11,
            };
            var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);
            timeMatrixSavedToDb.TimeMatrixId.Should().BePositive();
            timeMatrix.Code = "T200";
            timeMatrix.PreOperation = 11; 
            timeMatrixSavedToDb = timeMatrixRepository.Update(timeMatrix, true, timeMatrixSavedToDb.TimeMatrixId);
            timeMatrixSavedToDb.Code.Should().Be("T200"); 
            timeMatrix.PreOperation.Should().Be(11);    
        }

        [Test]
        public void AddAndUpdateAndDeleteTimeMatrixToDbViaRepositorySucceeds()
        {
            var timeMatrixRepository = new Repository>TimeMatrix>();

            var timeMatrix = new TimeMatrix
            {
                Code = "T100",
                IsActive = false,
                PostOperation = 11,
                PreOperation = 11,
            };
            var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);
            timeMatrixSavedToDb.TimeMatrixId.Should().BePositive();
            timeMatrix.Code = "T300";
            timeMatrix.PreOperation = 11;
            timeMatrixSavedToDb = timeMatrixRepository.Update(timeMatrix, true, timeMatrixSavedToDb.TimeMatrixId);
            timeMatrixSavedToDb.Code.Should().Be("T300");
            timeMatrixSavedToDb.PreOperation.Should().Be(11);
            timeMatrixSavedToDb = timeMatrixRepository.Delete(true, timeMatrixSavedToDb.TimeMatrixId);
            timeMatrixSavedToDb.Should().NotBeNull(); 
        }

        [Test]
        public void AddPostOfficeToDbViaRepositorySucceeds()
        {
            var postOfficeRepository = new Repository>PostOffice>();

            var postOffice = new PostOffice
            {
                PostalPlace = "Steinkjer",
                PostalCode = "7724"
            };
            var postOfficeSavedToDb = postOfficeRepository.Add(postOffice, true);
            postOfficeSavedToDb.Should().NotBeNull();
        }


    }
}

 
Also note the usage of Auto mapper here to automatically map between POCO entity objects to DTO (Data transfer objects, usually data contracts for example). Building a useful repository pattern in EF Core and combining it with Automapper (available on nuget) will probably reduce your Data Access Layer logic a bit .. In many cases maybe an understatement.. Note - this code have passed unit tests, but not been used in production (yet). Methods that demands keyValues to find entities do require your table to have primary keys on the table. This still is 'demo code' and WIP (Work in progress). Works !

Share this article on LinkedIn.

No comments:

Post a Comment