Showing posts with label Data Access Patterns. Show all posts
Showing posts with label Data Access Patterns. Show all posts

Monday, April 19, 2010

AAPL Part 5: How to Write a Data Access Object (DAO)

I recently decided that I needed to take a fresh look at how to build a persistence architecture that would provide the flexibility of an ORM, that would embrace change instead of resist it (making maintenance code easier), and would use ADO.Net.  I started building from the ground up, threw away any best practices that resulted in friction, and Agile ADO.Net Persistence Layer is the result.  Part 1 in the series can be found here: Agile ADO.Net Persistence Layer Overview

In this post, we’ll get into what is probably the most useful class in the framework, the Data Access Object (DAO).

Where does the DAO fit in?

Let’s backtrack a bit and take a quick look at the design of core.  Below we see a snapshot of Core’s folder structure.  We have a Persistence folder that contains our SqlDAO (a DAO written specifically for SqlServer), as well as our DataMappers and Persisters (we’ll go into these later).  The Rules folder contains business rules and validation rules for different entities / data shapes.  Security has a little plumbing code for business layer authorization.  Finally, the Services folder contains the service classes that we’ve focused on during the previous few posts. 

image

The service classes are the one stop shop that our UI code uses to get and save data.  A typical data access method looks like this.

public User GetUserByEmail(string email)

{

    string sql = @"SELECT *

                  FROM [User]

                  WHERE [Email] = @Email";

    SqlDao dao = SharedSqlDao;

    SqlCommand command = dao.GetSqlCommand(sql);

    command.Parameters.Add(dao.CreateParameter("@Email", email, 100));

    return dao.GetSingle<User>(command);

}

You’ll notice that this is a pretty simple method.  There’s no real Ado.Net code. We’re just defining a query, wrapping it in an SqlCommand, and then passing both the SqlCommand and our desired data shape <User> to our DAO.  This mystery DAO object does all the heavy lifting for us and even helps us create our query parameter in very efficient, single line of code.  This is a very intentional design.  After writing data access code for a few years, I finally noticed that the only things I really change from one data access method to the next are: the query; the parameters; and the return data shape. Everything else is just boilerplate plumbing code that gets repeated over and over again.  So, I took all that data access boilerplate code and extracted it out to a helper class called SqlDao.  The main functions of the DAO are:

  • The DAO is the single access point to our database. All communication with the database goes through the DAO.  No code goes around it.
  • The DAO encapsulates common ADO.Net tasks like getting a connection, creating query parameters, executing queries, etc.
  • The DAO defines methods that take an SqlCommand as a parameter, execute it, and return the data in one of our standard formats, a single DTO, as List<DTO>, a DataPage<DTO>, or a string value.     

There’s a lot of good stuff in there that makes writing data access method almost frictionless.  I can’t go over everything so I included a complete listing for the SqlDao at the end of this post.

The GetSingle<T> and GetList<T> methods

Now let’s look at something a little more interesting. The SqlDao.GetSingle<T> method takes an SqlCommand as a parameter and returns the restults of that command as a single T where T is the class of our desired datashape.  So, the line dao.GetSingle<User>(command); will execute command and then return an object of type User.  If no User was found then the method just returns null.  Here’s how it works.

// GetSingle

public T GetSingle<T>(SqlCommand command) where T : class

{

    T dto = null;

    try

    {

        if (command.Connection.State != ConnectionState.Open)

        {

            command.Connection.Open();

        }               

        SqlDataReader reader = command.ExecuteReader();

        if (reader.HasRows)

        {

            reader.Read();

            IDataMapper mapper = new DataMapperFactory().GetMapper(typeof(T));

            dto = (T)mapper.GetData(reader);

            reader.Close();

        }

    }

    catch (Exception e)

    {

        throw new Exception("Error populating data", e);

    }

    finally

    {

        command.Connection.Close();

    }

    // return the DTO, it's either populated with data or null.

    return dto;

}

