It’s now been a while since Microsoft released entity framework core and with this released I hoped for them to release an interface for DbContext. (Spoiler alert) Unfortunately they didn’t.
Not having an interface for DbContext sometimes makes it hard to do some testing when working with a pure interface based architecture especially when combining this with dependency injections.
When I’m creating projects and use entity framework, I normally create some sort of interface for my context i.e. IMyContext
. I later create a context by letting it derive from the interface and DbContext ie. (MyContext : DbContext, IMyContext
). This creates the problem that if I want to use a function that is in DbContext (like SaveChanges()
) I actually need to inject and use the class instead of my interface, either this or I need to implement the function in my interface and any other context interfaces I create i.e.
IDbContext
So the way I’ve solved this problem is by creating my own IDbContext that has all the functions in DbContext. I then let IMyContext inherit from IDbContext.
1// -------------------------------------------------------------------------------------------------2// Copyright (c) Johan Boström. All rights reserved.3// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.4// -------------------------------------------------------------------------------------------------56namespace Example.EntityFramework.Testing.Data.Abstract7{8 using System;9 using System.Collections.Generic;10 using System.Threading;11 using System.Threading.Tasks;12 using Microsoft.EntityFrameworkCore;13 using Microsoft.EntityFrameworkCore.ChangeTracking;14 using Microsoft.EntityFrameworkCore.Infrastructure;15 using Microsoft.EntityFrameworkCore.Internal;1617 public interface IDbContext : IDisposable, IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbQueryCache, IDbContextPoolable18 {19 DatabaseFacade Database { get; }20 ChangeTracker ChangeTracker { get; }21 EntityEntry Add(object entity);22 EntityEntry<TEntity> Add<TEntity>(TEntity entity) where TEntity : class;23 Task<EntityEntry> AddAsync(object entity, CancellationToken cancellationToken = default(CancellationToken));24 Task<EntityEntry<TEntity>> AddAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : class;25 void AddRange(IEnumerable<object> entities);26 void AddRange(params object[] entities);27 Task AddRangeAsync(IEnumerable<object> entities, CancellationToken cancellationToken = default(CancellationToken));28 Task AddRangeAsync(params object[] entities);29 EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;30 EntityEntry Attach(object entity);31 void AttachRange(params object[] entities);32 void AttachRange(IEnumerable<object> entities);33 EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;34 EntityEntry Entry(object entity);35 bool Equals(object obj);36 object Find(Type entityType, params object[] keyValues);37 TEntity Find<TEntity>(params object[] keyValues) where TEntity : class;38 Task<TEntity> FindAsync<TEntity>(params object[] keyValues) where TEntity : class;39 Task<object> FindAsync(Type entityType, object[] keyValues, CancellationToken cancellationToken);40 Task<TEntity> FindAsync<TEntity>(object[] keyValues, CancellationToken cancellationToken) where TEntity : class;41 Task<object> FindAsync(Type entityType, params object[] keyValues);42 int GetHashCode();43 DbQuery<TQuery> Query<TQuery>() where TQuery : class;44 EntityEntry Remove(object entity);45 EntityEntry<TEntity> Remove<TEntity>(TEntity entity) where TEntity : class;46 void RemoveRange(IEnumerable<object> entities);47 void RemoveRange(params object[] entities);48 int SaveChanges(bool acceptAllChangesOnSuccess);49 int SaveChanges();50 Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken));51 Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));52 DbSet<TEntity> Set<TEntity>() where TEntity : class;53 string ToString();54 EntityEntry Update(object entity);55 EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;56 void UpdateRange(params object[] entities);57 void UpdateRange(IEnumerable<object> entities);58 }59}
By creating this and implementing it like this: IMyContext : IdbContext
. I now can accomplish dependency injections without having to use the actual class and instead just inject IMyContext
.
Testing with ease
This makes it easier to test as well since now we can mock the interface instead of mocking the class. Lets say that I have class that contains some businss logic for adding an entity to the database and saving it. Sounds quite simple, if we were to use the class instead of the interface we would need to inject all of the class dependencies and/or mock them as well, which can become quite alot if you have a big project. Instead now we can just mock the interface functions and test the business logic.
Sample
The business logic
1// -------------------------------------------------------------------------------------------------2// Copyright (c) Johan Boström. All rights reserved.3// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.4// -------------------------------------------------------------------------------------------------56namespace Example.EntityFramework.Testing.BusinessLogic7{8 using System.Threading.Tasks;9 using Data.Db;1011 public class Business12 {13 private readonly ICustomContext context;1415 public Business(ICustomContext context)16 {17 this.context = context;18 }1920 public void AddCustomEntity(CustomEntity testEntity)21 {22 context.CustomEntities.Add(testEntity);23 context.SaveChanges();24 }25 }26}
Here we have what a test would look like to ensure that we have added an entity to the dataset in the custom context and then verify that we have saved to the datasbase that is beeing in IDbContext.
1// -------------------------------------------------------------------------------------------------2// Copyright (c) Johan Boström. All rights reserved.3// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.4// -------------------------------------------------------------------------------------------------56namespace Example.EntityFramework.Testing.Tests7{8 using System.Threading;9 using System.Threading.Tasks;10 using BusinessLogic;11 using Data.Db;12 using Microsoft.EntityFrameworkCore;13 using Moq;14 using Xunit;1516 public class BusinessTests17 {18 private readonly Mock<ICustomContext> testContext;19 private readonly Mock<DbSet<CustomEntity>> testEntities;2021 public BusinessTests()22 {23 // Initiate ICustomContext24 testContext = new Mock<ICustomContext>();2526 // Initiate DbSet27 testEntities = new Mock<DbSet<CustomEntity>>();2829 // Setup DbSet30 testContext.Setup(ctx => ctx.CustomEntities).Returns(testEntities.Object);31 }3233 [Fact]34 public void AddingTestEntity()35 {36 var business = new Business(testContext.Object);37 business.AddCustomEntity(new CustomEntity38 {39 Id = 1,40 Name = "TestName"41 });4243 testEntities.Verify(set => set.Add(It.Is<CustomEntity>(e => e.Id == 1 && e.Name == "TestName")), Times.Once);44 testContext.Verify(ctx => ctx.SaveChanges(), Times.Once);45 }46 }47}
Conclusion
In my scenario it makes it much easier to work with the DbContext as an interface and I hope that microsoft decides to create an interface themself to make it easier for my development process.
You can find the entire code sample here over at GitHub.