CSharpEssentials.Rules
6.1.0
See the version list below for details.
dotnet add package CSharpEssentials.Rules --version 6.1.0
NuGet\Install-Package CSharpEssentials.Rules -Version 6.1.0
<PackageReference Include="CSharpEssentials.Rules" Version="6.1.0" />
<PackageVersion Include="CSharpEssentials.Rules" Version="6.1.0" />
<PackageReference Include="CSharpEssentials.Rules" />
paket add CSharpEssentials.Rules --version 6.1.0
#r "nuget: CSharpEssentials.Rules, 6.1.0"
#:package CSharpEssentials.Rules@6.1.0
#addin nuget:?package=CSharpEssentials.Rules&version=6.1.0
#tool nuget:?package=CSharpEssentials.Rules&version=6.1.0
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: First-class async rules.
- Result Integration: Built on
CSharpEssentials.Results.
Installation
dotnet add package CSharpEssentials.Rules
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 (collects all failures)
// 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);
Linear: stop on first failure
// Class instances
Result linear = RuleEngine.Evaluate(
new IRule<UserContext>[] { new AgeRule(), new LicenseRule(repo) }.Linear(),
ctx);
// Func chaining with .Next() — reads like a pipeline
Result pipeline = 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);
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: Func array runs all rules and collects all failures
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.
// [Validation] Registration.NoLicense: A valid driver's license is required.
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"));
// Evaluate normally — works with all composition methods
Result r = RuleEngine.Evaluate(positiveRule, 42);
Result composed = RuleEngine.Evaluate(
new IRuleBase<int>[] { positiveRule, rangeRule }.And(),
value);
| 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.1.0)
- CSharpEssentials.Results (>= 6.1.0)
- System.Text.Json (>= 9.0.4)
-
net10.0
- CSharpEssentials.Core (>= 6.1.0)
- CSharpEssentials.Results (>= 6.1.0)
-
net11.0
- CSharpEssentials.Core (>= 6.1.0)
- CSharpEssentials.Results (>= 6.1.0)
-
net9.0
- CSharpEssentials.Core (>= 6.1.0)
- CSharpEssentials.Results (>= 6.1.0)
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 |