The code is pretty simple.  We declare a new object of type T called dto (Data Transfer Object) and initialize it to null. We then check the connection on our command and make sure it’s open.  We call ExecuteReader on the command which gets us an SqlDataReader.  If the reader has any rows then we go to our DataMapperFactory and get an IDataMapper for the type T.  A cool thing that is happening behind the scenes is that the DataMapperFactory checks to see if we have an IDataMapper that has been created specifically for type T, if we don’t it returns a GenericMapper that uses reflection to map SqlDataReader fields to fields on our type T.  Once we have a mapper it’s just a matter of calling the mapper.GetData(reader) method and returning the dto. 

Now let’s take a quick look at GetList<T>.  This method is very similar to GetSingle<T>, but it returns a generic list of objects of type T instead of a single object of type T.  If no results are returned when we execute the SqlCommand passed to GetList<T>, we don’t return null, instead we return an empty list.  Here’s the code.                   

// GetList

public List<T> GetList<T>(SqlCommand command) where T : class

{

    List<T> dtoList = new List<T>();

    try

    {

        if (command.Connection.State != ConnectionState.Open)

        {

            command.Connection.Open();

        }  

        SqlDataReader reader = command.ExecuteReader();

        if (reader.HasRows)

        {

            IDataMapper mapper = new DataMapperFactory().GetMapper(typeof(T));

            while (reader.Read())

            {

                T dto = null;

                dto = (T)mapper.GetData(reader);

                dtoList.Add(dto);

            }

            reader.Close();

        }

    }

    catch (Exception e)

    {

        throw new Exception("Error populating data", e);

    }

    finally

    {

        command.Connection.Close();

    }

    // We return either the populated list if there was data,

    // or if there was no data we return an empty list.

    return dtoList;

}

It’s really the same as GetSingle<T>.  We just have a different return type List<T>, and a while loop that get’s our individual DTOs and adds them to our return dtoList. The main logic for getting an IDataMapper from the DataMapperFactory and calling mapper.GetData(reader) to parse the reader results into an object of type T remains the same.

Conclusion

At this point you may be thinking, “That’s it???, you didn’t answer anything!  All you did is get some mapper object and call the GetData method on it.”  That’s true to some extent.  Our GetSingle<T> and GetList<T> methods just contain the logic that is always going to be common to those tasks regardless of the data shape passed to them.  Anything that is data shape specific, things like how do I get data from this field in my SqlDataReader and put it in this field in my returned data shape, are delegated to the IDataMapper.  This separation of logic means that it’s very easy to maintain our code.  We can add DataMapper logic for new data shapes and never touch any of the logic for GetSingle, GetList, and GetPage.  In fact, our logic for a specific data shape is encapsulated in a single IDataMapper class that touches nothing else in the application. That means it’s easy to change and maintenance friendly.  Plus, don’t forget that we have the reflection-based GenericMapper, so we never have to create a new IDataMapper if we don’t want to.

Next time we’ll take a look at a data shape specific IDataMapper and the DataMapperFactory. In the meantime, the full source for SqlDao is included below.  Have a look at the implementation for GetDataPage<T> and see if it makes sense now that we’ve gone over GetSingle<T> and GetList<T>.  The CreateParameter methods are worth a look too.   I went with a design where I just overload the method CreateParameter for each type of parameter that I need to create, so the usage syntax for CreateParameter(Guid value) and CreateParameter(int value) are almost identical. Each CreateParameter method was designed with the target of enabling that single line syntax that I use: command.Parameters.Add(dao.CreateParameter("@Email", email, 100)); so each method contains automatic null value checking.  You’ll also see some methods that weren’t in my original spec.  While using this framework on different projects I’ve found it convenient to have methods like GetSingleInt32(command) and GetStringList(command). 

Finally you can download a small, one page sample app that uses aapl at http://aapl.codeplex.com/

 

using System;

using System.Linq;

using System.Collections.Generic;

using System.Data.SqlClient;

using System.Data;

