.NET implementation of Specification design pattern

FluentSpecification is implementation of Specification design pattern with many small, built-in reusable specifications, perfect for validation of Domain Model in Domain-Driven-Design approach and other similar, where system is built around domain objects.

Get Started »

Fluent API

Allows to build and combine specifications in simple and clear way.

Built-in specifications

With validation and Linq expression support. Many of them available as negation.

Objects validation

Special result with error messages, trace of all used specifications, specifications parameters etc. Can be used not only for domain, but also for DTOs and user input, when you need to log result or present it on UI.

Linq expression

Specifications can be used in collections querying. Business logic of repositories can be separated to specifications and use interchangeably.

EF Core support

All specifications are tested with EF Core on real database and ca be used without errors.

EF 6 partial support

Many of specifications support LinqToEntities (LinqToSql) and can be used in Entity Framework 6 queries.

Usage


using FluentSpecification;
using FluentSpecification.Core;

var customerSpec = Specification
    // Specify not null Customer
    .NotNull<Customer>()

    // with Id between 100 and 200
    .And()
    .InclusiveBetween(c => c.CustomerId, 100, 200)

    // and LastName is "Smith"
    .And()
    .Equal(c => c.LastName, "Smith")

    // and Customer is active
    .And()
    .True(c => c.IsActive)

    // and Customer property "Email" is ...
    .And(Specification.ForProperty<Customer, string>(c => c.Email, Specification
        // ... valid email
        .Email()

        // longer than 15 characters
        .And()
        .MinLength(15)

        // on "gmail.com"
        .And()
        .Match("^.*@gmail.com$")))

    // with not empty Item collection ...
    .And()
    .ForProperty(c => c.Items, Specification
        .NotEmpty<ICollection<Item>>()

        // ... contains Item '1000'
        .And()
        .Contains(new Item { ItemId = 1000 }))

    // and Customer has credit card ...
    .And()
    .NotNull(c => c.CreditCard)

    // ... with valid number
    .And()
    .CreditCard(c => c.CreditCard.CardNumber)

    // and credit card is valid between 2019-03-12 ...
    .And(Specification
        .GreaterThanOrEqual<Customer, DateTime>(c => c.CreditCard.ValidityDate, 
            new DateTime(2019, 3, 12))
        
        // ... and 2019-05-31
        .Or()
        .LessThan(c => c.CreditCard.ValidityDate, new DateTime(2019, 6, 1)));

Is satisfied by

customerSpec.IsSatisfiedBy(new Customer
{
    CustomerId = 125,
    LastName = "Smith",
    IsActive = true,
    Email = "asmith@gmail.com",
    Items = new List()
    {
        new Item { ItemId = 1000 }
    },
    CreditCard = new CreditCard
    {
        CardNumber = "5500 0000 0000 0004",
        ValidityDate = DateTime.Parse("2019-03-12")
    }
}); // return true

Validation

customerSpec.IsSatisfiedBy(new Customer
{
    CustomerId = 90,
    LastName = "Jones",
    IsActive = false,
    Email = "mjones@hotmail.com",
    Items = null,
    CreditCard = new CreditCard
    {
        CardNumber = "5500 0000 1",
        ValidityDate = DateTime.Parse("2019-03-01")
    }
}, out var specResult);    // return false

Console.WriteLine(specResult.ToString());
// Field 'CustomerId' value is not valid
// Field 'CustomerId': [Value is not between [100] and [200]]
// Field 'LastName' value is not valid
// Field 'LastName': [Object is not equal to [Smith]]
// Field 'IsActive' value is not valid
// Field 'IsActive': [Value is False]
// Field 'Email' value is not valid
// Field 'Email': [String not match pattern [^.*@gmail.com$]]
// Field 'Items' value is not valid
// Field 'Items': [Object is empty]
// Field 'Items': [Collection not contains [FluentSpecification.Integration.Tests.Data.Item]]
// Field 'CreditCard.CardNumber' value is not valid
// Field 'CreditCard.CardNumber': [Value is not correct credit card number]

Querying

var customers = new List<Customer>()
{
    // fill customers
};
var result = customers
    .Where(customerSpec.AsPredicate()).ToList();

var dbResult = Context.Customers
    .Where(customerSpec.GetExpression()).ToList();    // Or customerSpec.AsExpression()

Translations / Custom messages

// Single error message for whole specifications chain
customerSpec
    .WithMessage(c => $"Validation failed: Incorrect Customer - ID '{c.CustomerId}'")
    .IsSatisfiedBy(new Customer
    {
        CustomerId = 90,
        LastName = "Jones",
        IsActive = false,
        Email = "mjones@hotmail.com",
        Items = null,
        CreditCard = new CreditCard
        {
            CardNumber = "5500 0000 1",
            ValidityDate = DateTime.Parse("2019-03-01")
        }
    }, out var specResult);    // return false

Console.WriteLine(specResult.ToString());
// Validation failed: Incorrect Customer - ID '90'


// Custom messages for each specification
var idSpec = Specification
    .NotEmpty<Customer, int>(c => c.CustomerId)
    .WithMessage(c => $"Unknown Customer ID: '{c.CustomerId}'");

var activeSpec = Specification
    .True<Customer>(c => c.IsActive)
    .WithMessage("Customer is archived");

idSpec.And(activeSpec)
    .IsSatisfiedBy(new Customer(), out var specResult);    // return false

Console.WriteLine(specResult.ToString());
// Unknown Customer ID: '0'
// Customer is archived

GitHub