#27 Creating A Mock Context Builder
Sunday, April 21, 2019
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.
Parts
- Part 29: Offset Pager Urls
- Part 28: Offset Pager Start
- Part 27: Mock Context Builder
- Part 26: Mock Repository
- Part 25: Mock Async
- Part 24: Picture Tag Helper
- Part 23: Img DPR Tag Helper
- Part 22: Img Responsive Tag Helper
- Part 21: Img Optimized Display
- Part 20: Img Optimization
- Part 19: Img Lazy Loading
- Part 18: Img Responsive
- Part 17: Bottom Nav
- Part 16: Main Nav Cookie
- Part 15: Main Nav Mobile
- Part 14: Main Nav Search
- Part 13: Main Nav Auth
- Part 12: Main Nav Anchors
- Part 11: Main Nav Logo
- Part 10: Search Results
- Part 9: Search Manager
- Part 8: Search Start
- Part 7: Seeding the Database
- Part 6: Domain Database
- Part 5: Emailing Exceptions
- Part 4: Mailkit
- Part 3: View Renderer
- Part 2: Upgrade to 2.1
- Part 1: Quick Start
A lot of things to remember to do
- WebUi
- Features
- Mocking
- MockContextBuilder.cs
- Mocking
- Features
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;
}
}
}
The builder in action
- WebUi
- Pages
- Mock-Async.cshtml.cs
- Pages
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);
}
...
}
}
What about navigation properties
- WebUi
- Features
- Mocking
- MockExtensions.cs
- Mocking
- Features
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;
}
...
}
}
Update the builder
- WebUi
- Features
- Mocking
- MockContextBuilder.cs
- Mocking
- Features
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);
}
}
}
}
Last update to our code behind
- WebUi
- Pages
- Mock-Async.cshtml.cs
- Pages
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; }
}
...
}
Show the final results
- WebUi
- Pages
- Mock-Async.cshtml
- Pages
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>