Advertisement

#27 Creating A Mock Context Builder

In this article we will focus on creating a database context builder class that will make it much easier for use to create data and use it as the data store for an async capable mock dataset. Once completed we will just need to retrieve the database context from the builder and use it when creating our mock repository.

A lot of things to remember to do

  • WebUi
    • Features
      • Mocking
        • MockContextBuilder.cs

Unfortunately as everything stands right now there are several things that we need to remember to do for every database set within our repository. The first thing we do is use a helper method to create the data, then an extension method to convert it to a mock database set then extract the object property from the return value and assign it to the appropriate property in our context. To make this process easier we are going to create a builder class that we can use to help us create the context (a).

MockContextBuilder.cs

using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;

namespace WebUi.Features.Mocking
{
    public interface IMockContextBuilderA<TContext>
    {
        IMockContextBuilderA<TContext> Add<TEntity>(int count, Expression<Func<TContext, 
            DbSet<TEntity>>> dbSetExpression, Func<int, TEntity> generator) where TEntity : class;
        TContext Context { get; }
    }

    public class MockContextBuilder<TContext> :  IMockContextBuilderA<TContext>
    {
        public MockContextBuilder()
        {
            Context = Activator.CreateInstance<TContext>();
        }

        public TContext Context { get; }

        public static IMockContextBuilderA<TContext> Start()
        {
            return new MockContextBuilder<TContext>();
        }

        public IMockContextBuilderA<TContext> Add<TEntity>(int count, 
            Expression<Func<TContext, DbSet<TEntity>>> dbSetExpression,
            Func<int, TEntity> generator) where TEntity : class
        {
            var expression = (MemberExpression)dbSetExpression.Body;
            var name = expression.Member.Name;

            var prop = Context.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
            if (prop != null && prop.CanWrite)
            {
                prop.SetValue(Context, MockExtensions.CreateMockData(count, generator)
                    .ToAsyncDbSetMock().Object);
            }

            return this;
        }
    }
}
(a) Our new context builder class makes use of a fluent interface to make it easier to direct users, including ourselves, on what we should be doing and when.

The builder in action

  • WebUi
    • Pages
      • Mock-Async.cshtml.cs

The pattern continues by us now returning the page code behind and updating it to use our new context builder (b).

Mock-Async.cshtml.cs

...
namespace WebUi.Pages
{
    public class MockAsyncModel : PageModel
    {
        ...
        public MockAsyncModel()
        {
            var context = MockContextBuilder<MockAsyncContext>.Start()
                .Add(20, x => x.Articles, x => new MockAsyncArticle
                {
                    Title = $"Title {x}"
                })
                .Add(20, x => x.Authors, x => new MockAsyncAuthor
                {
                    Name = $"Christopher R. Jamell {x}"
                })
                .Context;
            _repository = new MockAsyncRepository(context);
        }
        ...
    }
}
(b) Updating our page code behind to use the new context builder.

What about navigation properties

  • WebUi
    • Features
      • Mocking
        • MockExtensions.cs

So far the database sets that we have created have been simple, by which I mean, there is no connection between any of the sets. In just about all but the most trivial examples this will not be the case. To fix this the first thing we are going to do is add the ability to generate sample data where one of the arguments passed in will be our context (c).

MockExtensions.cs

...
namespace WebUi.Features.Mocking
{
    internal static class MockExtensions
    {
        ...
        public static List<TEntity> CreateMockData<TContext, TEntity>(
            int count, TContext context, Func<int, TContext, TEntity> generator)
        {
            var data = new List<TEntity>();
            for (var i = 1; i <= count; i++)
            {
                data.Add(generator(i, context));
            }

            return data;
        }
        ...
    }
}
(c) Adding a method to create sample data using the current state of the context.
Advertisement

Update the builder

  • WebUi
    • Features
      • Mocking
        • MockContextBuilder.cs

Next we need to update our context builder (d).

MockContextBuilder.cs

