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 inthe code development industry these days. Indeed, more and more development languagesincorporate object-oriented programming features. ASP.NET is one of thoselanguages; everything is an object that derives from System.Object. Thisapproach 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 notcompletely 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 ASPXpage s code-behind file all the data-processing logic for performingcalculations and decisions. While this isn t a bad technique, this approachprovides some testing and maintenance challenges. Out of the box, there isn t agood way to use a testing framework like NUnit to test an ASPX page s code (unlessyou re using a tool like WatiN or TypeMock s Ivonna). So, while an externaltool is always useful, a better way may be to change some of your codingpractices to use a more object-oriented way of developing data.
The challenges to embedding all the logic withinthe UI application, instead of placing it in the business layer, can berealized 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 overtime because of changes in business requirements.
Developing in an object-oriented way using designpatterns and practices isn t an easy task. It requires a lot of knowledge aboutthe proper use of design patterns and the application of these patterns. In addition,object-oriented development techniques aren t as cost effective at thebeginning 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 applicationdevelopment? What I mean by design patterns and object-oriented design may seemsketchy, 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 toseparating logic into different components is the strategy pattern. Thispattern is simple, yet effective in breaking up logic into different componentsin a clean way.
Before we discuss this approach, let s look at a simplifiedexample without using the strategy pattern in an ASPX page. Let s take a lookat the typical way to develop an ASP.NET application that processes customerinformation via business rules. The code in Figure 1 processes the return ofproducts to a store. In the example shown in Figure 1, the following logicprocesses exist:
For orders before the date of 11/1/2008, thecurrent return policy allows for 45-day returns. If the currently purchasedproducts are outside the 45-day return policy, an exception is thrown.
For orders beyond the date of 11/1/2008, thecurrent return policy allows for 90-day returns. If the currently purchasedproducts are outside the 90-day return policy, an exception is thrown.
For each product, the Product object sReturnPrice property is a temporary value holder for the return price: If theproduct is discontinued, only 70% of the cost is returned to the customer; ifthe product is defective, an extra 10% is added to the purchase cost (as a wayto say don t stop shopping at our store! ); otherwise, return the actualprice.
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 thevalid range of dates );
}
else
{
if (order.OrderDate <DateTime.Today.Subtract(TimeSpan.FromDays(90)))
throw new Exception( Order is outof 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, northat difficult to manage yet. But there can be some difficulty that comeswhenever the conditions change because of requirements. If you haven texperienced changing requirements, take my word for it when I say thatrequirements change a lot. For instance, what if another change to the returnpolicy happens on 12/1/2009, and again on 12/1/2010? What happens if there s aspecial condition that occurs for products flagged as stolen? The system has tobe flexible to support these different requirements.
The strategy pattern is a perfect way to handlethese changing requirements. The strategy pattern uses an abstract base classto identify the core functionality of the processing logic being calculated. Inthis example, I m going to create two base classes (OrderDateCondition andProductReturnProcessor); the first base class processes the order date rangecondition, the second base class processes the product return (see Figure 2).
public abstract class OrderDateCondition
{
public abstract boolIsCorrectCondition(Order order);
public abstract boolIsAvailableForReturn(Order order);
}
public abstract class ProductReturnProcessor
{
public abstract boolIsCorrectProcessor(Product product);
public abstract decimalGetActualPrice(Product product);
}
Figure 2: Strategy pattern base classes
These base classes identify the actions taken forprocessing product returns. For orders, the order needs to validate that thecustomer has returned a product within the specified correct time range. Forproducts, the actual price allotted to the customer is returned via a method.This functionality needs to be separated because the rules regarding whether anorder meets the proper return condition may change at times different than theproduct return rules.
To process these varying business rules, each ruleconfiguration will derive from one of the above base classes, and implement allthe corresponding logic. Starting with order date conditions, there are twoderived classes inheriting from OrderDateCondition. The first, shown in Figure3, is the object that represents the return policy rules from the origin of thecompany until 11/1/2008. Note that the example code is easy to create, whichadds to the simplicity of the solution at hand.
public class OriginalRulesOrderDateCondition : OrderDateCondition
{
public override boolIsCorrectCondition(Order order)
{
return (order.OrderDate < newDateTime(2008, 11, 1));
}
public override boolIsAvailableForReturn(Order order)
{
return(order.OrderDate.AddDays(45) >= DateTime.Today);
}
}
Figure 3: The order date condition for the originalreturn policy rules
The second condition handles processing order datesfrom November 2008 until now. The class definition currently lastsindefinitely, as shown in Figure 4.
public class November2008OrderDateCondition : OrderDateCondition
{
public override boolIsCorrectCondition(Order order)
{
return (order.OrderDate >= newDateTime(2008, 11, 1));
}
public override boolIsAvailableForReturn(Order order)
{
return(order.OrderDate.AddDays(90) >= DateTime.Today);
}
}
Figure 4: Processing November 2008 return policyrules
So we now have these two conditions for processingthe changes in the return policy. Notice the definition of IsCorrectCondition.This method determines whether the order being processed fits within the timeperiod that the rules are in play. For orders older than 11/1/2008, the oldrules are used (for historical purposes). Otherwise, the new rules are used.This method determines which strategy pattern implementation actuallycalculates the order, as only one pattern object will.
Let s look forward to the future. Say theprocessing will change in January 2010. To handle this, the November 2008component must be modified to stop processing orders at the end of 2009. A newcomponent will pick up, processing orders starting in January 2010. So why isthis better? Let s say the system is now a year older, and needs another changeto accommodate changes to the system for 1/1/2010. To make these changes, allthat is needed (at least at this point) is to create another class that willevaluate conditions for January 2010 and on. To see this in action, let s add anew condition for the January 2010 changes, as shown in Figure 5.
public class January2010OrderDateCondition : OrderDateCondition
{
public override boolIsCorrectCondition(Order order)
{
return (order.OrderDate >= newDateTime(2010, 1, 1));
}
public override boolIsAvailableForReturn(Order order)
{
return(order.OrderDate.AddDays(120) >= DateTime.Today);
}
}
Figure 5: Strategy pattern for January 2010 returnpolicy rules
It s that simple to add additional objects.Remember that in this new component, the end date of 12/31/2009 condition isadded to the November 2008 component.
This process may be a little shortsighted (in thesense that its name reflects that it only checks order dates), but a realisticsolution should include other information, as well (is the order taxable or fora tax-free organization, etc.). I ve used only dates to create a simpleexample.
So how can the system know which object to use?Normally, in the strategy pattern implementation, the correct pattern ismanually instantiated, but I take an alternative approach. I usually do thisthrough another parent object, either one that is static, or one that isinstantiated. For instance, take a look at the implementation in Figure 6.
public class ReturnPolicyManager
{
private ReturnPolicyManager() { }
public static ReturnPolicyManagerGetInstance()
{
ReturnPolicyManager manager = newReturnPolicyManager();
//Additional processing
return manager;
}
privateIEnumerable<OrderDateCondition> GetConditions()
{
return new OrderDateCondition[] {
newOriginalRulesOrderDateCondition(),
newNovember2008OrderDateCondition(),
new January2010OrderDateCondition()
};
}
public OrderDateConditionGetCondition(Order order)
{
returnthis.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 conditionsare loaded on demand, and the list of strategy patterns is manually added to anarray in the GetConditions method. If you need a more dynamic solution, use acustom configuration section and add the information to the configuration fileinstead.
So, using ReturnPolicyManager to process the orderdate, OrderDateCondition s IsCorrectCondition method is used to find thecorrect component to evaluate whether the order can be returned. TheFirstOrDefault LINQ query method comes in handy to find the correct conditionin this case, as only one object should be returned. The ReturnPolicyManagercan use the Boolean value returned to check for a false condition, and throw anexception if no conditions exist.
The product return processor strategy pattern worksin a similar way. The challenge that can come with determining the product sprice is caused by an external object reference (outside data that may affectthe actual price returned). Right now, the product return processor class lookslike that shown in Figure 7 (copied from Figure 2). I ve also included onederived class.
public abstract class ProductReturnProcessor
{
public abstract bool IsCorrectProcessor(Productproduct);
public abstract decimalGetActualPrice(Product product);
}
public class NormalProductReturnProcessing :ProductReturnProcessor
{
public override boolIsCorrectProcessor(Product product)
{
return (!product.IsDefective&& !product.IsDiscontinued);
}
public override decimalGetActualPrice(Product product)
{
return product.Price;
}
}
Figure 7: Product processing strategy pattern
This seems simple enough, but the one challenge Ican foresee is additional references that may be needed. For instance:
A product may rely on another object toreference to determine a discount appropriately.
A product may need to rely on the currentculture for the customer, as the discount or refund amount may differ percountry (or even per state in the US).
A product discount or refund may not be appliedif the order was a tax-free order (for a non-profit).
The product discount may be null and void if thecustomer is a preferred customer.
For whatever reason, the product discount or refundmay not be determinable by the Product object alone, unless the Product objecthas 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, itleaves 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 methoddefinition could be updated as shown in Figure 8.
public abstract class ProductReturnProcessor
{
public abstract boolIsCorrectProcessor(Product product, object relatedObject);
public abstract decimal GetActualPrice(Productproduct, object relatedObject);
}
Figure 8: Updated product processor definition
The object reference allows anything to be passedto the reference. This allows for a lot of flexibility. For instance, onestrategy piece of logic may use a reference to the customer, while another mayuse a reference to the order. This can be handled easily in theIsCorrectProcessor method (see Figure 9).
Public override bool IsCorrectProcessor(Product product, objectrelatedObject)
{
return (relatedObject is Customer);
}
Figure 9: Processing the related object
As shown in Figure 9, one strategy pattern may onlywork with a customer reference, while another works with an object reference.But what if it works with both? Another alternative could use a dictionary (seeFigure 10).
public abstract class ProductReturnProcessor
{
public abstract boolIsCorrectProcessor(Product product, IDictionary relatedObject);
public abstract decimalGetActualPrice(Product product, IDictionary relatedObject);
}
Figure 10: Updated product processor class with adictionary
The dictionary can store many values, so it canstore 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 toall related objects, or implements under the scenes a custom dictionary thatcan store a dynamic number of references in a more manageable way. Anothersolution could be to create properties at the base class level. The base classthen can share the property values across all concrete implementations. Thismay be more or less desirable for a variety of reasons.
So why is this better? The first point that comesto mind is that it supports unit testing. Each of these objects can be directlyinstantiated and tested via mock objects. It s a little more maintainable, andkeeps the overall code separated by adding a new class definition that supportsa new subset of rules, whereas the if/else approach grows rapidly.
Dynamic Calculations
But sometimes the calculations require more than astate/strategy implementation. For instance, logic may dynamically change onthe fly, and this requires something more than a state/strategy implementation.This may require a more configurable development option one that can changemore easily.
Before getting into the details, let s discuss theproblem at hand. The XYZ parent company conducts audits of its brick-and-mortarstores around the United States and Canada. This company uses its own internalrating system to determine the overall score for the store, based on four maincategories: Store Cleanliness, Store Security, Store Friendliness, and ProductAvailability. Based on the store rating, the store then becomes eligible forinternal rewards for ranking in the top three stores. While the system may notbe perfect, the XYZ company continues to strive to adjust the system to achievea fair evaluation solution for stores, regardless of population size, employeetotal, or any other factor.
Because the calculations of the store s evaluationchanges 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 easieroption: the configuration file. What I want to achieve is something like theexample shown in Figure 11.
<policies>
<add key="StoreCleanliness" effectiveDate="1/1/1900" endDate="9/30/200611:59:59 PM"
type="XYZ.Evaluation.InitialStoreCleanlinessEvaluator,XYZ.Business" />
<add key="StoreCleanliness" effectiveDate="10/1/2006" endDate="9/30/200711:59:59 PM"
type="XYZ.Evaluation.October2006RevisionStoreCleanlinessEvaluator,XYZ.Business" />
<add key="StoreCleanliness" effectiveDate="10/1/2007" endDate="6/30/200811:59:59 PM"
type="XYZ.Evaluation.October2007RevisionStoreCleanlinessEvaluator,XYZ.Business" />
<add key="StoreCleanliness" effectiveDate="7/1/2008" endDate="12/31/999911:59:59 PM"
type="XYZ.Evaluation.July2008RevisionStoreCleanlinessEvaluator,XYZ.Business" />
</policies>
Figure 11: Policy configuration section
Each category (Store Cleanliness, ProductAvailability, etc.) can be used as a key for the policy. Keys can beduplicated; uniqueness is determined by the key/effective date combination.Each policy is effective during a specific range of time. These ranges areevaluated by an effective/end date pair; the current evaluation would use thesedates to determine which policy is in effect. Every category would have one ormore entries with effective dates and end dates that are used to determine whichpolicy is in place at a given time.
You may wonder what good is it to know which typeis available to you in string form. There are four types of componentsreferenced by name in Figure 11. It seems like it may not be useful, but thatis where the reflection capabilities come into play. The .NET Frameworksupports reflecting on type definitions, but it also supports instantiating atype 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 thatfinds a given type in the XYZ.Business class library. This is not an objectinstance; the object hasn t been instantiated yet. But, at this point, we knowwhich object to instantiate. The Activator.CreateInstance method is whatactually instantiates a type based on the Type metadata.
Using this idea, at runtime the policy will beevaluated dynamically by getting the dynamic reference. Polymorphism allows usto reference the type as the PolicyEvaluator class because every evaluatorinherits from this base class, as shown in Figure 13.
public abstract class PolicyEvaluator
{
public abstract stringGetDisplayTitle();
public abstract voidInitialize(Evaluation evaluation);
public abstractIEnumerable<EvaluationCriteria> GetEvaluationCriteria();
public abstract voidUpdateEvaluationCriteria(IEnumerable<EvaluationCriteria> criteria);
}
Figure 13: PolicyEvaluator abstract class
This base class reads and writes evaluationcriteria, 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 andkept clean, but the parent company may have changed their minds and made this arequirement. Standards always change, so it s good to account for varyingstandards which is why the GetEvaluationCritieria is used to return thecollection. This allows the policy manager to make the call to get the criteriain place at that time.
Now that we know what operations take place, how dowe determine the correct policy to put in place? The first requirement is whereto embed the information. In this example, I m using the configuration file tostore 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 SQLdatabase, or some other convenient mechanism.
But these approaches tend to use the configurationfile because of the ease of use and maintenance. The configuration file is veryconvenient, and to create customized structures isn t very difficult. So forthe policy manager s needs, the class structures must store the contents of thepolicies 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 PoliciesSectionInstance
{
get { returnConfigurationManager.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 acollection of PolicyElement objects. Each policy element object represents the<add> element tag. The properties in PolicyElement withConfigurationProperty attributes must match the properties in the configurationfile. This means that the EndDate property in the PolicyElement class mustmatch the endDate attribute in the configuration file (lower case because ofthe name value provided in the ConfigurationProperty attribute statement).
Now some component must use this information in twoways:
First, find the unique list of keys registeredfor a specific evaluation. This provides flexibility, allowing for the totalnumber of categories to change from four to three, or four to five.
Using the list of keys, get the most recentevaluator and calculate the score.
I found the best way to make all of this happen isto define a separate component that uses the configuration information to dothe work. This makes it work like the provider pattern, with the exception thatthere isn t a static class; all of this is done in an instance class (seeFigure 15).
public class PolicyManager
{
private List<PolicyEvaluator>_evaluators = null;
public List<PolicyEvaluator>Evaluators
{
get
{
if (_evaluators == null)
_evaluators = newList<PolicyEvaluator>();
return _evaluators;
}
}
private PolicyManager() { }
public static PolicyManagerCreate(Evaluation evaluation)
{
PoliciesSection section =PoliciesSection.Instance;
if (section == null)
throw newNullReferenceException();
PolicyManager manager = newPolicyManager();
//Find the list of PolicyElementobjects that match the condition
var policies = from p insection.Policies
wherep.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 = newList<PolicyEvaluator>();
foreach (string key inuniqueKeys)
{
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 gettingthe policy evaluators for the current evaluation. The evaluation objectcontains a reference to the evaluating date, which is useful so that the daterange can be properly applied.
In ASP.NET, a web page can use the policy evaluatorto 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 betterdevelop applications in ASP.NET using design patterns. This article illustratedthis 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 touse design patterns to the fullest; that way they really can add to the benefitof ASP.NET, while supporting other features, such as being an easier approachto maintain, as well as the ability to test with NUnit or another testingframework.
Source codeaccompanying this article is available for download.
Brian Mains (bmains@hotmail.com) is a Microsoft MVP andconsultant with Computer Aid Inc., where he works with non-profit and stategovernment organizations.