This post is more a self-remainder but it can be useful for you if your are using NHV to validate even Business-Rules.
NHV was designed more thinking about properties-constraints than Business-Rules, by the way it was evolving and it can be used even for some business-rules. In the last 6 or 5 months I have received questions about how solve some specific situations and, each time, I’m giving a different solution using a different NHV’s feature. The last time I even proposed the implementation of a new and unnecessary feature (take a look at this thread)… at least as self-reminder a post is needed.
The case
public class Customer
{
public string CompanyName { get; set; }
public string VatIdentificationNumber { get; set; }
}
public class Invoice
{
public Customer Customer { get; set; }
public decimal Amount { get; set; }
}
Rules are:
- The customer must have a not empty CompanyName.
- The customer may have VatIdentificationNumber and if he has one it must be a valid Italian’s Patita-IVA.
- The invoice must have a valid Customer.
- When a Customer is used to emit an Invoice, the Customer must have a Italian’s Patita-IVA.
As you can see the Customer class has different constraints depending on the aggregate from where it is used/associated.
The test
public class BusinessRuleDemo
{
ValidatorEngine validatorEngine;
[TestFixtureSetUp]
public void CreateValidatorEngine()
{
var configure = new FluentConfiguration();
configure.Register(new[] { typeof(CustomerValidation), typeof(InvoiceValidation) })
.SetDefaultValidatorMode(ValidatorMode.UseExternal);
validatorEngine = new ValidatorEngine();
validatorEngine.Configure(configure);
}
[Test]
public void CustomerValidWhenHaveCompanyName()
{
(new Customer {CompanyName = "ACME"}).Satisfy(c => validatorEngine.IsValid(c));
}
[Test]
public void CustomerNotValidWhenHaveInvalidVatIdentificationNumber()
{
(new Customer { CompanyName = "ACME", VatIdentificationNumber = "123"}).Satisfy(c => !validatorEngine.IsValid(c));
}
[Test]
public void CustomerValidWhenHaveValidVatIdentificationNumber()
{
(new Customer {CompanyName = "ACME", VatIdentificationNumber = "01636120634"})
.Satisfy(c => validatorEngine.IsValid(c));
}
[Test]
public void InvoiceNotValidWhenDoesNotHaveCustomer()
{
(new Invoice()).Satisfy(invoice=> !validatorEngine.IsValid(invoice));
}
[Test]
public void InvoiceNotValidWhenHaveCustomerWithoutVatIdentificationNumber()
{
(new Invoice { Customer = new Customer { CompanyName = "ACME" } })
.Satisfy(invoice => !validatorEngine.IsValid(invoice));
}
[Test]
public void InvoiceNotValidWhenHaveCustomerWithInvalidVatIdentificationNumber()
{
(new Invoice { Customer = new Customer { CompanyName = "ACME", VatIdentificationNumber = "123" } })
.Satisfy(invoice => !validatorEngine.IsValid(invoice));
}
[Test]
public void InvoiceValidWhenHaveCustomerWithValidVatIdentificationNumber()
{
(new Invoice { Customer = new Customer { CompanyName = "ACME", VatIdentificationNumber = "01636120634" } })
.Satisfy(invoice => validatorEngine.IsValid(invoice));
}
}
The solution
I’m pushing you to use the new loquacious mapping of NHibernate.Validator because the class where you are defining all constraints become the class with responsibility of validation of an entity so, in this example, I will use our dear Loquacious.
public class CustomerValidation : ValidationDef<Customer>
{
public CustomerValidation()
{
// The customer must have a not empty CompanyName
Define(e => e.CompanyName).NotNullableAndNotEmpty();
// The customer may have VatIdentificationNumber and if he has one it must be a valid Italian’s Patita-IVA.
Define(e => e.VatIdentificationNumber).IsPartitaIva();
}
}
public class InvoiceValidation : ValidationDef<Invoice>
{
public InvoiceValidation()
{
ValidateInstance.By((invoice, validationContext) =>
{
bool isValid = true;
// When a Customer is used to emit an Invoice, the Customer must have a Italian’s Patita-IVA
if (invoice.Customer != null && string.IsNullOrEmpty(invoice.Customer.VatIdentificationNumber))
{
isValid = false;
validationContext.AddInvalid<Customer, string>("To be used in an invoice the Customer should have the VatNumber",
p => p.VatIdentificationNumber);
}
return isValid;
});
// The invoice must have a valid Customer
Define(e => e.Customer).IsValid().And.NotNullable();
}
}
As you can see I’m using the ValidateInstance but this time with a “special” overload where I can change the validation-context. A validation-context is created each time we are validating an object-graph (each time we are calling a validation-method of the ValidatorEngine) and we can use it to add invalid-values to the validation; in this case I’m using it to validate the existence of the VatIdentificationNumber.
That’s all.
This comment has been removed by a blog administrator.
ReplyDeleteFrom Raffaeu [http://blog.raffaeu.com]
ReplyDeleteI really like your solution as I need, like I do with the Fluent Mapping, a separate assembly for the validation that I can plug and play in my business layer without affecting the POCO domain.
Thanks
how to explicitly change the validationContext?
ReplyDelete