using System.Configuration;

using hf.Common;

using hf.Common.DataShapes;

using hf.Core.Persistence.DataMappers;

 

 

namespace hf.Core.Persistence

{

    public class SqlDao

    {

 

        #region "Database Helper Methods"

 

 

        // Connection

        private SqlConnection _sharedConnection;

        public SqlConnection SharedConnection

        {

            get

            {

                if (_sharedConnection == null)

                {

                    _sharedConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["hf"].ConnectionString);

                }

                return _sharedConnection;

            }

            set

            {

                _sharedConnection = value;

            }

        }

 

 

        // Constructors

        public SqlDao() { }

        public SqlDao(SqlConnection connection)

        {

            this.SharedConnection = connection;

        }

 

 

        // GetDbSqlCommand

        public SqlCommand GetSqlCommand(string sqlQuery)

        {

            SqlCommand command = new SqlCommand();

            command.Connection = SharedConnection;

            command.CommandType = CommandType.Text;

            command.CommandText = sqlQuery;

            return command;

        }

 

 

        // GetDbSprocCommand

        public  SqlCommand GetSprocCommand(string sprocName)

        {

            SqlCommand command = new SqlCommand(sprocName);

            command.Connection = SharedConnection;

            command.CommandType = CommandType.StoredProcedure;

            return command;

        }

 

 

        // CreateNullParameter

        public  SqlParameter CreateNullParameter(string name, SqlDbType paramType)

        {

            SqlParameter parameter = new SqlParameter();

            parameter.SqlDbType = paramType;

            parameter.ParameterName = name;

            parameter.Value = DBNull.Value;

            parameter.Direction = ParameterDirection.Input;

            return parameter;

        }

 

 

        // CreateNullParameter - with size for nvarchars

        public  SqlParameter CreateNullParameter(string name, SqlDbType paramType, int size)

        {

            SqlParameter parameter = new SqlParameter();

            parameter.SqlDbType = paramType;

            parameter.ParameterName = name;

            parameter.Size = size;

            parameter.Value = DBNull.Value;

            parameter.Direction = ParameterDirection.Input;

            return parameter;

        }

 

 

        // CreateOutputParameter

        public  SqlParameter CreateOutputParameter(string name, SqlDbType paramType)

        {

            SqlParameter parameter = new SqlParameter();

            parameter.SqlDbType = paramType;

            parameter.ParameterName = name;

            parameter.Direction = ParameterDirection.Output;

            return parameter;

        }

 

 

        // CreateOuputParameter - with size for nvarchars

        public  SqlParameter CreateOutputParameter(string name, SqlDbType paramType, int size)

        {

            SqlParameter parameter = new SqlParameter();

            parameter.SqlDbType = paramType;

            parameter.Size = size;

            parameter.ParameterName = name;

            parameter.Direction = ParameterDirection.Output;

            return parameter;

        }

 

 

        // CreateParameter - uniqueidentifier

        public  SqlParameter CreateParameter(string name, Guid value)

        {

            if (value.Equals(NullValues.NullGuid))

            {

                // If value is null then create a null parameter

                return CreateNullParameter(name, SqlDbType.UniqueIdentifier);

            }

            else

            {

                SqlParameter parameter = new SqlParameter();

                parameter.SqlDbType = SqlDbType.UniqueIdentifier;

                parameter.ParameterName = name;

                parameter.Value = value;

                parameter.Direction = ParameterDirection.Input;

                return parameter;

            }

        }

 

 

        // CreateParameter - int

        public  SqlParameter CreateParameter(string name, int value)

        {

            if (value == NullValues.NullInt)

            {

                // If value is null then create a null parameter

                return CreateNullParameter(name, SqlDbType.Int);

            }

            else

            {

                SqlParameter parameter = new SqlParameter();

                parameter.SqlDbType = SqlDbType.Int;

                parameter.ParameterName = name;

                parameter.Value = value;

                parameter.Direction = ParameterDirection.Input;

                return parameter;

            }

        }

 

 

