Application Security Testing: An Integral Part of DevOps
My first article on this topic demonstrated database reads-to-entities. Reads are relatively easy. Just read one or more result sets and build objects; you don't need transactions. This article demonstrates database writes. The write part of managing a data access layer (DAL) is where things can go awry. Writing requires managing changes, validation, transactions, and a variety of changes, including new, changed, and deleted objects.
This article demonstrates how to add and modify objects in a collection and then, using the basic DAL introduced in my previous article, how to manage writing those changes back to the persistence store. Although the code sample implicitly demonstrates insert and update behaviors, the text provides hints for handling validation and deletion too.
Modifying the Custom Objects
This example uses a simple console application to modify objects. (Listing 1 shows the code for the demo.) The beauty of a custom objects approach is that literally any kind of client can be used to modify the objects; they are just classes. Custom business objects also are lightweight and easy to use, and they behave in a persistence-layer-agnostic way.
Listing 1: Custom Business Objects Demo
Imports MYVB.BusinessObjects Imports MYVB.DataAccess Module Module1 Sub Main() Dim customers As List(Of Customer) = _ CustomerAccess.GetCustomers() For Each c As Customer In customers Console.WriteLine(c.ContactName) Next ' Add a customer, modify a second customer, and save Dim newCustomer As Customer = New Customer( _ "PAUKI", "Fatman Soda Company", "P. Kimmel", _ "Boss", "", "", "", "", "", "", "") customers.Add(newCustomer) customers(0).ContactName = "Paul Kimmel" ' Trying to write all is terribly inefficient ' You could A) track changes or B) insert only those you ' know are new or have changed CustomerAccess.PutCustomers(customers) Console.ReadLine() End Sub End Module
As you can see, you can modify the collection and objects in any way desired. All of the writing is done at the collection level. However, you could easily write one object at a time, manage deletes, or incorporate change management logic to promote writing only objects that have changed.
Implementing the Write Behavior
For the CustomerAccess class to work with the basic DAL, you simply need to orchestrate calling the basic DataAccess class's Write method (see Listing 2). The DataAccess.Write method needs to know the stored procedure to call (although you could include a literal SQL overloaded method), what kind of object it is writing, a write event handler, whether anything comes back from the write, and the list of parameters and values to send to the stored procedure.
Listing 2: Both DataAccess.Write Methods
Public Shared Sub Write(Of T)(ByVal o As T, _ ByVal procedureName As String, _ ByVal handler As WriteEventHandler(Of T), _ ByVal ParamArray parameters() As IDbDataParameter) Using connection As IDbConnection = _ factory.CreateConnection(factory.ConnectionString) connection.Open() Write(o, procedureName, handler, connection, Nothing, parameters) End Using End Sub Public Shared Sub Write(Of T)(ByVal o As T, _ ByVal procedureName As String, _ ByVal handler As WriteEventHandler(Of T), _ ByVal connection As IDbConnection, _ ByVal transaction As IDbTransaction, _ ByVal ParamArray parameters() As IDbDataParameter) Dim command As IDbCommand = factory.CreateCommand(procedureName) command.CommandType = CommandType.StoredProcedure command.Connection = connection command.Transaction = transaction If (parameters Is Nothing = False) Then Dim p As IDbDataParameter For Each p In parameters command.Parameters.Add(p) Next End If command.ExecuteNonQuery() If (handler <> Nothing) Then handler(o, command) End If End Sub
The first version of Write creates a connection for you, and the second version permits passing in both a connection and a transaction.
Because the basic DataAccess class manages just the general behavior, you do need to extend the specific entity data-access classes to indicate the objects to act on and how to act on them. For example, writing a Customer means you have to tell the DataAccess class about Customer stored procedures and Customer fields. The complete listing for the CustomerAccess class is shown in Listing 3, and the remaining sub-sections describe each of the new elements.
Listing 3: The Complete Implementation of the CustomerAccess Class
Imports System Imports System.Data Imports System.Diagnostics Imports System.Configuration Imports System.Transactions Imports System.Collections.Generic Imports System.Web Imports MYVB.BusinessObjects Public Class CustomerAccess Public Shared Function GetCustomers() As List(Of Customer) Return DataAccess.Read(Of List(Of Customer))("GetCustomers", _ AddressOf OnReadCustomers) End Function Public Shared Sub PutCustomers(ByVal customers As List(Of Customer)) ' Reference and import System.Transactions Using scope As TransactionScope = New _ TransactionScope(TransactionScopeOption.Required) For Each c As Customer In customers PutCustomer(c) Next scope.Complete() End Using End Sub ' what if unique key is auto. Use write handler to set after insert ' change stored proedure to insert on null key ' key becomes output parameters Public Shared Sub PutCustomer(ByVal cust As Customer) Debug.Assert(cust Is Nothing = False) Dim Factory As DbFactory = DbFactory.CreateFactory() Using connection As IDbConnection = Factory.CreateConnection( _ Factory.ConnectionString) connection.Open() ' build parameters and write Dim customerID As IDbDataParameter = _ Factory.CreateParameter("@CustomerID", cust.CustomerID) customerID.DbType = DbType.String customerID.Size = 5 customerID.Direction = ParameterDirection.Input Dim companyName As IDbDataParameter = _ Factory.CreateParameter("@CompanyName", cust.CompanyName) companyName.DbType = DbType.String companyName.Size = 40 companyName.Direction = ParameterDirection.Input Dim contactName As IDbDataParameter = _ Factory.CreateParameter("@ContactName", cust.ContactName) contactName.DbType = DbType.String contactName.Size = 30 contactName.Direction = ParameterDirection.Input Dim contactTitle As IDbDataParameter = _ Factory.CreateParameter("@ContactTitle", cust.ContactTitle) contactTitle.DbType = DbType.String contactTitle.Size = 30 contactTitle.Direction = ParameterDirection.Input Dim address As IDbDataParameter = _ Factory.CreateParameter("@Address", cust.Address) address.DbType = DbType.String address.Size = 60 address.Direction = ParameterDirection.Input Dim city As IDbDataParameter = _ Factory.CreateParameter("@City", cust.City) city.DbType = DbType.String city.Size = 15 city.Direction = ParameterDirection.Input Dim region As IDbDataParameter = _ Factory.CreateParameter("@Region", cust.Region) region.DbType = DbType.String region.Size = 15 region.Direction = ParameterDirection.Input Dim postalCode As IDbDataParameter = _ Factory.CreateParameter("@PostalCode", cust.PostalCode) postalCode.DbType = DbType.String postalCode.Size = 10 postalCode.Direction = ParameterDirection.Input Dim country As IDbDataParameter = _p> Factory.CreateParameter("@Country", cust.Country) country.DbType = DbType.String country.Size = 15 country.Direction = ParameterDirection.Input Dim As IDbDataParameter = _ Factory.CreateParameter("@Phone", cust.Phone) phone.DbType = DbType.String phone.Size = 24 phone.Direction = ParameterDirection.Input Dim fax As IDbDataParameter = _ Factory.CreateParameter("@Fax", cust.Fax) fax.DbType = DbType.String fax.Size = 24 fax.Direction = ParameterDirection.Input DataAccess.Write(Of Customer)(cust, _ "PutCustomer", Nothing, connection, Nothing, _ customerID, _ companyName, _ contactName, _ contactTitle, _ address, _ city, _ region, _ postalCode, _ country, _ phone, _ fax) End Using End Sub 'ALTER PROCEDURE dbo.GetCustomersAndOrders 'AS 'SELECT * FROM CUSTOMERS * FROM ORDERS Public Shared Function GetCustomersWithOrders() As List(Of Customer) Return DataAccess.Read(Of List(Of Customer)) _ ("GetCustomersAndOrders", _ AddressOf OnReadCustomersWithOrders) End Function Public Shared Function OnReadCustomersWithOrders( _ ByVal reader As IDataReader) As List(Of Customer) Debug.Assert(reader Is Nothing = False) Dim customers As List(Of Customer) = New List(Of Customer)() If (reader Is Nothing) Then Return customers customers = OnReadCustomers(reader) If (reader.NextResult()) Then Dim allOrders As List(Of Orders) = _ DataAccess.OnReadAnyList(Of Orders)(reader) Dim customer As Customer Dim _order As Orders For Each customer In customers For Each _order In allOrders If (_order.CustomerID = customer.CustomerID) Then customer.CustomerOrders.Add(_order) End If Next Next End If Return customers End Function Public Shared Function OnReadCustomers( _ ByVal reader As IDataReader) As List(Of Customer) If (reader Is Nothing) Then Return New List(Of Customer)() Dim customers As List(Of Customer) = New List(Of Customer)() While (reader.Read()) customers.Add(OnReadCustomer(reader)) End While Return customers End Function Private Shared Function OnReadCustomer( _ ByVal reader As IDataReader) As Customer Debug.Assert(reader Is Nothing = False) Dim customerID As String = "" Dim companyName As String = "" Dim contactName As String = "" Dim contactTitle As String = "" Dim address As String = "" Dim city As String = "" Dim region As String = "" Dim postalCode As String = "" Dim country As String = "" Dim phone As String = "" Dim fax As String = "" customerID = DataAccess.SafeRead(Of String)(customerID, _ reader, "CustomerID") companyName = DataAccess.SafeRead(Of String)(companyName, reader, _ "CompanyName") contactName = DataAccess.SafeRead(Of String)(contactName, reader, _ "ContactName") contactTitle = DataAccess.SafeRead(Of String)(contactTitle, reader, _ "ContactTitle") address = DataAccess.SafeRead(Of String)(address, reader, "Address") city = DataAccess.SafeRead(Of String)(city, reader, "City") region = DataAccess.SafeRead(Of String)(region, reader, "Region") postalCode = DataAccess.SafeRead(Of String)(postalCode, reader, _ "PostalCode") country = DataAccess.SafeRead(Of String)(country, reader, "Country") phone = DataAccess.SafeRead(Of String)(phone, reader, "Phone") fax = DataAccess.SafeRead(Of String)(fax, reader, "Fax") Return New Customer(customerID, companyName, contactName, _ contactTitle, address, city, region, postalCode, country, _ phone, fax) End Function End Class
Although this code is straightforward and monolithic, its consistency across entities in any domain promotes code generation. At a minimum, a tool like CodeRush (when it finally offers a VB version) will make writing this code a breeze.