Skip to main content

Vali-Flow.InMemory

Overview

loading...

Vali-Flow.InMemory is a synchronous, zero-dependency in-memory evaluator for the Vali-Flow ecosystem. It operates on plain IEnumerable<T> collections instead of a DbContext, making it ideal for:

  • Unit tests — inject a pre-populated evaluator in place of the real EF Core evaluator, no database required.
  • Caching layers — evaluate business rules against an in-memory cache without hitting the database.
  • Prototyping — explore Vali-Flow's API without any infrastructure setup.

How it differs from the EF Core evaluator

AspectVali-Flow (EF Core)Vali-Flow.InMemory
Data sourceDbContext / SQL databaseIEnumerable<T>
ExecutionAsynchronous (async/await)Synchronous
Query translationLINQ-to-SQL via EF CoreLINQ-to-Objects
Includes / navigationIEfInclude<T> (JOINs)Not applicable
Change trackingEF Core change trackerInternal tracked lists

Installation

dotnet add package Vali-Flow.InMemory

Setup

Constructor

public ValiFlowEvaluator<T, TProperty>(
IEnumerable<T>? initialData = null,
ValiFlow<T>? valiFlow = null,
Func<T, TProperty>? getId = null)

Type parameters

ParameterDescription
TEntity type (must be a reference type).
TPropertyType of the entity's primary key (e.g., int, Guid, string).

Constructor parameters

ParameterTypeDescription
initialDataIEnumerable<T>?Seed data copied into the internal store. Pass null for an empty store.
valiFlowValiFlow<T>?Default filter applied by every method when no per-call filter is supplied. Pass null to match all entities.
getIdFunc<T, TProperty>?Key extractor used by write operations to identify entities. If null, the constructor looks for a public property named Id via reflection (cached once at construction time). Throws InvalidOperationException if Id is not found.

Example

var products = new List<Product>
{
new() { Id = 1, Name = "Widget", Price = 10m, IsActive = true },
new() { Id = 2, Name = "Gadget", Price = 120m, IsActive = true },
new() { Id = 3, Name = "Doohickey", Price = 5m, IsActive = false },
};

var filter = new ValiFlow<Product>().IsTrue(p => p.IsActive);

// Explicit key extractor
var evaluator = new ValiFlowEvaluator<Product, int>(
initialData: products,
valiFlow: filter,
getId: p => p.Id);

// Auto-detect 'Id' property (Product has a public int Id)
var evaluatorAuto = new ValiFlowEvaluator<Product, int>(products, filter);

Examples

1) Query all and count

var spec = new ValiFlow<Product>()
.GreaterThan(p => p.Price, 10m);

var evaluator = new ValiFlowEvaluator<Product, int>(products, spec, p => p.Id);

var list = evaluator.EvaluateAll<int>(null);
var count = evaluator.EvaluateCount();

2) Get first failed

var spec = new ValiFlow<Product>().IsTrue(p => p.IsActive);
var evaluator = new ValiFlowEvaluator<Product, int>(products, spec, p => p.Id);

Product? firstFailed = evaluator.EvaluateGetFirstFailed<int>(null);

The negateCondition Parameter

Every read method accepts a bool negateCondition parameter (default false). When set to true, the ValiFlow filter is logically inverted using BuildNegated() internally — equivalent to wrapping the entire expression in a !.

This is useful when you want to query items that fail a validation rule without defining a separate filter.

Before/after example

var filter = new ValiFlow<Product>().GreaterThan(p => p.Price, 50m);

var evaluator = new ValiFlowEvaluator<Product, int>(products, filter, p => p.Id);

// Normal: products with Price > 50
IEnumerable<Product> expensive = evaluator.EvaluateAll<int>(null);

// Negated: products with Price <= 50 (same filter, inverted)
IEnumerable<Product> cheap = evaluator.EvaluateAll<int>(null, negateCondition: true);

Note: Negated conditions derived from the instance-level _valiFlow are compiled once and cached. Passing an explicit valiFlow per-call bypasses the cache and compiles on every call.

Behaviour table

