SpecOps 0.3.0

dotnet add package SpecOps --version 0.3.0
                    
NuGet\Install-Package SpecOps -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SpecOps" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SpecOps" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="SpecOps" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SpecOps --version 0.3.0
                    
#r "nuget: SpecOps, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SpecOps@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SpecOps&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=SpecOps&version=0.3.0
                    
Install as a Cake Tool

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.3.0 308 2/22/2026
0.2.0 131 2/22/2026
0.1.1 104 2/22/2026
0.1.0 106 2/22/2026