Patterns & Practices

Object-oriented Techniques in ASP.NET

CoverStory

LANGUAGES: C# | VB

ASP.NET VERSIONS: 3.5

Patterns & Practices

Object-oriented Techniques in ASP.NET

By Brian Mains

Object-oriented development is widely embraced in the code development industry these days. Indeed, more and more development languages incorporate object-oriented programming features. ASP.NET is one of those languages; everything is an object that derives from System.Object. This approach can be used in a way that couldn t be leveraged by its predecessor (ASP). But although ASP.NET is object-oriented, code developers often do not completely make use of all that object-oriented development has to offer.

By this I mean the increasing trend of using large if routines and embedding in the ASPX page s code-behind file all the data-processing logic for performing calculations and decisions. While this isn t a bad technique, this approach provides some testing and maintenance challenges. Out of the box, there isn t a good way to use a testing framework like NUnit to test an ASPX page s code (unless you re using a tool like WatiN or TypeMock s Ivonna). So, while an external tool is always useful, a better way may be to change some of your coding practices to use a more object-oriented way of developing data.

The challenges to embedding all the logic within the UI application, instead of placing it in the business layer, can be realized when it comes time to maintain it. While the code is all in one place, it becomes more difficult to maintain large if/else statements that change over time because of changes in business requirements.

Developing in an object-oriented way using design patterns and practices isn t an easy task. It requires a lot of knowledge about the proper use of design patterns and the application of these patterns. In addition, object-oriented development techniques aren t as cost effective at the beginning of a project s lifecycle. But using these techniques in the long run, instead of coding huge if/else statements or other chunks of processing logic, will save money, time, and frustration.

So what does this all mean to application development? What I mean by design patterns and object-oriented design may seem sketchy, but don t turn away just yet I ll go more in-depth here in a moment.

Refactoring to Strategy

One of my favorite patterns when it comes to separating logic into different components is the strategy pattern. This pattern is simple, yet effective in breaking up logic into different components in a clean way.

Before we discuss this approach, let s look at a simplified example without using the strategy pattern in an ASPX page. Let s take a look at the typical way to develop an ASP.NET application that processes customer information via business rules. The code in Figure 1 processes the return of products to a store. In the example shown in Figure 1, the following logic processes exist:

         For orders before the date of 11/1/2008, the current return policy allows for 45-day returns. If the currently purchased products are outside the 45-day return policy, an exception is thrown.

         For orders beyond the date of 11/1/2008, the current return policy allows for 90-day returns. If the currently purchased products are outside the 90-day return policy, an exception is thrown.

         For each product, the Product object s ReturnPrice property is a temporary value holder for the return price: If the product is discontinued, only 70% of the cost is returned to the customer; if the product is defective, an extra 10% is added to the purchase cost (as a way to say don t stop shopping at our store! ); otherwise, return the actual price.

Order order = (new OrdersBAL()).GetOrder (customerKey, transactionNumber);
if (order.OrderDate < new DateTime(2008, 11, 1))
{
if (order.OrderDate < DateTime.Today.Substract(TimeSpan.FromDays(45)))
throw new Exception( Order is out of the valid range of dates );
}
else
{
if (order.OrderDate < DateTime.Today.Subtract(TimeSpan.FromDays(90)))
throw new Exception( Order is out of the valid range of dates );
}

foreach (Product product in order.Products)
{
if (product.IsDiscontinued)
product.ReturnPrice = product.Price * .70;
else if (product.IsDefective)
product.ReturnPrice = product.Price * 1.10;
else
product.ReturnPrice = product.Price;
}

Figure 1: Processing the return of an order

The code in Figure 1 isn t all that complex, nor that difficult to manage yet. But there can be some difficulty that comes whenever the conditions change because of requirements. If you haven t experienced changing requirements, take my word for it when I say that requirements change a lot. For instance, what if another change to the return policy happens on 12/1/2009, and again on 12/1/2010? What happens if there s a special condition that occurs for products flagged as stolen? The system has to be flexible to support these different requirements.

The strategy pattern is a perfect way to handle these changing requirements. The strategy pattern uses an abstract base class to identify the core functionality of the processing logic being calculated. In this example, I m going to create two base classes (OrderDateCondition and ProductReturnProcessor); the first base class processes the order date range condition, the second base class processes the product return (see Figure 2).

