Skip to main content

Combining Vali-Flow Packages

The packages in the Vali-Flow ecosystem are independent but designed to be composed. This guide shows practical patterns for using multiple packages together in a single application.


The shared foundation

All packages translate the same ValiFlow<T> expression tree. Building a filter once and reusing it across technologies is the core value proposition:

// Define the filter once
var filter = new ValiFlow<Order>()
.EqualTo(x => x.Status, "Processing")
.GreaterThanOrEqualTo(x => x.Total, 100m);

// Use with EF Core
var efSpec = new QuerySpecification<Order>(filter);
var dbOrders = await efEvaluator.EvaluateQueryAsync(efSpec);

// Use with Dapper (SQL)
var sql = filter.ToSql(new PostgreSqlDialect());
var sqlOrders = await conn.QueryAsync<Order>($"SELECT * FROM orders WHERE {sql.Sql}", sql.Parameters);

// Use with in-memory list
var inMemOrders = inMemEvaluator.EvaluateQuery(filter);

// Use with MongoDB
var mongoFilter = filter.ToMongo();
var mongoOrders = await collection.Find(mongoFilter).ToListAsync();

The same conditions, four persistence technologies, zero duplication.


Pattern 1: EF Core + InMemory (test/prod parity)

The most common combination. Production code uses the EF Core evaluator; tests use the InMemory evaluator with the same filter.

// Shared specification
public class ActiveOrdersSpec : QuerySpecification<Order>
{
public ActiveOrdersSpec(decimal minTotal) : base(
new ValiFlow<Order>()
.EqualTo(x => x.IsActive, true)
.GreaterThanOrEqualTo(x => x.Total, minTotal))
{ }
}

// Production
public class OrderService
{
private readonly ValiFlowEvaluator<Order> _evaluator;

public OrderService(AppDbContext context)
=> _evaluator = new ValiFlowEvaluator<Order>(context);

public Task<IEnumerable<Order>> GetActiveOrdersAsync(decimal minTotal)
=> _evaluator.EvaluateQueryAsync(new ActiveOrdersSpec(minTotal));
}

// Tests
[Fact]
public void ActiveOrders_AboveThreshold_FiltersCorrectly()
{
var orders = new List<Order>
{
new() { Id = 1, IsActive = true, Total = 150m },
new() { Id = 2, IsActive = false, Total = 300m },
new() { Id = 3, IsActive = true, Total = 50m },
};

var evaluator = new ValiFlowEvaluator<Order, int>(orders, null, o => o.Id);
var spec = new ActiveOrdersSpec(100m);

var result = evaluator.EvaluateQuery(spec.Filter).ToList();

Assert.Single(result);
Assert.Equal(1, result[0].Id);
}

Pattern 2: EF Core + SQL (read/write split)

Use EF Core for writes and complex relational queries; use Vali-Flow.Sql + Dapper for read-heavy, performance-sensitive queries.

public class ReportingService
{
private readonly ValiFlowEvaluator<Order> _ef;
private readonly IDbConnection _conn;

public ReportingService(AppDbContext context, IDbConnection conn)
{
_ef = new ValiFlowEvaluator<Order>(context);
_conn = conn;
}

// Write path — EF Core (change tracking, validation, etc.)
public Task<Order> CreateOrderAsync(Order order)
=> _ef.AddAsync(order);

// Read path — Dapper (raw SQL, joins, projections)
public async Task<IEnumerable<OrderSummary>> GetSummaryAsync(decimal minTotal)
{
var filter = new ValiFlow<Order>()
.GreaterThanOrEqualTo(x => x.Total, minTotal)
.EqualTo(x => x.IsActive, true);

var sql = filter.ToSql(new PostgreSqlDialect());

return await _conn.QueryAsync<OrderSummary>(
$@"SELECT o.Id, o.Total, c.Name AS CustomerName
FROM orders o
JOIN customers c ON c.Id = o.CustomerId
WHERE {sql.Sql}
ORDER BY o.Total DESC",
sql.Parameters
);
}
}

Pattern 3: Multi-store fan-out

Some applications write to multiple stores (e.g., a relational DB as source of truth and Elasticsearch for search). A single filter can be used to read from both:

public class ProductSearchService
{
private readonly ValiFlowEvaluator<Product> _ef;
private readonly ElasticsearchClient _es;
private readonly IMongoCollection<Product> _mongo;

public async Task<SearchResult> SearchAsync(string category, decimal maxPrice)
{
var filter = new ValiFlow<Product>()
.EqualTo(x => x.Category, category)
.LessThanOrEqualTo(x => x.Price, maxPrice)
.EqualTo(x => x.IsActive, true);

// Search path — Elasticsearch (full-text, fast)
var esQuery = filter.ToElasticsearch();
var esResult = await _es.SearchAsync<Product>(s => s.Query(esQuery).Size(20));

// Fallback / validation — EF Core
var spec = new QuerySpecification<Product>(filter);
int dbCount = await _ef.EvaluateCountAsync(spec);

return new SearchResult
{
Items = esResult.Documents,
DbCount = dbCount,
};
}
}

Pattern 4: Cache layer with InMemory

Load a dataset into memory once, then serve filter queries from the cache without hitting the database:

public class ProductCacheService
{
private ValiFlowEvaluator<Product, int>? _cache;
private DateTime _loadedAt;

private async Task EnsureLoadedAsync(AppDbContext context)
{
if (_cache != null && DateTime.UtcNow - _loadedAt < TimeSpan.FromMinutes(5))
return;

var efEvaluator = new ValiFlowEvaluator<Product>(context);
var all = await efEvaluator.EvaluateQueryAsync(new QuerySpecification<Product>(null));

_cache = new ValiFlowEvaluator<Product, int>(all, null, p => p.Id);
_loadedAt = DateTime.UtcNow;
}

public async Task<IEnumerable<Product>> QueryAsync(ValiFlow<Product> filter, AppDbContext context)
{
await EnsureLoadedAsync(context);
return _cache!.EvaluateQuery(filter);
}
}

Pattern 5: Cross-cutting negation

negateCondition: true is available on read methods and inverts the filter. Use it to query both sides of a condition without building two separate filters:

var filter = new ValiFlow<Product>().EqualTo(x => x.IsActive, true);
var spec = new QuerySpecification<Product>(filter);

// Active products
var active = await evaluator.EvaluateQueryAsync(spec);

// Inactive products — same filter, negated
var inactive = await evaluator.EvaluateQueryAsync(spec, negateCondition: true);

Dependency injection setup (EF Core)

// Program.cs / Startup.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));

builder.Services.AddScoped(typeof(ValiFlowEvaluator<>), typeof(ValiFlowEvaluator<>));

// Usage in a service
public class OrderService
{
private readonly ValiFlowEvaluator<Order> _evaluator;

public OrderService(ValiFlowEvaluator<Order> evaluator)
=> _evaluator = evaluator;
}

See also