Using extension methods to keep things DRY – Part 2

So, in Part 1 we used simple, basic and reusable extension methods to make our code a little bit easier to read. Let's do something more advanced.

After Part 1 we ended up with our DAL code looking like this:

public Person GetPerson(int id)
{
    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetSingle", connection))
        {
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add("@id", SqlDbType.Int).Value = id;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                if (reader.Read())
                {
                    var person = new Person();
                    person.Id = id;
                    person.Name = reader.GetString("name");
                    person.DateOfBirth = reader.GetDateTime("dateOfBirth");
                    person.EmailAddress = reader.GetString("emailAddress");

                    return person;
                }

                // No results, return null
                return null;
            }
        }
    }
}

public List<Person> GetPersonsAll()
{
    var persons = new List<Person>();

    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetAll", connection))
        {
            command.CommandType = CommandType.StoredProcedure;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var person = new Person();
                    person.Id = reader.GetInt32("id");
                    person.Name = reader.GetString("name");
                    person.DateOfBirth = reader.GetDateTime("dateOfBirth");
                    person.EmailAddress = reader.GetString("emailAddress");

                    persons.Add(person);
                }
            }
        }
    }

    return persons;
}

We see that a lot of our code is focused on mapping the database columns to an object, and in both GetPerson and GetPersonsAll we are pretty much doing the same thing. The easiest method would be to extract that out into a separate method called ReadPerson which takes in a DbDataReader and returns a single instance of the object. So let's do that now and see what we end up with.

public Person GetPerson(int id)
{
    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetSingle", connection))
        {
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add("@id", SqlDbType.Int).Value = id;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                if (reader.Read())
                {
                    return this.ReadPerson(reader);
                }

                // No results, return null
                return null;
            }
        }
    }
}

public List<Person> GetPersonsAll()
{
    var persons = new List<Person>();

    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetAll", connection))
        {
            command.CommandType = CommandType.StoredProcedure;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    persons.Add(this.ReadPerson(reader));
                }
            }
        }
    }

    return persons;
}

private Person ReadPerson(DbDataReader reader)
{
    var person = new Person();
    person.Id = reader.GetInt32("id");
    person.Name = reader.GetString("name");
    person.DateOfBirth = reader.GetDateTime("dateOfBirth");
    person.EmailAddress = reader.GetString("emailAddress");

    return person;
}

This is looking better, but we really only saved one line of code, although we did make our code more reusable. The actual methods doing the reading are now looking better. However we are still not done. We can actually simplify this even more, by adding two small, and in my opinion incredibly useful, extension methods. Take a look at these beauties:

public static T ReadObject<T>(this DbDataReader reader, Func<DbDataReader, T> objectMapper)
{
    return reader.Read() ? objectMapper(reader) : default(T);
}

public static List<T> ReadList<T>(this DbDataReader reader, Func<DbDataReader, T> objectMapper)
{
    var list = new List<T>();

    while (reader.Read())
    {
        list.Add(objectMapper(reader));
    }

    return list;
}

Here we see two methods using two of my all time favourite constructs in .NET, Generics and the Func object. The Func object encapsulates a delegate which we can pass around as a parameter but without having to specify it first. In our case we say that the delegate must accept a DbDataReader, which is exactly what our ReadPersons method does, and it must retunr an object of type T, which is the generic way of saying that our extension methods will return the same object that the mapping method does. Using these extension methods we can now simplify our code to look like this:

public Person GetPerson(int id)
{
    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetSingle", connection))
        {
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add("@id", SqlDbType.Int).Value = id;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                return reader.ReadObject(this.ReadPerson);
            }
        }
    }
}

public List<Person> GetPersonsAll()
{
    using (var connection = new SqlConnection("SomeConnectionString"))
    {
        using (var command = new SqlCommand("dbo.PersonGetAll", connection))
        {
            command.CommandType = CommandType.StoredProcedure;

            connection.Open();
            using (var reader = command.ExecuteReader())
            {
                return reader.ReadList(this.ReadPerson);
            }
        }
    }
}

private Person ReadPerson(DbDataReader reader)
{
    var person = new Person();
    person.Id = reader.GetInt32("id");
    person.Name = reader.GetString("name");
    person.DateOfBirth = reader.GetDateTime("dateOfBirth");
    person.EmailAddress = reader.GetString("emailAddress");

    return person;
}

Now the code is really getting smaller, without losing any of it's readability and all of our extension methods are reusable for other cases as well, which is exactly what we want.

Stay tuned for Part 3 and happy coding!

Mastodon