In the sample above I've been using LINQ to SQL to define my
Product entity and perform my data access. So far, the only level of
domain rules/validation that I am using on my Product entity are those inferred
by LINQ to SQL from the SQL Server metadata (nulls, data type and length,
etc). This will catch scenarios like above (where we are trying to assign
bogus input to a Decimal). However, they won't be able to model business
issues that can't be easily declared using SQL metadata. For example:
disallowing the reorder level of a product to be greater than zero if it has
been discontinued, or disallowing a product to be sold for less than what our
supplier price is, etc. For scenarios like these we need to add code to
our model to express and integrate these business rules.
The wrong place to add this business rule logic is in the UI
layer of our application. Adding them there is bad for many
reasons. Among others it will almost certainly lead to duplicated code -
since you'll end up copying the rules around from UI to UI and from form to
form. In addition to being time-consuming, there is an excellent chance
doing so will lead to bugs when you change your business rule logic, and you
forget to update it everywhere.
A much better place to incorporate these business rules is
at your model or domain level. That way they can be used and applied
regardless of what type of UI or form or service works with it. Changes
to the rules can be made once, and picked up everywhere without having to
duplicate any logic.
There are several patterns and approaches we could take to
integrate richer business rules to the Product model object we've been using
above: we could define the rules within the object, or external from the
object. We could use declarative rules, a re-usable rules engine
framework, or imperative code. The key point is that ASP.NET MVC allows
us to use any or all of these approaches (there aren't a bunch of features that
require you to always do it one way - you instead have the flexibility to
reflect them however you want, and the MVC features are extensible enough to
integrate with almost anything).
For this blog post I'm going to use a relatively simple
rules approach. First I'm going to define a "RuleViolation"
class like below that we can use to capture information about a business rule
that is being violated within our model. This class will expose an
ErrorMessage string with details about the error, as well as expose the primary
property name and property value associated with it that is causing the
violation:
(note: For simplicity sake I'm only going to store only one
property - in more complex applications this might instead be a list so that
multiple properties could be specified).
I will then define an IRuleEntity interface that has a
single method - GetRuleViolations() - which returns back a list of all current
business rule violations with that entity:
I can then have my Product class implement this
interface. To keep the sample simple I'm embedding the rule definition
and evaluation logic inside the method. There are better patterns that
you can use to enable reusable rules, as well as to handle more complex rules.
If this sample grew I'd refactor the method so that the rules and their
evaluation where defined elsewhere, but for now to keep this simple we'll just
evaluate three business rules below like so:
Our application can now query the Product (or any other
IRuleEntity) instance to check its current validation status, as well as
retrieve back RuleViolation objects that can be used to help present UI that
can guide an end-user of the application to help fix them. It also allows
us to easily unit test our business rules independent of the application UI.
For this particular sample I am going to choose to enforce
that our Product object is never saved in the database in an invalid state
(meaning all RuleViolations must be fixed before the Product object can be
saved in the database). We can do this with LINQ to SQL by adding an
OnValidate partial method to the Product partial class. This method will
get called automatically by LINQ to SQL any time database persistence
occurs. Below I'm calling the GetRuleViolations() method we added above,
and am raising an exception if there are unresolved errors. This will
abort the transaction and prevent the database from being updated:
And now in addition to having a friendly helper method that
allows us to retrieve RuleViolations from a Product, we have enforcement that
those RuleViolations must be fixed before our database is ever updated.