Specification Pattern in C#


Foreword

The ‘Specification Pattern’ is a useful tool in the toolbox related to software development. I use it extensively and it has been a big contributor to a fairly low amount of bugs in my e-commerce application.

I use those Specifications to translate it into a query, which makes it useful for me.

Example of using the specification pattern in practise

Why? A change to a certain Spec ( eg. when a product is considered ‘active’) is in many cases not propagated/tested well through the application. It’s sometimes hard to consistently change that behavior through the application and bugs come up after the change, in areas that weren’t considered.

The Specification Pattern makes it easy to query your data in a re-usable way and makes it simpler.

Let’s dive in

One of the biggest mistakes in software development is copy-pasting your query conditions multiple times. Everything you filter your data on, should be re-usable, outside of your current query.

What do I mean by this?

Consider an e-commerce with a simplified product scheme:

"Product"
{
    "Title": "Parfum XYZ",
    "State": "Active/Inactive",
    "Price": 30.2,
    "Tag": ["Cosmetic","Dior"],
    "UpdatedOn": 2023-09-10,
    "CreatedOn": 2023-01-10  
}

Your end-users, will always need to see the active products. So some queries will look similar to this:


  • Listing all products ( frontend ):
        SELECT * FROM Products 
        WHERE [State] == 'Active';
  • Search products by name ( frontend )
        SELECT * FROM Products 
        WHERE [State] == 'Active' 
        AND [Title] LIKE '%{searchTerm}%';
  • Search products by name ( backend )
        SELECT * FROM Products 
        WHERE [Title] LIKE '%{searchTerm}%';

That’s all fine when you’re starting to build things and have a perfect plan ( which, in many cases you don’t). But if you’re building your e-commere site and expanding your product scheme, you’ll realistically end-up with something like this:

"Product"
{
    "Title": "Parfum Allézy",
    "SanitizedTitle" : "ParfumAllezy", //Removes spaces and removes Diacritis from the Title. Use-case: The end-user searches for 'Allezy', instead of 'Allézy'
    "SKU": "AZAA",
    "State": "Active/Inactive",
    "Price": 30.2,
    "Tag": ["Cosmetic","Dior"],
    "ValidFrom": 2023-09-01,
    "ValidTill": 2023-09-10,
    "UpdatedOn": 2023-09-10,
    "CreatedOn": 2023-01-10   
}

This would impact all queries mentioned above:


  • Listing all products ( frontend ):
        SELECT * FROM Products 
        WHERE [State] == 'Active' 
            AND ( 
                ( [ValidFrom] IS NULL AND [ValidTill] IS NULL )
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NOT NULL AND @CurrentDate BETWEEN [ValidFrom] AND [ValidTill])
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NULL AND @CurrentDate > [ValidFrom] )
                OR ( [ValidFrom] IS NULL AND [ValidTill] IS NOT NULL AND @CurrentDate < [ValidTill] )
            );
  • Search products by name ( frontend )
        SELECT * FROM Products 
        WHERE [State] == 'Active' 
            AND ( 
                ( [ValidFrom] IS NULL AND [ValidTill] IS NULL )
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NOT NULL AND @CurrentDate BETWEEN [ValidFrom] AND [ValidTill])
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NULL AND @CurrentDate > [ValidFrom] )
                OR ( [ValidFrom] IS NULL AND [ValidTill] IS NOT NULL AND @CurrentDate < [ValidTill] )
            ) AND 
            ([Title] LIKE '%{searchTerm}%' OR SanitizedTitle LIKE '%{searchTermWithoutDiacritics}%'  );
  • Search products by name ( backend )
        SELECT * FROM Products 
        WHERE ([Title] LIKE '%{searchTerm}%' OR SanitizedTitle LIKE '%{searchTermWithoutDiacritics}%' );

As you can see. The queries just became much more complex and if not changed everywhere in your application ( frontend + backend). Unexpected bugs will surely surface.

That’s where the Specification Pattern would have greatly helped to simplify things. It’s implementation boils down to:

  • Specify both specifications in POCO’s
  • Translate them to the query language ( eg. SQL )

This is how the queries would look like, using the repository pattern:


  • Listing all products ( frontend ):
    var spec = new ProductIsActive();
    var products = repo.Get(spec.ToSpecification()).ToListAsync();
  • Search products by name ( frontend )
    var spec = new IsActive().And(new BySearchTerm(searchTerm));
    var products = repo.Get(spec.ToSpecification()).ToListAsync();
  • Search products by name ( backend )
    var spec = new BySearchTerm(searchTerm);
    var products = await repo.List(spec.ToSpecification()).ToListAsync();

Both classes ( ‘IsActive’ andd ‘BySearchTerm’) are just POCO’s, that basically hold the value and do an (optional) additional check:

    public class IsActive : ISpecification<Product>
    {
        public void Accept(IProductSpecificationVisitor visitor) => visitor.Visit(this);

        public bool IsSatisfiedBy(Product item) => item.State == State.Active;
    }

    public class BySearchTerm : ISpecification<Product>
    {
        public string Q {get;private set;}
        public BySearchTerm(string searchTerm)
        {
            Q = searchTerm;
        }
        public void Accept(IProductSpecificationVisitor visitor) => visitor.Visit(this);

        public bool IsSatisfiedBy(Product item) => item.Title.Contains(Q);
    }

