1. Open the web.config configuration file (double-click on its name in Solution Explorer) and update the connectionStrings element like this:
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<appSettings/>
<connectionStrings>
<add name="BalloonShopConnection" connectionString="Server=
(local)\SqlExpress;
Integrated Security=True;Database=BalloonShop"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<system.web>
<!--
■ Note You might need to adapt the connection string to match your particular SQL Server configuration.
Also, you should type the <add> element on a single line, not split in multiple lines as shown in the previous code snippet.
2. Add the other necessary configuration data under the <appSettings> node in web.config, as shown here:
<appSettings>
<add key="MailServer" value="localhost" />
<add key="EnableErrorLogEmail" value="true" />
<add key="ErrorLogEmail" value="errors@yourballoonshopxyz.com" />
</appSettings>
■ Note Make sure you include a working server address instead of localhost and a valid email account instead of errors@yourballoonshopxyz.com, if you intend to use the email logging feature. Otherwise, just set EnableErrorLogEmail to false.
3. Right-click the project’s name in Solution Explorer and choose Add New Item from the context menu.
4. Choose the Class template, and set its name to ApplicationConfiguration.cs. Click Add.
5. You’ll be asked about adding the class into the App_Code folder. This is a special folder in ASP.NET 2.0.
Choose Yes.
6. Modify the ApplicationConfiguration class like this:
using System;
using System.Configuration;
/// <summary>
/// Repository for BalloonShop configuration settings /// </summary>
public static class BalloonShopConfiguration {
// Caches the connection string
private static string dbConnectionString;
// Caches the data provider name private static string dbProviderName;
static BalloonShopConfiguration() {
dbConnectionString =
ConfigurationManager.ConnectionStrings["BalloonShopConnection"].
ConnectionString;
dbProviderName =
ConfigurationManager.ConnectionStrings["BalloonShopConnection"].
ProviderName;
}
// Returns the connection string for the BalloonShop database public static string DbConnectionString
{ get {
return dbConnectionString;
} }
// Returns the data provider name public static string DbProviderName {
get {
return dbProviderName;
} }
8213592a117456a340854d18cee57603
// Returns the address of the mail server public static string MailServer
{ get {
return ConfigurationManager.AppSettings["MailServer"];
} }
// Send error log emails?
public static bool EnableErrorLogEmail {
get {
return bool.Parse(ConfigurationManager.AppSettings ["EnableErrorLogEmail"]);
} }
// Returns the email address where to send error reports public static string ErrorLogEmail
{ get {
return ConfigurationManager.AppSettings["ErrorLogEmail"];
} } }
7. Right-click the project’s name in Solution Explorer and choose Add New Item from the context menu.
8. Choose the Class template and set its name to Utilities.cs. Click Add. You’ll be asked about adding the class into the App_Code folder. Choose Yes.
9. Write the following code into Utilities.cs (note that we’ve removed the unnecessary using statements):
using System;
using System.Net.Mail;
/// <summary>
/// Class contains miscellaneous functionality /// </summary>
public static class Utilities {
static Utilities() {
//
// TODO: Add constructor logic here //
}
// Generic method for sending emails
public static void SendMail(string from, string to, string subject, string body)
{
// Configure mail client (may need additional // code for authenticated SMTP servers) SmtpClient mailClient = new SmtpClient (BalloonShopConfiguration.MailServer);
// Create the mail message
MailMessage mailMessage = new MailMessage(from, to, subject, body);
/*
// For SMTP servers that require authentication message.Fields.Add
("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate", 1);
message.Fields.Add
("http://schemas.microsoft.com/cdo/configuration/sendusername",
"SmtpHostUserName");
message.Fields.Add
("http://schemas.microsoft.com/cdo/configuration/sendpassword",
"SmtpHostPassword");
*/
// Send mail
mailClient.Send(mailMessage);
}
// Send error log mail
public static void LogError(Exception ex) {
// get the current date and time
string dateTime = DateTime.Now.ToLongDateString() + ", at "
+ DateTime.Now.ToShortTimeString();
// stores the error message
string errorMessage = "Exception generated on " + dateTime;
// obtain the page that generated the error
System.Web.HttpContext context = System.Web.HttpContext.Current;
errorMessage += "\n\n Page location: " + context.Request.RawUrl;
// build the error message
errorMessage += "\n\n Message: " + ex.Message;
errorMessage += "\n\n Source: " + ex.Source;
errorMessage += "\n\n Method: " + ex.TargetSite;
errorMessage += "\n\n Stack Trace: \n\n" + ex.StackTrace;
// send error email in case the option is activated in Web.Config if (BalloonShopConfiguration.EnableErrorLogEmail)
{
string from = "noreply@cristiandarie.ro";
string to = BalloonShopConfiguration.ErrorLogEmail;
string subject = BalloonShopConfiguration.SiteName + " error report";
string body = errorMessage;
SendMail(from, to, subject, body);
} } }
10. Right-click the project’s name in Solution Explorer and choose Add New Item from the context menu.
Choose the Class template and set its name to GenericDataAccess.cs. Click Add. You’ll be asked about adding the class into the App_Code folder. Choose Yes.
11. Write the following code into GenericDataAccess.cs:
using System;
using System.Data;
using System.Data.Common;
using System.Configuration;
/// <summary>
/// Class contains generic data access functionality to be accessed from /// the business tier
/// </summary>
public static class GenericDataAccess {
// static constructor static GenericDataAccess() {
//
// TODO: Add constructor logic here //
}
// executes a command and returns the results as a DataTable object public static DataTable ExecuteSelectCommand(DbCommand command) {
// The DataTable to be returned DataTable table;
// Execute the command making sure the connection gets closed in the end try
{
// Open the data connection command.Connection.Open();
// Execute the command and save the results in a DataTable DbDataReader reader = command.ExecuteReader();
table = new DataTable();
table.Load(reader);
// Close the reader reader.Close();
}
catch (Exception ex) {
Utilities.LogError(ex);
throw ex;
} finally {
// Close the connection command.Connection.Close();
}
return table;
}
// creates and prepares a new DbCommand object on a new connection public static DbCommand CreateCommand()
{
// Obtain the database provider name
string dataProviderName = BalloonShopConfiguration.DbProviderName;
// Obtain the database connection string
string connectionString = BalloonShopConfiguration.DbConnectionString;
// Create a new data provider factory
DbProviderFactory factory = DbProviderFactories.
GetFactory(dataProviderName);
// Obtain a database specific connection object DbConnection conn = factory.CreateConnection();
// Set the connection string
conn.ConnectionString = connectionString;
// Create a database specific command object DbCommand comm = conn.CreateCommand();
// Set the command type to stored procedure comm.CommandType = CommandType.StoredProcedure;
// Return the initialized command object return comm;
} }
12. In Solution Explorer, right-click on the App_Code folder and choose Add New Item. Using the window that appears, create a new class named CatalogAccess (which would reside in a file named CatalogAccess.cs). Add the new code to the file:
using System;
using System.Data;
using System.Data.Common;
/// <summary>
/// Product catalog business tier component /// </summary>
public static class CatalogAccess {
static CatalogAccess() {
//
// TODO: Add constructor logic here //
}
// Retrieve the list of departments public static DataTable GetDepartments() {
// get a configured DbCommand object
DbCommand comm = GenericDataAccess.CreateCommand();
// set the stored procedure name comm.CommandText = "GetDepartments";
// execute the stored procedure and return the results return GenericDataAccess.ExecuteSelectCommand(comm);
} }
How It Works: The Business Tier Let’s take some time to understand the code you just wrote.
First, you added some configuration settings to the web.config configuration file. web.config is an external configuration XML file managed by ASP.NET. This complex and powerful file can include many options regarding the application’s security, performance, behavior, and so on.
Saving data to web.config is beneficial because you can change it independently of your C# code, which now doesn’t need to be recompiled when you change the address of the mail server or the database connection string.
This detail, combined with the fact that the data access code is written to be database-independent, makes the whole data access code powerful.
Then, you added the BalloonShopConfiguration class, which is simply a collection of static properties that return data from web.config. Using this class instead of needing to read web.config all the time will make your life easier in the long run. The performance is improved as well because the class can cache the values read from web.config instead of reading them on every request. The first place you use the BalloonShopConfiguration class is the Utility class, which for now only contains code that sends emails.
Next, you implemented the GenericDataAccess class, whose purpose is to store a series of common database access operations, to avoid typing it all over again in other places. The two methods it contains now are
• CreateCommand creates a DbCommand object, sets some standard properties to it, and returns the configured object. If you preferred to use a database-specific command object, such as SqlCommand, the code would have been a bit simpler, but in this case we prefer to have database-independent access code, as explained earlier in this chapter. The CreateCommand method uses the steps presented earlier in this chapter to create a command object specific to the database implementation you’re working with, wrap that instance into a generic DbCommand reference, and return this reference. This way, external classes will be able to call CreateCommand to get an already configured—with a prepared connection—DbCommand object.
• ExecuteSelectCommand is essentially a wrapper for DbCommand’s ExecuteReader method, except it returns the results as a DataTable instead of a DataReader. Using the DataTable ensures that the database connection is kept open as short as possible. In this method, you implement an error- handling technique that guarantees that in case of an exception, the administrator is informed by email (if the application is configured to do so), the database connection is properly closed and the error is rethrown. We decided to let the error propagate because this class is at too low a level to know how to properly handle the errors. At this point, we’re only interested in keeping the database safe (by closing the connection) and reporting any eventual error. The best example of how a client class can use GenericDataAccess to work with the BalloonShop database is the GetDepartments method in the CatalogAccess class.
All the classes you’ve added are static classes, which are composed exclusively of static members. Note that some understanding of basic OOP terminology—such as classes, objects, constructors, methods, properties, fields, instance members and static members, public data and private data, and so on—is an important prerequisite for this book. These topics are covered in many articles on the Internet, such as the ones you can find for free download at http://www.cristiandarie.ro/downloads.html.
■Note Static class members (such as static properties and static methods) can be called by external classes without creating an instance of the class first; instead, they can be called directly using the class name. The perfect example for this is the Math class, which contains a number of static methods to perform various operations, such as Math.Cos, and so on. Under the hood, the static class members are called on a global instance of that class, which is not destroyed by the GC (Garbage Collector) after execution. When the first static member of a class is called, the global instance of the class is created, and its static constructor is executed. Because the static constructor is called only once per application’s lifetime, and the global instance is never destroyed, we can ensure that any initializations performed by the static constructor (such as reading the database connection string) are performed only once, and the values of any static members are persisted.
A static class member can call or access another static class member directly. However, if you needed to access an instance class member (nonstatic class member) from a static class member, you had to create an instance of the class, even from a method of that class, to access that member.
We’ve chosen to use static members mainly to improve performance. Because static classes and static members are initialized only once, they don’t need to be reinstantiated each time a new visitor makes a new request; instead, their “global” instances are used. In the presentation tier, you’ll display your list of departments with a call like this:
list.DataSource = CatalogAccess.GetDepartments();
If GetDepartments would have been an instance method, you would have needed to create a separate instance of the CatalogAccess class instead of using the static instance, which would have had, obviously, a bigger performance impact:
CatalogAccess catalogAccess = new CatalogAccess();
list.DataSource = catalogAccess.GetDepartments();
In BalloonShopConfiguration, you’ve implemented even an additional trick to improve performance by caching connection string data using static fields (dbConnectionString and dbProviderName), whose data is read in from web.config in the class’s static constructor. The static class constructor is called only once per application’s life cycle, so the web.config file won’t be read on every database operation, but just once when the class is initialized.