DDD, CQRS and testing with ASP.Net MVC
-
Upload
andy-butland -
Category
Technology
-
view
2.327 -
download
0
Transcript of DDD, CQRS and testing with ASP.Net MVC
DDD, CQRS & Testingwith ASP.Net
MVC26
September
2015
Just to introduce myself…• I’m Andy Butland• Blog (sporadically) at
http://web-matters.blogspot.it/• Find here a copy of slides and links to various
resources• Contact: [email protected] / @andybutland• I work for Zone – www.thisiszone.com - a digital
agency where I’m head of .NET development• We develop web sites and applications using
ASP.Net MVC and CMS platforms such as Umbraco and EPiServer
• We’re primarily UK based, in London and Bristol• But I’m lucky enough to live…
… here, in Bassano del Grappa, Italy
And what are we talking about…• We’ll be discussing some practices I follow when
building web applications using ASP.Net MVC• Digested and adapted from various opinions• Adopting best practices but in a practical way• For me at least… leads to a nicely organised,
testable and maintainable code base
Standing on the shoulders…• Much of what follows comes from various
speakers, bloggers and writers in the .Net community
• Finally read “Domain Driven Design” by Eric Evans• Rob Conery questioned some of practices around use of
repositories• Blog series on DDD, CQRS and other MVC practices from
Jimmy Bogard and Gabriel Schenker at lostechies.com • Julie Lerman’s articles on DDD with Entity Framework at
MSDN• Adam Tibi’s implementation of a CQRS pattern using the
IoC container Ninject was heavily borrowed for my own use
Contents
1
2
3
4
DDD with ASP.Net MVC and Entity Framework
CQRS using a mediator pattern Wrap up and Q&A
Unit testing our model, queries and commands
1. DDD with ASP.Net MVC and Entity Framework
Domain driven design (DDD)• Close replication of the real-world, business
problem• Focussed on an appropriate area of application
responsibility – the “bounded context”• Sharing of domain knowledge and vocabulary
between business experts, developers and, importantly, the software itself – the “ubiquitous language”
A rich domain model• In essence, a DDD approach looks to push more
logic and behaviour to the application’s core model classes
• Contrasted with an “anaemic” domain model• Consider the domain as an “API” that you provide to the rest
of the application – providing appropriate access points and retaining control
• Lean more on code and less on SQL/data access• More expressive, easier to write and maintain – and test• But careful balance needed as can’t forget performance
Control via the constructor• Avoid a public, parameterless, default constructor
• Prevents the instantiation of invalid objects by application code via the {} syntax, e.g.var product = new Product { Name = “Test product” };
private Question(){ Options = new List<QuestionOption>();}
public Question(string code, string text, int orderInSection, QuestionSection section, QuestionType type) : this(){ Code = code; Text = text; OrderInSection = orderInSection; Section = section; Type = type;}
EF requires a parameter-less
constructor, but it can be private.
By requiring the rest of the application to only call this constructor, we can
ensure we have a valid initialisation of an instance.
Control of property access• Use private property setters to prevent direct
access to properties• Prevents the modification of an instance into an invalid
state• Instead provide validated methods to allow related property
values to be set together, ensuring a consistent state is maintained
public int? NumericalMin { get; private set; }
public int? NumericalMax { get; private set; }
public void SetNumericalRange(int min, int max){ if (min > max) { throw new ArgumentException( “Max parameter must be greater than the min parameter."); }
NumericalMin = min; NumericalMax = max;}
Properties can’t be set directly
Instead a method must be called, which can validate and ensure related
properties are populated together.
Move behaviour into the model• Where possible implement business logic in the
domain model objects• Object graph must be populated sufficiently to support the
behaviour• Being POCO classes, there are no dependencies that
complicate unit testing
public int GetMaximumAvailableScore(){ if (Type.Id == (int)QuestionTypeId.MultipleSelect) { return Options.Sum(x => x.Score); } else if (Type.Id == (int)QuestionTypeId.SingleSelect) { return Options.Max(x => x.Score); } else { return 0; }}
Logic can be encapsulated in
methods…
[TestMethod]public void Question_GetMaximumScoreForMultiSelect_ReturnsCorrectValue(){ // Arrange var question = CreateMultiSelectQuestion();
// Act var maxScore = question.GetMaximumAvailableScore();
// Assert Assert.AreEqual(8, maxScore);} … which can be
easily unit tested.
2. CQRS using a mediator pattern
Command Query Responsibility Segregation• In essence, CQRS involves a separation between
read and write operations in an application• At scale we may have separate models – one
highly cached and de-normalised for reads, and a strict, validated model for writes
• Related concepts – “event sourcing”, “eventual consistency” – may be appropriate in certain situations
• Even for small to medium scale applications though, CQRS has benefits over a more typical CRUD style
Benefits of CQRS• “Slices over layers” – breaking down application
by features rather than technical tiers• More meaningful data operations over CRUD
using the “ubiquitous language”• e.g. “ShipOrderCommand” versus
“SaveOrder(Order order)”• Single, discrete transactions with the ORM• More… but smaller, more focussed and single
responsibility principle adhering classes
MVC CQRS Pattern: Reads
CONTROLLER VIEW
VIEWMODEL
VIEW MODELQUERY
HANDLER
VIEW MODELQUERY
DATABASE
Retrieve domain model objectsvia the Entity Framework context
Map to view model using AutoMapper
View model passed tostrongly typed view
Query passed as a GET parameterto the controller action method:• Might be a simple Id• Or something more
complex for a search feature
QUERYDISPATCHER
Controller calls query dispatcherpassing query.Appropriate query handler is foundfrom view model and query types(using Ninject)
MVC CQRS Pattern: Writes
CONTROLLER
COMMANDRESULT
COMMANDHANDLER
COMMAND
DATABASE
Retrieve and update domain model objectsand persist via the EF context
Simple command result is returned (success flag, error message and - sometimes - return data).
Command may be POSTed as a parameter to the controller action method or created within the method
COMMANDDISPATCHER
Controller calls command dispatcherpassing command.Appropriate command handler is foundfrom command type(using Ninject)
MVC CQRS Pattern: Validated Writes
CONTROLLER
COMMANDRESULT
COMMANDHANDLER
COMMAND
DATABASE
Map to domain model objectsand persist via the Entity Framework context
Simple command result is returned
Validated view model is mapped to command
COMMANDDISPATCHER
Controller calls command dispatcherpassing command.Appropriate command handler is foundfrom command type(using Ninject)
VIEWMODEL VIEW
View model is model bound to controller action method from form POST and validated.
In case of validation error, view model is re-populated and returned to view.
public abstract class BaseController : Controller{
public BaseController(IQueryDispatcher queryDispatcher, ICommandDispatcher
commandDispatcher){ QueryDispatcher = queryDispatcher; CommandDispatcher = commandDispatcher;}
}
Base controller has injected dependencies for dispatching
queries and commands.
All controllers inherit from this.
private static void RegisterServices(IKernel kernel){
kernel.Bind<IQueryDispatcher>().To<QueryDispatcher>();kernel.Bind<ICommandDispatcher>().To<CommandDispatcher>();
kernel.Bind(x => x .FromAssembliesMatching(“MyApplication.dll")
.SelectAllClasses().InheritedFrom(typeof(IQueryHandler<,>)) .BindAllInterfaces());
kernel.Bind(x => x .FromAssembliesMatching(“MyApplication.dll")
.SelectAllClasses().InheritedFrom(typeof(ICommandHandler<>)) .BindAllInterfaces());
}
Resolved using Ninject IoC container
Which also handles the convention based matching of
the appropriate handler to each query and command
public interface IQueryDispatcher{ Task<TResult> Dispatch<TParameter, TResult>(TParameter query) where TParameter : IQuery where TResult : IQueryResult;}
The query dispatcher has a single method that takes two type parameters: the query definition and the query
result.
It takes the query definition as the argument and
asynchronously returns the result.
public class QueryDispatcher : IQueryDispatcher{ private readonly IKernel _kernel;
public QueryDispatcher(IKernel kernel) { _kernel = kernel; }
public async Task<TResult> Dispatch<TParameter, TResult>(TParameter query)
where TParameter : IQuery where TResult : IQueryResult { var handler = _kernel.Get<IQueryHandler<TParameter, TResult>>(); return await handler.Retrieve(query); }}
The implementation retrieves the appropriate handler based on the type parameters from the services registered with the Ninject IoC container.
public interface ICommandDispatcher{ Task<CommandResult> Dispatch<TParameter>(TParameter command)
where TParameter : ICommand;}
Similarly the command dispatcher has a single method
that takes a single type parameters: the command
definition.
It takes the command definition as the argument,
asynchronously process it and return a status result.
public class CommandDispatcher : ICommandDispatcher{ private readonly IKernel _kernel;
public CommandDispatcher(IKernel kernel) { _kernel = kernel; }
public async Task<CommandResult> Dispatch<TParameter>(TParameter command)
where TParameter : ICommand { var handler = _kernel.Get<ICommandHandler<TParameter>>(); return await handler.Execute(command); }}
The implementation retrieves the appropriate
handler based on the type parameter from the
services registered with the Ninject IoC container.
public class CommandResult{ public bool Success { get; set; }
public string Message { get; set; }
public object Data { get; set; }}
The simple command result usually just returns the status of
the command.
On occasion it’s useful to return some data, most often the Id of
a newly generated record.
public async Task<ViewResult> Details(DetailsViewModelQuery query){ var vm = await QueryDispatcher.Dispatch<DetailsViewModelQuery,
DetailsViewModel>(query);
return View("Details", vm);}
The controller action method is very thin, delegating
immediately to the query dispatcher to create the view
model.
The query definition may be as simple as the
Id of the record to retrieve.
public class DetailsViewModelQueryHandler : IQueryHandler<DetailsViewModelQuery, DetailsViewModel>{
public async Task<DetailsViewModel> Retrieve(DetailsViewModelQuery query)
{ValidateArguments();Context = Context ?? new ApplicationDbContext();
var result = new DetailsViewModel(); var question = await Context.Questions .SingleOrDefaultAsync(x => x.Id == query.Id); Mapper.Map(question, result);
return result;} Query handler queries the EF
context and maps the domain model object to the view model.
EF context is instantiated or
passed in via the constructor for
testing.
[HttpPost][ValidateAntiForgeryToken]public async Task<RedirectToRouteResult> SignUpForActivity
(SignUpParticipantCommand command){ command.ParticipantId =
User.Identity.GetUserId(); command.AddedOn = DateTime.Now; var commandResult = await CommandDispatcher.Dispatch(command); if (commandResult.Success) { TempData["SignUpMessage"] = "Thank you for signing up. "; } else { // Handle failure of operation
} return RedirectToAction("Details", new {id = command.ActivityId, });}
Command is model bound from form post,
with additional details set in code.
Again controller action method delegates to the
command dispatcher execute the command.
public class SignUpParticipantCommandHandler : ICommandHandler<SignUpParticipantCommand>{ public async Task<CommandResult> Execute
(SignUpParticipantCommand command) { ValidateArguments(command);
Context = Context ?? new ApplicationDbContext();
var result = new CommandResult(); var activity = await Context.Activities .SingleOrDefaultAsync(x => x.Id == command.ActivityId); var participant = await Context.Participants .SingleOrDefaultAsync(x => x.Id == command.ParticipantId);
if (activity != null && participant != null) {
...
Related entity details are retrieved.
if (Context.ActivityParticipants .SingleOrDefault(x => x.Activity.Id == command.ActivityId && x.Participant.Id == command.ParticipantId) == null) { if (activity.NumberOfPlaces > Context.ActivityParticipants .Count(x => x.Activity.Id == command.ActivityId)) { var activityParticipant = new ActivityParticipant(activity, participant, command.AddedOn); Context.ActivityParticipants.Add(activityParticipant); await Context.SaveChangesAsync(); result.Success = true; } else { result.Success = false; result.Message = "There are not enough places remaining"; } ...
Further checks are made on the validity of the
command before execution.
} else { result.Success = false; result.Message = "Participant is already signed up"; } } else { result.Success = false; result.Message = "Participant and/or activity not found"; }
return result; }}
Appropriate results with status and error details
are provided to the calling code.
[HttpPost][ValidateAntiForgeryToken]public async Task<ActionResult> Edit(EditViewModel vm){ if (ModelState.IsValid) { var command = new AddOrEditCommand(); Mapper.Map(vm, command); var commandResult = await CommandDispatcher.Dispatch(command); if (commandResult.Success) { var newId = (int)commandResult.Data; // Do stuff with the generated Id of if we need to... return RedirectToAction("Index"); } }
return View("Edit", vm);}
View model is model bound and
validated. If something fails, return to view.
If validation passes, map the view model to
a command object.
3. Unit testing our model, queries and commands
Applying unit testing• By moving logic into our domain model we’ve
already made that easier to test• These have no dependencies so can simply be
instantiated with a known state before performing the operations under test
• With the use of the CQRS mediator pattern, our thin controllers mean there’s little value in testing them
• But we still have a logic that we should put under test in our query and command handlers
• These handlers are closely tied to data access code, specifically the use of Entity Framework
Unit testing Entity Framework• Testing with a database
• Slow• Brittle - as hard to maintain a known, isolated data
set for tests• Testing with in-memory objects
• Of limited value as LINQ to Objects != LINQ to Entities
• Using Effort – an in-memory database generated on the fly - written and maintained by Tamas Flamich
• http://effort.codeplex.com/ • Fast• Mimics true behaviour of EF very closely
Unit testing with Effort• Install via NuGet: PM> Install-Package Effort• Instantiate an empty, memory backed EF context• Seed the context with a known set of data
• Can use EF API for this• Or for faster tests and with less code, load from CSV
files• Create an instance of the query or command
handler, passing in the in-memory context• Execute the query or command• Assert the query result is as expected or the
command operations have persisted
[TestClass]public class RoomViewModelQueryHandlerTests : BaseDataTest{ [TestMethod] public void SampleTest() { // Arrange SetUpContextAndTestData(); var handler = new RoomViewModelQueryHandler(Context); var query = new RoomViewModelQuery { Id = 1 };
// Act var result = handler.Retrieve(query).Result;
// Assert Assert.AreEqual("Kitchen", result.RoomDescription); }}
Instantiate handler passing in in-memory,
seeded context
Retrieve the result (the view
model).
Assert the view model properties
public abstract class BaseDataTest{ protected ProductEntities Context { get; private set; }
protected void SetUpContextAndTestData() { InitContext(); SeedData(); }
private void InitContext() { var connection = Effort.EntityConnectionFactory .CreateTransient("name=ProductEntities"); Context = new ProductEntities(connection); }
...
Connection to in-memory representation of EDMX
meta-data created.
An alternative method supports the code-first
approach
private void SeedData() { var categories = new List<Category> { new Category { Id = 1, Description = "Kitchens", }, new Category { Id = 2, Description = "Bedrooms", }, };
Context.Categories.AddRange(categories);
Context.SaveChanges(); }}
Data seeded using EF API (or CSV files can be used)
4. Wrap-up and Q&A
In summary• Even with a “light-touch” of architectural patterns
we have an application that provides• Rich domain model or core• Data access separated into discrete read and write
operations, adhering to the single responsibility principle
• Testable logic and data access code• Whilst these patterns can be taken a lot further
where warranted, even for small-medium scale applications there’s value in their use
Lastly, some thanks…• To my colleagues at Zone
• Numerous discussions, questions and advice as we’ve evolved techniques and technologies over the years
• To everyone sharing knowledge, opinions and techniques
• Blogs, forum threads, talks and other community contributions that have influenced the thinking behind our work and this presentation
• Looking forward to more discussions this afternoon!
Provided as Creative Commons 3.0 Share Alike:
http://creativecommons.org/licenses/by-nc-sa/3.0/
webnextconf.eu