3-Tier Architecture Examples...
private void BindData() { CustomerBAL bal = new CustomerBAL(); DataTable customerInfo = bal.GetCustomers(20); this.gvwCustomers.DataSource = customerInfo; this.gvwCustomers.DataBind(); }Listing 1: ADO.NET Use in the UI As you can see, the data table is bound to the UI control manually; however, the ObjectDataSource control could replace this approach. To retrieve the DataTable object with the Customer data, the code makes use of a CustomerBAL class. This class provides access to CRUD operations against customer data. The makeup of the GetCustomers() method is as follows:
public class CustomerBAL { public DataTable GetCustomers(int pageCount) { if (pageCount < 0) throw new ArgumentOutOfRangeException(“pageCount”); CustomerDAL dal = new CustomerDAL(); DataTable results = dal.GetCustomers(pageCount); if (results.HasErrors) throw new Exception(); } }Listing 2: ADO.NET Business Layer – GetCustomers method The business layer validated the input coming into the method, as well as the output, ensuring that the data didn't have any erroring information. If not, the results are returned to the user. In my example, I throw an exception, but handle it anyway you want, by logging the errors, returning it to the caller, etc. But how does the data actually get to the presentation layer? The data layer receives a request for data and actually queries the database, returning the data to the business layer, as shown below:
public class CustomerDAL { public DataTable GetCustomers(int pageCount) { SqlConnection connection = new SqlConnection( ConfigurationManager.ConnectionStrings[0].ConnectionString); SqlDataAdapter adapter = new SqlDataAdapter(“spSelectCustomers”, Connection); DataTable table = new DataTable(); adapter.Fill(table); return table; } }Listing 3: ADO.NET Data Layer – GetCustomers method The next question is how are inserts, updates, and deletes performed? While retrieval of data is satisfied, other data manipulation topics weren’t covered. I cover the subject of data manipulation at the end of the article.
public class CustomerBAL : BaseBusinessComponent { #region " Methods " public SamplesDataSet.CustomersDataTable GetAll() { CustomersTableAdapter adapter = new CustomersTableAdapter(); SamplesDataSet.CustomersDataTable table = new SamplesDataSet.CustomersDataTable(); adapter.FillCustomers(table); base.HandleErrors(table); return table; } public SamplesDataSet.CustomersRow GetByAccountNumber(string accountNumber) { if (string.IsNullOrEmpty(accountNumber)) throw new ArgumentNullException(“accountNumber”); CustomersTableAdapter adapter = new CustomersTableAdapter(); SamplesDataSet.CustomersDataTable table = new SamplesDataSet.CustomersDataTable(); adapter.FillByAccountNumber(table, accountNumber); base.HandleErrors(table); if (table.Rows.Count == 0) return null; else return (SamplesDataSet.CustomersRow)table.Rows[0]; } #endregion }Listing 5: Business Component for Retrieving Customer Data The code above uses the FillBX, rather than the GetX, method to retrieve the data. I do this because I can check the data for errors using the HandleErrors method (defined in the base class, which simply checks the HasErrors property), and if OK return the correct results back. Note that the input is validated in the business component.
public class CustomerDAL : BaseDataAccessComponent { #region " Methods " public DataTable GetAll() { Database database = DatabaseFactory.CreateDatabase(); DbCommand command = database.GetStoredProcCommand("CustomersGetAll"); DataSet dataset = new DataSet(); database.LoadDataSet(command, dataset, "Customers"); return dataset.Tables[0]; } public DataTable GetByAccountNumber(string accountNumber) { Database database = DatabaseFactory.CreateDatabase(); DbCommand command = database.GetStoredProcCommand("CustomersGetByAcct"); DataSet dataset = new DataSet(); database.LoadDataSet(command, dataset, "Customers"); } #endregion }Listing 6: Enterprise Library Note the difference in the approach above; Enterprise Library uses a Database object as the central point of contact. DatabaseFactory.CreateDatabase() provides the way that Enterprise Library connects to the correct database through a provider. The CreateDatabase method uses either an empty constructor (pulls the database connection from the configuration file) or it takes the name of a connection string (using one of the connection strings in the <connectionStrings> element). Rather than accessing this directly, the business layer connects to the data layer as below (only one of the methods is shown). Because the data is transported via a DataTable object, it uses the same approach as shown above.
public class CustomerBAL : BaseBusinessComponent { #region " Methods " … public DataRow GetByAccountNumber(string accountNumber) { if (string.IsNullOrEmpty(accountNumber)) throw new ArgumentNullException("accountNumber"); CustomerDAL dal = new CustomerDAL(); DataTable table = dal.GetByAccountNumber(accountNumber); base.HandleErrors(table); if (table.Rows.Count > 1) throw new DataException(@"There are too many rows coming back with account number: " + accountNumber); if (table.Rows.Count == 0) return null; else return table.Rows[0]; } #endregion }Listing 7: Business Object Validating Input, Retrieving Data, Validating Output In the method above, the business layer validates the input and output received from the data layer. In this way, the business layer ensures the business rules of the system are intact and correct. It also improves the quality of the data.
public CustomerDAL(SamplesDataContext context) : base(context) { }Listing 8: Data Layer Constructor In the business layer, the DataContext needs to be passed in as well. There can be some variants to this approach, but unfortunately that’s out of scope. Getting back to the data layer, LINQ-to-SQL translates LINQ queries into SQL queries, and returns the data as a collection.
public IEnumerable<Customer> GetAll() { var results = from c in this.Context.Customers orderby c.LastName, c.FirstName select c; return results; } public Customer GetByAccountNumber(string accountNumber) { var results = from c in this.Context.Customers where c.AccountNumber == accountNumber select c; return results.FirstOrDefault(); }Listing 9: Data Layer LINQ queries In the first method, the data returns to the caller as an enumerable list; queries are returned in IOrderedQueryable<> form; however, IEnumerable<> will work as well. In the second approach, a Customer object is returned by using the FirstOrDefault() method, which returns the Customer if a match found and null if not found. The business layer calls the data layer, and returns the results. In the GetByAccountNumber method, the input is validated.
private CustomerDAL ConstructComponent() { return new CustomerDAL(this.Context); } public IEnumerable<Customer> GetAll() { CustomerDAL dal = this.ConstructComponent(); return dal.GetAll(); } public Customer GetByAccountNumber(string accountNumber) { if (string.IsNullOrEmpty(accountNumber)) throw new ArgumentNullException("accountNumber"); CustomerDAL dal = this.ConstructComponent(); return dal.GetByAccountNumber(accountNumber); }Listing 10: LINQ Data Access Layer
Public Customer InsertNew(Guid customerKey, string customerName, string account) { //Create new record }Listing 11: Parameterized DML Method This approach works well in ASP.NET, where data source controls utilize this approach to take. Personally, I don't like this approach simply because future requirements or changes to the parameter list break the interface of the method. Although overloaded methods can be added, that's not the best option. I prefer to create a new instance of an object or record in the application, and let the business layer validate the input using a process for business rules. For instance, if creating a new customer, I prefer an approach like this:
Public Customer InsertNew(Customer customer)
{
//Validate properties, and submit to database
}
Listing 12: Alternative DML Method Approach
Using the strongly-typed dataset architecture, the newly created row in the user interface is passed in as a parameter to the Insert method (more on that in a moment).
This method updates the inserted row. If there are any errors, an output string is created and is the source of the exception being thrown. Customer object references
like this also work well with other validation tools like the Validation Application Block.
public void Insert(SamplesDataSet.CustomersRow insertedRow) { CustomersTableAdapter adapter = new CustomersTableAdapter(); adapter.Update(insertedRow); if (insertedRow.HasErrors) { DataColumn[] columns = insertedRow.GetColumnsInError(); string output = null; foreach (DataColumn column in columns) output += insertedRow.GetColumnError(column); throw new DataException(output); } }Listing 13: Insert Customer Row Approach However, an exception doesn't always have to be thrown. Instead, the alternative approach can be to store the error information in a property of the business object. This object can be a custom error object that you create, a string value containing the message, or a reference to the exception that was thrown. Using this property, an ASP.NET page or windows form can use this to output a message to the screen. Take a look at a possible example using ASP.NET; note the code is in a custom page class:
private void InsertCustomer(Customer customer) { CustomerBAL bal = new CustomerBAL(); bal.InsertNew(customer); if (bal.Error != null) { Label errorLabel = this.Master.FindControl(“lblError”) as Label; If (errorLabel != null) errorLabel.Text = bal.Error.Message; } }Listing 14: Error Handling in ASP.NET This may not be the most practical in your situation, but the choice is up to you how you want to handle errors that occur in your business layer.