I'm creating a repository layer to encapsulate DbContext.
I would like to expose a fluent interface for building query, and dispose and create a new DbContext every time after a query request is sent:
var repo = new EntityRepository();
repo = EntityRepository.FilterByAge(30).FilterByGender("Male");
var people = repo.GetPeople();
// GetPeople() should send the request, get the result, then dispose
// the old DbContext and create a new one
repo = repo.FilterByOccupation("Programmer");
var programmers = repo.GetPeople();
// the age and gender filters should still apply here
Currently in my EntityRepository, I have a private DbContext and a IQueryable. In the Filter methods, I append the Linq methods onto the IQueryable. Then after finishing a request, I do:
db.Dispose();
db = new EntityContext();
But this does not work when I try to do another request. It says the DbContext has been disposed.
How do I keep the same query for a new DbContext?
I ended up keeping a list of "filters", which are anonymous functions that takes in IQueryable and return IQueryable. Then I apply them on a short-live DbContext :
Repository:
private IList<Func<IQueryable<Person>, IQueryable<Person>>> filters;
public Repository FilterByAge(int age)
{
var _filters = new List<Func<IQueryable<Person>, IQueryable<Person>>>(filters);
_filters.Add(q => q.Where(e => e.Age == age));
return new Repository(_filters);
}
public Repository OrderByName()
{
var _filters = new List<Func<IQueryable<Entity>, IQueryable<Entity>>>(filters);
_filters.Add(q => q.OrderBy(e => e.Name));
return new Repository(_filters);
}
private IQueryable<Person> ApplyFilters(AppContext db)
{
var result = db.People;
foreach (var filter in filters)
{
result = filter(result);
}
return result;
}
public IEnumerable<Person> GetPeople()
{
IEnumerable<Person> people;
using (var db = new AppContext())
{
people = ApplyFilters(db).ToList();
}
return people;
}
Usage
private Repository repo = new Repository();
var peopleOfThirty = repo.FilterByAge(30);
var orderedByName = peopleOfThirty.OrderByName();
if (wantOrder)
{
return peopleOfThirty.GetPeople();
}
else
{
return orderedByName.GetPeople();
}
It works for my purposes. However, please let me know if there is any problem doing it this way. Thanks!
Related
I have a nice extension method for mocking a DbSet:
public static class DbSetExtensions
{
public static DbSet<T> ToDbSet<T>(this IEnumerable<T> data) where T : class
{
var queryData = data.AsQueryable();
var dbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
((IQueryable<T>)dbSet).Provider.Returns(queryData.Provider);
((IQueryable<T>)dbSet).Expression.Returns(queryData.Expression);
((IQueryable<T>)dbSet).ElementType.Returns(queryData.ElementType);
((IQueryable<T>)dbSet).GetEnumerator().Returns(queryData.GetEnumerator());
return dbSet;
}
}
Which I am trying to use in a context file like this:
public class DatabaseContextContext<T> where T: DatabaseContextContext<T>
{
public DatabaseContext DatabaseContext;
protected DatabaseContextContext()
{
DatabaseContext = Substitute.For<DatabaseContext>();
}
public T WhenListSucceeds<TEntity>(IList<TEntity> data) where TEntity : class
{
var dbSet = data.ToDbSet();
DatabaseContext.Set<TEntity>().Returns(dbSet);
return (T)this;
}
public T WhenGetSucceeds<TEntity>(TEntity entity) where TEntity : class
{
var dbSet = new List<TEntity> { entity }.ToDbSet();
DatabaseContext.Set<TEntity>().Returns(dbSet);
return (T)this;
}
}
When I run my test on this method, it fails:
public ActionResult<List<Formula>> ListFormulas(int id) =>
Ok(_databaseContext.Formulas.Where(m => m.AttributeId.Equals(id)).ToList());
with this error message:
System.InvalidCastException : Unable to cast object of type 'Castle.Proxies.ObjectProxy_3' to type 'Microsoft.EntityFrameworkCore.Metadata.Internal.Model'.
So I tried to break it down a bit.
First, I changed my method to this:
public ActionResult<List<Formula>> ListFormulas(int id)
{
var s = _databaseContext.Formulas;
var x = _databaseContext.Formulas.ToList();
var t = _databaseContext.Formulas.Where(m => m.AttributeId.Equals(id)).ToList();
return Ok(t);
}
But when debugging, the code was not getting past the ToList() method. I was still getting the same issue. So I have changed my code to this:
public ActionResult<List<Formula>> ListFormulas(int id)
{
var p = _databaseContext.Set<Formula>();
var q = p.ToList();
var s = _databaseContext.Formulas;
var x = _databaseContext.Formulas.ToList();
var t = _databaseContext.Formulas.Where(m => m.AttributeId.Equals(id)).ToList();
return Ok(t);
}
The first 3 lines of code work, but as soon as it get's to the line var x = _databaseContext.Formulas.ToList(); it fails.
Does anyone have any idea why?
Here is the test:
[TestFixture]
public class ListShould
{
[Test]
public void ReturnList()
{
// Assemble
var services = GenericOrderProviderContext.GivenServices();
var provider = services.WhenCreateOrderProvider();
services.DatabaseContext.Attributes = new List<Attribute>().ToDbSet();
services.DatabaseContext.Set<Attribute>().ReturnsForAnyArgs(_ => new List<Attribute>().ToDbSet());
// Act
var result = provider.List();
// Assert
result.Failure.Should().BeFalse();
result.Result.Count().Should().Be(0);
}
}
I was able to reproduce your error when the db context .Formulas property wasn't configured. If you're using both .Set<Formula>() and .Formulas you'll need to configure both.
I did notice that your set up for the db set enumerator
((IQueryable<T>)dbSet).GetEnumerator().Returns(queryData.GetEnumerator());
cause some behaviour that I've seen before where only the first ToList() invocation returns a result. If you get that, you may need to reset the enumerator or use the Func<CallInfo, IEnumerator<Formula>> Returns overload.
I am trying to create unit tests(MSTest v2) for a DAL library(EF core)
DataService
public IQueryable<BalanceDTO> GetCollection()
{
var entities = dbContext.Balance;
var dtos = mapper.Map<ICollection<BalanceDTO>>(entities).ToList();
dtos.ForEach(_d =>
{
_d.MonthSort = _d.Date.Month;
_d.MonthName = (new DateTimeFormatInfo()).GetMonthName(_d.MonthSort);
});
return dtos.AsQueryable();
}
public async Task<IList<BalanceDTO>> GetBalancesByYear(int year)
{
return await GetCollection().Where(_d => _d.Date.Year == year).OrderBy(_d => _d.MonthSort).ToListAsync();
}
Test
[TestMethod()]
[DataTestMethod]
[DataRow(2020, 2019)]
public void GetBalancesByYearTest(int found, int notfound)
{
var _configuration = new ConfigurationBuilder()
.SetBasePath(AssemblyProperties.AssemblyDirectory)
.AddJsonFile("appsettings.json")
.Build();
var optionsBuilder = new DbContextOptionsBuilder<AccountManagerContext>();
optionsBuilder.UseSqlServer(_configuration.GetConnectionString("AccountManagerLocalDB"));
var balanceDataService = new BalanceDataService(optionsBuilder);
var elementsFound = balanceDataService.GetBalancesByYear(found);
var elementsNotFound = balanceDataService.GetBalancesByYear(notfound);
Assert.IsNotNull(balanceDataService);
Assert.IsTrue(elementsFound.Result.Count > 0);
Assert.IsTrue(elementsNotFound.Result.Count == 0);
}
But I get this error:
InvalidOperationException: The source IQueryable doesn't implement IAsyncEnumerable<AccountManager.DAL.DTO.BalanceDTO>.
Only sources that implement IAsyncEnumerable can be used for Entity Framework asynchronous operations.
I have found a couple of link but couldn't figure out how to resolve this.
ToArrayAsync() throws "The source IQueryable doesn't implement IAsyncEnumerable"
How to overcome the IQueryable doesn't implement IAsyncQueryProvider while Mocking the FromSql() method?
Any idea about how to create tests for my DataService methods?
Entity Framework works with DbSets, that are more than queriables.
You can use the In-Memory provider for your tests, or mock the Dbsets.
I ended up with this solution.
If anyone sees an error or something to be improved, I do appreciate any recommendations.
public IQueryable<BalanceDTO> GetCollection()
{
var entities = dbContext.Balance;
var dtos = mapper.Map<ICollection<BalanceDTO>>(entities);
foreach(var _d in dtos)
{
_d.MonthSort = _d.Date.Month;
_d.MonthName = (new DateTimeFormatInfo()).GetMonthName(_d.MonthSort);
};
return dtos.AsQueryable();
}
public async Task<IList<BalanceDTO>> GetBalancesByYear(int year)
{
var entities = dbContext.Balance.Where(_d => _d.Date.Year == year).OrderBy(_d => _d.Date);
var balanceDTOs = Task<IList<BalanceDTO>>.Factory.StartNew(() =>
{
var dtos = mapper.Map<IList<BalanceDTO>>(entities);
foreach (var _d in dtos)
{
_d.MonthSort = _d.Date.Month;
_d.MonthName = (new DateTimeFormatInfo()).GetMonthName(_d.MonthSort);
};
return dtos;
});
return await balanceDTOs;
}
I've modified GetBalancesByYear so it doesn't make use of the GetCollection as it would not be performant if it has to collect the complete collection of elements and transform them into DTO just before filtering what it's not needed.
I am quite confused on who to create correctly the async methods for my DAL when they need to return DTO and not just Entities.
I used test as below. I have one big method for initialize whole dbcontext (it is larger than pasted below, it is similar, but with more models and acces to each dbsets for MoQ Verify) something like fake db. Next I inject it to service and call method that using it.
Is it correct way for testing services (that using dbcontext) with unit tests? And if it is not correct, what is good way for testing services like this (maybe only write test that connect to real db)?
My Test:
[Test]
public void ExampleServiceTest()
{
var mock = InitializeMockContext();
var service = new TagsService(mock.Object);
var result = service.GetTags(2);
var expectection = 2;
Assert.AreEqual(expectection, result.Id);
}
Method that create mocked DBContext:
public Mock<MyDBContext> InitializeMockContext()
{
var mock = new Mock<MyDBContext>();
var mockDataTags = new List<Tags> {
new Tags { Id = 1, Count = 3 },
new Tags { Id = 2, Count = 2} }.AsQueryable();
var mockSet = new Mock<DbSet<Tags>>();
mockSet.As<IQueryable<Tags>>().Setup(m => m.Provider).Returns(mockDataTags.Provider);
mockSet.As<IQueryable<Tags>>().Setup(m => m.Expression).Returns(mockDataTags.Expression);
mockSet.As<IQueryable<Tags>>().Setup(m => m.ElementType).Returns(mockDataTags.ElementType);
mockSet.As<IQueryable<Tags>>().Setup(m => m.GetEnumerator()).Returns(mockDataTags.GetEnumerator());
mock.Setup(x => x.Tags).Returns(mockSet.Object);
//I have more than just one model here
return mock;
}
MyService:
public class TagsService
{
private readonly MyDBContext _ctx;
public TagsService(MyDBContext ctx)
{
_ctx = ctx;
}
public Tags GetTags(int count)
{
using (var db = _ctx)
{
return db.Tags.First(x => x.Count == count);
}
}
}
My Problem is the following.
I have a DataAccessLayer Project with a DalClass. In this Dal class I have many Methods for operations with Entity Framework. For example some pseudo-code to show how we do it.
class DalClass
{
public void SetEntityObject(EntityObject entityObject)
{
using (var context = new Entities())
{
context.EntityObjectSet.Attach(entityObject);
context.ChangeTracker.DetectChanges();
foreach (var entity in context.ChangeTracker.Entries())
{
entity.State = entityObject.EntityState;
}
context.SaveChanges();
}
}
public EntityObject GetEntitObject(Guid id)
{
EntityObject result;
using (var context = new Entities())
{
result = context.EntityObjectSet.Where(x => x.Id == Id);
}
return result;
}
}
Each time we do operations with EF we create a new instance context and do some work. My problem is that I have a method in my DalClass to get a List of EntityObjects Like this :
public List<EntityObject> GetEntityObjectsByIds(List<Guid> idList)
{
using (var context = new Entities())
{
var query = from entityObject in context.EntityObjectSet
where idList.Contains(entityObject.Id)
select entityObject;
return query.ToList();
}
}
After I return the List to my Business Layer class. I do some changes to each EntityObject, calculations, change some Property values and so on. After that I call a method from my BusinessLayer class to save the new edited List with this method:
public void UpdateEntityObjects(List<EntityObject> newEntityObjectList)
{
using (var context = new Entities())
{
var idList = entityObjectList.Select(x => x.Id).ToList();
var query = from entityObject in context.EntityObjectSet
where idList.Contains(entityObject.Id)
select entityObject;
var entityObjectList = query.ToList();
entityObjectList = newEntityObjectList;
context.ChangeTracker.DetectChanges();
foreach (var entity in context.ChangeTracker.Entries())
{
entity.State = EntityState.Modified;
}
context.SaveChanges();
}
}
Problem
I can not save newEntityObjectList that I Edited in my BusinessLayer. Obviously this is not working.
entityObjectList = newEntityObjectList;
So how is the best way to save the newEntityObjectList ?
Problem
I have to fier this query twice
var query = from entityObject in context.EntityObjectSet
where idList.Contains(entityObject.Id)
select entityObject;
Solution to 2. Problem could be if I use one Context instance for GetEntityObjectsByIds() and UpdateEntityObjects(). But in all our Dal Classes we avoided that. Even if I use one instance of Context class, this does not solve Problem 1. So how can I save newEntityObjectList ?
Well, my DAL class would most definetely be something like this:
class EntityObjectDAL
{
private ObjectContext/DbContext context = new ObjectContext/DbContext();
public List<EntityObject> GetObjects() { return context.EntityObjects.ToList(); }
// Tracked by context. Just change it in BL
public void SaveObjects() { context.SaveChanges(); }
// Just call SaveChanges(), no need for anything special...
public void InsertObject(EntityObject eo) { context.EntityObjects.Add(eo); }
public void UpdateObject(EntityObject eo) { context.Entry(eo).State = EntityState.Modified; }
}
Keep it simple...
so I am trying to make a generic function for a where query, not using repository
so it is possible to do something like this?
public IEnumerable<T> Something<T>(int authorId) where T : class
{
return Vmsb.Set<T>().Where(c => c.AuthorId== authorId);
}
now I can't because it dont know what c.AuthorId is
Create an interface IHaveAuthor and specify it on partial classes with this property:
public interface IHaveAuthor
{
int AuthorId { get; set; }
}
//Note that the interface is already implemented in auto-generated part.
//Or if it's Code First, just specify it directly on your classes.
public partial class Book : IHaveAuthor
{
}
public partial class Article : IHaveAuthor
{
}
Then point the interface under the generic type where constraint:
public IEnumerable<T> GetAuthorPublicationsOf<T>(int authorId)
where T : class, IHaveAuthor
{
return Vmsb.Set<T>().Where(c => c.AuthorId == authorId);
}
And the usage:
var authorBooks = query.GetAuthorPublicationsOf<Book>(authorId);
var authorArticles = query.GetAuthorPublicationsOf<Article>(authorId);
Adding on to Olexander's answer, since EF recommends you use the Unit of Work pattern, I usually don't assume a DbContext in my methods - I pass in the most generic object possible instead. Also just as a matter of style, I like to return the interface.
EDIT Updated to include Olexander's important fix to use IQueryable instead of IEnumerable.
So my method signature would look like:
public IQueryable<IHaveAuthor> Something(int authorId, IQueryable<IHaveAuthor> items)
{
return items.Where(c => c.AuthorId == authorId);
}
So calling this would be a bit different than your current calls to it - presumably something like:
var db = new MyDbContext();
var items = db.Books;
var itemForAuthor1 = Something(1, items);
Otherwise your "Something" method isn't terribly flexible - it assumes a single existing DbContext on your current object which might not be a safe assumption (since it's only supposed to live as long as this small chunk of work, whatever it is), you can't chain it with other commands, etc.
Diego hope my code helps u.
protected List<T> ListAll<T>() where T : class
{
using (MyDbContext db = new MyDbContext ())
{
return db.Set(typeof(T)).Cast<T>().AsNoTracking<T>().ToList();
}
}
protected T ListAllById<T>(int id) where T : class
{
using (MyDbContext db = new MyDbContext ())
{
return db.Set(typeof(T)).Cast<T>().Find(id);
}
}
protected void InsertObj(Object obj)
{
using (MyDbContext db = new MyDbContext())
{
db.Set(obj.GetType()).Add(obj);
db.SaveChanges();
}
}
protected void UpdateObj(Object obj)
{
try
{
using (MyDbContext db = new MyDbContext())
{
db.Set(obj.GetType()).Attach(obj);
db.Entry(obj).State = EntityState.Modified;
db.SaveChanges();
}
}
catch (System.Exception ex)
{
System.Windows.Forms.MessageBox.Show(" " + ex.Message);
}
}
protected void DeleteObj(Object obj)
{
using (MyDbContext db = new MyDbContext ())
{
db.Set(obj.GetType()).Attach(obj);
db.Entry(obj).State = EntityState.Deleted;
db.SaveChanges();
}
}