Click here to Skip to main content
15,868,440 members
Articles / Web Development / ASP.NET
Article

Delegates and Business Objects

Rate me:
Please Sign up or sign in to vote.
4.90/5 (100 votes)
21 May 20069 min read 279.2K   2.5K   268   58
An approach to implementing validation on custom business rules, using delegates.

Sample Image - DelegateBusinessObjects3.jpg

Introduction

The aim of this article is to give you an understanding of how I perform business object validation in Trial Balance, a personal accounting project of mine. This article was originally a blog post that featured on my website, which you can see here. My approach borrows largely from the technique published in Rocky Lhotka's Expert Business Objects book (don't worry if you haven't read it), but I've added a Stovellian twist to spice things up. I'm also going to detour into a discussion of IDataErrorInfo, a really neat interface provided as part of the .NET framework to make our lives easier.

A typical implementation

Let's take a very simple example of what I mean when I say business rule:

Account names can't be blank, and can't be longer than 20 characters.

It's not uncommon to see a rule like that implemented like this:

C#
public string Name 
{
    get { return _name; }
    set 
    {
        if (value == null
            || value.Trim().Length == 0
            || value.Trim().Length > 20) 
        {
            throw new ArgumentException("Account names can't be" + 
                  " blank, and can't be longer than 20 characters.");
        }
        _name = value; 
    }
}

Whilst this will stop anyone assigning the name of your account to "Supercallafragelisticexpialadocious", I feel it has a few drawbacks:

  • There may be times where you actually need to have an empty name. For example, as the default value for a "Create an account" form.
  • If you're relying on this to validate any data before saving, you'll miss the cases where the data is already invalid. By that, I mean, if you load an account from the database with an empty name and don't change it, you might not ever know it was invalid.
  • If you aren't using data binding, you have to write a lot of code with try/catch blocks to show these errors to the user. Trying to show errors on the form as the user is filling it out becomes very difficult.
  • I don't like throwing exceptions for non-exceptional things. A user setting the name of an account to "Supercalafragilisticexpialadocious" isn't an exception, it's an error. This is, of course, a personal thing.
  • It makes it very difficult to get a list of all the rules that have been broken. For example, on some websites, you'll see validation messages such as "Name must be entered. Address must be entered. Email must be entered". To display that, you're going to need a lot of try/catch blocks.

Rules in CSLA

Rocky's book approaches the idea of validation in a different manner. In Rocky's CSLA framework, a business object won't usually throw exceptions if you set a property to an invalid value, but instead the object will mark itself as invalid. If you try to attempt to save the business object, then an exception might be thrown.

There are two fundamental principles underlying this:

  1. There is nothing wrong with having an invalid business object, so long as you don't try to persist it.
  2. Any and all broken rules should be retrievable from the business object, so that data binding, as well as your own code, can see if there are errors and handle them appropriately.

When I was implementing rules for Trial Balance, I loved Rocky's idea, but there were a couple of things that concerned me:

  • Again, rules are only marked as being broken in the property setter. This is a problem if you're loading the data via another method.
  • Imagine you have a case where you have a StartDate and an EndDate. Your EndDate business rule would probably say that the EndDate must be greater than your StartDate, right?

    What if the following happens:

    C#
    StartDate = DateTime.MaxValue;
    EndDate = DateTime.Now; // EndDate is marked as invalid
    StartDate = DateTime.MinValue;

    So, although now EndDate is technically valid, the property setter hasn't been called, and thus EndDate is still marked as invalid. You can get around this with a little extra code, but it's not something I'd want to think about much personally.

  • We agreed before that there's nothing wrong with a business object being invalid, so long as we don't try to save it. In the case where we're not going to be doing any validation but we do call the property setters, our validation code is still called. This is a bit unnecessary.

Rules in Trial Balance

This led me to the following design goals:

  • A business object should be allowed to be invalid, so long as you don't try to save.
  • Rules should not be implemented in property setters, because it's too limiting for reasons outlined above.
  • A rule should not be checked unless it absolutely has to be.
  • No exceptions should be used, unless the object is trying to save without being validated.
  • Any rules that have been broken should be retrievable from the business object.

A typical Trial Balance property will look like this:

C#
public string Name {
    get { return _name; }
    set { _name = value; }
}

Notice how there are no rules in there?

If you take a look at the Account class and scroll about halfway down, you'll see a method called CreateRules that looks somewhat like this:

C#
protected override List<Rule> CreateRules() {
    List<Rule> rules = base.CreateRules();
    rules.Add(new SimpleRule("Name", "An account name is required" + 
              " and cannot be left blank.", 
              delegate { return this.Name.Length != 0; }));
    rules.Add(new SimpleRule("Name", "Account names cannot be" + 
              " more than 20 characters in length.", 
              delegate { return this.Name.Length <= 20; }));
    return rules;
}

