Exploring Active Record as a base for business entities in a business logic layer with Entity Framework

Microsoft .NET

For systems that are CRUD heavy, Active Record is still not a bad pattern of choice for some of your objects. While today it is avoided in much larger applications, I wouldn't deem it as obsolete, and when used right, can still provide a very powerful object model. I'm going to share my experience with the pattern, why I have used it, and we will have a bit of fun in the process.

Origins

I have been studying and designing layered architectures for about ten years now, and have implemented them in several large applications; one being a large enterprise banking application. I chose Active Record because:

  • Application was CRUD heavy
  • Already had an existing database that could not easily be changed
  • Database already employed a very strong table per type design, which fits right into Active Record
  • Active Record has a low learning curve, .NET was not a core competency of the team, though they could work with it
  • Data access layer was Entity Framework, which also lends itself to Active Record quite well
  • The majority of the application was a classic ASP application, and lived in a fast paced environment, so something simple was needed
  • Needed to cover the 90 + % percentile of developer uses cases, which are small units of work and are not performance critical
  • API needs to be simple to use, abstracting the underlying complexity of the database and business logic

I wrote the entire layered architecture for this application, so I had the benefit and freedom to make the necessary design decisions. It ended up very successful, where I wrote about 3,000 unit tests and covered a big chunk of the core application logic using these patterns. What I called the "core business logic" of the system, probably only represented about 25% of the overall system, however, but it was the foundation of the other 75%, and reuse was extensive. Hard to give true percentages, and suffice to say it came out well, but I will also cover the pain points during the process, and where the design falls short.

Expected Challenges

Because I have done this before, and I also implemented the first business layer as a Transaction Script design in the data access layer early on, I had a head start on identifying some major challenges. Entity Framework alone introduces some complexity, and there is a big focus on resolving this.

  • I would have to decide if the object model should be bidirectional or unidirectional (Customer.Loan.Customer vs. Customer.Loan), considering how to handle relationships and navigation properties
  • I would have to decide of relationships would be exposed via properties or methods
  • I would have to decide how to manage the lifetime of IDataContext (e.g. implementation of DbContext), and whether it should only exist within the active record, or should be shared for performing a unit of work on several active records
  • I would have determine how to keep the business logic agnostic of the platform it is used by, such as a desktop or web application, and the infrastructure such as accessing files and services in different contexts

A note on TDD

I'll mention that my development methodology of choice is test-driven development, usually writing tests first. For this article, I'm leaving it completely out and ignored other than a few references and remarks, but I will focus on how TDD is part of the overall design and architecture. This is so I can focus on the core topic where TDD can fragment what I am trying to convey by focusing too much on the writing of unit tests and other TDD type code.

Getting Started

I first had to decide my requirements for the classes that would provide the base functionality for the business layer. In this architecture, it was top down consisting of presentation, service/application, business, and data access layers. Naturally, the business layer objects and entities would directly consume the data access layer, where BusinessEntity would derive directly from ActiveRecord. The requirements ultimately revolved around both BusinessEntity and ActiveRecord, since these are the base of the architecture for the business layer.

  • A BusinessEntity needed to be able be loaded
  • A BusinessEntity needed to be able to be saved
  • A BusinessEntity needed to be able to be deleted
  • A BusinessEntity needed to support both simple and complex validation
  • A BusinessEntity should encapsulate exactly one row of data
  • A BusinessEntity should have an id, and identifiers might not be the same kind for all entities
  • A BusinessEntity should be able to take in dependencies
  • A BusinessEntity should be unit testable, and should be able to take in mocks or stubs
  • A BusinessEntity might have to work with the file system, or other external resources, but still needs to be testable
  • A BusinessEntity must work on all platforms
  • A class derived from BusinessEntity should be able to provide the logic for loading, saving, and deleting its data
  • A class derived from BusinessEntity should be able to provide its validation logic
  • A class derived from BusinessEntity should be able to manage its primary key, but not allow external manipulation
  • A class derived from BusinessEntity should be extensible

