SpecOps 0.3.0
dotnet add package SpecOps --version 0.3.0
NuGet\Install-Package SpecOps -Version 0.3.0
<PackageReference Include="SpecOps" Version="0.3.0" />
<PackageVersion Include="SpecOps" Version="0.3.0" />
<PackageReference Include="SpecOps" />
paket add SpecOps --version 0.3.0
#r "nuget: SpecOps, 0.3.0"
#:package SpecOps@0.3.0
#addin nuget:?package=SpecOps&version=0.3.0
#tool nuget:?package=SpecOps&version=0.3.0
SpecOps
A composable Specification pattern for .NET. Replace tangled Where clauses with readable, testable, reusable business rules — all translating directly to SQL via EF Core.
Why?
Queries like this are hard to read, impossible to unit test, and get copy-pasted everywhere with subtle variations:
var transactions = await dbContext.Transactions
.Where(t =>
t.Account.IsActive &&
!t.Account.IsFrozen &&
t.Amount > 10_000m &&
t.Currency == "GBP" &&
t.Status != TransactionStatus.Reversed &&
!(t.RiskScore > 0.7m ||
(t.CounterpartyCountry != "GB" &&
t.Amount > 50_000m)) &&
t.SettlementDate >= DateTime.UtcNow.AddDays(-30) &&
(!t.RequiresManualReview ||
t.ReviewedBy != null))
.OrderByDescending(t => t.Amount)
.ToListAsync();
With SpecOps, each rule is named, testable, and reusable:
var flagged = ActiveAccount()
.And().LargeTransaction(10_000m)
.And().InCurrency("GBP")
.And().NotReversed()
.And().Not().HighRisk()
.And().SettledWithin(30)
.And().ReviewComplete();
var transactions = await dbContext.Transactions
.WithSpecification(flagged)
.OrderByDescending(t => t.Amount)
.ToListAsync();
The query reads like a sentence. EF Core still translates it to a single SQL WHERE clause.
Installation
dotnet add package SpecOps
Quick Start
1. Define a specification
using System.Linq.Expressions;
using SpecOps;
public class ActiveClient : Specification<Client>
{
public override Expression<Func<Client, bool>> ToExpression()
=> client => client.IsActive;
}
2. Factory methods are generated automatically
SpecOps includes a source generator that creates a Specs class with factory and extension methods for every Specification<T> in your project. No boilerplate needed.
3. Compose and query
using static Specs;
var spec = Active()
.And().NameContaining("Acme")
.Or().CreatedAfter(lastMonth);
var results = await dbContext.Clients
.WithSpecification(spec)
.ToListAsync();
4. Test in isolation
var client = new Client { Name = "Acme", IsActive = true };
Active().IsSatisfiedBy(client).Should().BeTrue();
Active().IsNotSatisfiedBy(client).Should().BeFalse();
Composition
Explicit grouping
// email AND (name OR name)
var spec = ByEmail("fred@acme.com")
.And(NameContaining("Acme").Or(NameContaining("Widget")));
Fluent chaining (left-to-right)
// (email AND name) OR name — evaluated left-to-right
var spec = ByEmail("fred@acme.com")
.And().NameContaining("Acme")
.Or().NameContaining("Widget");
Negation
var inactive = Active().Not;
// In a chain
var spec = Active().And().Not().HighRisk();
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.