Skip to content

What is Domain Anemic Model?

Posted on:July 5, 2022 at 02:05 AM

Anemic Domain Model and the Rich Model Design Approach

Anemic Domain Model

An anemic domain model is an entity that only has { get; set; }, lacking methods and containing only a representation of a database table. The properties are often public, allowing external components to manipulate the data.

Example:

Domain Layer:

namespace AnemicModel.Domain.Orders
{
    public sealed class Order
    {
        public long Id { get; set; }
        public Guid CustomerId { get; set; }
        public OrderStatus Status { get; set; }
        public decimal TotalPrice { get; set; }
    }

    public class OrderItem
    {
        public Product Product { get; set; }
        public Order Order { get; set; }
    }
}

Application Layer:

namespace RichModel.Application.Orders.Commands.CreateOrder
{
    internal sealed class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
    {
        private readonly IOrdersRepository _ordersRepository;
        private readonly IProductsRepository _productsRepository;

        public CreateOrderCommandHandler(
          IOrdersRepository ordersRepository,
          IProductsRepository productsRepository)
        {
            _ordersRepository = ordersRepository;
            _productsRepository = productsRepository;
        }

        public async Task<Unit> Handle(CreateOrderComman createOrderRequest, CancellationToken cancellationToken)
        {
            var productsToOrder = await _productsRepository.GetAsync(createOrderRequest.ProductsIds, cancellationToken);
            var order = Order.Create(productsToOrder);
            await _ordersRepository.CreateAsync(order, cancellationToken);

            return Unit.Value;
        }
    }
}

Cure for Anemic Entities – Rich Model Design Approach

The Rich Model approach advocates moving business logic from the application layer to the domain entity, creating a rich model with both data and behaviors.

Example: Domain Layer:

namespace RichModel.Domain.Orders
{
    public sealed class Order
    {
        private const decimal DiscountPercent = 0.25m;
        private const decimal PriceBoundaryToCalculateDiscount = 1000;
        public Guid Id { get; }
        private OrderStatus Status {  get;  }
        public decimal TotalPrice => CalculateTotalPrice();
        private ICollection<OrderItem> Items { get; set; }
        private Order()
        {
        }
        private Order(List<Product> products)
        {
            Id = Guid.NewGuid();
            Status = OrderStatus.Payed;
            var readonlyProducts = products.AsReadOnly();
            SetItems(readonlyProducts);
        }
        public static Order Create(List<Product> products)
        {
            return new Order(products);
        }
        private void SetItems(IReadOnlyCollection<Product> products)
        {
            if (AreProductsEmpty())
                throw new OrderCannotBeEmptyException();
            Items = products.Select(product => OrderItem.Create(product, this)).ToList();
            bool AreProductsEmpty()
            {
                return products is null || !products.Any();
            }
        }
        private decimal CalculateTotalPrice()
        {
            var amount = Items.Select(orderItem => orderItem.Product.Price).Sum();
            if (amount <= PriceBoundaryToCalculateDiscount) return amount;
            return amount * (1 - DiscountPercent);
        }
    }
}

Application Layer:

namespace RichModel.Application.Orders.Commands.CreateOrder
{
    internal sealed class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
    {
        private readonly IOrdersRepository _ordersRepository;
        private readonly IProductsRepository _productsRepository;
        public CreateOrderCommandHandler(IOrdersRepository ordersRepository,
            IProductsRepository productsRepository)
        {
            _ordersRepository = ordersRepository;
            _productsRepository = productsRepository;
        }
        public async Task<Unit> Handle(CreateOrderCommand createOrderRequest, CancellationToken cancellationToken)
        {
            var productsToOrder = await _productsRepository.GetAsync(createOrderRequest.ProductsIds, cancellationToken);
            var order = Order.Create(productsToOrder);
            await _ordersRepository.CreateAsync(order, cancellationToken);
            return Unit.Value;
        }
    }
}

Now, the domain contains rich business logic, and the application layer is simplified.

Testing the Rich Model Tests are more readable and require fewer mocks.

Example Tests:

namespace RichModel.Domain.UnitTests.Orders
{
    public class OrderTotalPriceTests
    {
        private static object[] _orderDiscountNotCountedCases =
        {
            new object[] { 999.0m, 1.0m, 1000m },
            new object[] { 555.0m, 445.0m, 1000m },
            new object[] { 333.0m, 333.3m, 666.3m }
        };
        [TestCaseSourceAttribute(nameof(_orderDiscountNotCountedCases))]
        public void Given_GetTotalPrice_When_ProductsPriceAmountLessenThanPriceBoundaryToCalculateDiscount_Then_DiscountIsNotCounted
            (decimal firstProductPrice, decimal secondProductPrice, decimal expectedOrderTotalPrice)
        {
            var products = new List<Product>
            {
                Product.Create(firstProductPrice),
                Product.Create(secondProductPrice),
            };
            var order = Order.Create(products);
            var orderTotalPrice = order.TotalPrice;
            orderTotalPrice.ShouldBe(expectedOrderTotalPrice);
        }
        private static object[] _orderDiscountCountedCases =
        {
            new object[] { 2000.0m, 1000.0m, 2250m },
            new object[] { 2500.0m, 1500.0m, 3000m },
            new object[] { 999.0m, 2.0m, 750.75m },
        };
        [TestCaseSourceAttribute(nameof(_orderDiscountCountedCases))]
        public void Given_GetTotalPrice_When_ProductsPriceAmountGreaterThanPriceBoundaryToCalculateDiscount_Then_DiscountIsCounted
            (decimal firstProductPrice, decimal secondProductPrice, decimal expectedOrderTotalPrice)
        {
            var products = new List<Product>
            {
                Product.Create(firstProductPrice),
                Product.Create(secondProductPrice),
            };
            var order = Order.Create(products);
            var orderTotalPrice = order.TotalPrice;
            orderTotalPrice.ShouldBe(expectedOrderTotalPrice);
        }
    }
}

Summary

The Anemic Domain Model approach puts business logic in the wrong place, leading to poor encapsulation and maintainability issues. The Rich Model approach advocates moving logic to entities, improving encapsulation and maintaining an open/closed system.

Questions

How to Save Rich Entity in Database? Answer: “Shadow Properties.” How to Display Data on UI from an Entity with Private Properties? Answer: CQRS (Command Query Responsibility Segregation). More detailed answers will be covered in future posts.