Combinando paquetes de Vali-Flow
Los paquetes del ecosistema Vali-Flow son independientes pero están diseñados para ser compuestos. Esta guía muestra patrones prácticos para usar múltiples paquetes juntos en una sola aplicación.
La base compartida
Todos los paquetes traducen el mismo árbol de expresiones ValiFlow<T>. Construir un filtro una vez y reutilizarlo en diferentes tecnologías es el valor central del ecosistema:
// Definir el filtro una vez
var filter = new ValiFlow<Order>()
.EqualTo(x => x.Status, "Processing")
.GreaterThanOrEqualTo(x => x.Total, 100m);
// Usar con EF Core
var efSpec = new QuerySpecification<Order>(filter);
var dbOrders = await efEvaluator.EvaluateQueryAsync(efSpec);
// Usar con Dapper (SQL)
var sql = filter.ToSql(new PostgreSqlDialect());
var sqlOrders = await conn.QueryAsync<Order>($"SELECT * FROM orders WHERE {sql.Sql}", sql.Parameters);
// Usar con lista en memoria
var inMemOrders = inMemEvaluator.EvaluateQuery(filter);
// Usar con MongoDB
var mongoFilter = filter.ToMongo();
var mongoOrders = await collection.Find(mongoFilter).ToListAsync();
Las mismas condiciones, cuatro tecnologías de persistencia, cero duplicación.
Patrón 1: EF Core + InMemory (paridad test/producción)
La combinación más común. El código de producción usa el evaluador EF Core; los tests usan el evaluador InMemory con el mismo filtro.
// Especificación compartida
public class ActiveOrdersSpec : QuerySpecification<Order>
{
public ActiveOrdersSpec(decimal minTotal) : base(
new ValiFlow<Order>()
.EqualTo(x => x.IsActive, true)
.GreaterThanOrEqualTo(x => x.Total, minTotal))
{ }
}
// Producción
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_SobreUmbral_FiltraCorrectamente()
{
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);
}
Patrón 2: EF Core + SQL (separación lectura/escritura)
Usa EF Core para escrituras y consultas relacionales complejas; usa Vali-Flow.Sql + Dapper para consultas de lectura intensiva con alta performance.
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;
}
// Camino de escritura — EF Core (change tracking, validación, etc.)
public Task<Order> CreateOrderAsync(Order order)
=> _ef.AddAsync(order);
// Camino de lectura — Dapper (SQL puro, JOINs, proyecciones)
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
);
}
}
Patrón 3: Fan-out multi-almacén
Algunas aplicaciones escriben a múltiples almacenes (p.ej., una BD relacional como fuente de verdad y Elasticsearch para búsquedas). Un único filtro puede usarse para leer de ambos:
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);
// Camino de búsqueda — Elasticsearch (full-text, rápido)
var esQuery = filter.ToElasticsearch();
var esResult = await _es.SearchAsync<Product>(s => s.Query(esQuery).Size(20));
// Fallback / validación — EF Core
var spec = new QuerySpecification<Product>(filter);
int dbCount = await _ef.EvaluateCountAsync(spec);
return new SearchResult
{
Items = esResult.Documents,
DbCount = dbCount,
};
}
}
Patrón 4: Capa de caché con InMemory
Cargar un dataset en memoria una vez, luego servir consultas de filtro desde la caché sin acceder a la base de datos:
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);
}
}
Patrón 5: Negación transversal
negateCondition: true está disponible en los métodos de lectura e invierte el filtro. Úsalo para consultar ambos lados de una condición sin construir dos filtros separados:
var filter = new ValiFlow<Product>().EqualTo(x => x.IsActive, true);
var spec = new QuerySpecification<Product>(filter);
// Productos activos
var active = await evaluator.EvaluateQueryAsync(spec);
// Productos inactivos — mismo filtro, negado
var inactive = await evaluator.EvaluateQueryAsync(spec, negateCondition: true);
Configuración de inyección de dependencias (EF Core)
// Program.cs / Startup.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddScoped(typeof(ValiFlowEvaluator<>), typeof(ValiFlowEvaluator<>));
// Uso en un servicio
public class OrderService
{
private readonly ValiFlowEvaluator<Order> _evaluator;
public OrderService(ValiFlowEvaluator<Order> evaluator)
=> _evaluator = evaluator;
}