Case Study: The POP Forums Architecture
The POP Forums application, which is a Web forum app for ASP.NET (http://www.popforums.com), has an architecture that divides up responsibilities into a number of different layers that interact with the ASP.NET architecture and SQL Server. Figure 4.2 shows how these parts talk to each other.
Figure 4.2. The POP Forums architecture.

Listing 4.1. The simple act of inserting data into the database
C#[View full width]
VB.NET[View full width]
namespace PopForums.Data
{
public class SqlClient : IPopForumsData
{
public int CreateNewPost(int TopicID, DateTime PostTime,
string Name, int PeopleID, string IP, bool FirstInTopic,
string FullText, bool ShowSig)
{
SqlConnection connection = new SqlConnection(ConfigurationSettings.AppSettings["PopForumsDbConnect"]);
connection.Open();
string sql = "INSERT INTO pf_Posts (TopicID, PostTime, Name, "
+ "PeopleID, IP, FirstInTopic, FullText, ShowSig) VALUES ("
+ "@TopicID, @PostTime, @Name, @PeopleID, @IP, @FirstInTopic, "
+ "@FullText, @ShowSig)";
SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@TopicID",TopicID);
command.Parameters.AddWithValue("@PostTime",PostTime);
command.Parameters.AddWithValue("@Name",Name);
command.Parameters.AddWithValue("@PeopleID",PeopleID);
command.Parameters.AddWithValue("@IP",IP);
command.Parameters.AddWithValue("@FirstInTopic",FirstInTopic);
command.Parameters.AddWithValue("@FullText",FullText);
command.Parameters.AddWithValue("@ShowSig",ShowSig);
command.ExecuteNonQuery();
command.CommandText = "SELECT @@IDENTITY";
int postID = Convert.ToInt32(command.ExecuteScalar());
connection.Close();
return postID;
}
... more methods
}
}
Namespace PopForums.Data
Public Class SqlClient
Implements IPopForumsData
Public Function CreateNewPost(TopicID As Integer, _
PostTime As DateTime, Name As String, PeopleID As Integer, _
IP As String, FirstInTopic As Boolean, FullText As String, _
ShowSig As Boolean) As Integer
Dim connection As New _ SqlConnection(ConfigurationSettings.AppSettings("PopForumsDbConnect"))
connection.Open()
Dim sql As String = "INSERT INTO pf_Posts (TopicID, PostTime, "_
& "Name, PeopleID, IP, FirstInTopic, FullText, ShowSig) "_
& " VALUES (@TopicID,@PostTime,@Name,@PeopleID,@IP, "_
& "@FirstInTopic,@FullText,@ShowSig)"
Dim command As New SqlCommand(sql, connection)
command.Parameters.AddWithValue("@TopicID", TopicID)
command.Parameters.AddWithValue("@PostTime", PostTime)
command.Parameters.AddWithValue("@Name", Name)
command.Parameters.AddWithValue("@PeopleID", PeopleID)
command.Parameters.AddWithValue("@IP", IP)
command.Parameters.AddWithValue("@FirstInTopic", FirstInTopic)
command.Parameters.AddWithValue("@FullText", FullText)
command.Parameters.AddWithValue("@ShowSig", ShowSig)
command.ExecuteNonQuery()
command.CommandText = "SELECT @@IDENTITY"
Dim postID As Integer = _ Convert.ToInt32(command.ExecuteScalar())
connection.Close()
Return postID
End Function
... more methods
End Class
End Namespace
The AddWithValue() method of the ParameterCollection is new to v2.0 of .NET. If you're using a previous version, you can use the same syntax with the Add() method (though it has been marked as obsolete in v2.0). The v2.0 compiler will warn you if you use the obsolete Add() method.Chapter 2, "Classes: The Code Behind the Objects," an interface is little more than a signature that says what members a class must have. Listing 4.2 shows a little piece of our interface. Listing 4.2. The IPopForumsData interfaceC# VB.NET Our interface doesn't have any actual code in it. Its sole purpose is to let us know how our classes that implement it should look.That's great, but our business rules classes are going to need to hit all these methods in our SqlClient class. To do this, we'll create a class that loads our data access class and caches it, according to values in our web.config file. A single static method called Methods() in the ClientLoader class will do this, as shown in Listing 4.3.
Listing 4.3. Loading our data access layerC#[View full width] VB.NET[View full width] The important thing to understand about the class in Listing 4.3 is that it loads an instance of the class we choose, which happens to implement IPopForumsData. If one isn't specified, it loads the default, our SqlClient class (for the purpose of this project, the SqlClient class is compiled in the same assembly as the rest of this code). The result is that we can call any method in our SqlClient class by creating an instance of it via the ClientLoader class and its Methods() method: As you can see, you can actually create an object whose type is an interface! Then you might see that we can call our CreateNewPost() method like this: The IPopForumsData interface, the ClientLoader class, and the SqlClient class collectively make up our data access layer. This code is solely responsible for getting data in and out of the SQL Server database. To pipe the data in and out of another data store, we need only write a class that implements IPopForumsData and set values in our web.config file to load that class instead.Now that we've found a way to isolate all our data access methods, we can create business rules. We'll create a class called Post and give it properties that match each of the fields in our database table. We'll also give it a constructor to create a new instance of a Post object, as well as methods to Create(), Update(), and Delete() data.
Listing 4.4. The Create() method of the Post classC#[View full width] VB.NET[View full width] This method does several things that fit squarely in the business rules layer of our application that we wouldn't want to leave to the user interface or the data access layer. You can see that it removes two cache entriesone that stores data on the parent topic and one that stores data on the user making the post. Those cached values are invalidated because the topic has changed, as well as information about the user with regards to the posts they've made. After that has been taken care of, it creates an instance of our data access class and calls the method to create a new post. The parameters that begin with an underscore are the private members of the class that correspond to properties, as we saw in Chapter 2 in Listing 2.7. Finally, the method creates a Topic object (another business rule class) and causes it to update its data, which forces the topic to calculate a new "last post" time.It's not that critical that you understand all of those action items in the method, but it is important that you understand why they happen in this discrete business rules layer of the application. It isn't important to know how the data is stored or how the UI gets data into the class. Only the rules that deal with the data and how it's handled or calculated are important.This gets us to what I call the user interface "glue." Despite having a great many declarative controls, ASP.NET still needs a bit of code to make the user interface work with your business rules layer. This glue code can appear in any of three places: in the page itself in a <script> block, in a partial class (unofficially known as "code-beside"), or in a class that the page inherits (known as "code-behind," the default place for code in versions of Visual Studio prior to 2005). Where you put this code is a matter of preference, but if you put it in the .aspx page file, some people may consider this an application design fault because it doesn't create that separation many programmers require. I put this code in with the rest of the page for convenience and to make it accessible for people not using Visual Studio. Considering you can't change the .aspx page without impacting the code-behind, or vice-versa, I don't feel that this separation is entirely necessary. Partial classes are new as of v2.0 of .NET. In ASP.NET, they're merged on first-run with pages to form one complete class. We'll go into more detail in Chapter 6, "The Nuts and Bolts of IIS and Web Application." |
Listing 4.5. Using the Post class in our user interface glue
C#
VB.NET
Post post = new Post();
post.TopicID = topic.TopicID;
post.PostTime = DateTime.UtcNow;
post.Name = user.Username;
post.PeopleID = (int)user.UserId;
post.IP = Request.UserHostAddress;
post.FirstInTopic = false;
post.FullText = ReplyBox.Text;
post.ShowSig = ShowSigCheckBox.Checked;
post.Create();
Again, the important point to consider with this code is that it's very single-minded. Remember all of the things that the Create() method does with caching and updating the parent topic? This code doesn't need to worry about any of that. Its sole purpose is acquiring what's in the controls and the request and passing that to the business rules layer. Even further removed, it doesn't need to worry about the data store or how data is written to it.That leaves only the actual user interface layer. This final layer consists only of the HTML that defines the page and its controls. It doesn't need to know anything about how to feed data into the business rules.We've traversed five layers in the application, starting at the database itself and working our way back to the user interface. Each layer does its own thing, and it only has to know how to interact with the layers above and below it. The implementation in the adjacent layers is not important.This architecture is friendly to developers who have certain areas of expertise. I'm not much of an Oracle programmer, but a friend or co- worker that has guru status could jump in there and make a great Oracle data access layer to work with the rest of the application. I'm also not much of a designer, but another person could create a killer user interface and have no problem using the business rules classes to process the data.
Dim post As New Post()
post.TopicID = topic.TopicID
post.PostTime = DateTime.UtcNow
post.Name = user.Username
post.PeopleID = CInt(user.UserId)
post.IP = Request.UserHostAddress
post.FirstInTopic = False
post.FullText = ReplyBox.Text
post.ShowSig = ShowSigCheckBox.Checked
post.Create()