The Pragmatic DAL Architecture: Patterns & Decision Matrix
— A decision matrix is helpful in selecting the best options for a big data project.
Co-Authored by Bo Griffin
In this article, we will go over the different options for DAL patterns and showcase a decision matrix to help select the best options for your project. The DAL Patterns below show how a DAL’s implementation and application work together. The full code for these examples is available separately. To keep the examples short, we have included what a simple unit-test would look like using a particular pattern.
Pass-Thru is a DAL pattern that uses code generated from the database and does not contain any developer customizations. Pass-Thru is the thinnest DAL you can have. Any lower level and you are handling all the ORM aspects yourself. If you are using Entity Framework, this amounts to a project with your generated models, mappings, and context only. This provides a strongly typed minimal layer for the application to consume. This allows an application to use the database in a very free form way. Pass-Thru aligns well with the deferred data materialization pattern. This may be fully adequate if the generated code meets all the needs of the application.
The code below is an example of using Entity Framework in a Pass-Thru. The example instantiates a generated, strongly-typed Entity Framework DbContext derived object. The Find method retrieves the entity by its primary key.
The above code for getting an instance is just one implementation. Often the generated code allows for a few different ways of getting the same data. Developers are free to choose among those ways. This can result in inconsistent and redundant code. When different applications or libraries use the Pass-Thru DAL directly, it only gets worse. Since there is no customized DAL code, you do not really test it. You only test the application’s usage of the DAL.
The Static Repository pattern is a type of repository that exposes only static methods. The caller does not have to instantiate or configure the repository before using it. These features minimize the work required for an application to start using a DAL. Since there are no instances to manage, each repository is free to use other repositories to provide a rich developer experience, such as lazy properties using multiple other repositories.
The Static Repository aligns well with Eager Data Materialization. Deferred can be accomplished by returning an IDisposable that the caller must dispose of when done.
The code in below shows a Static Repository example. Notice how the caller does not have to construct anything to get started. Nothing needs to be disposed. The example retrieves the entity with the repository’s public static methods.
Since the repository is static, you cannot easily change the behavior of the methods. Most testing frameworks do not support mocking static methods. Internal configuration of the repository must be thread-safe.
The Instance Repository pattern is a repository that contains only instance methods. These repositories can be standalone but often implement an interface. In the simplest case, the caller just constructs an instance, uses it in the local scope, and then throws it away. A hybrid technique would be to combine the Instance Repository with the Singleton pattern. The result would be something closer to the Static Repository pattern.
The code in below shows an Instance Repository example. In this situation, the caller constructs an instance and uses it by its interface. The code uses the instance once before it falls out of scope. Later it will be garbage collected.
As long as the repositories always have an empty constructor, things stay simple. A repository’s constructor could need parameters or even have dependencies on other repositories. Things get progressively more intricate. If construction gets expensive, the lifetime of the repository instance starts becoming an issue. The developer has to manage coupling. The application needs to know where and how to instantiate a repository instance. That knowledge over time could be all over the app. All of these concerns can be handled by an Inversion of Control (IoC) container. Using an IoC is getting somewhat common place, but they do require interfaces or base classes. Picking a IoC is a big decision and needs to be a good fit for the team and application, since everything will become dependent on it. The example below shows the same code but using an IoC to handle the instance management.
Instance based solutions tend to align better with testability. The repositories could have constructor overloads that allow tweaking the internal behavior to allow for better test coverage.
The Instance Context pattern is a specialization of the Context pattern. The Context pattern is a type of container that holds object references and data. A perfect example is the HttpContext class in ASP.NET. The HttpContext gives the caller access to the HttpRequest, HttpResponse, and many other things. This container helps the developer by not having to deal with construction or passing individual members around. The Instance Context pattern is a container that holds references to Instance Repositories. The repositories are usually within the same database scope. This is significantly easier as a database grows and starts having dozens or hundreds of repositories. This relieves the caller from having to deal with the instantiation of all the individual repositories and just handle the context. The individual repositories can be separate or they can take the context as part of their construction. If the repositories do take the context as input, then they can use the other repositories within the same context.
The code in below shows a trivial example. Here we construct an instance of the Instance Context, then access the repository that it manages.
The only real downside of the Instance Context is that the implementation itself continues to grow in size. Each new repository added to the mix makes the classes get larger. This is not much different from the configuration of a large IoC container.
Unit of Work
The Unit of Work (UOW) pattern is a container that holds a sequence of actions to execute against objects. A Change Manager (CM) is a component that tracks data changes to individual objects. Often Unit of Work uses a Change Manager. As changes occur to the objects, the Unit of Work captures the actions to perform and the Change Manager keeps track of the data changes. The main benefit is the application can casually make changes to objects. When completed, all changes are saved to the database. The Unit of Work pattern lends itself to be stacked on top of the Instance Context pattern.
The code below shows an example or retrieving an object. Most UOW patterns implement a dispose pattern. This may look just like the Pass-Thru pattern listed above. The primary difference is that this implementation should be ORM agnostic.
A UOW can be stacked on top of Entity Framework but it would only be a thin wrapper since Entity Framework already has UOW built-in. When dealing with Disconnected Data the developer may need to ensure that the UOW has enough information to detect a change. Otherwise, the UOW may think it is a new object versus an update to an existing object.
The patterns listed above are each very capable. So how do you pick one for a project? The relevant primary factors to use for selection are timeframe and complexity. These factors focus on getting a project up and running quickly. Secondary factors are testability and maintainability. These factors are more important for longer-term projects. Your project may have different priorities or constraints.
The patterns are not exclusive. They can be combined as needed.
DAL patterns are a critical component to data handling. This series went over the architecture and background terms for data layers, materialization options, ORMs, and pattern options. We would love to hear your thoughts on data access layer and the strategies your team employs.