public abstract class OrderDateCondition
{
public abstract bool IsCorrectCondition(Order order);
public abstract bool IsAvailableForReturn(Order order);
}

public abstract class ProductReturnProcessor
{
public abstract bool IsCorrectProcessor(Product product);
public abstract decimal GetActualPrice(Product product);
}

Figure 2: Strategy pattern base classes

These base classes identify the actions taken for processing product returns. For orders, the order needs to validate that the customer has returned a product within the specified correct time range. For products, the actual price allotted to the customer is returned via a method. This functionality needs to be separated because the rules regarding whether an order meets the proper return condition may change at times different than the product return rules.

To process these varying business rules, each rule configuration will derive from one of the above base classes, and implement all the corresponding logic. Starting with order date conditions, there are two derived classes inheriting from OrderDateCondition. The first, shown in Figure 3, is the object that represents the return policy rules from the origin of the company until 11/1/2008. Note that the example code is easy to create, which adds to the simplicity of the solution at hand.

public class OriginalRulesOrderDateCondition : OrderDateCondition
{
public override bool IsCorrectCondition(Order order)
{
return (order.OrderDate < new DateTime(2008, 11, 1));
}

public override bool IsAvailableForReturn(Order order)
{
return (order.OrderDate.AddDays(45) >= DateTime.Today);
}
}

Figure 3: The order date condition for the original return policy rules

The second condition handles processing order dates from November 2008 until now. The class definition currently lasts indefinitely, as shown in Figure 4.

public class November2008OrderDateCondition : OrderDateCondition
{
public override bool IsCorrectCondition(Order order)
{
return (order.OrderDate >= new DateTime(2008, 11, 1));
}

public override bool IsAvailableForReturn(Order order)
{
return (order.OrderDate.AddDays(90) >= DateTime.Today);
}
}

Figure 4: Processing November 2008 return policy rules

So we now have these two conditions for processing the changes in the return policy. Notice the definition of IsCorrectCondition. This method determines whether the order being processed fits within the time period that the rules are in play. For orders older than 11/1/2008, the old rules are used (for historical purposes). Otherwise, the new rules are used. This method determines which strategy pattern implementation actually calculates the order, as only one pattern object will.

Let s look forward to the future. Say the processing will change in January 2010. To handle this, the November 2008 component must be modified to stop processing orders at the end of 2009. A new component will pick up, processing orders starting in January 2010. So why is this better? Let s say the system is now a year older, and needs another change to accommodate changes to the system for 1/1/2010. To make these changes, all that is needed (at least at this point) is to create another class that will evaluate conditions for January 2010 and on. To see this in action, let s add a new condition for the January 2010 changes, as shown in Figure 5.

public class January2010OrderDateCondition : OrderDateCondition
{
public override bool IsCorrectCondition(Order order)
{
return (order.OrderDate >= new DateTime(2010, 1, 1));
}

public override bool IsAvailableForReturn(Order order)
{
return (order.OrderDate.AddDays(120) >= DateTime.Today);
}
}

Figure 5: Strategy pattern for January 2010 return policy rules

It s that simple to add additional objects. Remember that in this new component, the end date of 12/31/2009 condition is added to the November 2008 component.

This process may be a little shortsighted (in the sense that its name reflects that it only checks order dates), but a realistic solution should include other information, as well (is the order taxable or for a tax-free organization, etc.). I ve used only dates to create a simple example.

So how can the system know which object to use? Normally, in the strategy pattern implementation, the correct pattern is manually instantiated, but I take an alternative approach. I usually do this through another parent object, either one that is static, or one that is instantiated. For instance, take a look at the implementation in Figure 6.

public class ReturnPolicyManager
{
private ReturnPolicyManager() { }

public static ReturnPolicyManager GetInstance()
{
ReturnPolicyManager manager = new ReturnPolicyManager();
//Additional processing
return manager;
}

private IEnumerable<OrderDateCondition> GetConditions()
{
return new OrderDateCondition[] {
new OriginalRulesOrderDateCondition(),
new November2008OrderDateCondition(),
new January2010OrderDateCondition()
};
}

public OrderDateCondition GetCondition(Order order)
{
return this.GetConditions().FirstOrDefault(i => i.IsCorrectCondition(order));
}
}