The translation to SQL is done in the SpecificationVisitor. Let’s demonstrate the simplified form:

      public class SQLQueryProductSpecVisitor : IProductSpecificationVisitor
    {
         public string QueryFilter { get; private set; } //Holds the complete query

         public static string SpecToQuery(ISpecification<Product, IProductSpecificationVisitor> spec)
            => $"SELECT * FROM PRODUCTS WHERE {SpecToQueryFilter(spec)}"; //This is how the query will be generated. It will aggregate all specifications

           public void Visit(IsActive spec) // Translates the 'IsActive' specification
            => QueryFilter = $"[State] = 'Active'";

             public void Visit(BySearchTerm spec) // Translates the 'IsActive' specification
            => QueryFilter = $"[Title] LIKE '%{searchTerm}%'";
    }

To implement the changes for our updated spec, we only need to update the ‘SQLQueryProductSpecVisitor’.

      public class SQLQueryProductSpecVisitor : IProductSpecificationVisitor
    {
         public string QueryFilter { get; private set; } //Holds the complete query

         public static string SpecToQuery(ISpecification<Product, IProductSpecificationVisitor> spec)
            => $"SELECT * FROM PRODUCTS WHERE {SpecToQueryFilter(spec)}"; //This is how the query will be generated. It will aggregate all specifications

           public void Visit(IsActive spec) // Translates the 'IsActive' specification
            => QueryFilter = @$"[State] == 'Active' 
            AND ( 
                ( [ValidFrom] IS NULL AND [ValidTill] IS NULL )
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NOT NULL AND @CurrentDate BETWEEN [ValidFrom] AND [ValidTill])
                OR ( [ValidFrom] IS NOT NULL AND [ValidTill] IS NULL AND @CurrentDate > [ValidFrom] )
                OR ( [ValidFrom] IS NULL AND [ValidTill] IS NOT NULL AND @CurrentDate < [ValidTill] )
            );"

             public void Visit(BySearchTerm spec) // Translates the 'IsActive' specification
            => QueryFilter = $"[Title] LIKE '%{searchTerm}%' OR SanitizedTitle LIKE '%{searchTermWithoutDiacritics}%'";
    }

This greatly benefits readability. We only have to make sure that the IsActive spec is translated to SQL correctly once. After this simple change, every query in our application will adhere to the updated spec.

Notes

My Specification Pattern library can be found on github. I’ll advise you to look at the unit tests for SQL first and Expression Trees ( eg. for EF) next.

Advanced notes

  • Specification pattern is not limited to SQL. If you are ever planning to use an alternative engine for performing fast searches (eg. Elastic Search …) you can easily create a new IProductSpecificationVisitor implementation for that. It’s much simpler to add a new Specification Visitor than changing your queries + implementation throughout your application.

  • The Specification usually contains a IsSpecified check, to see if the current POCO is valid to the stated Specification. You could use this behavior outside of ‘querying’ data. Eg. If you sync data and are looping over all data, you can do the following:

   //We're going to sync our products to somewhere else

   var isActiveSpec = new Active();
   foreach(var product in products)
   {
        if(isActiveSpec.IsSatisfiedBy(product))
        {
            // Check if product is active externally, and if not add it
        }else{
            // Check if the product is not active externally
        }
   }

It will make sure that your code definition of “What is an Active product”, will adhere to how it’s used as a Specification.

  • The Specification Pattern contains 2 layers of filters. On the POCO and through your “translated layer” ( eg. SQL). This becomes very usefull if your models don’t contain certain data and you don’t want to map them inside of your POCO’s.

Eg. In a multi-tenant db. Your db has more data for a product. TenantId, CreatedOn, UpdatedOn which are sometimes not mapped in your POCO:

//Example Product Scheme in C# ( = DTO )
{
    "Title": "Parfum XYZ",
    "State": "Active/Inactive",
    "Price": 30.2,
    "Tag": ["Cosmetic","Dior"],
}

//Example Product Scheme in sql#
{
    "Title": "Parfum XYZ",
    "State": "Active/Inactive",
    "Price": 30.2,
    "Tag": ["Cosmetic","Dior"],
    "UpdatedOn": 2023-09-10,
    "CreatedOn": 2023-01-10,
    "TenantId": "Tenant-A" 
}

Then this becomes your Specification:

 public class ByTenant
{
        public string TenantId {get;private set;}
        public ByTenant(string tenantId)
        {
            TenantId = tenantId;
        }
        public void Accept(IProductSpecificationVisitor visitor) => visitor.Visit(this);

        public bool IsSatisfiedBy(Product item) => true;
}

And this is your query in your SQL Visitor:

    public void Visit(ByTenant spec)
            => QueryFilter = $"TenantId == {spec.TenantId}";

Additionally, you might want to enforce the ByTenant everywhere. So it’s a good idea to add the specification in your repository to enforce data isolation like this:

   public async Task<IReadOnlyList<ProductAggregate>> GetProducts(ISpecification<ProductAggregate, ICatalogSpecificationVisitor> spec)
    {
        spec = spec.And(new CatalogByTenant(m_tenant));
        var expression = m_specVisitor.ConvertSpecToExpression(spec);
        
        //Typically:  
        // return await m_db.Products.Where(expression).ToListAsync()
        
    }

Finally

If you haven’t used the Specification Pattern yet, have a look at my github repo for it and play arround with it.

See also

g