Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

DDD, CQRS and testing with ASP.Net MVC

5 249 vues

Publié le

Slides from talk given at Web European
Conference 2015.

Publié dans : Technologie
  • Soyez le premier à commenter

DDD, CQRS and testing with ASP.Net MVC

  1. 1. DDD, CQRS & Testing with ASP.Net MVC 26 September 2015
  2. 2. 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: abutland73@gmail.com / @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…
  3. 3. … here, in Bassano del Grappa, Italy
  4. 4. 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
  5. 5. 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
  6. 6. 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
  7. 7. 1. DDD with ASP.Net MVC and Entity Framework
  8. 8. 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”
  9. 9. 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
  10. 10. 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” };
  11. 11. 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.
  12. 12. 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
  13. 13. 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.
  14. 14. 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
  15. 15. 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…
  16. 16. [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.
  17. 17. 2. CQRS using a mediator pattern
  18. 18. 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
  19. 19. 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
  20. 20. MVC CQRS Pattern: Reads CONTROLLER VIEW VIEW MODEL VIEW MODEL QUERY HANDLER VIEW MODEL QUERY DATABASE Retrieve domain model objects via the Entity Framework context Map to view model using AutoMapper View model passed to strongly typed view Query passed as a GET parameter to the controller action method: • Might be a simple Id • Or something more complex for a search feature QUERY DISPATCHER Controller calls query dispatcher passing query. Appropriate query handler is found from view model and query types (using Ninject)
  21. 21. MVC CQRS Pattern: Writes CONTROLLER COMMAND RESULT COMMAND HANDLER COMMAND DATABASE Retrieve and update domain model objects and 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 COMMAND DISPATCHER Controller calls command dispatcher passing command. Appropriate command handler is found from command type (using Ninject)
  22. 22. MVC CQRS Pattern: Validated Writes CONTROLLER COMMAND RESULT COMMAND HANDLER COMMAND DATABASE Map to domain model objects and persist via the Entity Framework context Simple command result is returned Validated view model is mapped to command COMMAND DISPATCHER Controller calls command dispatcher passing command. Appropriate command handler is found from command type (using Ninject) VIEW MODEL 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.
  23. 23. 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.
  24. 24. 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
  25. 25. 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.
  26. 26. 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.
  27. 27. 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.
  28. 28. 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.
  29. 29. 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.
  30. 30. 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.
  31. 31. 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.
  32. 32. [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.
  33. 33. 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.
  34. 34. 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.
  35. 35. } 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.
  36. 36. [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.
  37. 37. 3. Unit testing our model, queries and commands
  38. 38. 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
  39. 39. 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
  40. 40. 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
  41. 41. [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
  42. 42. 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
  43. 43. 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)
  44. 44. 4. Wrap-up and Q&A
  45. 45. 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
  46. 46. 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!
  47. 47. Provided as Creative Commons 3.0 Share Alike: http://creativecommons.org/licenses/by-nc-sa/3.0/
  48. 48. webnextconf.eu

×