Basic Scaffolding

The first place I started was with something that resembles an active record. An active record, as defined by Martin Fowler, is "An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data".

namespace App.Data
{
    using System;

    public abstract class ActiveRecord<TEntity, TKey> where TEntity : class, new()
    {
        protected readonly IDataContext Context;
        protected TEntity Entity;

        protected ActiveRecord(IDataContext context)
        {
            Context = context;
        }

        public TKey Id
        {
            get;
            private set;
        }

        public virtual void Delete()
        {
        }

        public virtual void Load(TKey id)
        {
        }

        public virtual void Save()
        {
        }
    }
}

TEntity

TEntity is just the POCO class that is used in the data access layer with Entity Framework. We need access to this so we can make changes the underlying data, and have access to it for load, save, delete, and validation logic. Wrapping TEntity is the heart of this active record pattern, being our row of encapsulated data.

TKey

Allowing for different primary key types is important, especially in databases you cannot redesign. Some tables might use a int key, while others might use uniqueidentifier, and compound keys also might exist, such as a key pair. In our system this was the case, and allowing each derived entity to specify its key via TKey solves the issue. Generics play nicely here, allowing us to have our Id property of type TKey, so we don't have to worry about type conversions when working with key values. For compound keys, you are able to create a struct to represent the key, so you might have something similar below.

public struct CompoundKey
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
}

Despite the generic names, this allows you to specify this as TKey, syntax looking like public class MyClass : ActiveRecord<CompoundKey>.

IDataContext

To support unit testing and proper dependency management, I made several key design choices to hide Entity Framework away completely. I decided to wrap our implementation of DbContext in a facade class, and extract an interface of that for testing. I exposed a few different things to make the interface to the data more palatable.

  • Implementation of IDataContext is a wrapper around our implementation of DbContext, not implemented by it.
  • Exposed all DbSet<T> as IQueryable<T> for testability.
  • Exposed DbSet<T>.Find as IDataContext.Find<TEntity> by calling down to DbContext.Set<TEntity>().Find(id).
  • Exposed several other convenience methods, such as Save, Add<T>, Remove<T>, and SqlQuery<T>.

NOTE: Entity Framework supports in memory unit testing and mocking of DbSet<T>, but there are severe limitations such as having to mock calls to Include and other similar scenarios. I started with this approach initially and quickly trashed the idea, and the experience confirms why there are not many resources on mocking Entity Framework available. I also had to expose the underlying DbSet<T> as IQueryable<T> instead of IEnumerable<T> because you must expose the IQueryable provider interface to allow proper evaluation and translation of the expression tree into the appropriate provider specific query. This is a small price to pay, requiring that you call AsQueryable() on your IEnumerable<T> return values when mocking or stubbing in data and return values.

Passing in IDataContext as a constructor parameter was the natural choice to provide the dependency. This is the most common form of dependency injection, allowing me to provide mock implementations for unit tests and real implementations for production code. Doing this also inverts control of the creation of this dependency, which allows proper management of connection strings, and other platform specific requirements.

This doesn't dictate the lifetime of IDataContext, however. I will get to that a bit later when we dive deeper into dependencies. At this moment, the scaffolding doesn't dictate whether the active record will own the context, and it most certainly doesn't care how it was created, but you will see how we solve the lifetime problem once we implement the rest of the infrastructure.

class, new() constraints

I put these here simply because the objects you use with Entity Framework must be reference types, and at some point the active record will need to be able to create a new instance of TEntity when we need to have a new row of data.

Load(TKey id)

There are about one million and one methods of designing an active record. All of them have drawbacks, and more often than not the syntax you really want isn't possible. I sat down and played with various syntax of how I envisioned developers creating a new instance of this class, whether syntax is correct or not. This is an important method in my toolbox for designing any API, and at this point it's basically just pseudo-code that won't compile to help me generate ideas and get design thoughts flowing.

