Interception and Attributes: A Design-By-Contract Sample

February 23, 2008

2 comments

A Design-By-Contract programming paradigm specifies that classes and methods in the language specify pre- and post-conditions which must hold when entering and leaving the class code.  For example, the following Eiffel snippet is a counter class which can be incremented, decremented and reset to 0.  Note the ensure, require and invariant clauses sprinkled across the definition:

class TINY COUNTER
feature
    item: INT
feature
    increment is 
        do
            item = item + 1
        ensure
            item = old item + 1
    end
    decrement is
        require
            item > 0
        do
            item := item - 1
        ensure
            item = old item - 1
    reset is
        do
            items := 0
        ensure item = 0
    end
invariant
    item >= 0
end

This kind of framework can be directly available in C# through the use of interception and attribute.  What we need in our implementation is a set of mechanisms for specifying pre- and post-conditions on methods, and an engine to generate the necessary code and call it.  There are at least three approaches:

  1. Explicit approach – call explicit methods when entering a method and when leaving a method to ensure that the invariants hold (for class-level invariants, this might be a little more difficult);
  2. Implicit approach #1 – use IL weaving or any other post-compilation mechanism (e.g. PostSharp) to plant the pre- and post-conditions into a method which requires it.  The method might ask for pre- and post-conditions through the use of attributes;
  3. Implicit approach #2 – use code generation to create a proxy which will check the pre- and post-conditions prior to dispatching the call to the actual target (this is obviously limited if only code-generation is used, e.g. only interfaces can be mocked).

I’ve chosen to demonstrate the third approach.  Assume we are looking at an implementation of a string-replace algorithm, which takes a string, an old substring and a new substring to replace the old one with.  Here’s the way to express the expectations we have from our interface:

public interface IStringReplace
{
    [ParameterNotNull("orig")]
    [ParameterNotNull("what")]
    [ParameterNotNull("withWhat")]
    [ParameterConstraint("what.Length <= orig.Length")]
    [ReturnValueConstraint("__ReturnValue.IndexOf(what) == -1")]
    [ReturnValueNotNull]
    string Replace(string orig, string what, string withWhat);
}

Note how fluently we can express our requirements!  I’m saying that the three parameters can’t be null, the return value can’t be null, the length of the string to replace can’t exceed the length of the original string, and after the replacement the string to be replaced shouldn’t be found in the return value.  These checks might have just saved me quite some parameter verification, and saved my callers lots and lots of checks when calling my method and not knowing what to expect.  The power of this approach is addictive; it is almost masochistically expressive.

In this sample, I implemented two “generic” conditions – the ParameterConstraint pre-condition and the ReturnValueConstraint pre-condition.  Additionally, I implemented two specific conditions – the ParameterNotNull and ReturnValueNotNull conditions.  (There’s lots of more interesting work to be done here, such as opting in and out of the checks, combining checks for multiple methods, specifying class invariants which must hold true for every method, customizing the behavior in case of failure, and many other interesting aspects.  By the way, with slight modifications this framework can be used to automatically generate unit tests as well!)  For now, let’s take a look at the interface implementation and usage example:

public class StringReplaceImpl : IStringReplace
{
    public string Replace(string orig, string what, string withWhat)
    {
        return orig.Replace(what, withWhat);
    }
}

class Program
{
    static void Main(string[] args)
    {
        IStringReplace replacer = 
            DbcActivator.CreateInstance<StringReplaceImpl, IStringReplace>();
        string result = replacer.Replace("Blah", "B", "A");
    }
}

What happens if the method implementation is incorrect, or if the parameters passed by the caller do not meet the pre-conditions?  An exception is thrown by the DBC interception layer outlined below, specifying the cause of failure:

image

image

So who is this DbcActivator?  It’s the initial interception point where all the magic happens.  By requiring the pre- and post-conditions to be specified on the interface and not the actual implementation, we can intercept the calls and perform pre- and post-condition verification in a proxy which wraps the actual invocation target.  The general architecture here is:

image 

Briefly speaking, the client asks the DbcActivator to return a proxy instance which implements the same interface he expects from the target class.  The proxy wraps the target class and delegates all calls to it, but checks pre-conditions before making calls and post-conditions after making the call but before returning to the client.  Since the proxy code is only generated once, and the pre- and post-conditions checks are code-generated and not determined in runtime, this is almost as efficient as we can get.  (IL weaving has more potential, but is far more complex without a big framework in place.)

The implementation is not particularly efficient or well-designed, but if you’re looking for a sample of code generation, interception and runtime services provided through the use of attributes, this is a great place to start.  Here‘s the sample as a single C# file.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

2 comments

  1. IlyaMarch 5, 2008 ב 6:52 PM

    Just wondering, were you aware of Spec# (http://research.microsoft.com/specsharp/)? I’ve only read about it a few years ago and didn’t actually use it, so I can’t recommend it.

    One cute thing they did is automatically creating preconditions for the BCL by analyzing its sources and converting boilerplate sanity checks (like those ArgumentNullException tests which all look just the same) into preconditions.

    Reply
  2. Sasha GoldshteinMarch 14, 2008 ב 6:00 PM

    Absolutely, I didn’t mention Spec# in the article but I’m aware of the technology. I wanted to provide a specific example of how something like that could be done without opting into a huge framework which modifies code. I think that if you see that code generation and interception are something you can accomplish (on a basic level) with only several KLOC, it makes this approach more available (and may I add, “approachable”) to the average programmer.

    Reply