I mentioned before the complications with some of the
ASP.NET collections objects, like Request, Response, Cache, Session, and
ViewState. These objects are specialized and to customize the actual
implementations can be complicated and can take some work. How can we expose
this? Let us look at the Session collection. Session is a specialized
collection and to use the Session at a class level is available through the
Page object as well as the HttpContext.Current property.
However, in a test environment, only a dictionary of string
keys and object values is needed, and because it is the test environment, the
test does not have as many problems as the web environment. Since the test
exists outside of any page lifecycle and will not be recycled at the unloading
of the page, the unit test does not need to worry about serializing and storing
the values. This is both good and bad.
But, what if abstraction was added? What if a custom class
representing both the ASP.NET caching mechanism and a testing mechanism can be
used instead? For instance, below could be a session state manager interface.
Listing 2
public interface ISessionStateManager
{
int Count
{
get;
}
object this[string key]
{
get;
set;
}
string UniqueKey
{
get;
}
void Add(string key, object value);
void Clear();
bool Contains(string key);
void Remove(string key);
void RemoveAt(int index);
}
Any class implementing this interface could use a custom
session state management, using any mechanism available. For instance, one class
can utilize the ASP.NET session manager and one could implement a test version using
an internal dictionary that stores key/value pairs.
Remember that I mentioned the Session could be accessed
through HttpContext.Current. The ASPNetSessionManager implements the session
interface as below. It uses the Session object as the sole point of contact
with the actual session object, which will be shown later.
Listing 3
public class AspNetSessionStateManager: ISessionStateManager
{
public int Count
{
get
{
return Session.Count;
}
}
public object this[string key]
{
get
{
return Session[key];
}
set
{
Session[key] = value;
}
}
public string UniqueKey
{
get
{
return Session.SessionID;
}
}
public void Add(string key, object value)
{
Session.Add(key, value);
}
public void Clear()
{
Session.Clear();
}
public bool Contains(string key)
{
return (Session["key"] != null);
}
public void Remove(string key)
{
Session.Remove(key);
}
public void RemoveAt(int index)
{
Session.RemoveAt(index);
}
}
The Session property is private and local to this instance
(not exposed by the ISessionStateManager interface) and returns an instance of
the HttpSessionState, returning the ASP.NET session object when in the ASP.NET
environment.
Listing 4
private System.Web.SessionState.HttpSessionState Session
{
get
{
if (HttpContext.Current == null)
throw new Exception("The context is currently null");
return HttpContext.Current.Session;
}
}
For testing purposes, I create a stub that stores the
session keys in an internal dictionary. Any custom page class using my session
interface, instead of the session object directly (which is still available)
can be safely tested, as long as the correct session manager is configured
(mentioned below). That is safe for unit testing and is only meant for unit testing
because the stub does not serialize and store the session values across ASP.NET
postbacks.
Listing 5
public class SessionStateManagerStub: ISessionStateManager
{
private Dictionary < string, object > _session = new Dictionary < string,
object > ();
public int Count
{
get
{
return _session.Count;
}
}
public object this[string key]
{
get
{
return _session[key];
}
set
{
_session[key] = value;
}
}
public string UniqueKey
{
get
{
return "1234567890";
}
}
public void Add(string key, object value)
{
_session.Add(key, value);
}
public void Clear()
{
_session.Clear();
}
public bool Contains(string key)
{
return _session.ContainsKey(key);
}
public void Remove(string key)
{
_session.Remove(key);
}
public void RemoveAt(int index)
{
int i = 0;
if (index < 0 || index >= _session.Count)
throw new ArgumentOutOfRangeException("index");
foreach (KeyValuePair < string, object > pair in _session)
{
if (index == i++)
{
this.Remove(pair.Key);
return ;
}
}
throw new KeyNotFoundException("The index was not found");
}
}
The problem with using this test version is that the ASP.NET
version never gets tested correctly, at least with unit testing alone. There
are other tools available, such as Plasma or WATIN that can unit test ASP.NET
pages by accessing it (using HTTP) and parsing the HTML.
Both implementations work great, but how can you switch
between the two? This is where the configuration capabilities come into play.
ASP.NET provides the ability to create custom configuration sections, as well
as specify the provider type as a string property within the custom section.
Because the provider type is a string, the type can be dynamically created
through the Type.GetType() method.
How does this dynamic session manager get exposed? Through a
SessionStateManagement static object, similar to how the provider pattern works. It exposes all
of the methods defined in ISessionStateManager, plus provides additional
properties. For instance, one of the properties is the Enabled property defined
in the configuration section, as shown below.
Listing 6
public static bool Enabled
{
get
{
if (SessionStateSettingsSection.Instance == null)
return false;
else
return SessionStateSettingsSection.Instance.Enabled;
}
}
If disabled, an exception will be thrown disallowing any
class to make use of the session capabilities. SessionStateSettingsSection also
defines the session provider type, which will be used in the call to
GetManager(). To expose the manager selected, the Manager property handles
this.
Listing 7
private static ISessionStateManager Manager
{
get
{
if (_manager == null)
{
if (SessionStateSettingsSection.Instance == null)
throw new ConfigurationErrorsException(@
"Session statesettings section has not been initialized ");if
(!SessionStateSettingsSection.Instance.Enabled)throw new Exception(
"Session state is not enabled");
_manager = SessionStateSettingsSection.Instance.GetManager();
}
return _manager;
}
}
All of the methods directly call the Manager's methods.
Listing 8
public static void Add(string key, object value)
{
Manager.Add(key, value);
}
public static void Remove(string key)
{
Manager.Remove(key);
}
The Manager property, in turn, calls the default provider
setup in the configuration section.
Listing 9
<sessionStateSettings type="Nucleo.Web.Session.SessionStateManagerStub,
Nucleo.TestingTools" />
And, the SessionStateManagerStub methods get called, which
work with the internal dictionary to add/remove items.
Listing 10
public void Add(string key, object value)
{
_session.Add(key, value);
}
public void Remove(string key)
{
_session.Remove(key);
}
A few things to note, because the SessionStateManagement
static class is being utilized, it requires that a configuration section be
used and enabled is set to true. If anything uses SessionStateManagement
without a provider being specified in the configuration file, an exception is
thrown (as you may have seen above). The dynamically referenced provider is
returned otherwise.
I have created an almost identical mechanism for caching;
the caching mechanism uses an ICachingManager interface, as well as a CacheManagement
static object. It has an ASP.NET implementation that exposes the
HttpContext.Current.Cache property, as well as a stubbed cache provider that
uses an internal dictionary. But with caching, there is another custom
implementation that can be used outside of the ASP.NET caching mechanism and a
stubbed implementation: the Enterprise Library Caching block. Below is an
internal property that gets an instance of the EL caching manager.
Listing 11
internal CacheManager Manager
{
get
{
if (_manager == null)
_manager = CacheFactory.GetCacheManager();
return _manager;
}
}
The cache manager takes a key for a non-default caching
manager implementation, so this implementation requires a default cache manager
setup, which can pose a challenge. Outside of this, everything works the same.