CSharpEssentials.Rules 6.5.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package CSharpEssentials.Rules --version 6.5.1
                    
NuGet\Install-Package CSharpEssentials.Rules -Version 6.5.1
                    
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="CSharpEssentials.Rules" Version="6.5.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CSharpEssentials.Rules" Version="6.5.1" />
                    
Directory.Packages.props
<PackageReference Include="CSharpEssentials.Rules" />
                    
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 CSharpEssentials.Rules --version 6.5.1
                    
#r "nuget: CSharpEssentials.Rules, 6.5.1"
                    
#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 CSharpEssentials.Rules@6.5.1
                    
#: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=CSharpEssentials.Rules&version=6.5.1
                    
Install as a Cake Addin
#tool nuget:?package=CSharpEssentials.Rules&version=6.5.1
                    
Install as a Cake Tool

CSharpEssentials.Rules

Composable, functional rule engine for .NET. Define business logic as small, reusable rules and combine them with a fluent API.

Features

  • Three definition styles: Class, Func field, or inline lambda. All are first-class.
  • No boilerplate conversions: Pass functions directly to RuleEngine; no explicit .ToRule() required at call sites.
  • Linear Sequences: Run rules in order, stop on first failure.
  • Conditional Rules: If/Then/Else branching.
  • And / Or Logic: All must pass, or at least one must pass.
  • Async Support: IAsyncRule rules evaluated without blocking via await RuleEngine.EvaluateAsync(...).
  • Result Integration: Built on CSharpEssentials.Results.

Installation

dotnet add package CSharpEssentials.Rules

Namespaces

using CSharpEssentials.Rules;         // IRule<T>, IAsyncRule<T>, RuleEngine, .And() / .Or() / .Linear() / .Next() / .ToRule()
using CSharpEssentials.ResultPattern; // Result, Result<T>
using CSharpEssentials.Errors;        // Error

The samples below assume these usings.

Rule Definition Styles

There are three ways to define a rule. All styles are interchangeable and can be mixed freely when composing.

Implement IRule<TContext> when a rule needs constructor-injected dependencies, internal state, or readable naming across a large codebase.

public sealed class AgeRule : IRule<UserContext>
{
    public Result Evaluate(UserContext ctx, CancellationToken ct = default) =>
        ctx.Age >= 18 ? Result.Success() : Error.Validation("Age.Underage", "Must be at least 18.");
}

public sealed class LicenseRule : IRule<UserContext>
{
    private readonly ILicenseRepository _repo;

    public LicenseRule(ILicenseRepository repo) => _repo = repo;

    public Result Evaluate(UserContext ctx, CancellationToken ct = default) =>
        _repo.IsValid(ctx.LicenseId) ? Result.Success() : Error.Validation("License.Invalid", "License not found.");
}

2. Func field (reusable, no class needed)

Declare rules as Func<TContext, Result> fields or variables. They can be stored, passed around, and used anywhere an IRule<TContext> is expected, with or without an explicit .ToRule() call.

// Defined once, reused anywhere
Func<UserContext, Result> regionRule = ctx =>
    ctx.IsAllowedRegion ? Result.Success() : Error.Forbidden("Region.Blocked", "Not available in your region.");

// Named method — method group works too
static Result CheckEmail(UserContext ctx) =>
    ctx.Email.Contains('@') ? Result.Success() : Error.Validation("Email.Invalid", "Invalid email address.");

3. Inline lambda (best for one-off or ad-hoc composition)

Write the rule directly at the call site. No variable, no class: maximum density for simple checks.

Result result = RuleEngine.Evaluate(
    (UserContext ctx) => ctx.Age >= 18 ? Result.Success() : Error.Validation("Age.Underage", "Must be at least 18."),
    userCtx);

Passing Rules Without Explicit Conversion

RuleEngine.Evaluate, .And(), .Or(), and .Next() all accept raw Func delegates. You never need to wrap them manually.

var ctx = new UserContext { Age = 20, HasLicense = true, IsAllowedRegion = true };

// Class instance
Result r1 = RuleEngine.Evaluate(new AgeRule(), ctx);

// Func variable — no .ToRule() needed
Result r2 = RuleEngine.Evaluate(regionRule, ctx);

// Inline lambda — no .ToRule() needed
Result r3 = RuleEngine.Evaluate(
    (UserContext c) => c.HasLicense ? Result.Success() : Error.Validation("License.Missing", "License required."),
    ctx);

// Named method group — works directly
Result r4 = RuleEngine.Evaluate(CheckEmail, ctx);

Combining Rules

All three styles compose freely. Mix class instances, Func variables, and lambdas in the same call.

And: all must pass

And: all must pass. Result rules stop at the first failure and return its errors; Result<T> rules evaluate every child and aggregate all errors.

// Using class instances
Result andResult = RuleEngine.Evaluate(
    new IRuleBase<UserContext>[] { new AgeRule(), new LicenseRule(repo) }.And(),
    ctx);

// Using Func array — no .ToRule() needed at all
Result andResult2 = RuleEngine.Evaluate(
    new Func<UserContext, Result>[] { regionRule, CheckEmail }.And(),
    ctx);

// Mixed: class + lambda
Result andResult3 = RuleEngine.Evaluate(
    new IRuleBase<UserContext>[]
    {
        new AgeRule(),
        regionRule.ToRule(),
        ((Func<UserContext, Result>)(c => c.HasLicense ? Result.Success() : Error.Validation("License.Missing", "License required."))).ToRule()
    }.And(),
    ctx);

Or: at least one must pass

// Func array — direct, no conversion
Result orResult = RuleEngine.Evaluate(
    new Func<UserContext, Result>[] { regionRule, CheckEmail }.Or(),
    ctx);

Or succeeds as soon as one rule passes. When every rule fails, the result carries the errors of all of them.

Linear: stop on first failure

// Class instances
Result linear = RuleEngine.Evaluate(
    new IRule<UserContext>[] { new AgeRule(), new LicenseRule(repo) }.Linear(),
    ctx);

// .Next() runs the second rule only when the first one passes
Result pipeline = RuleEngine.Evaluate(
    ((Func<UserContext, Result>)CheckEmail).Next(regionRule),
    ctx);

// Longer sequences: keep chaining Next, or use Linear()
Result longer = RuleEngine.Evaluate(
    ((Func<UserContext, Result>)CheckEmail).Next(regionRule).Next(c => c.Age >= 18 ? Result.Success() : Error.Validation("Age.Underage", "Must be at least 18.")),
    ctx);

Next links one rule to the rule after it. a.Next(b).Next(c) appends c to the end of the chain, so it behaves the same as a.Next(b.Next(c)) and new[] { a, b, c }.Linear(): a failing b stops the chain and c is not evaluated.

Static shortcuts

RuleEngine.And, RuleEngine.Or and RuleEngine.Linear take the array and the context directly, so no .And() / .Or() / .Linear() call is needed:

Result all = RuleEngine.And(new Func<UserContext, Result>[] { regionRule, CheckEmail }, ctx);
Result any = RuleEngine.Or(new IRule<UserContext>[] { new AgeRule(), new LicenseRule(repo) }, ctx);
Result seq = RuleEngine.Linear(new IRule<UserContext>[] { new AgeRule(), new LicenseRule(repo) }, ctx);

Every combinator also exists for rules with values (IRule<TContext, TResult>, Func<TContext, Result<TResult>>) and for async rules.

Conditional: if/then/else branching

// Class instances
Result conditional = RuleEngine.If(
    new AgeRule(),
    success: new GrantAccessRule(),
    failure: new DenyAccessRule(),
    ctx);

// Func lambdas — all three branches inline
Result conditional2 = RuleEngine.If(
    (UserContext c) => c.Age >= 18 ? Result.Success() : Error.Validation("Age.Underage", "Must be at least 18."),
    success: c => Result.Success(),
    failure: c => Error.Forbidden("Access.Denied", "Access denied."),
    ctx);

// Boolean shorthand
Result conditional3 = RuleEngine.If(
    condition: ctx.IsAllowedRegion,
    success: new GrantAccessRule(),
    failure: new DenyAccessRule(),
    ctx);

Rules with Values (Result<T>)

Rules can produce a typed result alongside success/failure.

// Class form
public sealed class GradeRule : IRule<int, string>
{
    public Result<string> Evaluate(int score, CancellationToken ct = default)
    {
        if (score >= 90) return "A";
        if (score >= 80) return "B";
        return Error.Validation("Grade.Failed", "Score too low.");
    }
}

// Func form — no .ToRule() at call site
Result<string> grade = RuleEngine.Evaluate(
    (int score) => score >= 90 ? Result<string>.Success("A") : Error.Validation("Grade.Failed", "Score too low."),
    85);

Integration with Domain Error Hierarchies

Rules integrate naturally with domain-specific Error factory classes, keeping validation logic close to domain language:

public static class RegistrationErrors
{
    public static readonly Error Underage =
        Error.Validation("Registration.Underage", "Applicant must be at least 18.");

    public static readonly Error NoLicense =
        Error.Validation("Registration.NoLicense", "A valid driver's license is required.");

    public static readonly Error RegionBlocked =
        Error.Forbidden("Registration.RegionBlocked", "Registration is not available in your region.");
}

// Class-based rules referencing the domain error catalogue
public sealed class AgeRule : IRule<ApplicantContext>
{
    public Result Evaluate(ApplicantContext ctx, CancellationToken ct = default) =>
        ctx.Age >= 18 ? Result.Success() : RegistrationErrors.Underage;
}

// Func-based rule — same domain errors, no class needed
Func<ApplicantContext, Result> regionRule =
    ctx => ctx.IsAllowedRegion ? Result.Success() : RegistrationErrors.RegionBlocked;

// Compose with And: Result rules stop at the first failure and return its errors
var applicant = new ApplicantContext { Age = 16, HasLicense = false, IsAllowedRegion = true };

Result result = RuleEngine.Evaluate(
    new Func<ApplicantContext, Result>[]
    {
        c => c.Age >= 18 ? Result.Success() : RegistrationErrors.Underage,
        c => c.HasLicense    ? Result.Success() : RegistrationErrors.NoLicense,
        regionRule
    }.And(),
    applicant);

result.Switch(
    onSuccess: () => Console.WriteLine("Approved"),
    onFailure: errors =>
    {
        foreach (Error e in errors)
            Console.WriteLine($"[{e.Type}] {e.Code}: {e.Description}");
    });
// [Validation] Registration.Underage: Applicant must be at least 18.

Rules return Result / Result<T>, the same type used throughout CSharpEssentials, so rule outcomes compose directly with Then, Match, and other chaining operations.

FromPredicate / FromPredicateAsync: Inline Rules Without Classes

// Static error — reuse the same error for any failing context
IRule<int> positiveRule = RuleEngine.FromPredicate<int>(
    x => x > 0,
    Error.Validation("Value.Negative", "Must be positive"));

// Error factory — error message can reference the failing value
IRule<int> rangeRule = RuleEngine.FromPredicate<int>(
    x => x is >= 1 and <= 100,
    x => Error.Validation("Value.OutOfRange", $"Value {x} must be between 1 and 100"));

// Async predicate
IAsyncRule<string> uniqueRule = RuleEngine.FromPredicateAsync<string>(
    async s => await _db.IsUniqueAsync(s),
    s => Error.Conflict("Name.Taken", $"'{s}' is already taken"));

// Sync-only trees can use Evaluate
Result r = RuleEngine.Evaluate(positiveRule, 42);
Result composed = RuleEngine.Evaluate(
    new IRuleBase<int>[] { positiveRule, rangeRule }.And(),
    value);

// Trees that contain async rules: await EvaluateAsync
Result unique = await RuleEngine.EvaluateAsync(uniqueRule, name, ct);

Async Rules: EvaluateAsync

An async rule implements IAsyncRule<TContext> (or IAsyncRule<TContext, TResult>), or comes from FromPredicateAsync above, or from a Func<TContext, CancellationToken, ValueTask<Result>> converted with .ToRule():

public sealed class UniqueNameRule(INameStore store) : IAsyncRule<string>
{
    public async ValueTask<Result> EvaluateAsync(string name, CancellationToken ct = default) =>
        await store.IsUniqueAsync(name, ct) ? Result.Success() : Error.Conflict("Name.Taken", $"{name} is already taken");
}

IAsyncRule<string> notReserved = ((Func<string, CancellationToken, ValueTask<Result>>)(async (name, ct) =>
{
    await Task.Yield();
    return name != "admin" ? Result.Success() : Error.Validation("Name.Reserved", "This name is reserved.");
})).ToRule();

RuleEngine.EvaluateAsync takes the same arguments as Evaluate and returns ValueTask<Result> / ValueTask<Result<T>>. It awaits every IAsyncRule in the tree, so no thread is blocked on database or HTTP calls.

Result result = await RuleEngine.EvaluateAsync(
    new IRuleBase<string>[]
    {
        RuleEngine.FromPredicate<string>(s => s.Length >= 3, Error.Validation("Name.TooShort", "Use at least 3 characters")),
        uniqueRule
    }.And(),
    name,
    ct);
  • Results, short-circuiting, and error order match Evaluate for every rule kind (Linear, Conditional, And, Or, nested trees).
  • Children of composites run one at a time, in order.
  • A tree made only of sync rules completes synchronously, without allocating a task.
  • Cancelling ct throws OperationCanceledException. It is not turned into an error Result. Evaluate propagates it until an enclosing sync rule (simple, And, Or, Linear, Conditional) returns it as an error Result, and throws when no sync rule encloses it. It also stops waiting for a pending async rule, which may keep running.
  • Rules must observe ct. The token is checked before each rule runs, but a rule that is already running is awaited until it returns; EvaluateAsync never abandons it.

Evaluate still works with async rules, but it blocks the calling thread until they finish. That can deadlock under a SynchronizationContext and wastes thread-pool threads. Use EvaluateAsync when the tree contains async rules.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.  net11.0 is compatible. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CSharpEssentials.Rules:

Package Downloads
CSharpEssentials

A comprehensive C# library enhancing functional programming capabilities with type-safe monads (Maybe, Result), discriminated unions (Any), and robust error handling. Features include: domain-driven design support, enhanced Entity Framework integration, testable time management, JSON utilities, and LINQ extensions. Built for modern C# development with focus on maintainability, testability, and functional programming principles.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.5.2 87 10/10/2026
6.5.1 94 10/10/2026
6.5.0 94 10/9/2026
6.4.0 90 10/9/2026
6.3.0 90 10/8/2026
6.2.0 97 10/8/2026
6.1.0 103 10/7/2026
6.0.0 105 10/7/2026
5.2.1 117 10/7/2026
5.2.0 111 10/7/2026
5.1.0 113 10/7/2026
5.0.0 120 10/6/2026
4.1.0 118 10/6/2026
4.0.0 122 10/5/2026
3.2.3 290 5/31/2026
3.2.2 186 5/31/2026
3.2.1 175 5/31/2026
3.2.0 170 5/30/2026
3.1.0 176 5/27/2026
3.0.8 152 5/20/2026
Loading failed