Figure 6: Getting the correct condition

This pattern is similar to the provider pattern, with the exception that it isn t a static object. Instead, all the conditions are loaded on demand, and the list of strategy patterns is manually added to an array in the GetConditions method. If you need a more dynamic solution, use a custom configuration section and add the information to the configuration file instead.

So, using ReturnPolicyManager to process the order date, OrderDateCondition s IsCorrectCondition method is used to find the correct component to evaluate whether the order can be returned. The FirstOrDefault LINQ query method comes in handy to find the correct condition in this case, as only one object should be returned. The ReturnPolicyManager can use the Boolean value returned to check for a false condition, and throw an exception if no conditions exist.

The product return processor strategy pattern works in a similar way. The challenge that can come with determining the product s price is caused by an external object reference (outside data that may affect the actual price returned). Right now, the product return processor class looks like that shown in Figure 7 (copied from Figure 2). I ve also included one derived class.

public abstract class ProductReturnProcessor
{
public abstract bool IsCorrectProcessor(Product product);
public abstract decimal GetActualPrice(Product product);
}

public class NormalProductReturnProcessing : ProductReturnProcessor
{
public override bool IsCorrectProcessor(Product product)
{
return (!product.IsDefective && !product.IsDiscontinued);
}

public override decimal GetActualPrice(Product product)
{
return product.Price;
}
}

Figure 7: Product processing strategy pattern

This seems simple enough, but the one challenge I can foresee is additional references that may be needed. For instance:

         A product may rely on another object to reference to determine a discount appropriately.

         A product may need to rely on the current culture for the customer, as the discount or refund amount may differ per country (or even per state in the US).

         A product discount or refund may not be applied if the order was a tax-free order (for a non-profit).

         The product discount may be null and void if the customer is a preferred customer.

For whatever reason, the product discount or refund may not be determinable by the Product object alone, unless the Product object has a reference to every other entity mentioned in this scenario. For instance, maybe the product requires a lookup table to figure out the price. Assuming, for the sake of the example, that it does not need any outside references, it leaves us with a very important question: How can we handle this?

There are two direct ways this can be handled. First, this can be handled by adding another property to the method. The method definition could be updated as shown in Figure 8.

public abstract class ProductReturnProcessor
{
public abstract bool IsCorrectProcessor(Product product, object relatedObject);
public abstract decimal GetActualPrice(Product product, object relatedObject);
}

Figure 8: Updated product processor definition

The object reference allows anything to be passed to the reference. This allows for a lot of flexibility. For instance, one strategy piece of logic may use a reference to the customer, while another may use a reference to the order. This can be handled easily in the IsCorrectProcessor method (see Figure 9).

Public override bool IsCorrectProcessor(Product product, object relatedObject)
{
return (relatedObject is Customer);
}

Figure 9: Processing the related object

As shown in Figure 9, one strategy pattern may only work with a customer reference, while another works with an object reference. But what if it works with both? Another alternative could use a dictionary (see Figure 10).

public abstract class ProductReturnProcessor
{
public abstract bool IsCorrectProcessor(Product product, IDictionary relatedObject);
public abstract decimal GetActualPrice(Product product, IDictionary relatedObject);
}

Figure 10: Updated product processor class with a dictionary

The dictionary can store many values, so it can store a reference to the customer and the order, referencing them by some key. A better solution would be a custom object, which stores direct references to all related objects, or implements under the scenes a custom dictionary that can store a dynamic number of references in a more manageable way. Another solution could be to create properties at the base class level. The base class then can share the property values across all concrete implementations. This may be more or less desirable for a variety of reasons.

So why is this better? The first point that comes to mind is that it supports unit testing. Each of these objects can be directly instantiated and tested via mock objects. It s a little more maintainable, and keeps the overall code separated by adding a new class definition that supports a new subset of rules, whereas the if/else approach grows rapidly.

Dynamic Calculations

But sometimes the calculations require more than a state/strategy implementation. For instance, logic may dynamically change on the fly, and this requires something more than a state/strategy implementation. This may require a more configurable development option one that can change more easily.

