Reputation: 8181
I want to use Specification
s to apply business rules in my N-Layerd DDD
application.
I used CQRS
pattern in my Application Layer
also.
So I defined following Interface
in my Domain
:
public interface ISpecification<T>
{
Expression<Func<T, bool>> Predicate { get; }
bool IsSatisfiedBy(T entity);
}
and also some Specification
s that implement above interface such as BigOrderSpecification : ISpecification<Order>
and SpecialOrderSpecification:ISpecification<Order>
.
In my OrderProcessCommandHandler
class I used these Specification
s:
public class OrderProcessCommandHandler : ICommandHandler<Order>
{
OrderCommand _command;
public OrderProcessCommandHandler(OrderCommand command)
{
_command = command;
}
public Handle()
{
var bigOrderSpec = new BigOrderSpecification();
var specialOrderSpec = new SpecialOrderSpecification();
var spec = bigOrderSpec.And(specialOrderSpec);
if (spec.IsSatisfiedBy(_commnand.Order))
// do some things
else
throw new BusinessException("Some business rules violated.")
}
}
As you see, if during order processing, one or more specification don't satisfy, I could not throw a BusinessException
with clear message to top layer, only
Some business rules violated.
How could I create clear massage that contain all BR
violation reasons and pass it throw my BusinessException
to top layers?
Upvotes: 2
Views: 2851
Reputation: 12253
You could consider validating separately before calling the command handler. Make command fire and forget. Validate first and then send to business layer assuming that it will work. Have a command validation handler that returns for example IEnumerable<IErrorMessage>
On failure make the domain just throw and not handle it in UI layer so no need to pass it all the way back. Handle in some other way e.g. send email with exception or raise an event in that is picked up async at a later point and displayed in UI.
In domain how likely is it that it validates correctly and then something occurs between validating and executing? It comes down to business value in informing users of certain sort of errors.
This Udi Dahan video covers the concept https://skillsmatter.com/skillscasts/1250-udi-dahan-command-query-responsibility-segregation
This blog post also covers it http://www.udidahan.com/2009/12/09/clarified-cqrs/ see Reasons valid commands fail and what to do about it
Upvotes: 0
Reputation: 153
Using a event, that is raised from the specification and handled in the command handler.
public class BusinessRuleFailure : EventArgs
{
public BusinessRuleFailure(string reason)
{
Reason = reason;
}
public string Reason { get; private set; }
}
public delegate void BusinessRuleFailureHandler(BusinessRuleFailure failure);
public interface ISpecification<T>
{
event BusinessRuleFailureHandler NotSatisified;
Expression<Func<T, bool>> Predicate { get; }
bool IsSatisfiedBy(T entity);
}
public class OrderProcessCommandHandler : ICommandHandler<Order>
{
OrderCommand _command;
public OrderProcessCommandHandler(OrderCommand command)
{
_command = command;
}
public Handle()
{
List<string> failures = new List<string>();
var bigOrderSpec = new BigOrderSpecification();
var specialOrderSpec = new SpecialOrderSpecification();
bigOrderSpec.NotSatisified += failure => failures.Add(failure.Reason);
specialOrderSpec.NotSatisified += failure => failures.Add(failure.Reason);
var spec = bigOrderSpec.And(specialOrderSpec);
if (spec.IsSatisfiedBy(_commnand.Order))
// do some things
else
throw new BusinessException("Some business rules violated.", failures);
}
}
Upvotes: 3
Reputation: 153
Add a event to ISpecification interface, which returns a string (or an object which contains the string). For each business rule, if it is not satisfied then fire the event containing the reason why. In your command handler, listen to the event of each specification (in a loop) and collect the strings in to a collection and then if no rules have been satisfied, throw your exception using your collection of errors.
Upvotes: 0
Reputation: 7283
Inspired by a comment some days ago. Change
bool IsSatisfiedBy(T entity);
to
Result IsSatisfiedBy(T entity);
public class Result
{
public boolean IsSatisfied{}
public List<String> message() {}
}
But you have to implement && , ! and || :
&& Result r1 = spec1.satisfied(o);
if (r1.isSatisfied()) {
Result r2 = spec2.satisfied(o);
if (r2.isSatisfied()) {
return new Result();
} else {
return r2;
}
} else {
return r1;
}
|| Result r1 = spec1.satisfied(o);
if (r1.isSatisfied()) {
return new Res();
} else {
Result r2 = spec2.satisfied(o);
if (r2.isSatisfied()) {
return new Result();
} else {
return r2.append(r1.message());
}
}
Upvotes: 1