Sunday, December 14, 2008

An example of IQuackFu usage in Boo

The IQuackFu interface lets you intercept calls to a duck type when called method or property doesn’t exist. You can use this to achieve many interesting things. In Boo’s examples there is a class for accessing XML data and also a class that allows dynamic inheritance. The IQuackFu interface has three methods:

  • QuackInvoke(name as string, args as (object)) as object – intercepts method calls
  • QuackSet(name as string, parameters as (object), value) as object – intercepts property setter calls
  • QuackGet(name as string, parameters as (object)) as object – intercepts property getter calls

As you can see each method has a name argument, which contains a name of the called method or property. This name can be parsed and appropriate action can be taken. My example will be a simple class for accessing data in a database table. Methods called on this class should be in form of:

Get <table name> By <column 1> [And <column 2> And …] ( <value 1> [, <value 2>, …] )

The method name specifies which table will be queried and what columns will be used in the WHERE clause. Arguments of the method specify values for the filter. The method returns a DataSet. Here is the code:

import System
import System.Text.RegularExpressions
import System.Collections.Generic
import System.Data.SqlClient
import System.Data

class Repository(IQuackFu):
    def QuackInvoke(name as string, args as (object)) as object:

        match = /^Get(?<table>\w+)By(?<column>\w+?)(And(?<column>\w+?))*$/.Match(name)
        if match != null and match.Success:
            table = match.Groups["table"].Value
            params = List[of string]()
            for c as Capture in match.Groups["column"].Captures:
            if len(args) == len(params):
                return RunQuery(table, params.ToArray(), args)
                raise InvalidOperationException("Column count doesn't match parameter count")
            raise InvalidOperationException("Method name has incorrect format")
    private def RunQuery(table as string, params as (string), args as (object)):
        paramString = ""
        for i in range(len(params)):
            paramString += " and ${params[i]} = @p${i}"
        commandText = "select * from ${table} where (1 = 1)${paramString}";
        using connection = SqlConnection("<your connection string here>"):
            command = SqlCommand(commandText, connection)
            for i in range(len(args)):
                command.Parameters.AddWithValue("@p${i}", args[i])

            dataSet = DataSet()
            return dataSet
    def QuackSet(name as string, parameters as (object), value) as object:
    def QuackGet(name as string, parameters as (object)) as object:

Sample usage of this class is:

repository = Repository()
customers = repository.GetCustomersByCountry("Germany") as DataSet
products = repository.GetProductsByUnitsInStockAndReorderLevel(0, 0) as DataSet

As you can see, the result of the method is casted to DataSet. Without this, the result would be of the duck type, which means its members would be resolved at runtime. While this may be OK, you should consider two things:

  • Duck typing is approx. 160 times slower than static typing.
  • There is a known issue with Boo 0.8.2 causing AmbiguousMatchException to be thrown when an overloaded indexer is accessed on a duck type. This means that accessing indexer of Tables collection of the DataSet class through duck typing will result in this exception.
DotNetKicks Image

No comments: