CSharpEssentials.Rules
6.5.2
dotnet add package CSharpEssentials.Rules --version 6.5.2
NuGet\Install-Package CSharpEssentials.Rules -Version 6.5.2
<PackageReference Include="CSharpEssentials.Rules" Version="6.5.2" />
<PackageVersion Include="CSharpEssentials.Rules" Version="6.5.2" />
<PackageReference Include="CSharpEssentials.Rules" />
paket add CSharpEssentials.Rules --version 6.5.2
#r "nuget: CSharpEssentials.Rules, 6.5.2"
#:package CSharpEssentials.Rules@6.5.2
#addin nuget:?package=CSharpEssentials.Rules&version=6.5.2
#tool nuget:?package=CSharpEssentials.Rules&version=6.5.2
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,
Funcfield, 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:
IAsyncRulerules evaluated without blocking viaawait 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.
1. Class (recommended for complex or injectable rules)
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
Evaluatefor 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
ctthrowsOperationCanceledException. It is not turned into an errorResult.Evaluatepropagates it until an enclosing sync rule (simple, And, Or, Linear, Conditional) returns it as an errorResult, 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;EvaluateAsyncnever 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 | Versions 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. |
-
.NETStandard 2.1
- CSharpEssentials.Core (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
- System.Text.Json (>= 9.0.4)
-
net10.0
- CSharpEssentials.Core (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
-
net11.0
- CSharpEssentials.Core (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
-
net9.0
- CSharpEssentials.Core (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
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 |