Before getting into the details, let s discuss the problem at hand. The XYZ parent company conducts audits of its brick-and-mortar stores around the United States and Canada. This company uses its own internal rating system to determine the overall score for the store, based on four main categories: Store Cleanliness, Store Security, Store Friendliness, and Product Availability. Based on the store rating, the store then becomes eligible for internal rewards for ranking in the top three stores. While the system may not be perfect, the XYZ company continues to strive to adjust the system to achieve a fair evaluation solution for stores, regardless of population size, employee total, or any other factor.

Because the calculations of the store s evaluation changes on a yearly basis, a flexible means for evaluating stores is required. Rather than using state/strategy, in this article I m going to use an easier option: the configuration file. What I want to achieve is something like the example shown in Figure 11.

<policies>

<add key="Store Cleanliness" effectiveDate="1/1/1900" endDate="9/30/2006 11:59:59 PM"

type="XYZ.Evaluation.InitialStoreCleanlinessEvaluator, XYZ.Business" />

<add key="Store Cleanliness" effectiveDate="10/1/2006" endDate="9/30/2007 11:59:59 PM"

type="XYZ.Evaluation.October2006RevisionStoreCleanlinessEvaluator, XYZ.Business" />

<add key="Store Cleanliness" effectiveDate="10/1/2007" endDate="6/30/2008 11:59:59 PM"

type="XYZ.Evaluation.October2007RevisionStoreCleanlinessEvaluator, XYZ.Business" />

<add key="Store Cleanliness" effectiveDate="7/1/2008" endDate="12/31/9999 11:59:59 PM"

type="XYZ.Evaluation.July2008RevisionStoreCleanlinessEvaluator, XYZ.Business" />

</policies>

Figure 11: Policy configuration section

Each category (Store Cleanliness, Product Availability, etc.) can be used as a key for the policy. Keys can be duplicated; uniqueness is determined by the key/effective date combination. Each policy is effective during a specific range of time. These ranges are evaluated by an effective/end date pair; the current evaluation would use these dates to determine which policy is in effect. Every category would have one or more entries with effective dates and end dates that are used to determine which policy is in place at a given time.

You may wonder what good is it to know which type is available to you in string form. There are four types of components referenced by name in Figure 11. It seems like it may not be useful, but that is where the reflection capabilities come into play. The .NET Framework supports reflecting on type definitions, but it also supports instantiating a type by its name. This can be done with the code shown in Figure 12.

