Ayende has a couple of recent posts which relate to entities, services and repositories. In some of the comments, it was briefly discussed about if/how you should validate dynamic business rules that require a call to a repository. Ayende mentioned this in one of his comments:
About business logic that needs talking to the database.
I would strive very hard to avoid doing it. Entities validate business logic by traversing the object graph they have, not by calling to the database.
Calling to the database makes it a lot harder to just start a new entity and run business logic.
I would agree it's not ideal and it does certainly make it harder to deal with the situation where you've changed the domain object but it's invalid, so you don't want to persist it. It just so happens I'm having to tackle this problem right now in one of my projects. Here's one way I've approached this so far, though I'm sure there are better ways. So please let me know what you think...
One Approach
First off, one of my current approaches to business rules in general is similar to what Jean-Paul Boodhoo demonstrates in his posts on Validation In The Domain Layer. I just really like how clean this kind of approach plays out. Anyways, here goes...
So a simple rule might be something like a product cannot be saved if it's name is a duplicate of another product (this is an actual rule, though not for products, that a handful of my domain objects have in my current project). So here's what a service might look like:
public class ProductService : IProductService
{
private readonly IProductRepository repository;
public ProductService(IProductRepository repository) // injected
{
this.repository = repository;
}
public IBusinessRuleSet SaveProduct(ProductDTO productDTO)
{
try
{
repository.BeginTransaction();
Product product = repository.FindById(productDTO.Id) ?? new Product();
product.Name = productDTO.Name;
IBusinessRuleSet brokenRules = GetBrokenRulesFor(product);
if (!brokenRules.IsEmpty)
{
repository.RollBackTransaction();
return brokenRules;
}
repository.Save(product);
repository.CommitTransaction();
return brokenRules;
}
catch (Exception)
{
repository.RollBackTransaction();
throw;
}
}
private IBusinessRuleSet GetBrokenRulesFor(Product productToSave)
{
productToSave.AddBusinessRule(InitializeDuplicateRuleUsing(productToSave.Name));
return productToSave.Validate();
}
private IBusinessRule<Product> InitializeDuplicateRuleUsing(string productName)
{
IBusinessRule<Product> duplicateRule =
Product.Rules.DuplicateProduct(delegate(Product productToValidate)
{
return
new DuplicateProductSpecification(
repository.FindByName(productName)).IsSatisfiedBy(
productToValidate);
});
return duplicateRule;
}
}
And here's an example of the Product domain object:
public class Product : DomainObject // base domain object has Validate and other validation properties and methods
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Product() : this(null)
{
}
public Product(string name)
{
this.name = name;
}
public static class Rules
{
public static IBusinessRule<Product> DuplicateProduct(
Predicate<Product> rulePredicate)
{
return
new BusinessRule<Product>("DuplicateProduct",
"A product with this name already exists.",
rulePredicate);
}
}
}
I like this because the rule itself including its description is still declared inside the domain object, but the logic for validating it is passed in via a Predicate. This keeps the domain object from directly referencing the repository, though I have thought about trying that approach as well.
Challenges with NHibernate
I did run into a couple challenges with NHibernate which I resolved by specifically managing the transaction and setting FlushMode to Commit instead of Auto. This is needed because, as Jeffrey Palermo points out, queries using ICriteria and IQuery may flush changes to the database when set to Auto which is not good news when the result of that query may determine whether or not you want to persist the object in the first place.
Conclusion
I'm still looking for a more elegant solution, but this is getting me by for now. One minor change might be to extract out the initialization of the duplicate rule into a class implementing the IBusinessRule<Product> interface to get rid of the private methods in the ProductService class. I'm sure I'll make other changes as needed and as I learn better ways to structure entities, services and repositories.
Looking forward to "getting my learn on" in Jean-Paul Boodhoo's upcoming class down here in Richmond, VA. :)