var db = new SomeContextImpl();

var customer = new Customer(db, 1);
entity.Load();

var customer = new Customer(db);
entity.Load(1);

var customer = Customer.Load(db, 1);

var customer = ActiveRecord<Customer, int>.Load(db, 1);

var customer = ActiveRecord.Load<Customer>(db, 1);

var customer = ActiveRecord.Load<Customer, int>(db, 1);

Some of the syntax above is not possible, or it requires a lot of complicated usage of generics and inheritance to make it work. At the time, I didn't even have all of these ideas yet, but due to time and complexity I stuck with the simpler approach from the scaffolding. I decided that while simple, the usage could be tackled through code documentation and minimal knowledge transfer to the team. Later I will discuss how I wish I could have improved this decision, but the reality is that this was the choice I had made at the time.

Save and Delete

The easiest part of the design. As long as you have a instance of an active record, you should be able to make changes and save it, or be able to delete it. I don't think there is much else to say here.

Implementing persistence

The overall design was in place, now I needed to figure out how the guts of this class would work. There are quite a few details like managing the key value, or how to determine when to create a new TEntity, but I will try to break down the process as best I can.

Loading

Loading is straightforward with Entity Framework, given that you are provided DbSet<T>.Find(params object[] keyValues).

public virtual void Load(TKey id)
{
    Entity = Context.Find<TEntity>(id);

    if (Entity == null)
    {
        throw new InvalidOperationException($"Entity type {typeof(TEntity).FullName} with key {Id} does not exist.");
    }

    Id = id;
}

Basic stuff. Find a TEntity with the specified id or throw an exception if one cannot be found, otherwise set the Id property to the value of the primary key. The great part here is that each implementation of ActiveRecord<TEntity will be able to customize finding the entity. This is important when TKey is not a primitive, such as a compound key, which in that case it may be necessary to use an actual LINQ expression with SingleOrDefault instead.

Saving

Easy.

public virtual void Save()
{
    Context.SaveChanges();
}

Wait.

How are derived classes going to implement their persistence logic? The first option is to simply allow derived classes to override Save, which is the whole point of making this virtual, but I also want to make a default implementation a bit easier to work with. I chose to implement an OnSaving method here, where derived classes would override this to plug in persistence logic, but ultimately not have to override Save unless absolutely necessary. I also needed a way to create the primary key if it is not a primitive, or if a default implementation is not provided by the database provider for a given table or type. Here I chose to implement a OnCreateKey(Entity) method where such logic could be provided by the derived class.

protected abstract TKey OnCreateKey(TEntity entity);

protected virtual void OnSaving()
{
    if (Entity == null)
    {
        Entity = new TEntity();

        Id = OnCreateKey(Entity);

        Context.Add(Entity);
    }
}

public virtual void Save()
{
    OnSaving();

    Context.SaveChanges();
}

If TEntity has an identity key, it would not be necessary to override this method. For compound keys, the derived class will probably need to implement something here, but it depends. In our database, keys were uniqueidentifiers (SQL) / Guid (C#) that were created by Guid.NewGuid() in each entity constructor, but we could have just as easily gone with NEWID() as a default constraint in the database. It depends on your needs, in our case the database was already created with over 100 tables and changing all of that was not an option at the time. At the end of the day derived classes would have to ensure a call first to OnSaving, but that wrapped up persistence.

Deletion

For deletion I decided to provide a very basic default implementation, again allowing it to be overridden.

public virtual void Delete()
{
    if (Entity == null)
    {
        throw new InvalidOperationException();
    }

    Context.Remove(Entity);
    Context.SaveChanges();
}

There isn't much else to say here. Our database did not enable cascading deletion, so we knew eventually we would be overriding this to place deletion logic, or delegate that work to a stored procedure for larger deletions that might need some additional performance. For many business entities we planned on implementing, this would suffice as a default behavior.

IDisposable and IDataContext lifetime

At this point our implementation was complete, so I then looked at how we needed to manage the lifetime of the context. I decided that it must be a requirement that ActiveRecord<TEntity own the provided IDataContext, and that it also be disposed when the active record is disposed. There were two parts to my decision here, first that passing in the context would be an implementation detail of the business layer, not something developers would typically do in product code outside of unit tests, and secondly, this is vital because you do not want changes to your current active record to be persisted by another if the context is shared among several active record classes. I put the final touches, ready to move on.

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
    if (disposing)
    {
        Context?.Dispose();
    }
}

