Wednesday, May 5, 2010

Introducing Business Objects

This is the second part of discussion on layered architecture in ASP.NET applications. I will extend the same examples, so you should read the first part here, or at least glance through the examples.

Let us extend the data access layer class to include a method to update the database.

ProductDataAccessLayer.cs

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
public class ProductDataAccessLayer
{
    string connectionString = ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ToString();

    public DataSet GetProducts()
    {
        SqlConnection conn = new SqlConnection(connectionString);
        DataSet dsProducts = new DataSet();
        SqlDataAdapter adapter = new SqlDataAdapter("SELECT ProductID,ProductName,UnitPrice FROM Products", conn);
        adapter.Fill(dsProducts);
        return dsProducts;
    }

    public int UpdateProduct(int productId, string productName, string quantityPerUnit, double unitPrice, int unitsInStock, int unitsOnOrder, int reorderLevel)
    {
        SqlConnection conn = new SqlConnection(connectionString);
        conn.Open();
        string sql = "UPDATE Products SET ProductName = @productName, QuantityPerUnit = @quantityPerUnit, UnitPrice = @unitPrice, UnitsInStock = @unitsInStock, UnitsOnOrder = @unitsOnOrder, ReorderLevel = @reorderLevel WHERE ProductId = @productId;";
        SqlCommand command = new SqlCommand(sql, conn);
        command.Parameters.AddWithValue("@productName", productName);
        command.Parameters.AddWithValue("@quantityPerUnit", quantityPerUnit);
        command.Parameters.AddWithValue("@unitPrice", unitPrice);
        command.Parameters.AddWithValue("@unitsInStock", unitsInStock);
        command.Parameters.AddWithValue("@unitsOnOrder", unitsOnOrder);
        command.Parameters.AddWithValue("@reorderLevel", reorderLevel);
        command.Parameters.AddWithValue("@productId", productId);
        return command.ExecuteNonQuery();
    }
}
Now in the business logic layer, we will add a wrapper to the update method. You might think that why we do not call the data access layer directly that calling in through the business logic layer. I agree, but when you have to add some business logic to the update method in future, then you will end up messing your code. Little extra effort makes your code more organized and keeps you away from problems in future.

ProductBusinessLogicLayer.cs
using System;
using System.Data;
using System.Data.SqlClient;

public class ProductBusinessLogicLayer
{
    public DataSet GetProducts()
    {
        DataSet dsUpdatedProducts = new DataSet();
        ProductDataAccessLayer productDAL = new ProductDataAccessLayer();
        dsUpdatedProducts = productDAL.GetProducts();
        foreach(DataRow row in dsUpdatedProducts.Tables[0].Rows)
        {
            double unitPrice = Double.Parse(row["UnitPrice"].ToString());
            row["UnitPrice"] = unitPrice + unitPrice * (12.5/100);
        }        return dsUpdatedProducts;
    }

    public int UpdateProduct(int productId, string productName, string quantityPerUnit, double unitPrice, int unitsInStock, int unitsOnOrder, int reorderLevel)
    {
        ProductDataAccessLayer productDAL = new ProductDataAccessLayer();
        return productDAL.UpdateProduct(productId, productName, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel);
    }
}
Now, what I have done in the presentation layer is that a few textboxes and an update button is added just above the grid. To make it easy for you, I have assigned some default values to the textboxes as well. When the update button is clicked, the values from all required textboxes is passed to the business logic layer update method and that in turns fires the update method in the data access layer where the actual update takes place.