Method familynegateCondition = falsenegateCondition = true
EvaluateAllEntities matching the filterEntities not matching the filter
EvaluateAllFailedEntities not matching the filterEntities matching the filter
GetFirst / GetLastFirst/last matching entityFirst/last non-matching entity
GetFirstFailed / GetLastFailedFirst/last non-matching entityFirst/last matching entity
Aggregates / groupingComputed over matching entitiesComputed over non-matching entities

Read Methods

Evaluate

Signature

bool Evaluate(T entity, ValiFlow<T>? valiFlow = null, bool negateCondition = false)

Description Evaluates a single entity against the filter. Returns true if the entity satisfies the condition; false otherwise.

Example

var product = new Product { Id = 1, Price = 80m, IsActive = true };
var filter = new ValiFlow<Product>().GreaterThan(p => p.Price, 50m);
var evaluator = new ValiFlowEvaluator<Product, int>(getId: p => p.Id);

bool passes = evaluator.Evaluate(product, filter); // true
bool fails = evaluator.Evaluate(product, filter, negateCondition: true); // false

EvaluateAny

Signature

bool EvaluateAny(IEnumerable<T>? entities, ValiFlow<T>? valiFlow = null, bool negateCondition = false)

Description Returns true if at least one entity in the collection satisfies the filter. Pass null for entities to operate on the internal store.

Example

bool hasExpensive = evaluator.EvaluateAny(null);
// true if any product in the store has Price > 50

EvaluateCount

Signature

int EvaluateCount(IEnumerable<T>? entities, ValiFlow<T>? valiFlow = null, bool negateCondition = false)

Description Counts the number of entities that satisfy the filter.

Example

int activeCount = evaluator.EvaluateCount(null);
// returns 2 (Widget and Gadget are active)

int inactiveCount = evaluator.EvaluateCount(null, negateCondition: true);
// returns 1 (Doohickey is inactive)

GetFirst

Signature

T? GetFirst(IEnumerable<T>? entities, ValiFlow<T>? valiFlow = null, bool negateCondition = false)

Description Returns the first entity that satisfies the filter, or null if none match. No ordering is applied; the result depends on enumeration order.

Example

Product? first = evaluator.GetFirst(null);
// Returns the first active product in store order

GetFirstFailed

Signature

T? GetFirstFailed(IEnumerable<T>? entities, ValiFlow<T>? valiFlow = null, bool negateCondition = false)

Description Returns the first entity that does not satisfy the filter. When negateCondition = true, the logic inverts and returns the first entity that satisfies the filter (equivalent to GetFirst).

Example

Product? firstInactive = evaluator.GetFirstFailed(null);
// Returns Doohickey (IsActive = false)

EvaluateAll

Signature

IEnumerable<T> EvaluateAll<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns all entities that satisfy the filter, with optional primary and secondary ordering.

Example

// All active products sorted by price descending
IEnumerable<Product> sorted = evaluator.EvaluateAll<decimal>(
entities: null,
orderBy: p => p.Price,
ascending: false);

EvaluateAllFailed

Signature

IEnumerable<T> EvaluateAllFailed<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns all entities that do not satisfy the filter, with optional ordering. Useful for validation reports — collect everything that failed a rule.

Example

IEnumerable<Product> inactive = evaluator.EvaluateAllFailed<string>(
entities: null,
orderBy: p => p.Name);

GetLast

Signature

T? GetLast<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns the last entity that satisfies the filter after optional ordering. Returns null if none match.

Example

Product? mostExpensive = evaluator.GetLast<decimal>(
entities: null,
orderBy: p => p.Price,
ascending: true);

GetLastFailed

Signature

T? GetLastFailed<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns the last entity that does not satisfy the filter after optional ordering.

Example

Product? lastInactive = evaluator.GetLastFailed<string>(
entities: null,
orderBy: p => p.Name);

EvaluatePaged

Signature

