![]() |
VOOZH | about |
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.5.1
NuGet\Install-Package Nuuvify.CommonPack.UnitOfWork.Abstraction -Version 2.5.1
<PackageReference Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" Version="2.5.1" />
<PackageVersion Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" Version="2.5.1" />Directory.Packages.props
<PackageReference Include="Nuuvify.CommonPack.UnitOfWork.Abstraction" />Project file
paket add Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.5.1
#r "nuget: Nuuvify.CommonPack.UnitOfWork.Abstraction, 2.5.1"
#:package Nuuvify.CommonPack.UnitOfWork.Abstraction@2.5.1
#addin nuget:?package=Nuuvify.CommonPack.UnitOfWork.Abstraction&version=2.5.1Install as a Cake Addin
#tool nuget:?package=Nuuvify.CommonPack.UnitOfWork.Abstraction&version=2.5.1Install as a Cake Tool
👁 .NET Standard 2.1
👁 Entity Framework
👁 NuGet Version
👁 License: MIT
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.
Equals to ContainsWithLikeForList (OR-based search).Sort().Filter() pode ser combinado com .Sort() e .ToPagedListAsync()Novidade: Os métodos ToPagedList<T> e ToPagedListAsync<T> agora são extension methods públicos no namespace Nuuvify.CommonPack.UnitOfWork!
Anteriormente, eram métodos internos. Agora você pode encadeá-los diretamente em qualquer IQueryable<T>:
using Nuuvify.CommonPack.UnitOfWork; // ✨ Adicione este namespace!
using Nuuvify.CommonPack.UnitOfWork.Abstraction.Extensions;
// ✅ NOVO: Encadeamento fluente completo
var result = await dbContext.Products
.Where(p => p.IsActive) // Filtros EF Core
.Filter(filterModel) // Filtros dinâmicos
.Sort("A-Name,D-Price") // Ordenação múltipla
.Select(p => new ProductDto // Projeção
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToPagedListAsync( // ✨ Extension method público!
pageIndex: 1,
pageSize: 20
);
// Result é IPagedList<ProductDto> com metadados completos:
// - Items: Lista de itens da página
// - PageIndex: 1
// - TotalCount: Total de registros
// - TotalPages: Total de páginas
// - HasNextPage/HasPreviousPage: Navegação
✅ Encadeamento Fluente: Combine com .Filter(), .Sort(), .Select() sem quebrar o pipeline
✅ Type-Safe: IntelliSense completo e validação em compile-time
✅ Sem Breaking Changes: Código legado continua funcionando
✅ Flexível: Funciona com qualquer IQueryable<T>, não apenas com repositórios
✅ Performance: Paginação executada no banco de dados (SQL Server, PostgreSQL, etc.)
// Versão síncrona (usa .Count() e .ToList())
public static IPagedList<T> ToPagedList<T>(
this IQueryable<T> source,
int pageIndex,
int pageSize,
int indexFrom = 0)
// Versão assíncrona (usa .CountAsync() e .ToListAsync())
public static async Task<IPagedList<T>> ToPagedListAsync<T>(
this IQueryable<T> source,
int pageIndex,
int pageSize,
int indexFrom = 0,
CancellationToken cancellationToken = default)
var pagedProducts = await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter)
.Sort("D-CreatedAt")
.ToPagedListAsync(pageIndex: 1, pageSize: 10);
var pagedOrders = await _dbContext.Orders
.Include(o => o.Items)
.Where(o => o.Status == OrderStatus.Pending)
.Filter(filter)
.ToPagedListAsync(pageIndex: 1, pageSize: 20);
var pagedDtos = await _dbContext.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name })
.ToPagedListAsync(pageIndex: 1, pageSize: 50);
var result = await _dbContext.Products
.Where(p => p.Stock > 0) // 1. Filtro fixo
.Filter(filterModel) // 2. Filtros dinâmicos
.Sort("A-Category,D-Price") // 3. Ordenação
.Select(p => new { p.Id, p.Name }) // 4. Projeção
.ToPagedListAsync(1, 20); // 5. Paginação
As classes abaixo estão marcadas como [Obsolete] e serão removidas em versões futuras:
IIQueryablePageList - Use extension methods diretamenteQueryablePageList - Use extension methods diretamenteMigração simples:
// ❌ Forma antiga (obsoleta)
var queryablePageList = new QueryablePageList();
var result = await queryablePageList.ToPagedListAsync(query, pageIndex, pageSize);
// ✅ Nova forma (recomendada)
using Nuuvify.CommonPack.UnitOfWork; // Adicione este namespace
var result = await query.ToPagedListAsync(pageIndex, pageSize);
Esta versão inclui 3 correções críticas no operador ContainsWithLikeForList:
🐛 Bug Expression.Constant(false) - CORRIGIDO
WHERE 0 = 1 (SQL inválido)null permite ignorar filtros vazios corretamente🐛 Bug Null Expression - CORRIGIDO
Expression.And/Orif (actualExpression == null) continue;🐛 Bug UnaryExpression Wrapping - CORRIGIDO
FilterBy encapsulado em UnaryExpression impedia acesso à listaUnaryExpression para extrair ConstantExpression-- Antes (bug): SELECT * FROM Products (sem WHERE)
-- Depois (✅): SELECT * FROM Products WHERE Name LIKE '%iPhone%' OR Name LIKE '%Samsung%'
Partial Classes: FiltersExtensions dividido em arquivos separados
FiltersExtensions.cs: API pública com documentação completaFiltersExtensions.Private.cs: Implementação privadaDocumentaçã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 para detalhes técnicos completos
# Pacote principal com implementações
dotnet add package Nuuvify.CommonPack.UnitOfWork
# Pacote com abstrações (interfaces)
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction
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();
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>();
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 }
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;
}
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";
}
using Nuuvify.CommonPack.UnitOfWork; // ✨ Namespace dos extension methods ToPagedList/ToPagedListAsync
using Nuuvify.CommonPack.UnitOfWork.Abstraction.Extensions;
[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>
/// ✨ Novo: Demonstra encadeamento completo com ToPagedListAsync como extension method
/// Filter() → Sort() → Select() → ToPagedListAsync()
/// </summary>
[HttpGet("search")]
public async Task<ActionResult<IPagedList<ProductDto>>> SearchProducts([FromQuery] ProductSearchModel filter)
{
try
{
// ✅ Pipeline completo encadeado - ToPagedListAsync agora é extension method público!
var result = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive) // Filtro fixo
.Filter(filter) // Filtros dinâmicos
.Sort(filter.Sort) // Ordenação: "A-Name,D-Price"
.Select(p => new ProductDto // Projeção para DTO
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
})
.ToPagedListAsync( // ✨ Extension method - pode encadear!
pageIndex: filter.PageIndex,
pageSize: 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 - encadeamento Filter() → Sort() → Select()
var products = await _unitOfWork.Repository<Product>()
.GetAll()
.Where(p => p.IsActive)
.Filter(filter) // Filtros dinâmicos
.Sort(filter.Sort) // Ordenaçã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 operador ContainsWithLikeForList + encadeamento completo
/// </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
};
// ✅ Encadeamento completo: Filter() → Sort() → Select() → ToPagedListAsync()
var result = await _unitOfWork.Repository<Product>()
.GetAll()
.Filter(filter) // ContainsWithLikeForList aplicado
.Sort(filter.Sort) // Ordenação
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Category = p.Category,
Price = p.Price
})
.ToPagedListAsync( // ✨ Extension method encadeado!
pageIndex: filter.PageIndex,
pageSize: 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);
}
}
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;
}
}
}
| 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 |
| 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 |
// ✨ 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);
}
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'
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)
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')
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;
}
A ordenação utiliza o formato de prefixo para indicar a direção:
D-Price resulta em ORDER BY Price DESCA-Name resulta em ORDER BY Name ASCSintaxe: "[D|A]-NomePropriedade"
Múltiplos campos: Separe por vírgula - Ex: "A-Category, D-Price, A-Name"
// 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)
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);
}
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();
}
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.).
public static IPagedList<TResult> From<TResult, TSource>(
IPagedList<TSource> source,
Func<IEnumerable<TSource>, IEnumerable<TResult>> converter)
// 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
// 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
})
);
// 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}"
})
);
// 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
})
);
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
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;
}
}
❌ 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
})
);
Para ver 6 exemplos completos e testados do método PagedList.From(), consulte:
Use conversão direta no SQL quando:
GroupBy e Sum direto no SQL// ✅ 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, ...);
// ✅ 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
// ✅ 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
// ✅ 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);
}
[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);
}
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);
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;
});
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>();
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:
dotnet add package Nuuvify.CommonPack.UnitOfWork.Abstraction --version 2.2.0filter.SearchTerms = new List<string> { "iPhone", "Samsung" }; // ✅ Correto
filter.SearchTerms = new List<string>(); // ❌ Lista vazia = sem filtro
filter.SearchTerms = null; // ❌ Null = sem filtro
[QueryOperator(Operator = WhereOperator.ContainsWithLikeForList, HasName = nameof(Product.Name))]
public List<string>? SearchTerms { get; set; }
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:
PageIndex = 1 é a primeira página (1-based)PageIndex = 0 para 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
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:
// ✅ BOM - validação
if (!string.IsNullOrEmpty(searchTerm))
{
filter.NameSearch = searchTerm;
}
// ❌ EVITAR - pode causar null expression
filter.NameSearch = ""; // String vazia pode gerar problemas
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%'
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:
// ✅ 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
Sintomas:
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();
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
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
Este projeto está licenciado sob a .
Contribuições são bem-vindas! Para contribuir:
git checkout -b feature/nova-funcionalidade)git commit -am 'Adiciona nova funcionalidade')git push origin feature/nova-funcionalidade)Para dúvidas e suporte técnico:
Este projeto segue o Semantic Versioning:
Consulte o para ver todas as mudanças.
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.
Desenvolvido com ❤️ pela equipe Nuuvify.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 was computed. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 netstandard2.1 is compatible. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
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 |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.1 | 166 | 6/14/2026 |
| 2.5.1-preview.26061404 | 55 | 6/14/2026 |
| 2.5.0 | 407 | 6/6/2026 |
| 2.5.0-preview.26060610 | 55 | 6/6/2026 |
| 2.5.0-preview.26060310 | 63 | 6/3/2026 |
| 2.4.0 | 287 | 4/17/2026 |
| 2.4.0-preview.26060604 | 63 | 6/6/2026 |
| 2.4.0-preview.26060305 | 55 | 6/3/2026 |
| 2.4.0-preview.26041709 | 58 | 4/17/2026 |
| 2.3.0 | 570 | 4/14/2026 |
| 2.3.0-preview.26041705 | 64 | 4/17/2026 |
| 2.3.0-preview.26041302 | 85 | 4/13/2026 |
| 2.3.0-preview.26040903 | 73 | 4/9/2026 |
| 2.3.0-preview.26033108 | 76 | 4/1/2026 |
| 2.2.0 | 1,773 | 11/3/2025 |
| 2.2.0-preview.25110102 | 171 | 11/1/2025 |
| 2.2.0-preview.25103002 | 198 | 10/30/2025 |
| 2.2.0-preview.25102906 | 196 | 10/29/2025 |
| 2.1.0 | 460 | 10/21/2025 |
| 2.1.0-preview.25102003 | 169 | 10/20/2025 |
# Changelog - Nuuvify.CommonPack.UnitOfWork.Abstraction
Todas as mudanças notáveis deste pacote serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-br/1.0.0/),
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/spec/v2.0.0.html).
## [Não Lançado]
### Adicionado
- Nova interface `IShortLivedDbContextFactory<TContext>` para criação explícita de contexto EF curto por operação com suporte a auditoria (`usernameContext`/`userIdContext`).
- Nova interface `IWorkerDbContextFactory<TContext>` para cenários de worker/background com contrato especializado sobre contexto EF curto.
### Alterado
### Corrigido
### Removido
### Segurança
## [Sem versão registrada] - 2025-10-30
### Removido
- `IIQueryablePageList` marcada como obsoleta — use os extension methods `ToPagedList` e `ToPagedListAsync` do namespace `Nuuvify.CommonPack.UnitOfWork`.
- `QueryablePageList` marcada como obsoleta — use os extension methods `ToPagedList` e `ToPagedListAsync` do namespace `Nuuvify.CommonPack.UnitOfWork`.
## [Sem versão registrada] - 2025-10-08
### Adicionado
- Release inicial do `Nuuvify.CommonPack.UnitOfWork.Abstraction`.
- Interface `IServiceBusMessageSender` com contrato completo para operações de filas e tópicos.
- Interfaces `IPagedList<T>`, `IRepository<T>` e `IUnitOfWork` para Repository Pattern.
- Operadores de filtro dinâmico via `QueryOperatorAttribute`.
- Interface `IIQueryablePageList` (obsoleta desde 2025-10-30).