Nuuvify.CommonPack.UnitOfWork.Abstraction
2.2.0-preview.25102906
See the version list below for details.
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.2.0-preview.25102906
NuGet\Install-Package Nuuvify.CommonPack.UnitOfWork.Abstraction -Version 2.2.0-preview.25102906
<PackageReference Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" Version="2.2.0-preview.25102906" />
<PackageVersion Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" Version="2.2.0-preview.25102906" />
<PackageReference Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" />
paket add Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.2.0-preview.25102906
#r "nuget: Nuuvify.CommonPack.UnitOfWork.Abstraction, 2.2.0-preview.25102906"
#:package Nuuvify.CommonPack.UnitOfWork.Abstraction@2.2.0-preview.25102906
#addin nuget:?package=Nuuvify.CommonPack.UnitOfWork.Abstraction&version=2.2.0-preview.25102906&prerelease
#tool nuget:?package=Nuuvify.CommonPack.UnitOfWork.Abstraction&version=2.2.0-preview.25102906&prerelease
Nuuvify.CommonPack.UnitOfWork
Uma biblioteca .NET poderosa e flexível para implementação do padrão Unit of Work com consultas dinâmicas, filtros avançados, paginação e ordenação. Projetada especificamente para Entity Framework Core e aplicações empresariais.
🚀 Características Principais
- Unit of Work Pattern: Implementação completa do padrão Unit of Work
- Consultas Dinâmicas: Sistema avançado de filtros com Expression Trees
- 13 Operadores de Filtro: From
EqualstoContainsWithLikeForList(OR-based search) - Paginação Inteligente: Paginação otimizada com metadados completos
- Ordenação Flexível: Múltiplos critérios de ordenação
- Thread-Safe: Totalmente thread-safe para aplicações concorrentes
- Type-Safe: Validação em tempo de compilação com enums tipados
- Performance Otimizada: Queries eficientes com projection e defer loading
- Nullable Support: Suporte completo a Nullable Reference Types
- Expression Validation: Validação robusta de expressões e parâmetros
🆕 O Que Há de Novo na v2.2.0 (2025-10-28)
✅ Correções Críticas
Esta versão inclui 3 correções críticas no operador ContainsWithLikeForList:
🐛 Bug Expression.Constant(false) - CORRIGIDO
- Problema: Listas vazias geravam
WHERE 0 = 1(SQL inválido) - Solução: Retornar
nullpermite ignorar filtros vazios corretamente
- Problema: Listas vazias geravam
🐛 Bug Null Expression - CORRIGIDO
- Problema: Expressões nulas causavam crashes em
Expression.And/Or - Solução: Adicionado
if (actualExpression == null) continue;
- Problema: Expressões nulas causavam crashes em
🐛 Bug UnaryExpression Wrapping - CORRIGIDO
- Problema:
FilterByencapsulado emUnaryExpressionimpedia acesso à lista - Solução: Unwrap automático do
UnaryExpressionpara extrairConstantExpression - SQL Gerado:
-- Antes (bug): SELECT * FROM Products (sem WHERE) -- Depois (✅): SELECT * FROM Products WHERE Name LIKE '%iPhone%' OR Name LIKE '%Samsung%'
- Problema:
🎨 Melhorias de Código
Partial Classes:
FiltersExtensionsdividido em arquivos separadosFiltersExtensions.cs: API pública com documentação completaFiltersExtensions.Private.cs: Implementação privada
Documentação XML: Exemplos práticos em todos os métodos públicos
Code Cleanup: Comentários inline substituídos por documentação XML
100% Tested: Todos os 12 testes passando com cobertura completa
📖 Veja o CHANGELOG.md para detalhes técnicos completos
📦 Instalação
# Pacote principal com implementações
dotnet add package Nuuvify.CommonPack.UnitOfWork
# Pacote com abstrações (interfaces)
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction
⚙️ Configuração Rápida
1. Configuração no Program.cs (.NET 8)
using Nuuvify.CommonPack.UnitOfWork.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Registrar Entity Framework
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// Registrar Unit of Work
builder.Services.AddUnitOfWork<AppDbContext>();
var app = builder.Build();
2. Configuração com Options Pattern
builder.Services.Configure<UnitOfWorkOptions>(options =>
{
options.DefaultPageSize = 20;
options.MaxPageSize = 100;
options.EnableQuerySplitting = true;
options.EnableChangeTracking = false; // Para queries de leitura
});
builder.Services.AddUnitOfWork<AppDbContext>();
🎯 Casos de Uso Completos
Modelo de Dados de Exemplo
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastUpdate { get; set; }
public List<string> Tags { get; set; } = new();
}
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string CustomerEmail { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? ShippedDate { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
public enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled }
Modelo de Filtro Completo - Demonstrando Todos os Operadores
public class ProductSearchModel : IQueryableCustom
{
// ===== OPERADORES DE IGUALDADE =====
/// <summary>
/// Filtro por ID exato - Operador: Equals
/// Exemplo: WHERE Id = @value
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Id))]
public int? ProductId { get; set; }
/// <summary>
/// Filtro por categoria exata - Operador: Equals (case-insensitive)
/// Exemplo: WHERE UPPER(Category) = UPPER(@value)
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Category), CaseSensitive = false)]
public string? CategoryExact { get; set; }
/// <summary>
/// Exclusão por categoria - Operador: NotEquals
/// Exemplo: WHERE Category <> @value
/// </summary>
[QueryOperator(Operator = WhereOperator.NotEquals, HasName = nameof(Product.Category))]
public string? ExcludeCategory { get; set; }
// ===== OPERADORES DE COMPARAÇÃO NUMÉRICA =====
/// <summary>
/// Preço mínimo - Operador: GreaterThanOrEqualTo
/// Exemplo: WHERE Price >= @value
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = nameof(Product.Price))]
public decimal? MinPrice { get; set; }
/// <summary>
/// Preço máximo - Operador: LessThanOrEqualTo
/// Exemplo: WHERE Price <= @value
/// </summary>
[QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = nameof(Product.Price))]
public decimal? MaxPrice { get; set; }
/// <summary>
/// Produtos com preço maior que - Operador: GreaterThan
/// Exemplo: WHERE Price > @value
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThan, HasName = nameof(Product.Price))]
public decimal? PriceGreaterThan { get; set; }
/// <summary>
/// Produtos com preço menor que - Operador: LessThan
/// Exemplo: WHERE Price < @value
/// </summary>
[QueryOperator(Operator = WhereOperator.LessThan, HasName = nameof(Product.Price))]
public decimal? PriceLessThan { get; set; }
// ===== OPERADORES DE COMPARAÇÃO COM NULLABLE =====
/// <summary>
/// Estoque mínimo (com suporte nullable) - Operador: GreaterThanOrEqualWhenNullable
/// Exemplo: WHERE Stock >= @value (com conversão automática de nullable)
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualWhenNullable, HasName = nameof(Product.Stock))]
public int? MinStock { get; set; }
/// <summary>
/// Estoque máximo (com suporte nullable) - Operador: LessThanOrEqualWhenNullable
/// Exemplo: WHERE Stock <= @value (com conversão automática de nullable)
/// </summary>
[QueryOperator(Operator = WhereOperator.LessThanOrEqualWhenNullable, HasName = nameof(Product.Stock))]
public int? MaxStock { get; set; }
/// <summary>
/// Data de atualização (com suporte nullable) - Operador: EqualsWhenNullable
/// Exemplo: WHERE LastUpdate = @value (com conversão automática de nullable)
/// </summary>
[QueryOperator(Operator = WhereOperator.EqualsWhenNullable, HasName = nameof(Product.LastUpdate))]
public DateTime? LastUpdateDate { get; set; }
// ===== OPERADORES DE TEXTO =====
/// <summary>
/// Busca por nome (contém) - Operador: Contains
/// Exemplo: WHERE Name.Contains(@value)
/// </summary>
[QueryOperator(Operator = WhereOperator.Contains, HasName = nameof(Product.Name), CaseSensitive = false)]
public string? NameSearch { get; set; }
/// <summary>
/// Nome que inicia com - Operador: StartsWith
/// Exemplo: WHERE Name.StartsWith(@value)
/// </summary>
[QueryOperator(Operator = WhereOperator.StartsWith, HasName = nameof(Product.Name), CaseSensitive = false)]
public string? NameStartsWith { get; set; }
/// <summary>
/// Busca em múltiplos campos - Operador: ContainsWithLikeForList (NOVO!)
/// Exemplo: WHERE (Name.Contains(@value1) OR Name.Contains(@value2) OR ...)
///
/// ✨ Este é o operador mais poderoso - permite busca OR em listas
/// Use para implementar busca global, tags, categorias múltiplas, etc.
/// </summary>
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Product.Name))]
public List<string>? GlobalSearch { get; set; }
// ===== OPERADORES LÓGICOS E DE COMBINAÇÃO =====
/// <summary>
/// Filtro com OR - demonstra uso de UseOr = true
/// Exemplo: WHERE (previous_conditions) OR (Category = @value)
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Category), UseOr = true)]
public string? AlternativeCategory { get; set; }
/// <summary>
/// Filtro com NOT - demonstra uso de UseNot = true
/// Exemplo: WHERE NOT (IsActive = @value)
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.IsActive), UseNot = true)]
public bool? ExcludeActive { get; set; }
// ===== PAGINAÇÃO E ORDENAÇÃO =====
/// <summary>
/// Página atual (1-based)
/// </summary>
[Key]
public int PageIndex { get; set; } = 1;
/// <summary>
/// Tamanho da página
/// </summary>
[Key]
public int PageSize { get; set; } = 10;
/// <summary>
/// Ordenação: "A-Name, D-Price, A-CreatedAt"
/// Formato: "D-Campo" (Descendente) ou "A-Campo" (Ascendente)
/// Suporta múltiplos campos separados por vírgula
/// </summary>
public string Sort { get; set; } = string.Empty;
}
Modelo de Filtro para Pedidos - Casos Avançados
public class OrderSearchModel : IQueryableCustom
{
// ===== FILTROS DE DATA COM RANGES =====
/// <summary>
/// Data inicial do pedido
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = nameof(Order.OrderDate))]
public DateTime? StartDate { get; set; }
/// <summary>
/// Data final do pedido
/// </summary>
[QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = nameof(Order.OrderDate))]
public DateTime? EndDate { get; set; }
// ===== FILTROS DE TEXTO AVANÇADOS =====
/// <summary>
/// Busca por múltiplos nomes de cliente (OR)
/// Exemplo: WHERE CustomerName.Contains("João") OR CustomerName.Contains("Maria")
/// </summary>
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Order.CustomerName))]
public List<string>? CustomerNames { get; set; }
/// <summary>
/// Email do cliente que inicia com
/// </summary>
[QueryOperator(Operator = WhereOperator.StartsWith, HasName = nameof(Order.CustomerEmail), CaseSensitive = false)]
public string? EmailDomain { get; set; }
// ===== FILTROS DE VALOR =====
/// <summary>
/// Valor mínimo do pedido
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = nameof(Order.TotalAmount))]
public decimal? MinAmount { get; set; }
/// <summary>
/// Valor máximo do pedido
/// </summary>
[QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = nameof(Order.TotalAmount))]
public decimal? MaxAmount { get; set; }
// ===== FILTROS DE ENUM E STATUS =====
/// <summary>
/// Status específico do pedido
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Order.Status))]
public OrderStatus? Status { get; set; }
/// <summary>
/// Excluir pedidos cancelados
/// </summary>
[QueryOperator(Operator = WhereOperator.NotEquals, HasName = nameof(Order.Status))]
public OrderStatus? ExcludeStatus { get; set; } = OrderStatus.Cancelled;
// ===== FILTROS DE DATA NULLABLE =====
/// <summary>
/// Pedidos que ainda não foram enviados (ShippedDate = null)
/// Para filtrar NULL, deixe ShippedDateExists = false e não preencha ShippedDate
/// </summary>
[QueryOperator(Operator = WhereOperator.EqualsWhenNullable, HasName = nameof(Order.ShippedDate))]
public DateTime? ShippedDate { get; set; }
// ===== COMBINAÇÕES LÓGICAS AVANÇADAS =====
/// <summary>
/// Busca alternativa com OR - pedidos urgentes OU de valor alto
/// </summary>
[QueryOperator(Operator = WhereOperator.GreaterThan, HasName = nameof(Order.TotalAmount), UseOr = true)]
public decimal? HighValueAlternative { get; set; }
/// <summary>
/// Exclusão com NOT - todos exceto os pendentes
/// </summary>
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Order.Status), UseNot = true)]
public OrderStatus? NotStatus { get; set; }
// ===== PAGINAÇÃO E ORDENAÇÃO =====
[Key]
public int PageIndex { get; set; } = 1;
[Key]
public int PageSize { get; set; } = 20;
/// <summary>
/// Exemplos de ordenação:
/// - "D-OrderDate" - Mais recentes primeiro (Descendente)
/// - "D-TotalAmount, D-OrderDate" - Por valor e data (ambos descendentes)
/// - "A-CustomerName, D-OrderDate" - Por cliente (ascendente) e data (descendente)
/// </summary>
public string Sort { get; set; } = "D-OrderDate";
}
🏗️ Implementação em Controllers/Services
Controller Básico com Todos os Recursos
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IUnitOfWork<AppDbContext> _unitOfWork;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IUnitOfWork<AppDbContext> unitOfWork, ILogger<ProductsController> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <summary>
/// Busca produtos com filtros dinâmicos e paginação
/// </summary>
[HttpGet("search")]
public async Task<ActionResult<IPagedList<ProductDto>>> SearchProducts([FromQuery] ProductSearchModel filter)
{
try
{
// ✅ Query com filtros dinâmicos, paginação e ordenação
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive) // Filtro fixo
.Filter(filter) // Filtros dinâmicos
.Sort(filter.Sort) // Ordenação
.Select(p => new ProductDto // Projeção para DTO
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
});
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
_logger.LogInformation("Produtos encontrados: {Count}/{Total}. Página: {Page}/{TotalPages}",
result.Items.Count, result.TotalCount, result.PageIndex, result.TotalPages);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao buscar produtos com filtros: {@Filters}", filter);
return StatusCode(500, "Erro interno do servidor");
}
}
/// <summary>
/// Busca produtos sem paginação (para dropdown, autocomplete, etc.)
/// </summary>
[HttpGet("list")]
public async Task<ActionResult<List<ProductDto>>> GetProductsList([FromQuery] ProductSearchModel filter)
{
try
{
// ✅ Query sem paginação - apenas filtros dinâmicos
var products = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive)
.Filter(filter) // Apenas filtros, sem paginação
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
})
.Take(100) // Limite para segurança
.ToListAsync();
return Ok(products);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao listar produtos");
return StatusCode(500, "Erro interno do servidor");
}
}
/// <summary>
/// Demonstra o novo operador ContainsWithLikeForList
/// </summary>
[HttpGet("global-search")]
public async Task<ActionResult<IPagedList<ProductDto>>> GlobalSearch([FromQuery] string[] terms)
{
var filter = new ProductSearchModel
{
GlobalSearch = terms?.ToList(), // Busca OR em múltiplos termos
Sort = "A-Name",
PageIndex = 1,
PageSize = 10
};
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Sort(filter.Sort)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
});
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
return Ok(result);
}
/// <summary>
/// Criar produto com Unit of Work pattern
/// </summary>
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct([FromBody] CreateProductRequest request)
{
try
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Category = request.Category,
Price = request.Price,
Stock = request.Stock,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
// ✅ Unit of Work - adiciona e salva em uma transação
await _unitOfWork.AddAsync(product);
await _unitOfWork.SaveChangesAsync();
var dto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Category = product.Category,
Price = product.Price
};
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, dto);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao criar produto: {@Request}", request);
return StatusCode(500, "Erro interno do servidor");
}
}
/// <summary>
/// Buscar produto por ID
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _unitOfWork.FindByIdAsync<Product>(id);
if (product == null)
return NotFound();
var dto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Category = product.Category,
Price = product.Price,
Stock = product.Stock
};
return Ok(dto);
}
}
Service Avançado - Demonstrando Casos Complexos
public class OrderService
{
private readonly IUnitOfWork<AppDbContext> _unitOfWork;
private readonly ILogger<OrderService> _logger;
public OrderService(IUnitOfWork<AppDbContext> unitOfWork, ILogger<OrderService> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <summary>
/// Busca avançada de pedidos - demonstra filtros complexos
/// </summary>
public async Task<IPagedList<OrderDto>> SearchOrdersAsync(OrderSearchModel filter)
{
try
{
// ✅ Demonstra query complexa com joins e filtros dinâmicos
var query = _unitOfWork.Repository<Order>()
.GetAll(
predicate: o => !o.Status.Equals(OrderStatus.Cancelled) || filter.ExcludeStatus != OrderStatus.Cancelled,
include: source => source.Include(o => o.Items))
.Filter(filter)
.Sort(filter.Sort)
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.CustomerName,
CustomerEmail = o.CustomerEmail,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
OrderDate = o.OrderDate,
ItemCount = o.Items.Count
});
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
_logger.LogInformation("Pedidos encontrados: {Count}/{Total}", result.Items.Count, result.TotalCount);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao buscar pedidos: {@Filter}", filter);
throw;
}
}
/// <summary>
/// Demonstra agregações e relatórios
/// </summary>
public async Task<OrderReportDto> GetOrderReportAsync(OrderSearchModel filter)
{
// ✅ Consulta sem paginação para relatórios
var orders = await _unitOfWork.Repository<Order>()
.GetAll()
.Filter(filter) // Aplica apenas filtros, sem paginação
.ToListAsync();
var report = new OrderReportDto
{
TotalOrders = orders.Count,
TotalAmount = orders.Sum(o => o.TotalAmount),
AverageAmount = orders.Any() ? orders.Average(o => o.TotalAmount) : 0,
OrdersByStatus = orders
.GroupBy(o => o.Status)
.ToDictionary(g => g.Key.ToString(), g => g.Count())
};
return report;
}
/// <summary>
/// Demonstra transações complexas com Unit of Work
/// </summary>
public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request)
{
// ✅ Unit of Work gerencia a transação automaticamente
try
{
var order = new Order
{
CustomerName = request.CustomerName,
CustomerEmail = request.CustomerEmail,
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending
};
// Adiciona o pedido
await _unitOfWork.AddAsync(order);
// Adiciona os itens (múltiplas operações na mesma transação)
foreach (var itemRequest in request.Items)
{
var product = await _unitOfWork.FindByIdAsync<Product>(itemRequest.ProductId);
if (product == null)
throw new InvalidOperationException($"Produto {itemRequest.ProductId} não encontrado");
var item = new OrderItem
{
Order = order,
ProductId = itemRequest.ProductId,
Quantity = itemRequest.Quantity,
UnitPrice = product.Price
};
await _unitOfWork.AddAsync(item);
order.TotalAmount += item.Quantity * item.UnitPrice;
// Atualiza estoque
product.Stock -= itemRequest.Quantity;
_unitOfWork.Update(product);
}
// ✅ Salva tudo em uma única transação
await _unitOfWork.SaveChangesAsync();
return new OrderDto
{
Id = order.Id,
CustomerName = order.CustomerName,
TotalAmount = order.TotalAmount,
Status = order.Status.ToString(),
OrderDate = order.OrderDate
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao criar pedido: {@Request}", request);
throw;
}
}
}
📋 Referência Completa dos Operadores
| Operador | Descrição | Exemplo SQL | Uso Comum |
|---|---|---|---|
| Equals | Igualdade exata | WHERE Field = @value |
IDs, status, categorias |
| NotEquals | Diferente de | WHERE Field <> @value |
Exclusões, filtros negativos |
| GreaterThan | Maior que | WHERE Field > @value |
Idades, valores mínimos |
| LessThan | Menor que | WHERE Field < @value |
Limites máximos |
| GreaterThanOrEqualTo | Maior ou igual | WHERE Field >= @value |
Datas início, preços mín |
| LessThanOrEqualTo | Menor ou igual | WHERE Field <= @value |
Datas fim, preços máx |
| Contains | Contém texto | WHERE Field.Contains(@value) |
Buscas de texto |
| StartsWith | Inicia com | WHERE Field.StartsWith(@value) |
Prefixos, códigos |
| GreaterThanOrEqualWhenNullable | Maior/igual (nullable) | WHERE Field >= @value |
Campos nullable |
| LessThanOrEqualWhenNullable | Menor/igual (nullable) | WHERE Field <= @value |
Campos nullable |
| EqualsWhenNullable | Igualdade (nullable) | WHERE Field = @value |
Campos nullable |
| ContainsWithLikeForList | OR múltiplo | WHERE (Field.Contains(@v1) OR Field.Contains(@v2)) |
🆕 Busca global, tags |
Modificadores de Comportamento
| Propriedade | Descrição | Exemplo |
|---|---|---|
| CaseSensitive | Controla sensibilidade a maiúsculas | CaseSensitive = false |
| UseOr | Usa OR ao invés de AND | UseOr = true |
| UseNot | Nega a condição | UseNot = true |
🎨 Exemplos de Uso dos Operadores
1. Operador ContainsWithLikeForList - Busca Global
// ✨ NOVO OPERADOR - Mais poderoso para buscas múltiplas
public class ProductGlobalSearchModel : IQueryableCustom
{
/// <summary>
/// Busca por múltiplos termos com OR
/// Exemplo: ["iPhone", "Samsung"] resulta em:
/// WHERE (Name.Contains("iPhone") OR Name.Contains("Samsung"))
/// </summary>
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Product.Name))]
public List<string>? SearchTerms { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Sort { get; set; } = string.Empty;
}
// Uso em controller
[HttpGet("global-search")]
public async Task<ActionResult> GlobalSearch([FromQuery] string[] terms)
{
var filter = new ProductGlobalSearchModel
{
SearchTerms = terms.ToList(), // ["Apple", "Samsung", "Xiaomi"]
PageIndex = 1,
PageSize = 10
};
// Resulta em: WHERE (Name.Contains("Apple") OR Name.Contains("Samsung") OR Name.Contains("Xiaomi"))
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
});
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
return Ok(result);
}
2. Filtros de Range (Datas e Valores)
public class ProductPriceRangeModel : IQueryableCustom
{
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = nameof(Product.Price))]
public decimal? MinPrice { get; set; }
[QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = nameof(Product.Price))]
public decimal? MaxPrice { get; set; }
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = nameof(Product.CreatedAt))]
public DateTime? StartDate { get; set; }
[QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = nameof(Product.CreatedAt))]
public DateTime? EndDate { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Sort { get; set; } = string.Empty;
}
// Uso: ?MinPrice=100&MaxPrice=500&StartDate=2024-01-01&EndDate=2024-12-31
// Resulta em: WHERE Price >= 100 AND Price <= 500 AND CreatedAt >= '2024-01-01' AND CreatedAt <= '2024-12-31'
3. Filtros com Lógica Complexa (OR e NOT)
public class ProductComplexFilterModel : IQueryableCustom
{
// Categoria principal
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Category))]
public string? PrimaryCategory { get; set; }
// Categoria alternativa (OR)
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Category), UseOr = true)]
public string? AlternativeCategory { get; set; }
// Excluir produtos inativos (NOT)
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.IsActive), UseNot = true)]
public bool? ExcludeInactive { get; set; } = false; // NOT (IsActive = false) => produtos ativos
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Sort { get; set; } = string.Empty;
}
// Uso: ?PrimaryCategory=Electronics&AlternativeCategory=Gadgets&ExcludeInactive=false
// Resulta em: WHERE (Category = 'Electronics' OR Category = 'Gadgets') AND NOT (IsActive = false)
4. Filtros Case-Insensitive
public class ProductTextSearchModel : IQueryableCustom
{
// Busca case-sensitive (padrão)
[QueryOperator(Operator = WhereOperator.Contains, HasName = nameof(Product.Name))]
public string? NameExact { get; set; }
// Busca case-insensitive
[QueryOperator(Operator = WhereOperator.Contains, HasName = nameof(Product.Name), CaseSensitive = false)]
public string? NameIgnoreCase { get; set; }
// Categoria case-insensitive
[QueryOperator(Operator = WhereOperator.Equals, HasName = nameof(Product.Category), CaseSensitive = false)]
public string? Category { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Sort { get; set; } = string.Empty;
}
// NameIgnoreCase=IPHONE resulta em: WHERE UPPER(Name).Contains(UPPER('IPHONE'))
// Category=electronics resulta em: WHERE UPPER(Category) = UPPER('electronics')
5. Filtros com Campos Nullable
public class ProductNullableFilterModel : IQueryableCustom
{
// Data de última atualização (pode ser null)
[QueryOperator(Operator = WhereOperator.EqualsWhenNullable, HasName = nameof(Product.LastUpdate))]
public DateTime? LastUpdate { get; set; }
// Estoque mínimo (nullable-safe)
[QueryOperator(Operator = WhereOperator.GreaterThanOrEqualWhenNullable, HasName = nameof(Product.Stock))]
public int? MinStock { get; set; }
// Preço máximo (nullable-safe)
[QueryOperator(Operator = WhereOperator.LessThanOrEqualWhenNullable, HasName = nameof(Product.Price))]
public decimal? MaxPrice { get; set; }
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Sort { get; set; } = string.Empty;
}
🔧 Ordenação Avançada
Formato do Sort
A ordenação utiliza o formato de prefixo para indicar a direção:
- D- = Descendente (Decrescente) - Ex:
D-Priceresulta emORDER BY Price DESC - A- = Ascendente (Crescente) - Ex:
A-Nameresulta emORDER BY Name ASC
Sintaxe: "[D|A]-NomePropriedade"
Múltiplos campos: Separe por vírgula - Ex: "A-Category, D-Price, A-Name"
Exemplos de Ordenação Múltipla
// Ordenação simples - Descendente (D-)
filter.Sort = "D-Name"; // ORDER BY Name DESC
filter.Sort = "D-Price"; // ORDER BY Price DESC
// Ordenação simples - Ascendente (A-)
filter.Sort = "A-Name"; // ORDER BY Name ASC
filter.Sort = "A-Price"; // ORDER BY Price ASC
// Ordenação múltipla
filter.Sort = "A-Category, D-Price"; // ORDER BY Category ASC, Price DESC
filter.Sort = "D-IsActive, A-Name, D-CreatedAt"; // Múltiplos campos
// Casos especiais
filter.Sort = "Price"; // ⚠️ INVÁLIDO - deve especificar D- ou A-
filter.Sort = ""; // Sem ordenação (ordem do banco)
Service com Ordenação Dinâmica
public async Task<IPagedList<ProductDto>> GetProductsWithSortingAsync(
string category,
string sortBy = "name",
string direction = "A", // "A" para Ascendente ou "D" para Descendente
int page = 1,
int pageSize = 20)
{
var filter = new ProductSearchModel
{
CategoryExact = category,
Sort = $"{direction}-{sortBy}", // Ex: "A-Name" ou "D-Price"
PageIndex = page,
PageSize = pageSize
};
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive)
.Filter(filter)
.Sort(filter.Sort)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price,
CreatedAt = p.CreatedAt
});
return await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
}
📄 DTOs e Modelos de Resposta
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string CustomerEmail { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public int ItemCount { get; set; }
}
// IPagedList<T> é retornado por ToPagedListAsync()
// Contém propriedades úteis para paginação:
// - Items: Lista de itens da página atual
// - PageIndex: Página atual (1-based)
// - PageSize: Tamanho da página
// - TotalCount: Total de registros
// - TotalPages: Total de páginas
// - HasPreviousPage: Indica se há página anterior
// - HasNextPage: Indica se há próxima página
// - IndexFrom: Índice inicial (default 0)
public class OrderReportDto
{
public int TotalOrders { get; set; }
public decimal TotalAmount { get; set; }
public decimal AverageAmount { get; set; }
public Dictionary<string, int> OrdersByStatus { get; set; } = new();
}
🔄 Conversão de PagedList - PagedList.From()
O método PagedList.From() permite converter um IPagedList<TSource> existente para um novo IPagedList<TResult>, preservando todos os metadados de paginação (página atual, total de registros, total de páginas, etc.).
Por Que Usar?
- ✅ Preserva metadados: Mantém PageIndex, TotalCount, TotalPages intactos
- ✅ Type-safe: Conversão tipada entre entidades e DTOs
- ✅ Flexível: Aceita qualquer função de conversão
- ✅ Clean Code: Evita reconstrução manual de metadados
- ✅ Performance: Converte apenas os itens da página atual
Assinatura do Método
public static IPagedList<TResult> From<TResult, TSource>(
IPagedList<TSource> source,
Func<IEnumerable<TSource>, IEnumerable<TResult>> converter)
Exemplos de Uso
1. Conversão Simples: Entidade → DTO
// Obter lista paginada de produtos (entidades)
var pagedProducts = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive)
.ToPagedListAsync(pageIndex: 1, pageSize: 10);
// Converter para DTOs preservando metadados
var pagedProductDtos = PagedList.From<ProductDto, Product>(
pagedProducts,
products => products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
})
);
// Resultado: IPagedList<ProductDto> com os mesmos metadados
// PageIndex, TotalCount, TotalPages, etc. permanecem inalterados
2. Conversão com Lógica de Negócio
// Aplicar regras de negócio durante a conversão
var pagedProductCards = PagedList.From<ProductCardDto, Product>(
pagedProducts,
products => products.Select(p => new ProductCardDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
// Lógica de negócio
DiscountPercentage = p.Price > 1000 ? 10 : 5,
Badge = p.Stock < 10 ? "LOW STOCK" : p.CreatedAt > DateTime.Now.AddDays(-7) ? "NEW" : null,
IsAvailable = p.Stock > 0 && p.IsActive
})
);
3. Conversão em Pipeline (Múltiplas Conversões)
// Primeira conversão: Product → ProductDto
var pagedProductDtos = PagedList.From<ProductDto, Product>(
pagedProducts,
products => products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
);
// Segunda conversão: ProductDto → ProductSummaryDto
var pagedSummaries = PagedList.From<ProductSummaryDto, ProductDto>(
pagedProductDtos,
dtos => dtos.Select(dto => new ProductSummaryDto
{
Id = dto.Id,
DisplayName = $"{dto.Name} - ${dto.Price:F2}"
})
);
4. Conversão com Agregações
// Obter pedidos paginados
var pagedOrders = await _unitOfWork.Repository<Order>()
.GetAll(include: source => source.Include(o => o.Items))
.ToPagedListAsync(pageIndex: 1, pageSize: 20);
// Converter para resumo com agregações
var pagedOrderSummaries = PagedList.From<OrderSummaryDto, Order>(
pagedOrders,
orders => orders.Select(o => new OrderSummaryDto
{
OrderId = o.Id,
CustomerName = o.CustomerName,
TotalAmount = o.TotalAmount,
// Agregações
ItemCount = o.Items.Count,
TotalQuantity = o.Items.Sum(i => i.Quantity),
AverageItemPrice = o.Items.Any() ? o.Items.Average(i => i.UnitPrice) : 0
})
);
5. Tratamento de Listas Vazias
var pagedProducts = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.Category == "NonExistent")
.ToPagedListAsync(pageIndex: 1, pageSize: 10);
// PagedList.From() funciona corretamente com listas vazias
var pagedDtos = PagedList.From<ProductDto, Product>(
pagedProducts,
products => products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
})
);
// Resultado:
// - Items: [] (lista vazia)
// - TotalCount: 0
// - TotalPages: 0
// - PageIndex: 1
Caso de Uso Completo em Service
public class ProductService
{
private readonly IUnitOfWork<AppDbContext> _unitOfWork;
public ProductService(IUnitOfWork<AppDbContext> unitOfWork)
{
_unitOfWork = unitOfWork;
}
/// <summary>
/// Busca produtos com filtros e retorna cards paginados
/// </summary>
public async Task<IPagedList<ProductCardDto>> GetProductCardsAsync(
string? category = null,
decimal? minPrice = null,
decimal? maxPrice = null,
int pageIndex = 1,
int pageSize = 12)
{
// 1. Buscar entidades com filtros
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive);
if (!string.IsNullOrEmpty(category))
query = query.Where(p => p.Category == category);
if (minPrice.HasValue)
query = query.Where(p => p.Price >= minPrice.Value);
if (maxPrice.HasValue)
query = query.Where(p => p.Price <= maxPrice.Value);
var pagedProducts = await query
.OrderBy(p => p.Name)
.ToPagedListAsync(pageIndex, pageSize);
// 2. Converter para DTOs com lógica de negócio
var pagedCards = PagedList.From<ProductCardDto, Product>(
pagedProducts,
products => products.Select(p => new ProductCardDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Category = p.Category,
ImageUrl = $"/images/products/{p.Id}.jpg",
// Lógica de desconto
DiscountPercentage = CalculateDiscount(p),
// Badge dinâmico
Badge = GetProductBadge(p),
// Disponibilidade
IsAvailable = p.Stock > 0
})
);
return pagedCards;
}
private decimal CalculateDiscount(Product product)
{
if (product.Price > 1000) return 15;
if (product.Price > 500) return 10;
if (product.Price > 100) return 5;
return 0;
}
private string? GetProductBadge(Product product)
{
if (product.Stock < 5) return "LAST UNITS";
if (product.CreatedAt > DateTime.Now.AddDays(-7)) return "NEW";
if (product.Price < 50) return "SALE";
return null;
}
}
Comparação: Com vs Sem PagedList.From()
❌ Sem PagedList.From() - Reconstrução Manual:
var pagedProducts = await query.ToPagedListAsync(pageIndex, pageSize);
var productDtos = pagedProducts.Items.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
}).ToList();
// ⚠️ Precisa reconstruir manualmente todos os metadados
var result = new PagedList<ProductDto>(
productDtos,
pagedProducts.PageIndex,
pagedProducts.PageSize,
pagedProducts.TotalCount,
pagedProducts.IndexFrom
);
✅ Com PagedList.From() - Conversão Direta:
var pagedProducts = await query.ToPagedListAsync(pageIndex, pageSize);
// ✅ Metadados preservados automaticamente
var result = PagedList.From<ProductDto, Product>(
pagedProducts,
products => products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
})
);
📚 Exemplos Adicionais
Para ver 6 exemplos completos e testados do método PagedList.From(), consulte:
ExamplesPagedListConversion.cs- 6 cenários práticos comentadosExamplesPagedListConversionTest.cs- 29 testes unitários
Quando NÃO Usar PagedList.From()?
Use conversão direta no SQL quando:
- Precisa filtrar após conversão: Aplique filtros antes da paginação
- Agregações complexas: Use
GroupByeSumdireto no SQL - Joins necessários: Faça joins antes de paginar
// ✅ Melhor: Converter no SQL antes de paginar
var pagedDtos = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive)
.Select(p => new ProductDto // Conversão no SQL
{
Id = p.Id,
Name = p.Name
})
.ToPagedListAsync(pageIndex, pageSize);
// ❌ Evitar: Paginar entidades e converter depois (2 consultas)
var pagedProducts = await query.ToPagedListAsync(pageIndex, pageSize);
var pagedDtos = PagedList.From<ProductDto, Product>(pagedProducts, ...);
🚀 Performance e Otimizações
1. Queries Eficientes
// ✅ BOM: Projeção para DTO
var products = await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name }) // Apenas campos necessários
.ToListAsync();
// ❌ EVITAR: Carregar entidade completa desnecessariamente
var products = await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.ToListAsync(); // Carrega todos os campos
2. Paginação Otimizada
// ✅ BOM: Paginação direta no banco
var query = _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name });
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize); // Skip/Take no SQL
// ❌ EVITAR: Paginação em memória
var allProducts = await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.ToListAsync();
var pagedProducts = allProducts.Skip(skip).Take(take); // Carrega tudo na memória
3. Includes Inteligentes
// ✅ BOM: Include apenas quando necessário
var orders = await _unitOfWork.Repository<Order>()
.GetAll(include: source => source
.Include(o => o.Items.Take(5))) // Limit related data
.Filter(filter)
.ToListAsync();
// ❌ EVITAR: Include desnecessário
var orders = await _unitOfWork.Repository<Order>()
.GetAll(include: source => source
.Include(o => o.Items)
.Include(o => o.Customer)
.Include(o => o.ShippingAddress)) // Dados não utilizados
.Filter(filter)
.ToListAsync();
.ToListAsync();
## 🧪 Testes
### Exemplo de Teste Unitário
```csharp
[Test]
public async Task Filter_WithContainsWithLikeForList_ShouldReturnCorrectResults()
{
// Arrange
var products = new List<Product>
{
new() { Id = 1, Name = "iPhone 15 Pro", Category = "Electronics" },
new() { Id = 2, Name = "Samsung Galaxy S24", Category = "Electronics" },
new() { Id = 3, Name = "iPad Air", Category = "Tablets" },
new() { Id = 4, Name = "MacBook Pro", Category = "Laptops" }
};
var filter = new ProductSearchModel
{
GlobalSearch = new List<string> { "iPhone", "Samsung" },
PageIndex = 1,
PageSize = 10
};
// Act
var result = products.AsQueryable()
.Filter(filter)
.ToList();
// Assert
Assert.That(result.Count, Is.EqualTo(2));
Assert.That(result.Any(p => p.Name.Contains("iPhone")), Is.True);
Assert.That(result.Any(p => p.Name.Contains("Samsung")), Is.True);
}
Teste de Integração
[Test]
public async Task SearchProducts_WithComplexFilters_ShouldReturnPagedResults()
{
// Arrange
using var context = new TestDbContext();
var unitOfWork = new UnitOfWork<TestDbContext>(context);
await SeedTestData(context);
var filter = new ProductSearchModel
{
MinPrice = 100,
MaxPrice = 1000,
NameSearch = "Pro",
Sort = "D-Price",
PageIndex = 1,
PageSize = 5
};
// Act
var query = unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Sort(filter.Sort)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
});
var result = await query.ToPagedListAsync(filter.PageIndex, filter.PageSize);
// Assert
Assert.That(result.Items.Count, Is.LessThanOrEqualTo(5));
Assert.That(result.TotalCount, Is.GreaterThan(0));
Assert.That(result.Items.All(p => p.Price >= 100 && p.Price <= 1000), Is.True);
Assert.That(result.Items.All(p => p.Name.Contains("Pro")), Is.True);
}
📊 Dependências
- .NET Standard 2.1 - Framework base
- Entity Framework Core - ORM principal
- Microsoft.Extensions.DependencyInjection - Container de DI
- System.Linq.Expressions - Expression Trees
- Nuuvify.CommonPack.Extensions - Extensões úteis
🔒 Thread Safety
A biblioteca é completamente thread-safe:
// ✅ Seguro usar como Singleton
services.AddSingleton<IUnitOfWork<AppDbContext>>();
// ✅ Seguro usar como Scoped (recomendado para web)
services.AddScoped<IUnitOfWork<AppDbContext>>();
// ✅ Múltiplas threads podem usar simultaneamente
var tasks = Enumerable.Range(1, 10).Select(async i =>
{
var filter = new ProductSearchModel { NameSearch = $"Product {i}" };
return await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.ToListAsync();
});
var results = await Task.WhenAll(tasks);
🔧 Configurações Avançadas
Options Pattern
public class UnitOfWorkOptions
{
public int DefaultPageSize { get; set; } = 20;
public int MaxPageSize { get; set; } = 100;
public bool EnableQuerySplitting { get; set; } = true;
public bool EnableChangeTracking { get; set; } = true;
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30);
}
// Configuração
builder.Services.Configure<UnitOfWorkOptions>(options =>
{
options.DefaultPageSize = 25;
options.MaxPageSize = 200;
});
Interceptors Personalizados
public class QueryInterceptor : IQueryInterceptor
{
public IQueryable<T> BeforeQuery<T>(IQueryable<T> query) where T : class
{
// Aplicar filtros globais, auditoria, etc.
if (typeof(T).GetInterface(nameof(ISoftDelete)) != null)
{
query = query.Where(e => !((ISoftDelete)e).IsDeleted);
}
return query;
}
}
// Registro
builder.Services.AddScoped<IQueryInterceptor, QueryInterceptor>();
🔍 Troubleshooting
Problema: Filtro ContainsWithLikeForList não gera WHERE clause
Sintoma: Query retorna todos os registros ignorando o filtro de lista.
SQL Gerado:
SELECT COUNT(*) FROM [Products] AS [p]
-- ❌ Sem WHERE clause
Causa: Este era um bug conhecido (corrigido na v2.2.0) onde FilterBy era um UnaryExpression ao invés de ConstantExpression.
Solução:
- Atualize para v2.2.0+:
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.2.0 - Verifique sua lista não está vazia:
filter.SearchTerms = new List<string> { "iPhone", "Samsung" }; // ✅ Correto filter.SearchTerms = new List<string>(); // ❌ Lista vazia = sem filtro filter.SearchTerms = null; // ❌ Null = sem filtro - Valide o operador:
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Product.Name))] public List<string>? SearchTerms { get; set; }
Problema: PageIndex inconsistente
Sintoma: PageIndex = 1 retorna registros da segunda página.
Causa: Confusão entre 0-based e 1-based index (corrigido na v2.2.0).
Solução:
- v2.2.0+:
PageIndex = 1é a primeira página (1-based) - Versões anteriores: Use
PageIndex = 0para primeira página (0-based)
// v2.2.0+
filter.PageIndex = 1; // ✅ Primeira página
filter.PageIndex = 2; // ✅ Segunda página
// Versões < 2.2.0
filter.PageIndex = 0; // Primeira página
filter.PageIndex = 1; // Segunda página
Problema: NullReferenceException em Expression.And/Or
Sintoma: Exception ao combinar múltiplos filtros.
System.NullReferenceException: Object reference not set to an instance of an object
at System.Linq.Expressions.Expression.And(Expression left, Expression right)
Causa: Filtros retornando null (corrigido na v2.2.0).
Solução:
- Atualize para v2.2.0+ onde null expressions são automaticamente ignoradas
- Valide valores antes de atribuir:
// ✅ BOM - validação if (!string.IsNullOrEmpty(searchTerm)) { filter.NameSearch = searchTerm; } // ❌ EVITAR - pode causar null expression filter.NameSearch = ""; // String vazia pode gerar problemas
Problema: Case-insensitive não funciona
Sintoma: Busca por "iphone" não encontra "iPhone".
SQL Esperado vs Gerado:
-- ✅ Esperado:
WHERE UPPER([p].[Name]) LIKE UPPER('%iphone%')
-- ❌ Gerado (bug):
WHERE [p].[Name] LIKE '%iphone%'
Solução:
Verifique o atributo:
[QueryOperator(Operator = WhereOperator.Contains, HasName = nameof(Product.Name), CaseSensitive = false)] // ✅ Essencial! public string? NameSearch { get; set; }Para ContainsWithLikeForList (v2.2.0+):
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Product.Name), CaseSensitive = false)] // ✅ Funciona na v2.2.0+ public List<string>? SearchTerms { get; set; } // SQL Gerado: WHERE Name LIKE '%IPHONE%' OR Name LIKE '%SAMSUNG%'
Problema: SQL gerado com "WHERE 0 = 1"
Sintoma: Query retorna 0 registros mesmo com dados no banco.
SQL Gerado:
SELECT * FROM Products WHERE 0 = 1
Causa: Bug em listas vazias (corrigido na v2.2.0).
Solução:
- Atualize para v2.2.0+
- Evite listas vazias:
// ✅ BOM if (terms != null && terms.Any()) { filter.SearchTerms = terms; } // Deixe null se não houver termos // ❌ EVITAR em versões < 2.2.0 filter.SearchTerms = new List<string>(); // Gera WHERE 0 = 1
Problema: Performance lenta em queries grandes
Sintomas:
- Queries demoram muito tempo
- Alto consumo de memória
- Timeout de banco de dados
Soluções:
Use projeção (Select):
// ✅ BOM - apenas campos necessários var products = await _unitOfWork.Repository<Product>() .GetAll() .Filter(filter) .Select(p => new ProductDto { Id = p.Id, Name = p.Name }) .ToListAsync(); // ❌ EVITAR - carrega entidade completa var products = await _unitOfWork.Repository<Product>() .GetAll() .Filter(filter) .ToListAsync();Limite o PageSize:
// ✅ BOM filter.PageSize = 50; // Máximo 50 registros por página // ❌ EVITAR filter.PageSize = 10000; // Muito grande!Use AsNoTracking para leitura:
var products = await _unitOfWork.Repository<Product>() .GetAll(disableTracking: true) // ✅ Melhora performance em queries read-only .Filter(filter) .ToListAsync();Evite Include desnecessário:
// ✅ BOM - Include apenas quando necessário var orders = await _unitOfWork.Repository<Order>() .GetAll(include: source => source .Include(o => o.Items)) .Filter(filter) .ToListAsync(); // ❌ EVITAR - Includes desnecessários var orders = await _unitOfWork.Repository<Order>() .GetAll(include: source => source .Include(o => o.Items) .Include(o => o.Customer) .Include(o => o.ShippingAddress) .Include(o => o.PaymentDetails)) // Dados não utilizados .Filter(filter) .ToListAsync();
Problema: Ordenação não funciona
Sintoma: Registros não ordenados conforme especificado.
Solução:
Sintaxe correta:
// ✅ CORRETO - Formato com prefixo D- ou A- filter.Sort = "A-Name"; // Ascendente filter.Sort = "D-Price"; // Descendente filter.Sort = "A-Category, D-Price, A-Name"; // Múltiplos campos // ❌ INCORRETO - Formato antigo (não suportado) filter.Sort = "Name asc"; // ❌ Use "A-Name" filter.Sort = "Price desc"; // ❌ Use "D-Price" filter.Sort = "Name,Price"; // ❌ Faltam prefixos D- ou A-Propriedade existe na entidade:
// ✅ Propriedade existe em Product filter.Sort = "A-Name"; // ❌ Propriedade não existe filter.Sort = "D-InvalidProperty"; // Runtime error
Recursos de Debug
Habilitar logging de SQL (EF Core):
// appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.EnableSensitiveDataLogging() // ⚠️ Apenas em desenvolvimento!
.LogTo(Console.WriteLine, LogLevel.Information);
});
Inspecionar expressões geradas:
var filterExpression = _unitOfWork.Repository<Product>()
.GetAll()
.FilterExpression(filter);
Console.WriteLine(filterExpression?.ToString());
// Saída: p => (p.Name.Contains("iPhone") OR p.Name.Contains("Samsung")) AND p.Price >= 100
📄 Licença
Este projeto está licenciado sob a Licença MIT.
🤝 Contribuição
Contribuições são bem-vindas! Para contribuir:
- Fork o projeto
- Crie uma feature branch (
git checkout -b feature/nova-funcionalidade) - Commit suas mudanças (
git commit -am 'Adiciona nova funcionalidade') - Push para a branch (
git push origin feature/nova-funcionalidade) - Abra um Pull Request
📞 Suporte
Para dúvidas e suporte técnico:
- 📧 Email: suporte@zocate.li
- 📋 Issues: GitHub Issues
- 📖 Documentação: Wiki do Projeto
📈 Versionamento
Este projeto segue o Semantic Versioning:
- MAJOR: Mudanças incompatíveis na API
- MINOR: Novas funcionalidades mantendo compatibilidade
- PATCH: Correções de bugs mantendo compatibilidade
Consulte o CHANGELOG.md para ver todas as mudanças.
🏢 Sobre a Nuuvify
A Nuuvify é uma empresa especializada em soluções tecnológicas para transformação digital, oferecendo bibliotecas e ferramentas robustas para acelerar o desenvolvimento de aplicações empresariais.
Outros Pacotes da CommonPack
Nuuvify.CommonPack.AzureServiceBus- Azure Service Bus integrationNuuvify.CommonPack.Email- Biblioteca para envio de emailsNuuvify.CommonPack.Security- Ferramentas de segurançaNuuvify.CommonPack.Middleware- Middlewares customizadosNuuvify.CommonPack.Extensions- Extensões úteis para .NET
Desenvolvido com ❤️ pela equipe Nuuvify.
| 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 was computed. 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 was computed. 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. |
| .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
- Nuuvify.CommonPack.Extensions (>= 2.2.0-preview.25102906)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Nuuvify.CommonPack.UnitOfWork.Abstraction:
| Package | Downloads |
|---|---|
|
Nuuvify.CommonPack.AutoHistory
Extensco do EntityFrameworkCore para ser utilizado junto com UnitOfWork a fim de incluir na tabela AutoHistory todas as alteracces feitas em um registro em qualquer entidade da aplicacco |
|
|
Nuuvify.CommonPack.UnitOfWork
Essa biblioteca tem objetivo de implementar UnitOfWork para qualquer banco de dados, implementando metodos de uso basico e comumente utilizado em projetos. Deve ser instalada no projeto Infra.IoC |
GitHub repositories
This package is not used by any popular GitHub repositories.
# Changelog
Todas as mudanças notáveis neste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
e este projeto adere ao [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Aguardando
- Suporte para operadores IN e NOT IN
- Integração com AutoMapper para projeções automáticas
- Suporte para agregações dinâmicas (SUM, COUNT, AVG)
- Cache de queries compiladas para melhor performance
## 2025-10-28
### 🐛 Corrigido
- **CRITICAL FIX**: Correção de 3 bugs críticos no operador `ContainsWithLikeForList`:
1. **Bug Expression.Constant(false)**: Filtros vazios geravam SQL inválido `WHERE 0 = 1`
- Solução: Retornar `null` ao invés de `Expression.Constant(false)`
- Permite que filtros vazios sejam ignorados corretamente
2. **Bug Null Expression**: Expressões nulas causavam crashes em `Expression.And/Or`
- Solução: Adicionado check `if (actualExpression == null) continue;`
- Previne exceções ao processar filtros que retornam null
3. **Bug UnaryExpression Wrapping**: `FilterBy` estava encapsulado em `UnaryExpression` (Convert)
- Problema: `ExpressionFactory.GetClosureOverConstant` encapsula `List<string>?` para conversão de tipo
- Solução: Unwrap do `UnaryExpression` para extrair o `ConstantExpression` interno
- Código adicionado em `ContainsWithLikeForListExpression`:
```csharp
Expression filterExpression = expression.FilterBy;
if (filterExpression is UnaryExpression unaryExpression &&
unaryExpression.NodeType == ExpressionType.Convert)
{
filterExpression = unaryExpression.Operand;
}
```
- **Paginação**: Correção no `PageIndex` para ser 1-based em todos os cenários
- Anteriormente: `PageIndex` era 0-based internamente causando confusão
- Agora: `PageIndex = 1` representa a primeira página corretamente
### 🔧 Melhorado
- **Case-insensitive handling**: Melhor tratamento de busca case-insensitive em `ContainsWithLikeForList`
- `ToUpper()` aplicado individualmente em cada item da lista
- Não aplica `ToUpper()` no loop principal (evita erros com `List<string>`)
- Gera SQL otimizado: `WHERE Name LIKE '%IPHONE%' OR Name LIKE '%SAMSUNG%'`
- **Null validation**: Validação robusta de valores nulos e listas vazias
- Filtros com listas vazias são ignorados (não geram WHERE)
- Filtros com valores null são pulados automaticamente
- Previne geração de SQL inválido
- **Performance**: Queries mais eficientes com `ContainsWithLikeForList`
- Expressões compiladas de forma mais otimizada
- Redução de conversões desnecessárias
- Melhor uso de `Expression.OrElse` para OR logic
### 🎨 Refatorado
- **Partial Classes**: Classe `FiltersExtensions` dividida em arquivos separados
- `FiltersExtensions.cs`: Métodos públicos com documentação XML completa
- `FiltersExtensions.Private.cs`: Métodos privados de implementação
- Benefícios:
- Melhor organização e manutenibilidade
- Separação clara entre API pública e implementação
- Facilita navegação no código
- **Documentação XML**: Documentação completa adicionada aos métodos públicos
- `Filter<TEntity>`: Documentação detalhada com exemplos de uso
- `FilterExpression<TEntity>`: Explicação do comportamento e casos de uso
- Exemplos práticos de código incluídos nos summaries
- Demonstração do SQL gerado em cada exemplo
- **Code cleanup**: Removidos comentários inline do código
- Comentários técnicos substituídos por documentação XML
- Código mais limpo e profissional
- Comentários mantidos apenas onde necessário para contexto
### 📚 Documentação
- **README.md**: Atualizado com exemplos do operador `ContainsWithLikeForList`
- Casos de uso práticos e reais
- Exemplos de SQL gerado
- Guia de troubleshooting para bugs conhecidos
- **CHANGELOG.md**: Documentação detalhada das correções
- Descrição técnica dos 3 bugs corrigidos
- Código de exemplo das soluções
- Impacto e benefícios de cada correção
### ✅ Testes
- **100% Coverage**: Todos os testes passando (12/12)
- `ExamplesQueryOperatorsTest`: Suite completa de testes
- Testes para `ContainsWithLikeForList` com múltiplos cenários
- Validação de SQL gerado
- Testes de paginação e ordenação
- **Integration Tests**: Testes com Testcontainers.MsSql
- SQL Server 2022 em container Docker
- Testes end-to-end com banco de dados real
- Validação de queries complexas
### 🔍 Detalhes Técnicos
**Root Cause Analysis - Bug UnaryExpression:**
O `ExpressionFactory.GetClosureOverConstant` cria um `UnaryExpression` (Convert node) ao converter `List<string>?` (nullable) para `List<string>` (non-nullable). Isso é um padrão normal do Expression Tree para conversões de tipo.
Antes da correção:
```csharp
if (expression.FilterBy is ConstantExpression constantExpression)
{
// ❌ NUNCA EXECUTAVA - FilterBy era UnaryExpression, não ConstantExpression
}
```
Após a correção:
```csharp
Expression filterExpression = expression.FilterBy;
// Unwrap UnaryExpression para acessar o ConstantExpression interno
if (filterExpression is UnaryExpression unaryExpression &&
unaryExpression.NodeType == ExpressionType.Convert)
{
filterExpression = unaryExpression.Operand; // ✅ Extrai o ConstantExpression
}
if (filterExpression is ConstantExpression constantExpression)
{
// ✅ AGORA FUNCIONA - constantExpression.Value contém List<string>
}
```
**SQL Gerado (Antes vs Depois):**
Antes (bug):
```sql
SELECT COUNT(*) FROM [Products] AS [p]
-- ❌ Sem WHERE clause - filtro completamente ignorado
```
Depois (corrigido):
```sql
SELECT COUNT(*) FROM [Products] AS [p]
WHERE [p].[Name] LIKE N'%iPhone%' OR [p].[Name] LIKE N'%Samsung%'
-- ✅ WHERE clause correto com OR logic
```
### ⚠️ Breaking Changes
Nenhuma breaking change nesta versão. Todas as correções são backwards-compatible.
## 2024-01-15
### ✨ Adicionado
- **🆕 NOVO OPERADOR**: `ContainsWithLikeForList` - Busca OR em listas de strings
- Permite busca global com múltiplos termos: `WHERE (Field.Contains(@v1) OR Field.Contains(@v2))`
- Ideal para implementar busca global, tags, categorias múltiplas
- Suporte completo a `IEnumerable<string>`, `List<string>`, `Collection<string>`
- Validação robusta de nulls e listas vazias
- **Enums tipados** para melhor type safety:
- `ExpressionParameterName` - Nomes de parâmetros padronizados
- `MethodName` - Nomes de métodos de expressão
- Extensões de string para conversão automática
- **Documentação XML** completa em todos os métodos públicos
- **Properties.cs** para configuração centralizada de `InternalsVisibleTo`
- **README.md** abrangente com exemplos de todos os operadores
- **Examples** folder com classes demonstrativas
### 🔧 Melhorado
- Método `GetClosureOverConstant` otimizado com:
- Melhor tratamento de tipos nullable
- Validação mais robusta de expressões
- Suporte aprimorado para conversões de tipo
- Documentação XML detalhada
- Performance geral da biblioteca:
- Expressões compiladas de forma mais eficiente
- Redução de alocações desnecessárias
- Validações otimizadas
### 🐛 Corrigido
- Correção em operadores nullable que não funcionavam corretamente em alguns cenários
- Melhoria na validação de parâmetros de entrada
- Correção de warnings de nullable reference types
### 🔄 Alterado
- **BREAKING**: Movida configuração `InternalsVisibleTo` do `.csproj` para `Properties.cs`
- Melhora organização e permite configurações mais flexíveis
- Segue padrão estabelecido nos outros projetos CommonPack
### 📚 Documentação
- README.md completamente reescrito com:
- Exemplos de todos os 13 operadores
- Casos de uso reais com modelos completos
- Seções de performance e otimização
- Guias de troubleshooting
- Referência completa da API
- CHANGELOG.md seguindo padrão Keep a Changelog
- Examples com classes demonstrativas de uso
## 2023-12-10
### 🐛 Corrigido
- Correção de bug em paginação com ordenação múltipla
- Melhoria na validação de expressões lambda
- Correção de memory leak em queries de longa duração
### 📚 Documentação
- Atualização de exemplos no README
- Correção de links quebrados na documentação
## 2023-11-20
### ✨ Adicionado
- Suporte completo ao .NET 8.0
- Novos operadores de comparação:
- `GreaterThanOrEqualWhenNullable`
- `LessThanOrEqualWhenNullable`
- `EqualsWhenNullable`
- Suporte a ordenação múltipla com sintaxe simples
- Sistema de validação aprimorado para expressões
### 🔄 Alterado
- **BREAKING**: Atualização para .NET 8.0 como target principal
- **BREAKING**: Refatoração das interfaces para melhor extensibilidade
- Melhoria significativa na performance das queries dinâmicas
### ⚠️ Removido
- **BREAKING**: Descontinuado suporte ao .NET Standard 2.0
- Removidos métodos obsoletos da versão 2.x
## 2023-09-15
### ✨ Adicionado
- Operador `StartsWith` para buscas por prefixo
- Suporte a case-insensitive em operadores de texto
- Modificadores `UseOr` e `UseNot` em QueryOperator
- Sistema de interceptors para queries
### 🔧 Melhorado
- Performance das queries com Contains otimizada
- Redução de 40% no tempo de compilação de expressões
- Melhor tratamento de caracteres especiais em buscas
### 🐛 Corrigido
- Correção em filtros com valores null
- Melhoria na estabilidade com DbContext concorrente
## 2023-08-01
### 🐛 Corrigido
- Correção crítica em paginação com filtros complexos
- Melhoria na validação de PageSize para prevenir valores inválidos
- Correção de race condition em cenários multi-thread
### 🔧 Melhorado
- Otimização de memória em queries grandes
- Melhoria nos logs de debug
## 2023-07-10
### 🐛 Corrigido
- Correção de NullReferenceException em filtros vazios
- Melhoria na serialização de modelos de filtro
- Correção de comportamento inconsistente em ordenação
## 2023-06-20
### ✨ Adicionado
- Suporte completo a Entity Framework Core 7.0
- Novo sistema de paginação com metadados avançados
- Operadores de comparação numérica: `GreaterThan`, `LessThan`, etc.
- Suporte a filtros em propriedades navegacionais
### 🔧 Melhorado
- Melhoria na performance de queries complexas em 30%
- Otimização do sistema de cache interno
- Melhor integração com DI container
### 🐛 Corrigido
- Correção em filtros com DateTime e fusos horários
- Melhoria na compatibilidade com diferentes providers de banco
## 2023-04-15
### ✨ Adicionado
- Sistema de filtros dinâmicos com QueryOperator attribute
- Operadores básicos: `Equals`, `NotEquals`, `Contains`
- Paginação básica com PageIndex e PageSize
- Suporte inicial a ordenação
### 🔧 Melhorado
- Refatoração completa da arquitetura interna
- Melhor separação de responsabilidades
- Documentação inicial das APIs públicas
## 2023-03-01
### ✨ Adicionado
- Implementação inicial do padrão Unit of Work
- Integração básica com Entity Framework Core
- Suporte a operações CRUD genéricas
- Sistema básico de transações
### 🔧 Melhorado
- Estrutura de projeto organizada
- Testes unitários básicos
- CI/CD pipeline configurado
## 2023-02-10
### ✨ Adicionado
- Interfaces abstratas para Unit of Work
- Estrutura base para filtros dinâmicos
- Documentação inicial do projeto
## 2023-01-15
### ✨ Adicionado
- Versão inicial do projeto
- Estrutura básica de classes
- Configuração do projeto .NET
### 📚 Documentação
- README inicial
- Estrutura de versionamento definida
---
## Convenções do Changelog
### Tipos de Mudanças
- **✨ Adicionado** para novas funcionalidades
- **🔧 Melhorado** para mudanças em funcionalidades existentes
- **🔄 Alterado** para mudanças que afetam a API existente
- **🐛 Corrigido** para correção de bugs
- **⚠️ Removido** para funcionalidades removidas
- **🔒 Segurança** para correções de vulnerabilidades
- **📚 Documentação** para mudanças apenas na documentação
### Versionamento Semântico
- **MAJOR** (X.0.0): Mudanças incompatíveis na API
- **MINOR** (x.Y.0): Novas funcionalidades mantendo compatibilidade
- **PATCH** (x.y.Z): Correções de bugs mantendo compatibilidade
### Links de Comparação
[Unreleased]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v3.1.0...HEAD
[3.1.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v3.0.1...v3.1.0
[3.0.1]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.5.0...v3.0.0
[2.5.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.4.2...v2.5.0
[2.4.2]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.4.1...v2.4.2
[2.4.1]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.4.0...v2.4.1
[2.4.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.3.0...v2.4.0
[2.3.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.2.0...v2.3.0
[2.2.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/nuuvify/Nuuvify.CommonPack/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/nuuvify/Nuuvify.CommonPack/releases/tag/v2.0.0