Saturday, 16 November 2013

Applied Domain-Driven Design (DDD), Part 6 - Application Services

We have developed our domain, setup our infrastructure, now we need to expose our domain to Applications. This is where Application Service comes in.

Any kind of application should be able to consume your Application Service and use it, mobile, desktop or web. It's good to keep this in mind when you setup your Application Service and Distributed Interface Layer (Web Service).

Application Service Contract (lives in a separate project):
    public interface ICartService
    {
        CartDto Add(Guid customerId, CartProductDto cartProductDto);
        CartDto Remove(Guid customerId, CartProductDto cartProductDto);
        CartDto Get(Guid customerId);
        CheckOutResultDto CheckOut(Guid customerId);
    }

   //Dto's are Data Transfer Objects, they are very important as they allow you to input and get the output from Application Services without exposing the actual Domain.
    public class CartDto
    {
        public Guid CustomerId { get; set; }
        public List<CartProductDto> Products { get; set; }
        public DateTime Created { get; set; }
        public DateTime Modified { get; set; }
    }

    public class CartProductDto
    {
        public Guid ProductId { get; set; }
        public int Quantity { get; set; }
    }

    public class CheckOutResultDto
    {
        public Nullable<Guid> PurchaseId { get; set; }
        public Nullable<CheckOutIssue> CheckOutIssue { get; set; }
    }



Application Service Implementation:
 

    public class CartService : ICartService
    {
        readonly IRepository<Customer> repositoryCustomer;
        readonly IRepository<Product> repositoryProduct;
        readonly IUnitOfWork unitOfWork;
        readonly ITaxDomainService taxDomainService;

        public CartService(IRepository<Customer> repositoryCustomer, 
            IRepository<Product> repositoryProduct, IUnitOfWork unitOfWork, ITaxDomainService taxDomainService)
        {
            this.repositoryCustomer = repositoryCustomer;
            this.repositoryProduct = repositoryProduct;
            this.unitOfWork = unitOfWork;
            this.taxDomainService = taxDomainService;
        }

        public CartDto Add(Guid customerId, CartProductDto productDto)
        {
            CartDto cartDto = null;
            Customer customer = this.repositoryCustomer.FindById(customerId);
            Product product = this.repositoryProduct.FindById(productDto.ProductId);

            this.validateCustomer(customerId, customer);
            this.validateProduct(product.Id, product);

            decimal tax = this.taxDomainService.Calculate(customer, product);

            customer.Cart.Add(CartProduct.Create(customer.Cart, product, productDto.Quantity, tax));

            cartDto = Mapper.Map<Cart, CartDto>(customer.Cart);
            this.unitOfWork.Commit();
            return cartDto;
        }

        public CartDto Remove(Guid customerId, CartProductDto productDto)
        {
            CartDto cartDto = null;
            Customer customer = this.repositoryCustomer.FindById(customerId);
            Product product = this.repositoryProduct.FindById(productDto.ProductId);

            this.validateCustomer(customerId, customer);
            this.validateProduct(productDto.ProductId, product);

            customer.Cart.Remove(product);
            cartDto = Mapper.Map<Cart, CartDto>(customer.Cart);
            this.unitOfWork.Commit();
            return cartDto;
        }

        public CartDto Get(Guid customerId)
        {
            Customer customer = this.repositoryCustomer.FindById(customerId);
            this.validateCustomer(customerId, customer);
            return Mapper.Map<Cart, CartDto>(customer.Cart);

        }

        public CheckOutResultDto CheckOut(Guid customerId)
        {
            CheckOutResultDto checkOutResultDto = null;
            Customer customer = this.repositoryCustomer.FindById(customerId);
            this.validateCustomer(customerId, customer);

            Nullable<CheckOutIssue> checkOutIssue = customer.Cart.IsCheckOutReady();

            if (!checkOutIssue.HasValue)
            {
                Purchase purchase = customer.Cart.Checkout();
                checkOutResultDto = Mapper.Map<Purchase, CheckOutResultDto>(purchase);
                this.unitOfWork.Commit();
            }

            return checkOutResultDto;
        }

        //this is just an example, don't hard code strings like this, use reference data or error codes
private void validateCustomer(Guid customerId, Customer customer)
        {
            if (customer == null)
                throw new Exception(String.Format("Customer was not found with this Id: {0}", customerId));
        }

        private void validateProduct(Guid productId, Product product)
        {
            if (product == null)
                throw new Exception(String.Format("Product was not found with this Id: {0}", productId));
        }
    }

Example usage:
            
            this.cartService.Add(
                this.customer.Id, 
                new CartProductDto()
                {
                    ProductId = viewModel.ProductId,
                    Quantity = 2
                }
            );

            CheckOutResultDto checkoutResult = this.cartService.CheckOut(this.customer.id);



Summary:
  • Application Service is a gateway in to your Domain Model Layer via Dto's (Data Transfer Objects)
  • Application Service should not encapsulate any domain logic, it should be really thin 
  • Application Service method should do only one thing and do it well with one region of the domain, don't mix it to "make it more performance efficient for the Application that's consuming it".
  • To access Application Service you expose interface and Dto's for inputs and outputs (it's important not to expose your Domain Entity in a raw format, Dto is a proxy and it protects your domain)
  • Presenter (mobile app, desktop or web), should call different services to get data it needs and manipulate it to suit the UI. This might seem inefficient, or wasteful at first. You will realise that actually it's just as fast (if not faster), easier to test and maintain. 

Tips:
  • Use AutoMapper to map your Domain Entity to Dto's, don't waste your time with manual mapping. It clutters your implementation code and maintenance becomes a nightmare. 
  • Don't think of screens when it comes to exposing Application Services, it's an API, think how a mobile app would access it, or how external potential customers would use it.
  • Realise that you will end up writing Application Services that suit your UI. This is only natural as this is what you been doing for a while. It will take a few goes before you change your thinking.
  • Application Service can be consumed directly if you don't need distribution i.e. your MVC app will just reference Application Service directly, you can then just try and catch errors in your Controller.
  • Application Service can be exposed via Web Service (Distributed Interface Layer). This further abstraction give you ability to "try and catch" errors so they can be exposed in a friendlier manner. Additionally it allows you to future proof your application e.g. versioning.

Useful links:
  • SOA.com Service-oriented architecture explained in 7 steps

*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment. 

4 comments:

  1. I saw several validation of product and customer in your application service. But, would you put product availability (quantity) validation in here as well? Or would you have the purchase entity to do the validation?

    Second, how do you update product quantity availabilities? Is it good idea to create event handler of PurchaseCreated and reduce product quantity there?

    ReplyDelete
    Replies
    1. Hi, thanks for the comment!

      1. Application Service & Entity Validation
      Application service should be super thin and it should not contain any logic. So in this particular case I would probably put quantity check inside the cart class (see https://github.com/zkavtaskin/Domain-Driven-Design-Example/blob/master/eCommerce/DomainModelLayer/Carts/Entity/Cart.cs , methods such as a Cart.Checkout could be a great place for it.

      2. Outside of aggregate root data updates
      Your cart should not have access to all of the products in the system, so you can’t really do anything there. So yes I would recommend that you raise a domain event and have a handler update the quantity as soon as the purchase was made.

      I hope this helps and sorry for the late reply. Let me know if this has answered your question!

      Delete
  2. Will Presentation layer be responsible for initializing repositories before they are passed to CartService? I thought Presentation layer should be unaware of persistence/infrastructure, no? What is the best practice?

    ReplyDelete
    Replies

    1. Hi Sergei,

      Thank you for reading.

      Dependency Injection will initialize your repositories. Please take a look at this:

      Presentation
      https://github.com/zkavtaskin/Domain-Driven-Design-Example/blob/master/eCommerce.WebService/Controllers/ProductController.cs

      Service
      https://github.com/zkavtaskin/Domain-Driven-Design-Example/blob/master/eCommerce/ApplicationLayer/Products/ProductService.cs

      Dependency Injection Installation
      https://github.com/zkavtaskin/Domain-Driven-Design-Example/blob/master/eCommerce.WebService/App_Start/BootstrapConfig.cs

      Delete