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 (bmains@hotmail.com) is a Microsoft MVP and
consultant with Computer Aid Inc., where he works with non-profit and state
government organizations.