In Part 1 of this series, we started with a basic Data-Reader / SQL Connection/Command pattern and illustrated how it is possible to abstract the parsing of the Data Reader into a standalone object that can be fully unit tested in isolation of the calling code. In Part two of the series, we will highlight a very simple optimisation that can be made to the “DataReader” convertor and the required update to the tests to capture/verify the changes. In this revision, the original “CustomerDRConvertor” has been updated to include extremely basic caching, which for the duration of the object’s existence should ensure that only the first call needs to reference the “GetOrdinal(…)” method to find the element index of each desired column. Subsequent calls can then use this “cached” index to reference the column by position rather than name.
namespace DataAccess.Example { using System.Data; using System.Data.BoilerPlater; public class CustomerDRConvertorPart2 : IConvertDataReader<Customer> { private int idIndex = -1; private int firstNameIndex = -1; private int surnameIndex = -1; public Customer Parse(IDataReader dataReader) { if (idIndex == -1) { idIndex = dataReader.GetOrdinal(“Id”); firstNameIndex = dataReader.GetOrdinal(“FirstName”); surnameIndex = dataReader.GetOrdinal(“Surname”); } return new Customer { Id = dataReader.GetGuid(idIndex), FirstName = dataReader.GetString(firstNameIndex), Surname = dataReader.GetString(surnameIndex) }; } } }
In traditional ASP applications (back in the day) the above-caching pattern used to result in reasonable performance gains. I’ve not looked into the benefits within a modern-day .NET application and in some instances could be classed as premature optimisation. But for the purpose of this example, it provides a perfect illustration as to how the abstracting the data reader parsing from the connection/command code can provide many benefits. Updated objects can be developed and tested in complete isolation of the existing code and then plugged into the code base with only minimal changes.
This updated code can be verified using the unit test below. In the test the “Parse(…)” method is called once and the mocked objects are verified that they were called correctly. The “Parse(…)” method is then called again and the mocked objects verified to make sure that the second call only resulted in an additional call to the GetGuid(…) and GetString(…) methods. Due to the very basic caching, that was implemented there is no need for the second call to make any GetOrdinal(…) references, which the verification of the mocked objects can confirm. The tests verify the expected behaviour, not the inner workings of any implementation of a DataReader object.
namespace DataAccess.Example.Tests { using System; using System.Data; using Microsoft.VisualStudio.TestTools.UnitTesting; using NUnit.Framework; using Moq; using Assert = NUnit.Framework.Assert; [TestClass] public class CustomerDRConvertorPart2Tests { [TestMethod] public void CustomerDRConvertor_GoodCall() { var dataReader = new Mock<IDataReader>(); dataReader.Setup(dr=>dr.GetOrdinal(“Id”)).Returns(1); dataReader.Setup(dr=>dr.GetOrdinal(“FirstName”)).Returns(2); dataReader.Setup(dr=>dr.GetOrdinal(“Surname”)).Returns(3); var id = Guid.NewGuid(); const string firstName = “John”; const string surname = “Doe”; dataReader.Setup(dr=>dr.GetGuid(1)).Returns(id); dataReader.Setup(dr=>dr.GetString(2)).Returns(firstName); dataReader.Setup(dr=>dr.GetString(3)).Returns(surname); var convertor = new CustomerDRConvertorPart2(); var customer = convertor.Parse(dataReader.Object); Assert.That(customer.Id, Is.EqualTo(id)); Assert.That(customer.FirstName, Is.EqualTo(firstName)); Assert.That(customer.Surname, Is.EqualTo(surname)); dataReader.Verify(dr=>dr.GetOrdinal(It.IsAny<string>()), Times.Exactly(3)); dataReader.Verify(dr=>dr.GetOrdinal(“Id”), Times.Once()); dataReader.Verify(dr=>dr.GetOrdinal(“FirstName”), Times.Once()); dataReader.Verify(dr=>dr.GetOrdinal(“Surname”), Times.Once()); dataReader.Verify(dr=>dr.GetGuid(It.IsAny<int>()), Times.Once()); dataReader.Verify(dr=>dr.GetGuid(1), Times.Once()); dataReader.Verify(dr=>dr.GetString(It.IsAny<int>()), Times.Exactly(3)); dataReader.Verify(dr=>dr.GetString(2), Times.Once()); dataReader.Verify(dr=>dr.GetString(3), Times.Once()); convertor.Parse(dataReader.Object); dataReader.Verify(dr=>dr.GetOrdinal(It.IsAny<string>()), Times.Exactly(3)); dataReader.Verify(dr=>dr.GetOrdinal(“Id”), Times.Once()); dataReader.Verify(dr=>dr.GetOrdinal(“FirstName”), Times.Once()); dataReader.Verify(dr=>dr.GetOrdinal(“Surname”), Times.Once()); dataReader.Verify(dr=>dr.GetGuid(It.IsAny<int>()), Times.Exactly(2)); dataReader.Verify(dr=>dr.GetGuid(1), Times.Exactly(2)); dataReader.Verify(dr=>dr.GetString(It.IsAny<int>()), Times.Exactly(4)); dataReader.Verify(dr=>dr.GetString(2), Times.Exactly(2)); dataReader.Verify(dr=>dr.GetString(3), Times.Exactly(2)); } } }
In part three of this series, I will cover how the above code can be moved into an abstract base class for data access that all inheriting classes can utilise through interfaces and generics.
Part 3 builds on the code developed in parts 1 & 2 into a usable solution.
As part of his fantastic ‘What is .NET standard‘ presentation at DDD12, Adam Ralph provided an amazing amount of detail in such a short amount of time. One of the most valuable points, which is completely obvious when you think about it, is how you should work with .NET standard when creating libraries. NET standard now comes in a multitude of flavours: currently 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6 and 2.0. When starting out . . .
If you’re trying to access a class library (.NET Standard) from a traditional console application (in VS2017 those can be found under ‘Windows Classic Desktop’) you will run into problems; which can feel a little strange for something that was pretty simple in VS2015 and earlier. You can add a reference to the class library project (Resharper will even volunteer to add the dependency / namespace reference if you don’t already have it). But the . . .
where is part 3??
I've finally got around to adding part 3, have updated this post with a link to it.