- ASP.NET 3.5 Social Networking
- Andrew Siemer
- 4119字
- 2025-02-26 05:48:01
Wrappers for everything!
In order to continue down the path of heavily abstracting all our objects, it is important to consider how coupled your application is to the .NET framework, or other frameworks, or third-party tools that you don't have control over. This may not seem important initially, but you will eventually find yourself in a place where the framework that Microsoft provides you with may not cut the mustard any more.
A quick example of this might be the caching objects that are provided. While these work great out of the box, they don't really scale well. What happens when you build a site that becomes so popular that you need to do everything possible to eek out that last bit of performance? The first response is usually to just go to a web farm. The next response is to add more boxes to the farm. The third response is to add more boxes to the farm…huh?
How often can adding more hardware to address performance issues be a standard response? At some point in time you will have to improve your application. If you could achieve a huge performance gain by swapping out the caching object, would you be able to? If you have references to the caching object scattered all over your code, you would have to go in and swap it out for your new third-party caching object. While this can be done, it would be tedious and error prone. A better option would be to stick to the principle of wrapping anything you don't personally control. If you create a cache wrapper that wraps the .NET cache object, you could easily and quickly swap out the MS cache object for something like MemCached.
I learned a saying in the Army that has served me well, and applies here too:
"It is better to have and not need, than need and not have!"
Here, it is better to have wrappers around everything and not need to swap anything out, than need to swap everything out and have wrappers around nothing.
Configuration
For the configuration wrapper, we want to create something that is capable of returning a strongly typed item out of our configuration source. Of course, this configuration source will initially be the standard web.config
. As the web.config
only holds string values, our configuration object will be responsible for casting out the appropriate type.
using System; using System.Configuration; using StructureMap; namespace Fisharoo.FisharooCore.Core.Impl { [Pluggable("Default")] public class Configuration : IConfiguration { private static object getAppSetting(Type expectedType, string key) { string value = ConfigurationManager.AppSettings.Get(key); if (value == null) { throw new Exception(string.Format("AppSetting: {0} is not configured.", key)); } try { if (expectedType.Equals(typeof(int))) { return int.Parse(value); } if (expectedType.Equals(typeof(string))) { return value; } throw new Exception("Type not supported."); } catch (Exception ex) { throw new Exception(string.Format("Config key:{0} was expected to be of type {1} but was not.", key, expectedType), ex); } } } }
As you can see, from the start I have included a StructureMap attribute so that we can easily swap out this object for a stub if needed.
[Pluggable("Default")]
Directly after that, you will notice that we have inherited from a Configuration
interface. As this class doesn't currently have any public methods, this interface is currently empty! The goal of this class is to provide methods that can be called, which in turn will call the static
getAppSetting()
method. As we don't yet have any items in our configuration file, we don't have any public methods yet.
If we did have something to get to, we would make a method that looked similar to this:
public virtual int GetAuthCookieTimeoutInMinutes() { int value = (int) getAppSetting(typeof (int), "AuthCookieTimeoutInMinutes"); return value; }
This method is basically providing a clean way for our domain objects to get to a configuration value as a specified type. Internally, the method calls getAppSetting
, specifying the type expected and the name of the key to be fetched. It then casts the return value to that expected type and returns the value.
The getAppSetting()
method itself currently communicates with the config
file using the ConfigurationManager
and gets the string value that was specified by the key that was passed in.
string value = ConfigurationManager.AppSettings.Get(key);
It then tests whether the retrieved value is null, and throws an error if it is.
if (value == null) { throw new Exception(string.Format("AppSetting: {0} is not configured.", key)); }
We then try to return the expected type. It is currently testing for an int
or string
value. If the expected type is int
, then we attempt to use int.Parse
on the value and return it. Otherwise, we try to return the string
value.
try { if (expectedType.Equals(typeof(int))) { return int.Parse(value); } if (expectedType.Equals(typeof(string))) { return value; } throw new Exception("Type not supported."); }
If the expected type is not int
or string
, then we throw an error stating that the requested type is not supported. If anything in the initial try
statement goes wrong, we throw an error stating that the key was not of the expected type.
catch (Exception ex) { throw new Exception (string.Format("Config key:{0} was expected to be of type {1} but was.not.", key, expectedType),.ex); }
As our application grows and we add items to our configuration file, we will extend this file and its interface to include one method per entry in the configuration file.
Cache
I like to have a cache wrapper so that we can immediately plan to cache items in our site. However, I know that down the road, the basic .NET cache implementation will not work in a high-traffic environment. I would prefer to use something like MemCached or something similar. However, we will discuss wrapping the basic HttpContext Cache
object for now.
To get started, we need to add a reference to System.Web
in our FisharooCore project (as it is an assembly project, which doesn't have that reference by default!). To do this, right-click on the project root, and select add reference. It may take a while, but eventually, the Add Reference window should pop up. Select the .NET tab, and scroll down till you encounter System.Web. Select that item and then click the OK button.

That will allow us to access the cache object in our assembly. Now, let's add a settings file so that we can set the default cache time out.
In order to add this Settings.settings
file, navigate to the FisharooCore project in Windows Explorer (not in Visual Studio). There, you should have a Properties
folder. In this folder, create a new XML file called Settings.settings
. Back in your Visual Studio environment, navigate to your FisharooCore project. Then, click the show all files button at the top of the Solution Explorer window .
You should now see your Settings.settings
file. Right-click on that file and select include in project. Now click the show all files button again. The Settings.settings
file should now be included and visible in your project. Double-click the file to open it in Visual Studio. You should see a window that somewhat resembles a spreadsheet! Type in the following information as you see it here:

Once you have done this, click Save. You should now have a functioning Settings
class to work with! Let's build our Cache
wrapper now. Here is the code in its entirety.
using System; using System.Collections; using System.Collections.Generic; using System.Web; using System.Web.Caching; using Fisahroo.FisharooCore.Properties; namespace Fisharoo.FisharooCore.Core.Impl { public class Cache { private static System.Web.Caching.Cache cache; private static TimeSpan timeSpan = new TimeSpan( Settings.Default.DefaultCacheDuration_Days, Settings.Default.DefaultCacheDuration_Hours, Settings.Default.DefaultCacheDuration_Minutes, 0); static Cache() { cache = HttpContext.Current.Cache; } public static object Get(string cache_key) { return cache.Get(cache_key); } public static List<string> GetCacheKeys() { List<string> keys = new List<string>(); IDictionaryEnumerator ca = cache.GetEnumerator(); while (ca.MoveNext()) { keys.Add(ca.Key.ToString()); } return keys; } public static void Set(string cache_key, object cache_object) { Set(cache_key, cache_object, timeSpan); } public static void Set(string cache_key, object cache_object, DateTime expiration) { Set(cache_key, cache_object, expiration, CacheItemPriority.Normal); } public static void Set(string cache_key, object cache_object, TimeSpan expiration) { Set(cache_key, cache_object, expiration, CacheItemPriority.Normal); } public static void Set(string cache_key, object cache_object, DateTime expiration, CacheItemPriority priority) { cache.Insert(cache_key, cache_object, null, expiration, System.Web.Caching.Cache.NoSlidingExpiration, priority, null); } public static void Set(string cache_key, object cache_object, TimeSpan expiration, CacheItemPriority priority) { cache.Insert(cache_key, cache_object, null, System.Web.Caching.Cache.NoAbsoluteExpiration, expiration, priority, null); } public static void Delete(string cache_key) { if (Exists(cache_key)) cache.Remove(cache_key); } public static bool Exists(string cache_key) { if (cache[cache_key] != null) return true; else return false; } public static void Flush() { foreach (string s in GetCacheKeys()) { Delete(s); } } } }
To get started, note that at the top we have several namespace references.
using System; using System.Collections; using System.Collections.Generic; using System.Web; using System.Web.Caching; using Fisahroo.FisharooCore.Properties;
The most important ones to notice are the System.Web.Caching
and the Fisharoo.FisharooCore.Properties
. The System.Web.Caching
provides us with the object that we are wrapping with this file. The Fisharoo.FisharooCore.Properties
is what gives us access to our newly created settings file.
The next thing to notice is that we have two static variables declared—cache
and timeSpan
. The cache object is a reference to the HttpContext.Current.Cache
object. This is where we will be storing all of our cached items. The timeSpan
variable is a defined TimeSpan
, which will be used for the methods that don't provide a TimeSpan
declaration, a default TimeSpan
so to speak.
We then define a static constructor for our cache object. This basically means that our cache object is refreshed prior to any cache object being created. The exact time can't be determined but can possibly be done at the time that the assembly is loaded.
static Cache() { cache = HttpContext.Current.Cache; }
Now, we can get into our method definitions. We define a way to get something from the cache, get a list of keys currently in the cache, several ways to add items to the cache, a way to delete items from the cache, a way to see if a key is currently present in the cache, and a way to totally flush the cache. Let's look at each of these.
To get something from the cache we have a Get()
method. This method simply requires a key value to be passed to it. We then use the cache implementation of Get()
and return the value. Keep in mind that this could return the cached item or a NULL
value.
public static object Get(string cache_key) { return cache.Get(cache_key); }
We then have a definition for getting a list of all the key values currently residing in the cache. This method returns a generic list of type string. The way it works is that we first define a keys List
that we can add our keys to. We then declare an IDictionaryEnumerator
and assign the cache.GetEnumerator()
values to it. Once we have the Enumeration defined, we iterate through each item in the collection by checking the ca.MoveNext()
method. With each iteration, we add the key value to our keys
collection. We then return the keys
collection.
public static List<string> GetCacheKeys() { List<string> keys = new List<string>(); IDictionaryEnumerator ca = cache.GetEnumerator(); while (ca.MoveNext()) { keys.Add(ca.Key.ToString()); } return keys; }
We then define several Set
methods. This allows us to add items to the cache. Each method is a simple wrapper for all the Set
methods that the cache object
provides. All these methods require that a string key be provided along with the object that is to be cached. Some of them allow for various time-outs to be specified. Others allow you to additionally provide a priority for the cached items expiration. Here are the method declarations:
public static void Set(string cache_key, object cache_object) { Set(cache_key, cache_object, timeSpan); } public static void Set(string cache_key, object cache_object, DateTime expiration) { Set(cache_key, cache_object, expiration, CacheItemPriority.Normal); } public static void Set(string cache_key, object cache_object, TimeSpan expiration) { Set(cache_key, cache_object, expiration, CacheItemPriority.Normal); } public static void Set(string cache_key, object cache_object, DateTime expiration, CacheItemPriority priority) { cache.Insert(cache_key, cache_object, null, expiration, System.Web.Caching.Cache.NoSlidingExpiration, priority, null); } public static void Set(string cache_key, object cache_object, TimeSpan expiration, CacheItemPriority priority) { cache.Insert(cache_key, cache_object, null, System.Web.Caching.Cache.NoAbsoluteExpiration, expiration, priority, null); }
We then move to another simple method wrapper. This one provides a way to delete a cached item. It accepts the key to be deleted. The method then checks to see if the key exists and removes the key from the cache collection.
public static void Delete(string cache_key) { if (Exists(cache_key)) cache.Remove(cache_key); }
The Exists
method simply checks to see if an item is still in the cache collection. The reason for this method is that while items can be freely added to the cache collection, you can never count on them being there when you try accessing them the next time. The item could be removed or may not exist for several reasons:
- The item may have timed out and been removed
- It may have been pushed out of the collection due to the presence of many other new items added to the collection
- The collection may have been re-initialized intentionally, or due to some glitch in the system
This method returns a true
or false
value based on whether the key that is being checked exists in the collection or not. This only checks whether the key exists. It does not pull the item out of the collection and cast it to the appropriate type. The key value may exist while the item may not. For this reason, always check that your casted item is not null before using it!
public static bool Exists(string cache_key) { if (cache[cache_key] != null) return true; else return false; }
Now that we have all these nifty ways to add items, get items, and delete items, we need a way to clear the entire cache collection. This is easily accomplished by iterating through all the keys in the collection and calling Delete
on each of them.
public static void Flush() { foreach (string s in GetCacheKeys()) { Delete(s); } }
Session
The session object is another item that is frequently used in most web applications. That being said, it is also something that we can squeeze performance out of down the road. Even if performance wasn't an issue, the session object by itself does not really conform to the most basic of OOP principles. Rather than trying to cast an item out of thin air—or the HttpContext.Current.Session
—it would be much better if we could call an object that returned the appropriate object for us. Here is the basic wrapper:
using System.Web; using StructureMap; namespace Fisharoo.FisharooCore.Core.Impl { [Pluggable("Default")] public class WebContext : IWebContext { public void ClearSession() { HttpContext.Current.Session.Clear(); } public bool ContainsInSession(string key) { return HttpContext.Current.Session[key] != null; } public void RemoveFromSession(string key) { HttpContext.Current.Session.Remove(key); } private string GetQueryStringValue(string key) { return HttpContext.Current.Request.QueryString.Get(key); } private void SetInSession(string key, object value) { if (HttpContext.Current == null || HttpContext.Current.Session == null) { return; } HttpContext.Current.Session[key] = value; } private object GetFromSession(string key) { if (HttpContext.Current == null || HttpContext.Current.Session == null) { return null; } return HttpContext.Current.Session[key]; } private void UpdateInSession(string key, object value) { HttpContext.Current.Session[key] = value; } } }
Keep in mind that much like the cache object that was shown earlier, we would continue to extend this object to have specific methods that could handle some of the dirty work. Say we stored a person in the session as the current user. We could have a GetCurrentUserFromSession()
method defined that would interact with our wrapper methods. It would retrieve the user and cast the object as a person. This is much better OOP-wise. Let's look at the wrapper.
Of course, the first thing to notice—as you will notice in most of our classes—is that this method is part of the StructureMap framework and has a Pluggable
attribute defined.
[Pluggable("Default")]
You will then notice that the class inherits from IWebContext
. This is so that we can use StructureMap to retrieve the appropriate class for us.
After that we jump right into the public method definitions. These are all pretty easy to understand and primarily work with the HttpContext
object. We have a ClearSession()
method that simply resets the session. After that, we have a ContainsInSession()
method that takes a key value. It checks whether that key is present in the session and returns true or false. Next is the RemoveFromSession()
method that takes in a key and attempts to remove that key from the session.
After our public methods, we have a few private methods left to build, namely, GetQueryStringValue()
, SetInSession()
, GetFromSession()
, and UpdateInSession()
. Before we discuss these methods, I need you to understand why they are private. We could make all of these public and they would work just fine. However, making them public would also mean that we would scatter the code about our application that directly interacts with the session. My preference is that we extend this object to provide more specific methods that work with these private methods, which in turn work with the session. This provides us a bit more encapsulation regarding the session interaction.
Let's have a look at these methods. The GetQueryStringValue()
method takes in a key value and retrieves the item from the query string. The SetInSession()
method allows you to pass in a key and an object. The object is then stored in the session under that key name. GetFromSession()
does just that—it takes a key and retrieves that corresponding object. UpdateInSession()
is very similar to SetInSession()
with the exception that it assumes that a key already exists and updates the value that is currently stored there. This method will throw an error if a key does not exist. Therefore prior to using this method, you should check that your key exists in the session collection!
Redirection
From an OOP point of view, Response.Redirect
is about as useful as the session object is (I'm starting to see a trend here). It simply provides a way of sending you from one place to another. It would be nice if we could work with it using methods. It would be even better if we could hide some logic in those methods if need be. Our initial wrapper is very easy.
using System.Web; using StructureMap; namespace Fisharoo.FisharooCore.Core.Impl { [Pluggable("Default")] public class Redirector : IRedirector { public void GoToHomePage() { Redirect("~/Default.aspx"); } private void Redirect(string path) { HttpContext.Current.Response.Redirect(path); } } }
This class, like the others, uses StructureMap so that it can be used for stubbed out for testing later. Currently there are two methods—one an example, and another handling redirection. Let's look at the Redirect()
method. It takes a path parameter and then uses the HttpContext
object to redirect the user to the appropriate location. An example is the GoToHomePage()
method. It asks the Redirect()
method to send the user to the homepage.
Of course, this class can be expanded with as many new methods as needed to redirect for any purpose. We can extend this object to be a bit more versatile too. We can also perform all sorts of logic inside these methods prior to using the redirection, obviously without degrading the overall design and where actually required.
Sending emails is one task that every website has to be capable of. How many emails you plan to send should certainly determine how you go about sending that email. As we do not yet know how many emails we plan to send, we will initially rely upon the tools that are provided in the .NET framework to send our email. Our wrapper looks like this:
using System.Net.Mail; using StructureMap; namespace Fisharoo.FisharooCore.Core.Impl { [Pluggable("Default")] public class Email : IEmail { const string TO_EMAIL_ADDRESS = "website@fisharoo.com"; const string FROM_EMAIL_ADDRESS = "website@fisharoo.com"; public void SendEmail(string From, string Subject, string Message) { MailMessage mm = new MailMessage(From,TO_EMAIL_ADDRESS); mm.Subject = Subject; mm.Body = Message; Send(mm); } public void SendEmail(string To, string CC, string BCC, string Subject, string Message) { MailMessage mm = new MailMessage(FROM_EMAIL_ADDRESS,To); mm.CC.Add(CC); mm.Bcc.Add(BCC); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); } public void SendEmail(string[] To, string[] CC, string[] BCC, string Subject, string Message) { MailMessage mm = new MailMessage(); foreach (string to in To) { mm.To.Add(to); } foreach (string cc in CC) { mm.CC.Add(cc); } foreach (string bcc in BCC) { mm.Bcc.Add(bcc); } mm.From = new MailAddress(FROM_EMAIL_ADDRESS); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); } public void SendIndividualEmailsPerRecipient(string[] To, string Subject, string Message) { foreach (string to in To) { MailMessage mm = new MailMessage(FROM_EMAIL_ADDRESS,to); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); } } private void Send(MailMessage Message) { SmtpClient smtp = new SmtpClient(); smtp.Send(Message); } } }
As with all our objects, we have the StructureMap attribute in place that makes this a Pluggable class. The class itself inherits from our IEmail
interface. Then, you will see a couple of constants declared—one for the websites receiving the email account and another for the websites sending the email account. (We could have used one variable for both, but a little flexibility never hurt anyone!) We then jump into our first method:
public void SendEmail(string From, string Subject, string Message) { MailMessage mm = new MailMessage(From,TO_EMAIL_ADDRESS); mm.Subject = Subject; mm.Body = Message; Send(mm); }
This method is one of the overrides for the SendEmail()
method. This one is a bit different from the others in that it is used for the site to send email to another site rather than to a user. In that case, the user of this method will provide the email address that the message is from, the subject and the message. This method would be used in a 'Contact Us' page or a similar mail form. At the bottom of this method, you will see a Send()
method call. This method spins up an SmtpClient
and sends the email message as do each of the following methods.
The remaining SendEmail()
methods are used in various ways for the site to send email to the users of the site. The first one allows single email addresses to be passed in for the To, CC, and BCC inputs.
public void SendEmail(string To, string CC, string BCC, string Subject, string Message) { MailMessage mm = new MailMessage(FROM_EMAIL_ADDRESS,To); mm.CC.Add(CC); mm.Bcc.Add(BCC); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); }
The second method allows you to pass in an array for each email address input to specify multiple recipients.
public void SendEmail(string[] To, string[] CC, string[] BCC, string Subject, string Message) { MailMessage mm = new MailMessage(); foreach (string to in To) { mm.To.Add(to); } foreach (string cc in CC) { mm.CC.Add(cc); } foreach (string bcc in BCC) { mm.Bcc.Add(bcc); } mm.From = new MailAddress(FROM_EMAIL_ADDRESS); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); }
The next to last method allows us to iterate through each of the recipients and send one email to each recipient rather than have them all in one of the recipient lines.
public void SendIndividualEmailsPerRecipient(string[] To, string Subject, string Message) { foreach (string to in To) { MailMessage mm = new MailMessage(FROM_EMAIL_ADDRESS,to); mm.Subject = Subject; mm.Body = Message; mm.IsBodyHtml = true; Send(mm); } }
Finally, we get to the Send()
method that is used by all of the other methods. This method is responsible for actually sending the emails. But before this method performs any action, we need to add the following section to our web config
just after the configuration
tag. It is responsible for telling the .NET framework how to connect to our mail server.
<system.net> <mailSettings> <smtp> <network host="serverHostName" port="portnumber" userName="username" password="password" /> </smtp> </mailSettings> </system.net>
Now, we can define the last method Send()
as:
private void Send(MailMessage Message) { SmtpClient smtp = new SmtpClient(); smtp.Send(Message); }
This configuration is very flexible in how it sends email. However, it still requires that the webpage be responsible for sending emails directly. This creates a page with lots of overheads given that there could be a lot of recipients to process, or a lot of network lag involved in the transactions.
The nice thing about having this wrapper is that we can easily create another class that implements the IEmail
interface but uses a mail queue instead of requiring the page to send the email. This would allow the website to just create mail messages and put them in the queue, which is much faster than actually processing and sending the emails. We could then have a queue processor somewhere that would be responsible for sending our emails.
While I am sure that there will be other items that might need a wrapper, which we will come across, this small library should be good enough to get us started!