Encapsulating Specifications

Specifications can start to get complicated. And you might find yourself using the same specification in multiple places. To make things easier to read, you can encapsulate parts of specifications into properties of the facts themselves.

Condition

Some specifications are conditional upon the existence of a successor fact. You will use Any to write those conditions. One example that we already looked at was the existence of an AlbumDelete fact.

var albumsByUser = Given<User>.Match((user, facts) =>
    from album in facts.OfType<Album>()
    where album.creator == user &&
        !facts.Any<AlbumDelete>(delete => delete.album == album &&
            !facts.Any<AlbumRestore>(restore => restore.delete == delete))
    select album);

That specification would be more readable if we encapsulated the condition into a property of the Album fact. To do that, we use the Condition type.

[FactType("Album")]
public record Album(User creator, DateTime createdAt)
{
    public Condition Deleted => Condition.Define(facts =>
        facts.OfType<AlbumDelete>().Any(delete => delete.album == this &&
            !facts.OfType<AlbumRestore>().Any(restore => restore.delete == delete))
    );
}

Now the specification is much easier to read.

var albumsByUser = Given<User>.Match((user, facts) =>
    from album in facts.OfType<Album>()
    where album.creator == user && !album.Deleted
    select album);

Relation

Every specification traces a path from one fact to another. These paths often appear in several different specifications. You can encapsulate the path into a property of the fact. This property resembles a navigation property in Entity Framework.

For example, we defined a complicated specification that lists all of the carts for a user.

var cartsInEnvironmentForUser = Given<Environment, User>.Match((environment, user, facts) =>
    from customerBuyer in facts.OfType<CustomerBuyer>()
    where customerBuyer.buyer == user
    from customer in facts.OfType<Customer>()
    where customerBuyer.customer == customer &&
        customer.environment == environment
    from cart in facts.OfType<Cart>()
    where cart.customer == customer
    select new
    {
        Cart = cart,
        CustomerNames =
            from customerName in facts.OfType<CustomerName>()
            where customerName.customer == customer &&
                !facts.Any<CustomerName>(next => next.prior.Contains(customerName))
            select customerName.value,
        Items =
            from cartItem in facts.OfType<CartItem>()
            where cartItem.cart == cart
            select new
            {
                CartItem = cartItem,
                ProductNames =
                    from productName in facts.OfType<ProductName>()
                    where productName.product == cartItem.product
                    where !facts.Any<ProductName>(next => next.prior.Contains(productName))
                    select productName.value,
                Quantities =
                    from quantity in facts.OfType<CartItemQuantity>()
                    where quantity.item == cartItem
                    select quantity.value
            }
    });

We can improve this specification in stages. First we will encapsulate the path from Customer to CustomerName.

[FactType("Customer")]
public record Customer(Environment environment, Guid guid)
{
    public Relation<string> Names => Relation.Define(facts =>
        from customerName in facts.OfType<CustomerName>()
        where customerName.customer == this &&
            !facts.Any<CustomerName>(next => next.prior.Contains(customerName))
        select customerName.value
    );
}

Then we will encapsulate the path from Cart to CartItem.

[FactType("Cart")]
public record Cart(Customer customer, Month month, DateTime createdAt)
{
    public Relation<CartItem> Items => Relation.Define(facts =>
        from cartItem in facts.OfType<CartItem>()
        where cartItem.cart == this
        select cartItem
    );
}

Let's apply the same pattern to the CartItem to get the product name and quantity.

[FactType("CartItem")]
public record CartItem(Cart cart, Product product)
{
    public Relation<string> ProductNames => Relation.Define(facts =>
        from productName in facts.OfType<ProductName>()
        where productName.product == this.product &&
            !facts.Any<ProductName>(next => next.prior.Contains(productName))
        select productName.value
    );

    public Relation<int> Quantities => Relation.Define(facts =>
        from quantity in facts.OfType<CartItemQuantity>()
        where quantity.item == this
        select quantity.value
    );
}

When we using these properties in the projection, the specification becomes much easier to read.

var cartsInEnvironmentForUser = Given<Environment, User>.Match((environment, user, facts) =>
    from customerBuyer in facts.OfType<CustomerBuyer>()
    where customerBuyer.buyer == user
    from customer in facts.OfType<Customer>()
    where customerBuyer.customer == customer &&
        customer.environment == environment
    from cart in facts.OfType<Cart>()
    where cart.customer == customer
    select new
    {
        Cart = cart,
        CustomerNames = customer.Names,
        Items = cart.Items.Select(cartItem => new
        {
            CartItem = cartItem,
            ProductNames = cartItem.ProductNames,
            Quantities = cartItem.Quantities
        })
    });

Extension Methods

You can also define a condition or relation on a fact type that you don't own. Let's use this to improve the specification even further.

The User fact type is defined in the Jinaga library. We can't modify it. But we can define an extension method that encapsulates the path from User to Customer.

public static class UserExtensions
{
    public static Relation<Customer> Customers(this User user) =>
        Relation.Define(facts =>
            from customerBuyer in facts.OfType<CustomerBuyer>()
            where customerBuyer.buyer == user
            from customer in facts.OfType<Customer>()
            where customerBuyer.customer == customer
            select customer
        );
}

Applying this extension method to the specification improves readability even further.

var cartsInEnvironmentForUser = Given<Environment, User>.Match((environment, user, facts) =>
    from customer in user.Customers()
    where customer.environment == environment
    from cart in facts.OfType<Cart>()
    where cart.customer == customer
    select new
    {
        Cart = cart,
        CustomerNames = customer.Names,
        Items = cart.Items
    });

Extension methods can even be used to extend facts that you do own. This can be useful when you want a clean separation between modules. The module that defines the successor can add the relation to a predecessor defined in another module.

When C# support for extension properties becomes available, this pattern will become even more powerful.

Continue With

Syntax Alternatives

Jinaga is a product of Jinaga LLC.

Michael L Perry, President