Johan Boström

4 minute read

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.

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.Data.Abstract
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.ChangeTracking;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Microsoft.EntityFrameworkCore.Internal;

    public interface IDbContext : IDisposable, IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbQueryCache, IDbContextPoolable
    {
        DatabaseFacade Database { get; }
        ChangeTracker ChangeTracker { get; }
        EntityEntry Add(object entity);
        EntityEntry<TEntity> Add<TEntity>(TEntity entity) where TEntity : class;
        Task<EntityEntry> AddAsync(object entity, CancellationToken cancellationToken = default(CancellationToken));
        Task<EntityEntry<TEntity>> AddAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : class;
        void AddRange(IEnumerable<object> entities);
        void AddRange(params object[] entities);
        Task AddRangeAsync(IEnumerable<object> entities, CancellationToken cancellationToken = default(CancellationToken));
        Task AddRangeAsync(params object[] entities);
        EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;
        EntityEntry Attach(object entity);
        void AttachRange(params object[] entities);
        void AttachRange(IEnumerable<object> entities);
        EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
        EntityEntry Entry(object entity);
        bool Equals(object obj);
        object Find(Type entityType, params object[] keyValues);
        TEntity Find<TEntity>(params object[] keyValues) where TEntity : class;
        Task<TEntity> FindAsync<TEntity>(params object[] keyValues) where TEntity : class;
        Task<object> FindAsync(Type entityType, object[] keyValues, CancellationToken cancellationToken);
        Task<TEntity> FindAsync<TEntity>(object[] keyValues, CancellationToken cancellationToken) where TEntity : class;
        Task<object> FindAsync(Type entityType, params object[] keyValues);
        int GetHashCode();
        DbQuery<TQuery> Query<TQuery>() where TQuery : class;
        EntityEntry Remove(object entity);
        EntityEntry<TEntity> Remove<TEntity>(TEntity entity) where TEntity : class;
        void RemoveRange(IEnumerable<object> entities);
        void RemoveRange(params object[] entities);
        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken));
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
        DbSet<TEntity> Set<TEntity>() where TEntity : class;
        string ToString();
        EntityEntry Update(object entity);
        EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
        void UpdateRange(params object[] entities);
        void UpdateRange(IEnumerable<object> entities);
    }
}

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

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.BusinessLogic
{
    using System.Threading.Tasks;
    using Data.Db;

    public class Business
    {
        private readonly ICustomContext context;

        public Business(ICustomContext context)
        {
            this.context = context;
        }

        public void AddCustomEntity(CustomEntity testEntity)
        {
            context.CustomEntities.Add(testEntity);
            context.SaveChanges();
        }
    }
}

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.

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.Tests
{
    using System.Threading;
    using System.Threading.Tasks;
    using BusinessLogic;
    using Data.Db;
    using Microsoft.EntityFrameworkCore;
    using Moq;
    using Xunit;

    public class BusinessTests
    {
        private readonly Mock<ICustomContext> testContext;
        private readonly Mock<DbSet<CustomEntity>> testEntities;

        public BusinessTests()
        {
            // Initiate ICustomContext
            testContext = new Mock<ICustomContext>();
            
            // Initiate DbSet
            testEntities = new Mock<DbSet<CustomEntity>>();

            // Setup DbSet
            testContext.Setup(ctx => ctx.CustomEntities).Returns(testEntities.Object);
        }

        [Fact]
        public void AddingTestEntity()
        {
            var business = new Business(testContext.Object);
            business.AddCustomEntity(new CustomEntity
            {
                Id = 1,
                Name = "TestName"
            });

            testEntities.Verify(set => set.Add(It.Is<CustomEntity>(e => e.Id == 1 && e.Name == "TestName")), Times.Once);
            testContext.Verify(ctx => ctx.SaveChanges(), Times.Once);
        }
    }
}

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.