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.
Allows to build and combine specifications in simple and clear way.
With validation and Linq expression support. Many of them available as negation.
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.
Specifications can be used in collections querying. Business logic of repositories can be separated to specifications and use interchangeably.
All specifications are tested with EF Core on real database and ca be used without errors.
Many of specifications support LinqToEntities (LinqToSql) and can be used in Entity Framework 6 queries.
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)));
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
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]
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()
// 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