        // CreateParameter - bool

        public SqlParameter CreateParameter(string name, bool value)

        {

            SqlParameter parameter = new SqlParameter();

            parameter.SqlDbType = SqlDbType.Bit;

            parameter.ParameterName = name;

            parameter.Value = value ? 1 : 0;

            parameter.Direction = ParameterDirection.Input;

            return parameter;

        }

 

 

        // CreateParameter - datetime

        public  SqlParameter CreateParameter(string name, DateTime value)

        {

            if (value == NullValues.NullDateTime)

            {

                // If value is null then create a null parameter

                return CreateNullParameter(name, SqlDbType.DateTime);

            }

            else

            {

                SqlParameter parameter = new SqlParameter();

                parameter.SqlDbType = SqlDbType.DateTime;

                parameter.ParameterName = name;

                parameter.Value = value;

                parameter.Direction = ParameterDirection.Input;

                return parameter;

            }

        }

 

 

        // CreateParameter - nvarchar

        public  SqlParameter CreateParameter(string name, string value, int size)

        {

            if (String.IsNullOrEmpty(value))

            {

                // If value is null then create a null parameter

                return CreateNullParameter(name, SqlDbType.NVarChar);

            }

            else

            {

                SqlParameter parameter = new SqlParameter();

                parameter.SqlDbType = SqlDbType.NVarChar;

                parameter.Size = size;

                parameter.ParameterName = name;

                parameter.Value = value;

                parameter.Direction = ParameterDirection.Input;

                return parameter;

            }

        }

 

 

        // CreateTextParameter

        public SqlParameter CreateTextParameter(string name, string value)

        {

            if (String.IsNullOrEmpty(value))

            {

                // If value is null then create a null parameter

                return CreateNullParameter(name, SqlDbType.Text);

            }

            else

            {

                SqlParameter parameter = new SqlParameter();

                parameter.SqlDbType = SqlDbType.Text;

                parameter.ParameterName = name;

                parameter.Value = value;

                parameter.Direction = ParameterDirection.Input;

                return parameter;

            }

        }

 

        #endregion

 

 

 

        #region "Data Projection Methods"

 

 

        // ExecuteNonQuery

        public void ExecuteNonQuery(SqlCommand command)

        {

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                command.ExecuteNonQuery();

            }

            catch (Exception e)

            {

                throw new Exception("Error executing query", e);

            }

            finally

