Skip to main content

Guide — Getting Started

Installation

Prerequisites

  • .NET 8.0 or .NET 9.0
  • No additional dependencies required

NuGet

dotnet add package Vali-Flow.Core

Or in the .csproj file:

<PackageReference Include="Vali-Flow.Core" Version="x.x.x" />

Core Concept: The Builder Is an Object

Unlike a validation library that uses attributes ([Required], [MinLength]), Vali-Flow builds rules as objects. This means:

  • Rules can be stored in variables
  • They can be passed as parameters
  • They can be combined and extended
  • They can be reused in different contexts (validation, filtering, etc.)

First Example: Validating an Object

using Vali_Flow.Core.Builder;

// Define the entity
public class User
{
public string? Email { get; set; }
public int Age { get; set; }
public bool IsActive { get; set; }
}

// Define the rule
var rule = new ValiFlow<User>()
.NotNull(u => u.Email)
.MinLength(u => u.Email, 5)
.IsEmail(u => u.Email)
.GreaterThan(u => u.Age, 18)
.IsTrue(u => u.IsActive);

// Validate an object
var user = new User { Email = "test@example.com", Age = 25, IsActive = true };

bool isValid = rule.IsValid(user); // true
bool isNotValid = rule.IsNotValid(user); // false

AND and OR

By default, all conditions are combined with AND. To use OR, call .Or() before the next condition:

var rule = new ValiFlow<User>()
.IsTrue(u => u.IsAdmin) // IsAdmin
.Or()
.IsTrue(u => u.IsSuperUser); // OR IsSuperUser

// Equivalent to: u => u.IsAdmin || u.IsSuperUser

// With mixed AND and OR:
var rule2 = new ValiFlow<User>()
.GreaterThan(u => u.Age, 18) // A
.IsTrue(u => u.IsActive) // AND B
.Or()
.IsTrue(u => u.IsAdmin); // OR C

// Equivalent to: u => (u.Age > 18 && u.IsActive) || u.IsAdmin
// AND always has higher precedence than OR (same as in C#)

Explicit Subgroups

To explicitly control precedence, subgroups can be used:

var rule = new ValiFlow<User>()
.AddSubGroup(g => g
.IsTrue(u => u.IsActive)
.Or()
.IsTrue(u => u.IsPending))
.GreaterThan(u => u.Age, 18);

// Equivalent to: u => (u.IsActive || u.IsPending) && u.Age > 18

Getting the Expression for Filtering

var rule = new ValiFlow<User>()
.GreaterThan(u => u.Age, 18)
.IsTrue(u => u.IsActive);

// Get as Expression<Func<T, bool>>
Expression<Func<User, bool>> expr = rule.Build();

// Use with in-memory lists:
var adults = users.Where(expr.Compile()).ToList();

// Or use BuildCached() to avoid recompiling on each call:
var compiled = rule.BuildCached();
var adults = users.Where(compiled).ToList();

Use with EF Core (ValiFlowQuery)

For database filters, use ValiFlowQuery<T> instead of ValiFlow<T>:

using Vali_Flow.Core.Builder;

// ValiFlowQuery only exposes methods that EF Core can translate to SQL
var filter = new ValiFlowQuery<User>()
.NotNull(u => u.Email)
.GreaterThan(u => u.Age, 18)
.IsTrue(u => u.IsActive);

// Use directly with IQueryable:
var users = await dbContext.Users
.Where(filter.Build())
.ToListAsync();

// Build() returns Expression<Func<User,bool>>
// EF Core translates it to: WHERE Email IS NOT NULL AND Age > 18 AND IsActive = 1

If a non-EF-safe method is used on ValiFlowQuery<T>, the compiler (Analyzer VF001) emits a warning:

var filter = new ValiFlowQuery<User>()
.IsEmail(u => u.Email); // warning VF001: not EF Core translatable

Negation

var activeRule = new ValiFlow<User>().IsTrue(u => u.IsActive);

// Normal build: x => x.IsActive
Expression<Func<User, bool>> active = activeRule.Build();

// Negated build: x => !x.IsActive
Expression<Func<User, bool>> inactive = activeRule.BuildNegated();

Conditional Validations: When and Unless

A condition can be made to apply only when another condition is met:

var rule = new ValiFlow<User>()
.When(
condition: u => u.IsEmployee, // only applies if employee
then: g => g.NotNull(u => u.EmployeeId) // validate EmployeeId
);

// If u.IsEmployee is false, the condition u.EmployeeId != null is not evaluated