Products.aspx
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <style type="text/css">
        .style1
        {
            width: 125px;
        }
        .style2
        {
            width: 144px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>           
 <tr>
  <th>Column</th>
                <th>Value</th>
        </tr>           
 <tr>
                <td>Id</td>
                <td>
                    <asp:TextBox ID="ProductId" runat="server">10</asp:TextBox>
                </td>
        </tr>
        <tr>
                <td>Name</td>
                <td>
                    <asp:TextBox ID="ProductName" runat="server">Ikura</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td>Quantity</td>
                <td>
                    <asp:TextBox ID="QuantityPerUnit" runat="server">12 - 200 ml jars</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td>Unit Price</td>
                <td>
                    <asp:TextBox ID="UnitPrice" runat="server">31.20</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td>Units in stock</td>
                <td>
                    <asp:TextBox ID="UnitsInStock" runat="server">31</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td>Units on order</td>
                <td>
                    <asp:TextBox ID="UnitsOnOrder" runat="server">0</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td>Reorder level</td>
                <td>
                    <asp:TextBox ID="ReorderLevel" runat="server">0</asp:TextBox>
                </td>
        </tr>
 <tr>
                <td></td>
                <td>
                    <asp:Button ID="btnUpdate" runat="server" Text="Update" onclick="btnUpdate_Click" />
                </td>
        </tr>
        </table>
        <asp:GridView ID="GridView1" runat="server"></asp:GridView>
    </div>
    </form>
</body>
</html>
Products.aspx.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ProductBusinessLogicLayer productBAL = new ProductBusinessLogicLayer();
        GridView1.DataSource = productBAL.GetProducts();
        GridView1.DataBind();
    }
    protected void btnUpdate_Click(object sender, EventArgs e)
    {
        ProductBusinessLogicLayer productBAL = new ProductBusinessLogicLayer();
        int res = productBAL.UpdateProduct(Int32.Parse(ProductId.Text), ProductName.Text, QuantityPerUnit.Text, Double.Parse(UnitPrice.Text), Int32.Parse(UnitsInStock.Text), Int32.Parse(UnitsOnOrder.Text), Int32.Parse(ReorderLevel.Text));
    }
}
Similarly, we can extend the class to include actions like inserting into or deleting from database.

Seems like we have a perfect architecture in hand, but I feel something is still wrong. You feel the same? Good! Let’s see where the problem is. We have an update method added that updates the fields in the Products table. I have intentionally left many columns of the table and not included them in the example to keep it simple. If the table is really huge with a number of columns, how lengthy our update methods’ parameter list is going to be. Also maintaining the fields by passing them as parameters is a complicated solution. We need a handy solution to maintain these fields.

To find a solution, let us think different. Here, a Product is an entity or say an object for us. All the fields we are talking about are its attributes. What if we create a class called Product that has the fields as its properties! We will be dealing with Product objects instead of a long list of parameters. Sounds better? Lets proceed with creating a Product class.

Product.cs
public class Product
{
    int productId = 0;
    string productName = string.Empty;
    string quantityPerUnit = string.Empty;
    double unitPrice = 0;
    int unitsInStock = 0;
    int unitsOnOrder = 0;
    int reorderLevel = 0;
 
    public int ProductId
    {
        get { return productId; }
        set { productId = value; }
    }
 
    public string ProductName
    {
        get { return productName; }
        set { productName = value; }
    }
 
    public string QuantityPerUnit
    {
        get { return quantityPerUnit; }
        set { quantityPerUnit = value; }
    }
 
    public double UnitPrice
    {
       get { return unitPrice; }
       set { unitPrice = value; }
    }
 
    public int UnitsInStock
    {
       get { return unitsInStock; }
       set { unitsInStock = value; }
    }
 
    public int UnitsOnOrder
    {
       get { return unitsOnOrder; }
       set { unitsOnOrder = value; }
    }
 
    public int ReorderLevel
    {
       get { return reorderLevel; }
       set { reorderLevel = value; }
    }
}
We will change our data access layer so that its update method accepts a Product object than a long list of parameters.

ProductDataAccessLayer.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
public class ProductDataAccessLayer
{
    string connectionString = ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ToString();
 
 public DataSet GetProducts()
    {
       SqlConnection conn = new SqlConnection(connectionString);
       DataSet dsProducts = new DataSet();
       SqlDataAdapter adapter = new SqlDataAdapter("SELECT ProductID,ProductName,UnitPrice FROM Products", conn);
       adapter.Fill(dsProducts);
       return dsProducts;
   }
 
