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;
namespaceSomeAcme.DAL.Pattern
{
publicclassRepository<T> : IRepository<T> whereT : class
{
private System.Data.Entity.DbContext GetContext()
{
returnnew 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, paramsobject[] 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, paramsobject[] keyValues)
{
using (var dbContext = GetContext())
{
var entity = dbContext.Set<T>().Find(keyValues);
if (entity == null)
returnnull;
var entry = dbContext.Entry(entity);
if (entry == null)
returnnull;
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(paramsobject[] 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, paramsobject[] keyValues)
{
using (var dbContext = GetContext())
{
var entity = asNoTracking ? dbContext.Set<T>().AsNoTracking().FirstOrDefault() : dbContext.Set<T>().Find(keyValues);
return entity;
}
}
publicvoidSaveChanges(object context)
{
var dbContext = context as System.Data.Entity.DbContext;
if (dbContext == null)
{
thrownew ArgumentException("Must be of type System.Data.Entity.DbContext", nameof(context));
}
dbContext.SaveChanges();
}
public T Update(T entity, bool saveImmediate = false, paramsobject[] keyValues)
{
return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
{
var entityInDb = dbContext.Set<T>().Find(keyValues);
if (entityInDb == null)
returnnull;
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;
namespaceSomeAcme.Service.Implementation.Test.Pattern
{
[TestFixture]
publicclassRepositoryTest
{
private IMapper _mapper;
[SetUp]
publicvoidTestInitialize()
{
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]
publicvoidGetAllReturnsExpected()
{
var pasSystemRepository = new Repository>PasSystem>();
var allPasSystem = pasSystemRepository.GetAll()?.Select(x => _mapper.Map>PasSystemDataContract>(x)).ToList();
allPasSystem?.Count().Should().BePositive();
}
[Test]
publicvoidGetReturnsExpected()
{
var jobTitleRepo = new Repository>Title>();
var jobTitle = jobTitleRepo.Get(6);
Assert.IsNotNull(jobTitle);
jobTitle.Text.Should().Be("Anestesisykepleier");
}
[Test]
publicvoidAddRangeDoesNotThrow()
{
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]
publicvoidGetByConditionReturnsNonEmpty()
{
var timeMatrixRepository = new Repository>TimeMatrix>();
var timematrices = timeMatrixRepository.GetAllByCondition(t => t.PreOperation > 80).ToList();
timematrices.Count().Should().BePositive();
}
[Test]
publicvoidAddTimeMatrixToDbViaRepositorySucceeds()
{
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]
publicvoidAddAndUpdateTimeMatrixToDbViaRepositorySucceeds()
{
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]
publicvoidAddAndUpdateAndDeleteTimeMatrixToDbViaRepositorySucceeds()
{
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]
publicvoidAddPostOfficeToDbViaRepositorySucceeds()
{
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 !