IEnumerable<T> EvaluatePaged<TKey>(
IEnumerable<T> entities,
int page,
int pageSize,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns a paginated slice of the filtered entities. page is 1-based.

Note: Both page and pageSize must be >= 1, otherwise an ArgumentOutOfRangeException is thrown.

Example

// Page 2, 10 items per page, active products sorted by name
IEnumerable<Product> page2 = evaluator.EvaluatePaged<string>(
entities: allProducts,
page: 2,
pageSize: 10,
orderBy: p => p.Name);

EvaluatePagedResult

Signature

PagedResult<T> EvaluatePagedResult<TKey>(
IEnumerable<T>? entities,
int page,
int pageSize,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Like EvaluatePaged, but returns a PagedResult<T> that includes pagination metadata.

PagedResult<T> properties

PropertyTypeDescription
ItemsIReadOnlyList<T>Items on the current page.
TotalCountintTotal matching entities across all pages.
PageintCurrent page number (1-based).
PageSizeintMaximum items per page.
TotalPagesintTotal number of pages.
HasNextPageboolWhether a next page exists.
HasPreviousPageboolWhether a previous page exists.

Example

PagedResult<Product> result = evaluator.EvaluatePagedResult<decimal>(
entities: null,
page: 1,
pageSize: 5,
orderBy: p => p.Price,
ascending: false);

Console.WriteLine($"Page {result.Page} of {result.TotalPages}{result.TotalCount} total");

EvaluateTop

Signature

IEnumerable<T> EvaluateTop<TKey>(
IEnumerable<T> entities,
int count,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns the top N entities that satisfy the filter. count must be > 0.

Example

// Top 3 most expensive active products
IEnumerable<Product> top3 = evaluator.EvaluateTop<decimal>(
entities: null,
count: 3,
orderBy: p => p.Price,
ascending: false);

EvaluateDistinct

Signature

IEnumerable<T> EvaluateDistinct<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> selector,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns one representative entity per distinct key value, keeping only the first occurrence within each group after the filter is applied.

Example

// One product per category
IEnumerable<Product> onePerCategory = evaluator.EvaluateDistinct<string>(
entities: null,
selector: p => p.Category);

EvaluateDuplicates

Signature

IEnumerable<T> EvaluateDuplicates<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> selector,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns all entities whose key value appears more than once in the filtered set.

Example

// Products with a duplicate SKU among active ones
IEnumerable<Product> dupSkus = evaluator.EvaluateDuplicates<string>(
entities: null,
selector: p => p.Sku);

GetFirstMatchIndex

Signature

int GetFirstMatchIndex<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns the zero-based index of the first matching entity after ordering. Returns -1 if no entity matches.

Example

int idx = evaluator.GetFirstMatchIndex<decimal>(
entities: null,
orderBy: p => p.Price);
// Index of the cheapest active product in the ordered list

GetLastMatchIndex

Signature

int GetLastMatchIndex<TKey>(
IEnumerable<T>? entities,
Func<T, TKey>? orderBy = null,
bool ascending = true,
IEnumerable<InMemoryThenBy<T, TKey>>? thenBys = null,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)

Description Returns the zero-based index of the last matching entity after ordering. Returns -1 if no entity matches.

Example

int idx = evaluator.GetLastMatchIndex<decimal>(
entities: null,
orderBy: p => p.Price);
// Index of the most expensive active product in the ordered list

EvaluateMin

Signature

TResult EvaluateMin<TResult>(
IEnumerable<T>? entities,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult>

Description Computes the minimum value of a numeric property among filtered entities. Returns TResult.Zero if the filtered set is empty.

Example

decimal minPrice = evaluator.EvaluateMin<decimal>(null, p => p.Price);

EvaluateMax

Signature

TResult EvaluateMax<TResult>(
IEnumerable<T>? entities,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult>

Description Computes the maximum value of a numeric property among filtered entities. Returns TResult.Zero if the filtered set is empty.

Example

decimal maxPrice = evaluator.EvaluateMax<decimal>(null, p => p.Price);

EvaluateAverage

Signature

decimal EvaluateAverage<TResult>(
IEnumerable<T>? entities,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult>

Description Computes the average of a numeric property among filtered entities. Returns 0 if the filtered set is empty. Always returns decimal.

Example

decimal avgPrice = evaluator.EvaluateAverage<decimal>(null, p => p.Price);

EvaluateSum

Signature

TResult EvaluateSum<TResult>(
IEnumerable<T>? entities,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult>

Description Computes the sum of a numeric property among filtered entities.

Example

decimal totalRevenue = evaluator.EvaluateSum<decimal>(null, p => p.Price);

EvaluateAggregate

Signature

TResult EvaluateAggregate<TResult>(
IEnumerable<T>? entities,
Func<T, TResult> selector,
Func<TResult, TResult, TResult> aggregator,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult>

Description Applies a custom accumulator function over the selected numeric property of filtered entities. Starting accumulator value is TResult.Zero.

Example

// Custom product of stock quantities
int product = evaluator.EvaluateAggregate<int>(
entities: null,
selector: p => p.Stock,
aggregator: (acc, val) => acc + val * 2);

EvaluateGrouped

Signature

Dictionary<TKey, List<T>> EvaluateGrouped<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TKey : notnull

Description Groups filtered entities by a key and returns a dictionary of key → entity list.

Example

Dictionary<string, List<Product>> byCategory =
evaluator.EvaluateGrouped<string>(null, p => p.Category);

EvaluateCountByGroup

Signature

Dictionary<TKey, int> EvaluateCountByGroup<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TKey : notnull

Description Groups filtered entities by a key and returns a dictionary of key → count.

Example

Dictionary<string, int> countPerCategory =
evaluator.EvaluateCountByGroup<string>(null, p => p.Category);

EvaluateSumByGroup

Signature

Dictionary<TKey, TResult> EvaluateSumByGroup<TKey, TResult>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult> where TKey : notnull

Description Groups filtered entities by a key and returns the sum of a numeric property per group.

Example

Dictionary<string, decimal> revenueByCategory =
evaluator.EvaluateSumByGroup<string, decimal>(null, p => p.Category, p => p.Price);

EvaluateMinByGroup

Signature

Dictionary<TKey, TResult> EvaluateMinByGroup<TKey, TResult>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult> where TKey : notnull

Description Groups filtered entities by a key and returns the minimum value of a numeric property per group.

Example

Dictionary<string, decimal> minPriceByCategory =
evaluator.EvaluateMinByGroup<string, decimal>(null, p => p.Category, p => p.Price);

EvaluateMaxByGroup

Signature

Dictionary<TKey, TResult> EvaluateMaxByGroup<TKey, TResult>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult> where TKey : notnull

Description Groups filtered entities by a key and returns the maximum value of a numeric property per group.

Example

Dictionary<string, decimal> maxPriceByCategory =
evaluator.EvaluateMaxByGroup<string, decimal>(null, p => p.Category, p => p.Price);

EvaluateAverageByGroup

Signature

Dictionary<TKey, decimal> EvaluateAverageByGroup<TKey, TResult>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
Func<T, TResult> selector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TResult : INumber<TResult> where TKey : notnull

Description Groups filtered entities by a key and returns the average of a numeric property per group as decimal.

Example

Dictionary<string, decimal> avgPriceByCategory =
evaluator.EvaluateAverageByGroup<string, decimal>(null, p => p.Category, p => p.Price);

EvaluateDuplicatesByGroup

Signature

Dictionary<TKey, List<T>> EvaluateDuplicatesByGroup<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TKey : notnull

Description Groups filtered entities by a key and returns only the groups that contain more than one entity (duplicate groups).

Example

// Find categories with more than one active product
Dictionary<string, List<Product>> duplicates =
evaluator.EvaluateDuplicatesByGroup<string>(null, p => p.Category);

EvaluateUniquesByGroup

Signature

Dictionary<TKey, T> EvaluateUniquesByGroup<TKey>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TKey : notnull

Description Groups filtered entities by a key and returns only the groups with exactly one entity (unique groups). The dictionary value is the single entity, not a list.

Example

// Categories with exactly one active product
Dictionary<string, Product> uniques =
evaluator.EvaluateUniquesByGroup<string>(null, p => p.Category);

EvaluateTopByGroup

Signature

Dictionary<TKey, List<T>> EvaluateTopByGroup<TKey, TOrderKey>(
IEnumerable<T>? entities,
Func<T, TKey> keySelector,
int count,
Func<T, TOrderKey>? orderBy = null,
bool ascending = true,
ValiFlow<T>? valiFlow = null,
bool negateCondition = false)
where TKey : notnull

Description Groups filtered entities by a key and returns the top N entities per group, with optional intra-group ordering.

Example

// Top 2 most expensive products per category
Dictionary<string, List<Product>> top2PerCategory =
evaluator.EvaluateTopByGroup<string, decimal>(
entities: null,
keySelector: p => p.Category,
count: 2,
orderBy: p => p.Price,
ascending: false);

Write Methods

All write methods operate on the internal store by default. Pass an explicit entities collection to target an external list instead.

Add

Signature

bool Add(T entity, IEnumerable<T>? entities = null)

Description Adds a single entity to the store. Returns true on success.

Example

var newProduct = new Product { Id = 4, Name = "Thingamajig", Price = 30m, IsActive = true };
bool added = evaluator.Add(newProduct);

AddRange

Signature

void AddRange(IEnumerable<T> entitiesToAdd, IEnumerable<T>? entities = null)

Description Adds multiple entities to the store in one call.

Example

evaluator.AddRange(new[]
{
new Product { Id = 5, Name = "Alpha", Price = 15m, IsActive = true },
new Product { Id = 6, Name = "Beta", Price = 25m, IsActive = true },
});

Update

Signature

T? Update(T entity, IEnumerable<T>? entities = null)

Description Replaces the stored entity with the same key as the provided entity. Returns the updated entity, or null if no matching entity was found.

Example

var modified = new Product { Id = 1, Name = "Widget Pro", Price = 15m, IsActive = true };
Product? result = evaluator.Update(modified);

UpdateRange

Signature

IEnumerable<T> UpdateRange(IEnumerable<T> entitiesToUpdate, IEnumerable<T>? entities = null)

Description Updates multiple entities. Returns the collection of successfully updated entities.

Example

IEnumerable<Product> updated = evaluator.UpdateRange(modifiedProducts);

Delete

Signature

bool Delete(T entity, IEnumerable<T>? entities = null)

Description Removes a single entity from the store by matching its key. Returns true if found and removed.

Example

bool removed = evaluator.Delete(product);

DeleteRange

Signature

int DeleteRange(IEnumerable<T> entitiesToDelete, IEnumerable<T>? entities = null)

Description Removes multiple entities from the store. Returns the count of successfully removed entities.

Example

int count = evaluator.DeleteRange(staleProducts);

DeleteByCondition

Signature

int DeleteByCondition(Func<T, bool> predicate, IEnumerable<T>? entities = null)

Description Removes all entities that satisfy a predicate. Returns the count of removed entities.

Example

// Remove all inactive products
int removed = evaluator.DeleteByCondition(p => !p.IsActive);

Upsert

Signature

T Upsert(T entity, IEnumerable<T>? entities = null)

Description Inserts the entity if its key does not exist in the store; updates it if the key already exists. Returns the entity after the operation.

Example

var order = new Order { Id = 99, CustomerId = 1, Total = 250m };
Order upserted = evaluator.Upsert(order);

UpsertRange

Signature

IEnumerable<T> UpsertRange(IEnumerable<T> entitiesToUpsert, IEnumerable<T>? entities = null)

Description Performs Upsert on each entity in the collection. Returns all upserted entities.

Example

IEnumerable<Order> results = evaluator.UpsertRange(incomingOrders);

SaveChanges

Signature

void SaveChanges(IEnumerable<T>? entities = null)

Description Applies any pending tracked changes (additions, updates, deletions) to the store. Call this after batching write operations if you deferred them.

Example

evaluator.Add(productA);
evaluator.Add(productB);
evaluator.SaveChanges();

SetValiFlow

Signature

void SetValiFlow(ValiFlow<T> valiFlow)

Description Replaces the instance-level filter at runtime. Clears the cached negated condition so it is recompiled on the next negated call. Throws ArgumentNullException if valiFlow is null.

Useful when the evaluator is shared across test cases and each case requires a different filter.

Example

var evaluator = new ValiFlowEvaluator<User, int>(users, getId: u => u.Id);

// Initially no filter — all users
int allCount = evaluator.EvaluateCount(null);

// Switch to only premium users
evaluator.SetValiFlow(new ValiFlow<User>().IsTrue(u => u.IsPremium));
int premiumCount = evaluator.EvaluateCount(null);