charlessolar / Aggregates.NET

.NET event sourced domain driven design model via NServiceBus and GetEventStore

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Business Rules

charlessolar opened this issue · comments

Running rules in entities currently involve a lot of If's thens, and throws

We can make this easier.

There's a few changes I can make right off the bat - adding a Rule method on entities to validate against the state of the entity automatically.

   public void PostInvoice() {

       // Instead of....
       if( State.Address == null )
           throw new BusinessException("No address set");
     
       // Do this!
       Rule("Address Set", x => x.Address == null, "No address set");

       Apply<Events.Posted>();            
   }

In the long term however we want to support dynamic rules and validation against data that may not be inside the entity. If for instance we want to make sure the customer is in good standing before posting invoice - we'd have to load the customer entity to check. And a business manager might want to override a rule or create his own rules without involving a programmer to do so.

The above works for static validation - so lets also add dynamic!

I want each public method on a entity to be available for rule creation dynamically via the app.

I envision some kind of extendable entity validator. Something like

    RuleFor<Invoice>()
        .When(x => x.PostInvoice)
                .Rule("Address Set", x => x.Address == null, "No address set");
    

we already know how to serialize expressions so aggregates.net would just need to keep track of rules for entities and apply them while executing commands and events.

public Task Handle(Commands.AddRuleToInvoices command, IMessageHandlerContext ctx) {
    Aggregates.For<Invoice>()
              .When(command.Action)
              .Rule(command.Name, command.Expression, command.Message);
}

Dynamically loading other entities for checking

Aggregates.For<Invoice>()
    .When(x => x.PostInvoice)
    .Load<Customer>(x => x.CustomerId)
    .Load<PaymentMethod, Customer>(customer => customer.DefaultPaymentMethodId)
    .Rule<PaymentMethod>("Payment Is CreditCard", method => method.Type == Methods.CreditCard);

Todos

  • Static RuleFor on entities
  • Expressive rules on entity types
  • Dynamic rules via serialized expressions
  • Test generation for seeing new rules in action (?)

Defined rules would be internal aggregate.net data - which would mean an internal aggregates.net stream. Perhaps the projection we're planning in #33 can be useful

Should accomplish this via a pipeline behavior

Rules can be defined on commands, the behavior would load and evaluate any dynamic rules on a command

A user would add/edit/remove rules via other commands

public Task Handle(Commands.AddRuleToInvoices command, IMessageHandlerContext ctx) {
    Aggregates.Command<PostInvoice>()
        .Load<Invoice>(x => x.InvoiceId) // on PostInvoice command
        .Load<Invoice, Customer>(x => x.CustomerId) // on Invoice state object
        .Load<Customer, PaymentMethod>(x => x.DefaultPaymentMethod) // on Customer state object
        .Rule<PaymentMethod>("Payment is CreditCard, x => x.Type == Methods.CreditCard);

}