    public int UpdateProduct(Product product)
    {
       SqlConnection conn = new SqlConnection(connectionString);
       conn.Open();
       string sql = "UPDATE Products SET ProductName = @productName, QuantityPerUnit = @quantityPerUnit, UnitPrice = @unitPrice, UnitsInStock = @unitsInStock, UnitsOnOrder = @unitsOnOrder, ReorderLevel = @reorderLevel WHERE ProductId = @productId;";
       SqlCommand command = new SqlCommand(sql, conn);
       command.Parameters.AddWithValue("@productName", product.ProductName);
       command.Parameters.AddWithValue("@quantityPerUnit", product.QuantityPerUnit);
       command.Parameters.AddWithValue("@unitPrice", product.UnitPrice);
       command.Parameters.AddWithValue("@unitsInStock", product.UnitsInStock);
       command.Parameters.AddWithValue("@unitsOnOrder", product.UnitsOnOrder);
       command.Parameters.AddWithValue("@reorderLevel", product.ReorderLevel);
       command.Parameters.AddWithValue("@productId", product.ProductId);
       return command.ExecuteNonQuery();
    }
}
Update the business logic layer also accordingly.

ProductBusinessLogicLayer.cs
using System;
using System.Data;
using System.Data.SqlClient;
public class ProductBusinessLogicLayer
{
    public DataSet GetProducts()
    {
        DataSet dsUpdatedProducts = new DataSet();
        ProductDataAccessLayer productDAL = new ProductDataAccessLayer();
        dsUpdatedProducts = productDAL.GetProducts();
  
       foreach(DataRow row in dsUpdatedProducts.Tables[0].Rows)
       {
          double unitPrice = Double.Parse(row["UnitPrice"].ToString());
          row["UnitPrice"] = unitPrice + unitPrice * (12.5/100);
       }
  
       return dsUpdatedProducts;
    }
 
    public int UpdateProduct(Product product)
    {
       ProductDataAccessLayer productDAL = new ProductDataAccessLayer();
       return productDAL.UpdateProduct(product);
    }
}
And our presentation layer will be where we will create the Product object from the values filled in the form. While the aspx file remains the same, the code-behind will now be:

Products.aspx.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ProductBusinessLogicLayer productBAL = new ProductBusinessLogicLayer();
        GridView1.DataSource = productBAL.GetProducts();
        GridView1.DataBind();
    }
 
    protected void btnUpdate_Click(object sender, EventArgs e)
    {
        Product product = new Product();
        product.ProductId = Int32.Parse(ProductId.Text);
        product.ProductName = ProductName.Text;
        product.QuantityPerUnit = QuantityPerUnit.Text;
        product.UnitPrice = Double.Parse(UnitPrice.Text);
        product.UnitsInStock = Int32.Parse(UnitsInStock.Text);
        product.UnitsOnOrder = Int32.Parse(UnitsOnOrder.Text);
        product.ReorderLevel = Int32.Parse(ReorderLevel.Text);
        ProductBusinessLogicLayer productBAL = new ProductBusinessLogicLayer();
        int res = productBAL.UpdateProduct(product);
    }
}
All we did was to add a new Business Object layer (the Product class), and we introduced a better way to deal with the Product as an object. I know you are excited and you are eagerly waiting for me to say that we have just seen 4-tier architecture with Business Objects coming into picture. We will call it Business Object Layer or better, Business Entities Layer.


The sad news is that not everyone agrees that the above follows 4-tier architecture. Often, the Business Entity Layer is considered as a sub layer of the Business Logic Layer, and hence still considered as 3-tier architecture. It won’t be a surprise if you find many applications that implement the Business Object Layer and the Business Logic Layer in the same class, while there are a number of examples where even the Business Logic Layer is spread over multiple layers of its own. Even with Data Access Layer, it is often a preferred choice to create a separate layer of Helper classes. When it comes to designing a solution, there is never a universal thumb rule!

Now that we have a very clear idea about tiered architecture including 4-tier architecture (let’s say), our next discussion will focus on layered architecture in more depth as we look at different ways of designing applications on Microsoft.NET framework.

By the way, the examples I included in this post and the earlier one, and the ones I will include further, are all meant to be simple and hence can be written in better and organized way. I just focused on defining clear layers and explaining the objectives to you. Writing neat code, managing object references and error handling are a few things that are clearly omitted. I leave them to you when you develop the real applications.

0 comments: