In short, specification pattern allows you to chain business queries.
Example:
ISpecification<Customer> spec = new CustomerRegisteredInTheLastDays(30).And(new CustomerPurchasedNumOfProducts(2));
Entity from previous posts in this series:
public class Customer : IDomainEntity { private List<Purchase> purchases = new List<Purchase>(); public Guid Id { get; protected set; } public string FirstName { get; protected set; } public string LastName { get; protected set; } public string Email { get; protected set; } public string Password { get; protected set; } public DateTime Created { get; protected set; } public bool Active { get; protected set; } public ReadOnlyCollection<Purchase> Purchases { get { return this.purchases.AsReadOnly(); } } public static Customer Create(string firstname, string lastname, string email) { Customer customer = new Customer() { FirstName = firstname, LastName = lastname, Email = email, Active = true, Created = DateTime.Today }; DomainEvents.Raise<CustomerCreated>(new CustomerCreated() { Customer = customer }); return customer; } public Purchase Checkout(Cart cart) { Purchase purchase = Purchase.Create(this, cart.Products); this.purchases.Add(purchase); DomainEvents.Raise<CustomerCheckedOut>(new CustomerCheckedOut() { Purchase = purchase }); return purchase; } }Specification Examples:
public class CustomerRegisteredInTheLastDays : SpecificationBase<Customer> { readonly int nDays; public CustomerRegisteredInTheLastDays(int nDays) { this.nDays = nDays; } public override Expression<Func<Customer,bool>> SpecExpression { get { return customer => customer.Created >= DateTime.Today.AddDays(-nDays) && customer.Active; } } } public class CustomerPurchasedNumOfProducts : SpecificationBase<Customer> { readonly int nPurchases; public CustomerPurchasedNumOfProducts(int nPurchases) { this.nPurchases = nPurchases; } public override Expression<Func<Customer,bool>> SpecExpression { get { return customer => customer.Purchases.Count == this.nPurchases && customer.Active; } } }
Abstract Repository Query Example:
IRepository<Customer> customerRepository = new Repository<Customer>(); ISpecification<Customer> spec = new CustomerRegisteredInTheLastDays(30).And(new CustomerPurchasedNumOfProducts(2)); IEnumerable<Customer> customers = customerRepository.Find(spec);
Abstract Repository Example:
public interface IRepository<TEntity> where TEntity : IDomainEntity { TEntity FindById(Guid id); TEntity FindOne(ISpecification<TEntity> spec); IEnumerable<TEntity> Find(ISpecification<TEntity> spec); void Add(TEntity entity); void Remove(TEntity entity); }
Summary:
- Specification allows you to query data in a abstract way i.e. you can query memory collections or an RDBMS. This ensures persistence/infrastructure ignorance.
- Specification encapsulates a business rule in one spec.
- Specification pattern allows you to chain your business rules up.
- Specification makes your domain layer DRY i.e. you don't need to write same LINQ all over again.
- Specifications are easy to unit test.
- Specifications are stored in the domain layer, this provides full visibility.
- Specifications are super elegant.
- Break complex business logic rules down in your specification as NHibernate might struggle to interpret them in to a SQL query. This is a generally good tip as you don't want messy SQL hitting your database.
- Query data around the entity properties, don't try and change the properties on the entity i.e. instead of writing customer.Created.AddDays(30) >= DateTime.Today write customer.Created >= DateTime.Today.AddDays(-30). The former will try and compile it as a SQL and will fail as it's too complex, the latter will convert the value to a parameter.
- As specifications are logical queries they should not change state of the caller or the calling objects. i.e. don't call state changing methods, such as customer.Checkout(....) && customer.Active == true. This tip goes hand in hand with the tip above.
Useful links:
- Specifications, Expression Trees, and NHibernate a fantastic article with great examples on how to use spefifications with NHibernate.
- Specification Pattern, basic explanation of boolean specification pattern.
*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.
Why not use domain types for some of the fields in Customer?
ReplyDelete