..
using System.Collections.Generic;
....
namespace WebUi.Features.Mocking
{
    public interface IMockContextBuilderA<TContext>
    {
        ...
        IMockContextBuilderA<TContext> Add<TEntity>(int count, Expression<Func<TContext,
            DbSet<TEntity>>> dbSetExpression,
            Func<int, TContext, TEntity> generator) where TEntity : class;
    }
    
    public class MockContextBuilder<TContext> :  IMockContextBuilderA<TContext>
    {
        ...
        public IMockContextBuilderA<TContext> Add<TEntity>(int count, Expression<Func<TContext,
            DbSet<TEntity>>> dbSetExpression,
            Func<int, TEntity> generator) where TEntity : class
        {
            SetValue(dbSetExpression, MockExtensions.CreateMockData(count, generator));

            return this;
        }

        public IMockContextBuilderA<TContext> Add<TEntity>(int count, Expression<Func<TContext,
            DbSet<TEntity>>> dbSetExpression,
            Func<int, TContext, TEntity> generator) where TEntity : class
        {
            SetValue(dbSetExpression, MockExtensions.CreateMockData(count, Context, generator));

            return this;
        }

        private void SetValue<TEntity>(Expression<Func<TContext, DbSet<TEntity>>> dbSetExpression,
            IEnumerable<TEntity> entities) where TEntity : class
        {
            var expression = (MemberExpression)dbSetExpression.Body;
            var name = expression.Member.Name;

            var prop = Context.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
            if (prop != null && prop.CanWrite)
            {
                prop.SetValue(Context, entities.ToAsyncDbSetMock().Object);
            }
        }
    }
}
(d) In our second add method we will include the ability to make use of the current state of the context.

Last update to our code behind

  • WebUi
    • Pages
      • Mock-Async.cshtml.cs

For the last time we will return to our code behind and make some changes to see how we can add a connection between our authors and the articles stored in the context (e). The way we are doing this of course makes it important to pay attention to order that the data is added to the context.

Mock-Async.cshtml.cs

...
using System.Linq;
...
namespace WebUi.Pages
{
    public class MockAsyncModel : PageModel
    {
        ...
        public MockAsyncModel()
        {
            var context = MockContextBuilder<MockAsyncContext>.Start()
                .Add(20, x => x.Articles, x => new MockAsyncArticle
                {
                    Id = x,
                    Title = $"Title {x}"
                })
                .Add(20, x => x.Authors, (x, y) =>
                {
                    var article = y.Articles.First(z => z.Id == x);
                    return new MockAsyncAuthor
                    {
                        Articles = new List<MockAsyncArticle>
                        {
                            article
                        },
                        Id = x,
                        Name = $"Christopher R. Jamell {x}"
                    };
                })
                .Context;
            _repository = new MockAsyncRepository(context);
        }
        ...
        public async Task OnGet()
        {
            Articles = await _repository.Articles
                .ToListAsync();

            Authors = await _repository.Authors
                .Include(x => x.Articles)
                .ToListAsync();
        }
    }

    public class MockAsyncArticle
    {
        public int Id { get; set; }
        ...
    }

    public class MockAsyncAuthor
    {
        public int Id { get; set; }
        ...
        public virtual ICollection<MockAsyncArticle> Articles { get; set; }
    }
    ...
}
(e) Last update to our code behind shows how we can populate the navigation properties of our database sets.

Show the final results

  • WebUi
    • Pages
      • Mock-Async.cshtml

Now we just need to update our view to make sure that all of our changes are still working the way we intend them to (f).

Mock-Async.cshtml

...
<div class="authors">
    @foreach (var author in Model.Authors)
    {
        <div class="author">
            <div class="name">@author.Name</div>
            @foreach (var article in author.Articles)
            {
                <div class="article">@article.Title</div>
            }
        </div>
    }
</div>
(f) Updating the view so that we can see that we have indeed populate our navigation properties as we expected.
Exciton Interactive LLC
Advertisement