You might wonder how I could guarantee that only one active record have a a single context. Think big picture. Dependency injection and management outside of unit testing is the primary mechanism for this. I will discuss later how I solved some architectural problems, such as related entities and deep dependencies, but ultimately the design of the business layer as a whole will guarantee the lifetime of an IDataContext to a single active record, which we will touch on later.

ActiveRecord Demonstrated

public abstract class ActiveRecord<TEntity, TKey> : IDisposable where TEntity : class, new()
{
    protected readonly IDataContext Context;
    protected TEntity Entity;

    protected ActiveRecord(IDataContext context)
    {
        Context = context;
    }

    public TKey Id
    {
        get;
        private set;
    }

    public virtual void Delete()
    {
        if (Entity == null)
        {
            throw new InvalidOperationException();
        }

        Context.Remove(Entity);
        Context.SaveChanges();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public virtual void Load(TKey id)
    {
        Entity = Context.Find<TEntity>(id);

        if (Entity == null)
        {
            throw new InvalidOperationException($"Entity type {typeof(TEntity).FullName} with key {Id} does not exist.");
        }

        Id = id;
    }

    public virtual void Save()
    {
        OnSaving();

        Context.SaveChanges();
    }
        
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            Context?.Dispose();
        }
    }

    protected abstract TKey OnCreateKey(TEntity entity);

    protected virtual void OnSaving()
    {
        if (Entity == null)
        {
            Entity = new TEntity();

            Id = OnCreateKey(Entity);

            Context.Add(Entity);
        }
    }
}

Introducing BusinessEntity<TEntity, TKey>

With a complete implementation of our active record, I had to get an idea of what a business entity derived from it would look like. Considering validation, and some of the other mechanical requirements, I came up with this.

namespace App.Business
{
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;

    public abstract class BusinessEntity<TEntity, TKey> : ActiveRecord<TEntity, TKey> where TEntity : class, new()
    {
        protected BusinessEntity(IDataContext context) : base(context)
        {
        }

        public virtual bool IsValid()
        {
            return IsValid(out _);
        }

        public virtual bool IsValid(out IEnumerable<ValidationResult> validationResults)
        {
            var context = new ValidationContext(this);
            var results = new List<ValidationResult>();

            bool isValid = Validator.TryValidateObject(this, context, results, true) && OnValidate(results);

            validationResults = results;

            return isValid;
        }

        public override void Save()
        {
            if (!IsValid(out var results))
            {
                throw new ValidationException($"Entity was not be saved because there are {results.Count()} validation errors.");
            }

            base.Save();
        }

        public virtual bool TrySave(out IEnumerable<ValidationResult> validationResults)
        {
            bool isValid = IsValid(out validationResults);

            if (isValid)
            {
                base.Save();
            }

            return isValid;
        }

        protected virtual bool OnValidate(ICollection<ValidationResult> results)
        {
            return !results.Any();
        }
    }
}

Implementing the business entity class was much easier for me, primarily because the complexity is in managing the data access logic. Business entities are primarily domain specific, and validation is made pretty simple with one of the many frameworks out there.

Validation

My validation framework of choice is Data Annotations plus a method that can be overridden for any custom validation logic. This is very easy to implement, and does not require a whole lot of thought to it, but I did have some basic use cases I wanted to ensure could be met. Validation is the sole purpose of the base BusinessEntity class, and validation is one of the most common and most important aspects to any class that implements business logic.