Type type = Type.GetType("XYZ.Evaluation.July2008RevisionStoreCleanlinessEvaluator,

XYZ.Business");
PolicyEvaluator obj = (PolicyEvaluator)Activator.CreateInstance(type);

Figure 12: Instantiating a type by its name

The Type class has a static GetType method that finds a given type in the XYZ.Business class library. This is not an object instance; the object hasn t been instantiated yet. But, at this point, we know which object to instantiate. The Activator.CreateInstance method is what actually instantiates a type based on the Type metadata.

Using this idea, at runtime the policy will be evaluated dynamically by getting the dynamic reference. Polymorphism allows us to reference the type as the PolicyEvaluator class because every evaluator inherits from this base class, as shown in Figure 13.

public abstract class PolicyEvaluator
{
public abstract string GetDisplayTitle();
public abstract void Initialize(Evaluation evaluation);
public abstract IEnumerable<EvaluationCriteria> GetEvaluationCriteria();
public abstract void UpdateEvaluationCriteria(IEnumerable<EvaluationCriteria> criteria);
}

Figure 13: PolicyEvaluator abstract class

This base class reads and writes evaluation criteria, which is used to calculate the overall score. As standards change, what is defined as the evaluation criteria may change over time. For instance, initially the store didn t care about whether the front walkway was swept and kept clean, but the parent company may have changed their minds and made this a requirement. Standards always change, so it s good to account for varying standards which is why the GetEvaluationCritieria is used to return the collection. This allows the policy manager to make the call to get the criteria in place at that time.

Now that we know what operations take place, how do we determine the correct policy to put in place? The first requirement is where to embed the information. In this example, I m using the configuration file to store the types associated with a specific policy at a specific point in time. But this doesn t have to be; the approach can be rewritten to use a SQL database, or some other convenient mechanism.

But these approaches tend to use the configuration file because of the ease of use and maintenance. The configuration file is very convenient, and to create customized structures isn t very difficult. So for the policy manager s needs, the class structures must store the contents of the policies in a collection, as shown in Figure 14.

public class PoliciesSection : System.Configuration.ConfigurationSection
{
[
ConfigurationProperty("", IsDefaultCollection=true),
ConfigurationCollection(typeof(PolicyElementCollection))
]
public PolicyElementCollection Policies
{
get { return (PolicyElementCollection)this["policies"]; }
}

public static PoliciesSection Instance
{
get { return ConfigurationManager.GetSection("policies") as PoliciesSection; }
}
}

public class PoliciesElement : Nucleo.Configuration.ConfigurationElementBase
{
[ConfigurationProperty("endDate", IsRequired=true)]
public DateTime EndDate
{
get { return (DateTime)this["endDate"]; }
}

[ConfigurationProperty("effectiveDate", IsRequired=true)]
public DateTime EffectiveDate
{
get { return (DateTime)this["effectiveDate"]; }
}

[ConfigurationProperty("key", IsRequired=true)]
public string Key
{
get { return (string)this["key"]; }
}

[ConfigurationProperty("type", IsRequired=true)]
public string Type
{
get { return (string)this["type"]; }
}

protected override string UniqueKey
{
get { return this.Type; }
}
}

public class PolicyElementCollection :

Nucleo.Configuration.ConfigurationCollectionBase<PolicyElement>
{
}

Figure 14: Policy configuration section code

At the top level, the PoliciesSection class has a collection of PolicyElement objects. Each policy element object represents the <add> element tag. The properties in PolicyElement with ConfigurationProperty attributes must match the properties in the configuration file. This means that the EndDate property in the PolicyElement class must match the endDate attribute in the configuration file (lower case because of the name value provided in the ConfigurationProperty attribute statement).

Now some component must use this information in two ways:

         First, find the unique list of keys registered for a specific evaluation. This provides flexibility, allowing for the total number of categories to change from four to three, or four to five.

         Using the list of keys, get the most recent evaluator and calculate the score.

I found the best way to make all of this happen is to define a separate component that uses the configuration information to do the work. This makes it work like the provider pattern, with the exception that there isn t a static class; all of this is done in an instance class (see Figure 15).

public class PolicyManager
{
private List<PolicyEvaluator> _evaluators = null;

public List<PolicyEvaluator> Evaluators
{
get
{
if (_evaluators == null)
_evaluators = new List<PolicyEvaluator>();
return _evaluators;
}
}

private PolicyManager() { }

public static PolicyManager Create(Evaluation evaluation)
{
PoliciesSection section = PoliciesSection.Instance;
if (section == null)
throw new NullReferenceException();

PolicyManager manager = new PolicyManager();

//Find the list of PolicyElement objects that match the condition
var policies = from p in section.Policies
where p.EffectiveDate >= evaluation.EvaluationDate
&& p.EndDate <= evaluation.EvaluationDate
select p;

//Get the unique list of keys
var uniqueKeys = policies.Select(p => p.Key).Distinct();
manager._evaluators = new List<PolicyEvaluator>();

foreach (string key in uniqueKeys)
{
var policy = policies.First(i => i.Key == key);
policy.Initialize(evaluation);
manager._evaluators.Add(policy);
}

return manager;
}
}

Figure 15: PolicyManager definition

So the policy manager is responsible for getting the policy evaluators for the current evaluation. The evaluation object contains a reference to the evaluating date, which is useful so that the date range can be properly applied.

In ASP.NET, a web page can use the policy evaluator to generate the different categories and their associated evaluation criteria. This can be done as shown in the following snippet:

protected override void OnInit(EventArgs e)

{

int key = int.Parse(Request.QueryString.Get(

evaluation ));

Evaluation evaluation = EvaluationStore.GetEvaluation(

key);

PolicyManager manager = PolicyManager.Create(

evaluation);

manager.

}

Conclusion

The approach outlined here is another way to better develop applications in ASP.NET using design patterns. This article illustrated this through various design patterns and the use of custom configuration files. While these specific patterns may not apply to you, I want to encourage you to use design patterns to the fullest; that way they really can add to the benefit of ASP.NET, while supporting other features, such as being an easier approach to maintain, as well as the ability to test with NUnit or another testing framework.

Source code accompanying this article is available for download.

Brian Mains ([email protected]) is a Microsoft MVP and consultant with Computer Aid Inc., where he works with non-profit and state government organizations.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish