Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Delegates in C# - Attempt to look inside: Part 5

0.00/5 (No votes)
4 Nov 2010 1  
Using delegates and generic types to improve development.

Interlude

After a modest success of my series of articles about delegates, I want to introduce some useful examples about how to utilize delegates in your code.

In this article, along with delegates, I have brought in the notion of generic data types. If you are not familiar with delegates, I encourage you to read my other articles here:

Problem

If you are a web developer, there is a typical situation when you want to use caching to improve the performance of your website. You would also want to be able to turn on/off the caching ability and your procedures should reflect the changes immediately. Usually, this is done through the configuration (web.config file). Typically, when you write your business object to get, say, a list of some objects, it checks if caching is enabled; if yes, it goes and checks if the objects are in the cache; if not, it puts it to the cache from a data-storage and then returns the list. As you can see, the logic is just straightforward, but you have to repeat it for every procedure in which you retrieve the list of objects. If you have hundreds of possible procedures, you can imagine the code you would have to write.

Solution

Delegates and generic types are very good to be used together and makes our life much easier. As a matter of fact, delegates have proved to be really useful to reduce code redundancy.

My example

In the following example, I will introduce two different techniques for how to accomplish caching of data: classical, and via delegates and generic types. I will provide explanations along with code snippets. I want to make myself clear that this article is not about caching! I have used caching just as an example to show off delegates. Caching itself is a bigger topic to discuss, and not a subject for our article.

Environment

I wrote the example using VS 2010, but it will work for VS 2008 as long as your .NET Framework version is not older than 3.5. The project was created as a C# Web Application. There is no UI at all. There is no database; we assume that a database exists and DataClass provides the communication with the database.

Business Objects

There are three business objects: Product, ProductGroup, and ProductType.

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int GroupID { get; set; }
    public double Price { get; set; }
    public int TypeID { get; set; }
    public bool InStock { get; set; }
}


public class ProductType
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class ProductGroup
{
    public int ID { get; set; }
    public string Name { get; set; }
}

DataClass object

A special object represents the Data Layer and communicates with a database. There are three static functions to retrieve data from a database:

public class DataClass
{
    public static List<Product> GetProductsByGroupID(int groupID)
    {
        return new List<Product>();
    }

    public static List<Product> 
           GetProductsByGroupIDandAvailability(int groupID,bool inStock)
    {
        return new List<Product>();
    }

    public static List<Product> 
           GetProductsByTypeIDandGroupID(int typeID, int groupID)
    {
        return new List<Product>();
    }
}

As you can see, there are no real database calls; all three functions just return the new List of Product type objects in order for the code to compile. It is done only for learning purposes.

GlobalUtility

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Web;
using System.Web.Caching;


public class GlobalUtility
{
    //usually set via web.config:
    public static int CacheDuration = 30;

    //usually set via web.config:
    public static bool EnableCaching = true;

    public static Cache Cache
    {
        get
        {
            try
            {
                return HttpContext.Current.Cache;
            }
            catch { return new Cache(); }
        }
    }

    public static void CacheData(string key, object data)
    {
        Cache.Insert(key, data, null, 
          DateTime.Now.AddSeconds(CacheDuration), TimeSpan.Zero);
    }

    public static void PurgeCacheItems(string prefix)
    {
        prefix = prefix.ToLower();
        List<string> itemsToRemove = new List<string>();
        try
        {
            IDictionaryEnumerator enumerator = Cache.GetEnumerator();
            while (enumerator.MoveNext())
            {

                if (enumerator.Key.ToString().ToLower().StartsWith(prefix))
                    itemsToRemove.Add(enumerator.Key.ToString());
            }
            foreach (string itemToRemove in itemsToRemove)
                Cache.Remove(itemToRemove);
        }
        catch { }
    }     
}

This utility class has a set of static procedures to handle caching. Everything is simplified. The CacheDuration and EnableCaching variables are just set to some values. In real life, those values would be taken from the application configuration file. The Cache procedure returns the Cache object. CacheData caches an object, associating it with a key. The PurgeCacheItems procedure removes from cache all the objects whose keys start with a key prefix.

Product class static procedures

Now, let us add some static procedures into the Product class.

#region Static Functions

public static List<Product> GetByGroupID(int groupID)
{
    string key =  "Product_GetByGroupID_" + groupID.ToString();

    List<Product> dataObject;
    if (GlobalUtility.EnableCaching && null != GlobalUtility.Cache[key])
        dataObject = (List<Product>)GlobalUtility.Cache[key];
    else
    {
        dataObject = DataClass.GetProductsByGroupID(groupID);
        if (GlobalUtility.EnableCaching) GlobalUtility.CacheData(key, dataObject);
    }
    return dataObject;
}

public static List<Product> GetByGroupIDandAvailability(int groupID, bool inStock)
{
    string key = "Product_GetByGroupIDandAvailability_" + 
                 groupID.ToString() + "_" + inStock.ToString();
    List<Product> dataObject;
    if (GlobalUtility.EnableCaching && null != GlobalUtility.Cache[key])
        dataObject = (List<Product>)GlobalUtility.Cache[key];
    else
    {
        dataObject = DataClass.GetProductsByGroupIDandAvailability(groupID, inStock);
        if (GlobalUtility.EnableCaching) GlobalUtility.CacheData(key, dataObject);
    }
    return dataObject;
}

public static List<Product> GetByTypeIDandGroupID(int groupID, int typeID)
{
    string key = "Product_GetByTypeIDandGroupID_" + 
                 typeID.ToString() + "_" + groupID.ToString();

    List<Product> dataObject;
    if (GlobalUtility.EnableCaching && null != GlobalUtility.Cache[key])
        dataObject = (List<Product>)GlobalUtility.Cache[key];
    else
    {
        dataObject = DataClass.GetProductsByTypeIDandGroupID(groupID, typeID);
        if (GlobalUtility.EnableCaching) GlobalUtility.CacheData(key, dataObject);
    }
    return dataObject;
}

#endregion

If the cache is enabled, they look inside the cache for the result; otherwise, they get the result using the DataClass object.

Imaging though that you have not just three functions with cache enabled functionality, but about a hundred more procedures like these. And for each of them, you will have to repeat similar code. Quite an assignment!!! And more - what if you want to change some code in the caching block? Then you have to change it in every function! Not a pleasant thing at all...

Some thoughts

Let us take a look at these three functions. First, we will look for similarities.

  1. Structurally they are very much alike.
  2. The logical sequence of the operations is the same.
  3. The usage of key is the same.
  4. They all return values.

Now, what are the differences?

  1. Similar functions might return different data types.
  2. Different number and data types of input parameters.
  3. Different DataClass functions are called inside.

If they are structurally and logically similar, delegates should be considered to reduce redundancy. If the parameter data types and return value data types are unknown, the idea is to utilize generic data types. If all the functions return a value, we can try to use Func Delegates. And if the number of parameters are different, it means that we will have to use overloading methods.

Generic function with Func

In the GlobalUtility class, we add a new function:

public static TOutput GetCashedDataObject<T1,TOutput>(string key, 
       T1 t1, Func<T1,TOutput> func)
{
    string key = key + t1.ToString();
    TOutput dataObject;
    if (GlobalUtility.EnableCaching && null != Cache[key])
        dataObject = (TOutput)Cache[key];
    else
    {
        dataObject = func(t1);
        if (GlobalUtility.EnableCaching) CacheData(key, dataObject);
    }

    return dataObject;
}

Let's slice it and look at it like under a microscope:

Toutput GetCashedDataObject<T1,TOutput>

When you place some weird <T1,TOutput> inside angle brackets, you actually explain to the compiler that those are data types that you want to use. For all functions that declare generic data types, the last parameter is a return value data type. When you call the function from your code, you specify the concrete data types that will be used. This function accepts some parameters:

(string key, T1 t1, Func<T1,TOutput> func)
  1. The key is just a string.
  2. t1 is a parameter of T1 data type.
  3. The func is a delegate of Func type. Func type delegates are created by the environment, and they are also the generic data type of the delegates. Actually, here it says that the third parameter is a function which accepts a parameter of T1 type and returns a value of Toutput type. Just make a mental note that this function accepts only one parameter.

And at last is the body of the function:

{
    string key = key + t1.ToString();
    TOutput dataObject;
    if (GlobalUtility.EnableCaching && null != Cache[key])
        dataObject = (TOutput)Cache[key];
    else
    {
        dataObject = func(t1);
        if (GlobalUtility.EnableCaching) CacheData(key, dataObject);
    }

    return dataObject;
}

As you can see, this is the exact same functionality as in the previous example, but instead of:

dataObject = DataClass.GetProductsByGroupID(groupID);

we use:

dataObject = func(t1);

Remember the mental note? The func function can represent any function which accepts only one parameter and returns a value. Now we will try to change the existing function:

public static List<Product> GetByGroupID(int groupID)
{
    string key =  "Product_GetByGroupID_" + groupID.ToString();

    List<Product> dataObject;
    if (GlobalUtility.EnableCaching && null != GlobalUtility.Cache[key])
        dataObject = (List<Product>)GlobalUtility.Cache[key];
    else
    {
        dataObject = DataClass.GetProductsByGroupID(groupID);
        if (GlobalUtility.EnableCaching) GlobalUtility.CacheData(key, dataObject);
    }
    return dataObject;
}

with the new one:

public static List<Product> GetByGroupID(int groupID)
{
    string keyPrefix = "Product_GetByGroupID_";

    return GlobalUtility.GetCashedDataObject<int,List<Product>>(
                         keyPrefix, groupID, grID =>
    {
        return DataClass.GetProductsByGroupID(grID);
    });
}

As you can see, we create a key string and call the GlobalUtility.GetCashedDataObject<int,List<Product>> function. The first parameter is the key prefix. The second parameter is the group ID. The third parameter is the delegate which we define in-line, by calling the data class function. We use Lambda syntax in this case:

grID =>
{
  return DataClass.GetProductsByGroupID(grID);
});

We can use our generic data type for any data type. The only limitation is the number of input parameters. It must be only one. If we want to handle more than one, we need to overload the GetCashedDataObject function. For two parameters, it will be:

public static TOutput GetCashedDataObject<T1,T2,TOutput>(string keyPrefix, 
                                   T1 t1, T2 t2, Func<T1,T2,TOutput> func)
{
    string key = keyPrefix + t1.ToString() + "_" + t2.ToString();
    TOutput dataObject;
    if (GlobalUtility.EnableCaching && null != Cache[key])
        dataObject = (TOutput)Cache[key];
    else
    {
        dataObject = func(t1, t2);
        if (GlobalUtility.EnableCaching) CacheData(key, dataObject);
    }

    return dataObject;
}

You see the pattern, do you not? It is really simple. Now we can replace the other two functions. They accept two parameters; even though the data types for the input parameters are different, it does not matter. So the entire section for the static functions of the Product class is now:

public static List<Product> GetByGroupID(int groupID)
{
    string keyPrefix = "Product_GetByGroupID_";

    return GlobalUtility.GetCashedDataObject<int,List<Product>>(
           keyPrefix, groupID, grID =>
    {
       return DataClass.GetProductsByGroupID(grID);
    });

}

public static List<Product> GetByGroupIDandAvailability(int groupID, bool inStock)
{
    string keyPrefix = "Product_GetByGroupIDandAvailability_";

    return GlobalUtility.GetCashedDataObject<int,bool, 
           List<Product>>(keyPrefix, groupID, inStock,(grID,inst) =>
    {
        return DataClass.GetProductsByGroupIDandAvailability(grID, inst);

    });
}

public static List<Product> GetByTypeIDandGroupID(int groupID, int typeID)
{
    string keyPrefix = "Product_GetByGroupIDandAvailability_";

    return GlobalUtility.GetCashedDataObject<int,int,
           List<Product>>(keyPrefix, groupID, typeID, (grID, tpID) =>
    {
        return DataClass.GetProductsByTypeIDandGroupID(grID, tpID);

    });
}

Quite a difference! And if you want to change the functionality for caching, it need only be done in one place.

Conclusion

This is one of numerous examples of how to use delegates and utilize generic data types in development. It helps get rid of redundant code, and makes your program more object-oriented and scalable. If you like the article, please vote for it. It really helps me understand if my articles are in need.

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