IsValid()

I provided this method so a caller could quickly determine the valid state when they also don't care about the actual validation errors. This is nice if you just need a quick check and the ability to move on, without the extra syntax of having to consume an enumerable or ignore an out parameter.

IsValid(out IEnumerable)

Complementary to the above, we also need the ability to get the actual validation errors so they can be used for user feedback, logging, or error handling in calling applications. I chose those approach because not only can a developer get the bool return value, but they can also choose to inline the out syntax (e.g. bool b = IsValid(out var results)), or to completely ignore them altogether (e.g. bool b = IsValid(out var _)). Either way, the caller has both overloads and the flexibility of choosing what is needed.

OnValidate()

I wanted a way to perform validation that might be too complex for a data annotation, especially validation that might require access to the underlying IDataContext. Derived classes have the option to ignore this if there is no need to override, or they can override to implement their additional validation logic. You can think of this as an extensibility point, of sorts.

Save()

Because validation is often vital, and you never want to save invalid state (usually), the default behavior of Save is to ensure that appropriate calls to ensure the entities valid state are made. I created this overload primarily so that an exception would be thrown to the developer, indicating they did not make proper usage of the IsValid methods, or to prevent persistence from occurring of the entity is in a bad state. In the event that the developer does not care about the validation state, or validation errors, such as needing to quickly handle the exception and move on, this overload will suit that use case without the extra overhead of using the other validation methods or consuming validation results. Though, they still have the option to by catching the ValidationException, which also contains the results.

TrySave(out IEnumerable)

Similar to the Save() overload, provides a similar syntax to IsValid(out IEnumerable<ValidationResult>) that allows the same type of flexibility. Most of the benefits are the same.

Flexibility and Scalability

I could have provided fewer methods for this class, but it is important to remember that this is essentially a framework level class, and must support a wide variety of use cases. If you make the API too minimal, developers will become frustrated that functionality is not available, and they will either try to modify it themselves, which may not lead to good design, or they may avoid using the class altogether. Worst case scenario is they modify it and destroy the design and make a mess, best base scenario you have a great developer on your team who can change the API and make everyone happy. Sometimes not using the API is the best option, which leaves room for proper design, and you can always replace code later. This is generally my rule of thumb when building frameworks of any kind, is to never modify the design for one specific use case, but keep it in mind as you build it properly. It is almost impossible to correct a design once it is implemented everywhere.

At this point, the overall design of both the active record and business entity classes are complete, and are ready for the first class to derive from them and implement some business logic.

Implementing the first Business Entity

The best way to prove a design, is to not only implement it, but also to ship it. Especially in regards to abstract classes, you should always ship a class implementation that demonstrates the usage. I'm not going to share code from the application, but I will demonstrate a similar and simple implementation. Before writing any code, it is important to note that there are still pending design decisions at this point. First, to stay true to the active record pattern, we must ensure at all times that we are only encapsulating one row of data. This means that we do not want to affect other rows of data in a way that violates the pattern, or would make the entity confusing to work with. Secondly, this means that we now have constraints on how to design the entity, and on how to handle relationships or child entities.

The Employee Entity

The Developer Business Entity

class Developer : BusinessEntity<Employee, int>
{
    public Developer(Product product) : base(product)
    {
    }

    [Required]
    public string FirstName => Entity.FirstName;

    [Required]
    public string LastName => Entity.LastName;

    [Required, EmailAddress]
    public string Email => Entity.Email;

    [MinLength(0)]
    public decimal Salary => Entity.Salary;
        
    [MaxLength(64)]
    public string Title => Entity.Title;

    protected override int OnCreateKey(Employee entity)
    {
        return entity.Id;
    }
}

** THE REST OF THIS ARTICLE IS BEING WRITTEN AND WILL BE AVAILABLE BY JAN 26 2018 **

Leave a Comment