// Unless is the inverse: applies the condition when the predicate is NOT met
var rule2 = new ValiFlow<User>()
.Unless(
condition: u => u.IsGuest,
then: g => g.NotNull(u => u.Email) // only if NOT a guest
);

Validating Navigation Properties

public class Order
{
public Customer? Customer { get; set; }
}

public class Customer
{
public string? Email { get; set; }
public string? Phone { get; set; }
}

var rule = new ValiFlow<Order>()
.ValidateNested(
o => o.Customer,
customer => customer
.NotNull(c => c.Email)
.NotNull(c => c.Phone)
);

// Equivalent to: o => o.Customer != null && o.Customer.Email != null && o.Customer.Phone != null
// The null-check for Customer is added automatically

Method Catalog by Type

Boolean

.IsTrue(u => u.IsActive)
.IsFalse(u => u.IsDeleted)

Comparison

.IsNull(u => u.MiddleName)
.NotNull(u => u.Email)
.EqualTo(u => u.Status, "Active")
.NotEqualTo(u => u.Status, "Deleted")

String

.MinLength(u => u.Name, 2)
.MaxLength(u => u.Name, 100)
.ExactLength(u => u.Code, 10)
.LengthBetween(u => u.Name, 2, 100)
.StartsWith(u => u.Code, "USR")
.EndsWith(u => u.Email, ".com")
.Contains(u => u.Bio, "developer")
.IsEmail(u => u.Email) // ValiFlow<T> only
.IsUrl(u => u.Website) // ValiFlow<T> only
.IsGuid(u => u.ExternalId) // ValiFlow<T> only
.IsNullOrEmpty(u => u.MiddleName)
.IsNotNullOrEmpty(u => u.Name)
.IsNullOrWhiteSpace(u => u.Notes)
.RegexMatch(u => u.Code, @"^\d{5}$") // ValiFlow<T> only

Numeric (int, long, double, decimal, float, short)

.GreaterThan(u => u.Age, 18)
.GreaterThanOrEqualTo(u => u.Score, 60)
.LessThan(u => u.Price, 1000)
.LessThanOrEqualTo(u => u.Quantity, 99)
.Between(u => u.Age, 18, 65)
.IsZero(u => u.Balance)
.IsNotZero(u => u.Price)
.IsPositive(u => u.Score)
.IsNegative(u => u.Discount)
.IsEven(u => u.Count)
.IsOdd(u => u.Count)
.IsMultipleOf(u => u.Quantity, 5)
.MinValue(u => u.Age, 0)
.MaxValue(u => u.Age, 120)

Collection

.IsEmpty(u => u.Tags)
.IsNotEmpty(u => u.Roles)
.CountEquals(u => u.Items, 3)
.CountGreaterThan(u => u.Items, 0)
.CountLessThan(u => u.Items, 100)
.Contains(u => u.Roles, "Admin") // collection contains an element
.All(u => u.Items, i => i.Price > 0) // ValiFlow<T> only
.Any(u => u.Items, i => i.IsActive) // ValiFlow<T> only
.None(u => u.Items, i => i.IsDeleted) // ValiFlow<T> only

DateTime

.IsInFuture(u => u.ExpiresAt)
.IsInPast(u => u.CreatedAt)
.IsBefore(u => u.ExpiresAt, DateTime.Now.AddDays(30))
.IsAfter(u => u.CreatedAt, new DateTime(2020, 1, 1))
.IsBetween(u => u.EventDate, startDate, endDate)
.IsToday(u => u.ScheduledAt)
.IsWeekday(u => u.AppointmentDate)
.IsWeekend(u => u.DayOff)

Reusing Rules as a Base

// Base rule
var baseUserRule = new ValiFlow<User>()
.NotNull(u => u.Email)
.GreaterThan(u => u.Age, 18);

// Implicit freeze on first use
baseUserRule.IsValid(someUser);

// Create extended rules (forks) without modifying the base
var adminRule = baseUserRule.IsTrue(u => u.IsAdmin);
var activeRule = baseUserRule.IsTrue(u => u.IsActive);

// baseUserRule is still only Email + Age
// adminRule = Email + Age + IsAdmin
// activeRule = Email + Age + IsActive

Explaining a Rule

Useful for logging and debugging:

var rule = new ValiFlow<User>()
.GreaterThan(u => u.Age, 18)
.IsTrue(u => u.IsActive);

Console.WriteLine(rule.Explain());
// Output: "(x.Age > 18) AND (x.IsActive == True)"