            {

                command.Connection.Close();

            }

        }

 

 

        // ExecuteScalar

        public Object ExecuteScalar(SqlCommand command)

        {

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                return command.ExecuteScalar();

            }

            catch (Exception e)

            {

                throw new Exception("Error executing query", e);

            }

            finally

            {

                command.Connection.Close();

            }

        }

 

 

        // GetSingleValue

        public T GetSingleValue<T>(SqlCommand command)

        {

            T returnValue = default(T);

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    reader.Read();

                    if (!reader.IsDBNull(0)) { returnValue = (T)reader[0]; }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return returnValue;

        }

 

 

        // GetSingleInt32

        public Int32 GetSingleInt32(SqlCommand command)

        {

            Int32 returnValue = default(int);

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    reader.Read();

                    if (!reader.IsDBNull(0)) { returnValue = reader.GetInt32(0); }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return returnValue;

        }

 

 

        // GetSingleString

        public string GetSingleString(SqlCommand command)

        {

            string returnValue=null;

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    reader.Read();

                    if (!reader.IsDBNull(0)) { returnValue = reader.GetString(0); }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return returnValue;

        }

 

 

        // GetGuidList

        public List<Guid> GetGuidList(SqlCommand command)

        {

            List<Guid> returnList = new List<Guid>();

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    returnList = new List<Guid>();

                    while (reader.Read())

                    {

                        if (!reader.IsDBNull(0)) { returnList.Add(reader.GetGuid(0)); }

                    }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return returnList;

        }

 

 

        // GetStringList

        public List<string> GetStringList(SqlCommand command)

        {

            List<string> returnList = new List<string>();

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    returnList = new List<string>();

                    while (reader.Read())

                    {

                        if (!reader.IsDBNull(0)) { returnList.Add(reader.GetString(0)); }                   

                    }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return returnList;

        }

 

 

        // GetSingle

        public T GetSingle<T>(SqlCommand command) where T : class

        {

            T dto = null;

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }               

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    reader.Read();

                    IDataMapper mapper = new DataMapperFactory().GetMapper(typeof(T));

                    dto = (T)mapper.GetData(reader);

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            // return the DTO, it's either populated with data or null.

            return dto;

        }

 

 

        // GetList

        public List<T> GetList<T>(SqlCommand command) where T : class

        {

            List<T> dtoList = new List<T>();

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }  

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    IDataMapper mapper = new DataMapperFactory().GetMapper(typeof(T));

                    while (reader.Read())

                    {

                        T dto = null;

                        dto = (T)mapper.GetData(reader);

                        dtoList.Add(dto);

                    }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            // We return either the populated list if there was data,

            // or if there was no data we return an empty list.

            return dtoList;

        }

 

 

 

 

        // GetDataPage

        public DataPage<T> GetDataPage<T>(SqlCommand command, int pageIndex, int pageSize) where T : class

        {  

            DataPage<T> page = new DataPage<T>();

            page.PageIndex = pageIndex;

            page.PageSize = pageSize;

            try

            {

                if (command.Connection.State != ConnectionState.Open)

                {

                    command.Connection.Open();

                }

                SqlDataReader reader = command.ExecuteReader();

                if (reader.HasRows)

                {

                    IDataMapper mapper = new DataMapperFactory().GetMapper(typeof(T));

                    while (reader.Read())

                    {

                        // get the data for this row

                        T dto = null;

                        dto = (T)mapper.GetData(reader);

                        page.Data.Add(dto);

                        // If we haven't set the RecordCount yet then set it

                        if (page.RecordCount == 0) { page.RecordCount = mapper.GetRecordCount(reader); }

                    }

                    reader.Close();

                }

            }

            catch (Exception e)

            {

                throw new Exception("Error populating data", e);

            }

            finally

            {

                command.Connection.Close();

            }

            return page;

        }

 

        #endregion

 

    }

}

Tuesday, January 19, 2010

Agile ADO.Net Persistence Layer: Part 3 Service Class Single<DTO> Data Access Method

When I say data access methods, I’m talking about the methods my UI is going to call whenever it needs to get data.  When my Posts controller needs a list of BlogPosts to display, it’s going to call a BAL method like GetAllBlogPosts() or GetAllBlogPostsForCategory().  Last time I mentioned (over and over) that I like to keep things simple.  When I need to get or save data, I don’t want to have to search through 3 different classes just to find the one with the method I need.  Instead, I’m putting all my persistence logic for a given aggregate in just one place, a service class.  This is not a web service.  I’m using service in the Domain Driven Design sense here. That means that I have a BlogService class that is my one stop shop for all persistence that has to to with Blogs, BlogPosts, SubmittedBlogUrls, and anything else that falls within the Blog aggregate. Here is what my BlogService class looks like.  You can see that it’s mostly “Get” data access methods.

image

What’s an Aggregate?

I keep using the word aggregate.  If you’re not familiar with the term, it just means a group of entities that all share the same persistence class (whether that be a repository, a service, or something else).  This is a key concept in Domain Driven Design. If you want to know more I would recommend picking up Eric Evans’ book or Jimmy Nilsson’s book on DDD.  For now, all you need to know is that a BlogPost can never exist without a Blog, so there’s no point in BlogPost having it’s own persistence class.  In fact we find that if we do break BlogPost out into it’s own persistence class, it will lead to problems down the road due to BlogPost’s dependency on Blog.  What’s the solution?  We put data access methods for both Blog and BlogPost in the same persistence class and call it an aggregate.  That is why BlogService has methods for both Blog and BlogPost entities.