For each rule that applies to an object, I'm using delegates to validate the rule. Since these are generally short validation routines, they can usually be done in just one line of code.

IDataErrorInfo

Before I dive into the details about how I validate a rule, I'd like to talk about a very important interface you should know about if you're a .NET developer. It's called IDataErrorInfo, and it's in the System.ComponentModel namespace.

IDataErrorInfo is used to inform people of errors on your object. It's got two parts – a string property called Error, where you return a list of all the errors on the object (for example: "Name can't be blank. Email can't be blank. Address can't be blank..."), and an indexer that takes the name of a property and returns an associated error message (for example, account["Name"] might return "Name can't be blank").

In .NET, if a control like a DataGridView is data-bound to an object, and that object implements IDataErrorInfo, the DataGridView is so smart that it will actually call the methods on the business object for you. So, if you've implemented IDataErrorInfo correctly, you might see this:

An image of a DataGridView bound to some business objects that implement IDataErrorInfo.

Alternatively, if you have some data-bound TextBoxes or CheckBoxes or ComboBoxes, you can drag an ErrorProvider onto your form. Simply set the DataSource of the error provider to your business object, and you'll also get the errors reported:

A form with a TextBox and ErrorProvider bound to an object that implements IDataErrorInfo.

The ErrorProvider works by looking at the data source, seeing what other controls are bound to it, and then displaying errors on those controls when there are errors on the data source.

Take note – in the screens above, the code that triggers the error to be shown is in the business object, not in the GUI. I didn't have to write a single line of GUI code – it's already done by data binding and IDataErrorInfo.

In Trial Balance, my DomainObject base class implements the IDataErrorInfo, so all of my business objects inherit this functionality.

Back to rules

Here's the class diagram to the important classes:

A class diagram with the Rule, Simple Rule and DomainObject classes.

In Trial Balance, every business object has a CreateRules method, which it inherits from the DomainObject base class. When any of the validation methods on the object are called for the first time, the CreateRules method is called to get a list of all the rules that apply. This is an important point that I'd like to stress – unless you actually call one of the validation methods or properties, the CreateRules method will never be called.

The CreateRules method returns a generic List of Rules to the base class. The Rule class is an abstract class, that contains two properties and an abstract method:

  • PropertyName – Gets the name of the property that the rule belongs to.
  • Description – Gets the descriptive text about the rule that's been broken.
  • ValidateRule() – This is the abstract method that returns whether or not your code is valid.

SimpleRule

Since most rules are very simple one-line constructs, there's an additional class that inherits from Rule, called SimpleRule. This class takes a delegate in its constructor, and this delegate is called by the ValidateRule() method.

If you take a look at the DomainObject class, about half way down, you'll see a method called GetBrokenRules():

C#
public virtual ReadOnlyCollection<Rule> GetBrokenRules(string property) {
    property = CleanString(property);
   
    // If we haven't yet created the rules, create them now.
    if (_rules == null) {
        _rules = new List<Rule>();
        _rules.AddRange(this.CreateRules());
    }
    List<Rule> broken = new List<Rule>();

   
    foreach (Rule r in this._rules) {
        // Ensure we only validate a rule that
        // applies to the property we were given
        if (r.PropertyName == property || property == string.Empty) {
            bool isRuleBroken = !r.ValidateRule(this);
            // [...Snip...]
            if (isRuleBroken) {
                broken.Add(r);
            }
        }
    }

    return broken.AsReadOnly();
}

First, we check to see whether the CreateRules() method has been called, and if not, we'll call it for the first time. Then, we cycle through every rule that was returned. If the rule applies to the property name that was passed in as a parameter to the method, or if no property name was specified, the abstract ValidateRule() method is called. If that returns false, that rule is added to a list to be returned to the caller.

The property indexer that is there, thanks to IDataErrorInfo, makes use of GetBrokenRules():

C#
public virtual string this[string propertyName] {
    get {
        string result = string.Empty;

        propertyName = CleanString(propertyName);

        foreach (Rule r in GetBrokenRules(propertyName)) {
            result += r.Description;
            result += Environment.NewLine;
        }
        result = result.Trim();
        if (result.Length == 0) {
            result = null;
        }
        return result;
    }
}

After calling GetBrokenRules(), it cycles through each one, and if they apply to it, they're added as a long list of error messages to be returned. So, for example, if the indexer is called with "Name" as a parameter, only the rules that apply to the Name property will be included in the result.

The other property that we have to implement as part of IDataErrorInfo, Error, calls the indexer with no property name specified. In this case, the indexer will validate all rules, which is exactly what the Error property is supposed to return.

Conclusion

I feel that my approach gives developers a lot of power, because they aren't just limited to using delegates as rules. The Rule class is designed to follow the Strategy pattern, so you can simply subclass Rule to create common, reoccurring rules, such as a NotBlankRule, a DateRangeRule, or even very complex rules that use multiple properties that go off and call web services to validate.

I think I'll summarize this post by pointing out that there's really no single best solution when it comes to how you implement your business rules. It's something that always varies from project to project, and it's up to us, as developers, to make the best call. This article was just an attempt to outline one possible alternative that you may consider when it comes to developing your own system of business rules, and a discussion about what I believe are the important properties of any business rule system.

Revision history

  • 24 May 2006 - Uploaded demo project.

One last thing!

I'd also like to say that the data binding features in Windows Forms are very rich, and it doesn't take much to use them to your advantage and create a very rich GUI. If you haven't heard of IDataErrorInfo, INotifiesPropertyChanged, or IEditableObject, you would really be doing yourself a favour by heading over to MSDN and having a read through the list of interfaces related to data binding.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Octopus Deploy
Australia Australia
My name is Paul Stovell. I live in Brisbane and develop an automated release management product, Octopus Deploy. Prior to working on Octopus I worked for an investment bank in London, and for Readify. I also work on a few open source projects. I am a Microsoft MVP for Client Application Development.

Comments and Discussions

 
GeneralRe: Great article, but a little constructive criticism Pin
Matthew Law28-Oct-06 13:31
Matthew Law28-Oct-06 13:31 
GeneralRe: Great article, but a little constructive criticism Pin
twesterd6-Dec-06 21:38
twesterd6-Dec-06 21:38 
GeneralRe: Great article, but a little constructive criticism Pin
Tomer Noy12-Dec-06 1:24
Tomer Noy12-Dec-06 1:24 
GeneralRe: Great article, but a little constructive criticism Pin
twesterd12-Dec-06 11:21
twesterd12-Dec-06 11:21 
GeneralRe: Great article, but a little constructive criticism Pin
Tomer Noy12-Dec-06 21:49
Tomer Noy12-Dec-06 21:49 
GeneralRe: Great article, but a little constructive criticism Pin
twesterd12-Dec-06 22:39
twesterd12-Dec-06 22:39 
GeneralAbout resources usage Pin
wode29-May-06 21:17
wode29-May-06 21:17 
QuestionHow the rules belong to? Pin
GalloAndres29-May-06 4:26
GalloAndres29-May-06 4:26 
Hi,
I read your article and I agree with some concepts, but I believe the strategy used will have, at least, one inconvenient. I think that the rules don’t belong to the objects that represents domain entities, indeed they belong to the actions that we are trying to do with them. For example, if we have a rule, let's say: “an account with balance greater than u$s 1.000 can’t be deleted”. Clearly, in this context, the rule must work only if we are deleting, but not in others operations (creation, modification). Other important point is that you are putting in the account the responsibility to check if it’s valid by running the rules, and I think you need other object with this responsibility (a Validator). I hope my notes can help you. I enjoyed the article.


Gallo
QuestionAuto-recovering model [modified] Pin
IgorC26-May-06 10:35
IgorC26-May-06 10:35 
GeneralRE: "I don't like throwing exceptions for non-exceptional things." Pin
KHadden24-May-06 8:01
KHadden24-May-06 8:01 
GeneralRe: RE: "I don't like throwing exceptions for non-exceptional things." Pin
sgeddes11429-May-06 14:39
sgeddes11429-May-06 14:39 
GeneralNon-Databinding Pin
G35Guy24-May-06 6:57
G35Guy24-May-06 6:57 
GeneralRe: Non-Databinding Pin
Paul Stovell24-May-06 7:35
Paul Stovell24-May-06 7:35 
QuestionQuestion... Pin
BoneSoft24-May-06 4:46
BoneSoft24-May-06 4:46 
AnswerRe: Question... Pin
Koru.nl25-May-06 5:56
Koru.nl25-May-06 5:56 
GeneralRe: Question... Pin
BoneSoft25-May-06 7:53
BoneSoft25-May-06 7:53 
GeneralError vs. Exception Pin
Mel Grubb II24-May-06 4:25
Mel Grubb II24-May-06 4:25 
GeneralRe: Error vs. Exception Pin
KHadden24-May-06 8:46
KHadden24-May-06 8:46 
GeneralRe: Error vs. Exception Pin
IgorC24-May-06 10:03
IgorC24-May-06 10:03 
GeneralRe: Error vs. Exception Pin
Mel Grubb II24-May-06 10:07
Mel Grubb II24-May-06 10:07 
GeneralRe: Error vs. Exception Pin
twesterd14-Jun-06 9:34
twesterd14-Jun-06 9:34 
GeneralCSLA 2.0 Pin
MSoulia24-May-06 4:05
MSoulia24-May-06 4:05 
GeneralExcellent Pin
chaldon23-May-06 23:25
chaldon23-May-06 23:25 
GeneralThanks! [modified] Pin
Paul Stovell22-May-06 4:43
Paul Stovell22-May-06 4:43 
GeneralRe: Thanks! [modified] Pin
davidj623-May-06 8:50
davidj623-May-06 8:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.