What type of data will data access methods return?

We covered this last post, but to recap all data will be returned as a Data Transfer Object (DTO).  The DTOs are all defined in our Common assembly in the DataShapes folder.  Our BAL will return data in one of the following 4 formats.

  • a single DTO
  • a List<DTO>
  • a DataPage<DTO>
  • a string value

For more see last week’s post Agile ADO.Net Persistence Layer: Part 2 Use DTOs.

A simple Single<DTO> data access method

Let’s look at the simplest possible data access method.  GetBlogPost() takes a postGuid for a parameter, defines the query to find the BlogPost entity for that postGuid, and then returns the result as a single BlogPost DTO.  Here’s the complete method.

public BlogPost GetBlogPost(Guid postGuid)

{

    string query = @"SELECT p.*, s.Score

                    FROM [dbo].[BlogPost] p

                    LEFT JOIN [dbo].[BlogPostReputationScore] s on s.PostGuid = p.PostGuid

                    WHERE PostGuid = @PostGuid";

    SqlDao dao = new SqlDao();

    SqlCommand command = dao.GetSqlCommand(query);

    command.Parameters.Add(dao.CreateParameter("@PostGuid", postGuid));

    return dao.GetSingle<BlogPost>(command);

}

The first thing you’ll notice is that this isn’t a lot of code.  All we’re really doing here is defining a parameterized TSQL query, wrapping that query up in a SqlCommand, and then passing the command and our desired return type off to a Data Access Object (DAO) that automagically executes the command and maps the results to our desired type.  It may seem counter intuitive to write code like this when we haven’t even written the DAO yet, but that’s exactly how I did it when I wrote this code for the very first time.  I decided that my data access methods should be very simple.  I would start with the query and the DTO type that I wanted it to return, then I would pass them both to some type of helper class that would handle the details of running the query and figuring out how to map the query results to the properties of my DTO.  By using this top down approach, I gave myself a very clear picture of how I needed my DAO to behave.

What’s a DAO (Data Access Object)?

By looking at the query logic above, you can see that I have this thing called a DAO or Data Access Object.  This is a class that encapsulates the helper logic for working with my database.  The DAO handles things like creating parameters, getting a connection, and most importantly it implements methods to return my four main data formats, GetSingle<DTO>, GetList<DTO>, GetDataPage<DTO>, and GetStringValue(). The DAO and it’s associated DataMappers are where you’ll find the special sauce that makes this architecture work.  We’ll get into their implementation later on.

A BAL that embraces change

It’s easy to look at the simple code above and miss something that I think is very important.  In fact that thing is the whole reason that I wrote this framework.  That simple data access method above is the blueprint for a flexible persistence layer that makes changing your entities and associated persistence code easy and almost painless.  It sets up a simple 3 step process for all data access in your application.

  1. Define a DTO in the exact data shape that you’re looking for.  That means create a DTO property for each data field that you need out of the database.
  2. Define a query that gets the data.  It can be as simple or as complex as you like.  You can develop it in Sql Server Management Studio.  You can easily optimize it.  Use whatever process or tools work for you. When you’re done just paste the query into your data access method.
  3. Pass both the query and your DTO to the DAO and it will automatically handle field mappings and pass the results back in the data shape you requested.

This is a very powerful way to work.  I can’t count the number of times that I’ve worked with and architecture where I dreaded any changes because I knew that any data fields added would require me to modify a sproc, a DAL method, a BAL method, parsing logic, an entity class, it all adds up to a lot of friction that resists any change.  This BAL design embraces change.  It’s written with the attitude that we know change is going to happen so we’re going to give you as few things as possible to modify, and make sure we don’t have any cross cutting dependencies, so that you can make changes easily.

Next time, more on the service classes.

Next Post: Agile ADO.Net Persistence Layer Part 4: Writing data access for new data shapes