CVAMF.Repository 1.4.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package CVAMF.Repository --version 1.4.1
                    
NuGet\Install-Package CVAMF.Repository -Version 1.4.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CVAMF.Repository" Version="1.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CVAMF.Repository" Version="1.4.1" />
                    
Directory.Packages.props
<PackageReference Include="CVAMF.Repository" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CVAMF.Repository --version 1.4.1
                    
#r "nuget: CVAMF.Repository, 1.4.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CVAMF.Repository@1.4.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CVAMF.Repository&version=1.4.1
                    
Install as a Cake Addin
#tool nuget:?package=CVAMF.Repository&version=1.4.1
                    
Install as a Cake Tool

CVAMF.Repository

Generic Repository Pattern implementation for Entity Framework Core with support for filters, pagination, and multiple primary key types.

Features

  • ✅ Generic Repository Pattern for EF Core
  • Unit of Work pattern with transaction support
  • Include (Eager Loading) support for related entities
  • AsNoTracking for 30-40% faster read-only queries
  • Soft Delete with flexible field naming (IsDeleted or Deleted)
  • Audit Fields (optional CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
  • Multi-Targeting: Compatible with .NET 9.0 and 10.0
  • ✅ Support for Guid and Int primary keys
  • ✅ Filtering with Expression Functions
  • ✅ Optional pagination
  • ✅ Full CRUD operations
  • Automatic transaction management
  • ✅ Async/await support
  • ✅ Easy dependency injection integration

Installation

dotnet add package CVAMF.Repository

Or via NuGet Package Manager:

Install-Package CVAMF.Repository

Compatibility

This package supports multiple .NET versions through multi-targeting:

  • .NET 9.0 (with EF Core 9.x)
  • .NET 10.0 (with EF Core 10.x)

The correct version is automatically selected based on your project's target framework. No additional configuration needed!

📖 For more details on multi-targeting, see MULTITARGETING.md.

Quick Start

1. Create your entities

For Guid primary key (recommended):

using CVAMF.Repository.Entities;

public class Product : EntityBase
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public bool IsActive { get; set; }
    public string Category { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
}

For Int primary key:

using CVAMF.Repository.Entities;

public class Category : EntityBaseInt
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public bool IsActive { get; set; }
}

Custom entity implementing IEntity<T>:

using CVAMF.Repository.Entities;

public class Order : IEntity<Guid>
{
    public Guid Id { get; set; }
    public string OrderNumber { get; set; } = string.Empty;
    public decimal TotalAmount { get; set; }
    public DateTime OrderDate { get; set; }
    public string Status { get; set; } = string.Empty;
}

2. Configure your DbContext

using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Your entity configurations here
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Price).HasPrecision(18, 2);
        });
    }
}

3. Register services in Program.cs

Option 1: Using Repositories only

using CVAMF.Repository.Extensions;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add repositories - This registers IRepository<,> for all entities!
builder.Services.AddRepositories();

// Add your services
builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<CategoryService>();

var app = builder.Build();
app.Run();

Option 2: Using Unit of Work (Recommended for complex scenarios)

using CVAMF.Repository.Extensions;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add both Repositories and Unit of Work
builder.Services.AddRepositoriesWithUnitOfWork<ApplicationDbContext>();

// Add your services
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<ProductService>();

var app = builder.Build();
app.Run();

4. Use in your services

using CVAMF.Repository.Interfaces;
using CVAMF.Repository.Models;

public class ProductService
{
    private readonly IRepository<Product, Guid> _productRepository;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        IRepository<Product, Guid> productRepository,
        ILogger<ProductService> logger)
    {
        _productRepository = productRepository;
        _logger = logger;
    }

    // CRUD operations examples below...
}

Complete Usage Examples

📖 Read Operations

Get by ID
public async Task<Product?> GetProductById(Guid productId)
{
    var product = await _productRepository.GetByIdAsync(productId);

    if (product == null)
    {
        _logger.LogWarning("Product {ProductId} not found", productId);
        return null;
    }

    return product;
}
Get All
public async Task<IEnumerable<Product>> GetAllProducts()
{
    return await _productRepository.GetAllAsync();
}
Simple Filter
public async Task<IEnumerable<Product>> GetActiveProducts()
{
    return await _productRepository.GetAsync(
        filter: p => p.IsActive);
}
Filter with Ordering
public async Task<IEnumerable<Product>> GetProductsByCategory(string category)
{
    return await _productRepository.GetAsync(
        filter: p => p.Category == category && p.IsActive,
        orderBy: q => q.OrderBy(p => p.Name));
}
Complex Filters
public async Task<IEnumerable<Product>> GetProductsInPriceRange(
    decimal minPrice, 
    decimal maxPrice, 
    string? searchTerm = null)
{
    return await _productRepository.GetAsync(
        filter: p => p.IsActive 
                  && p.Price >= minPrice 
                  && p.Price <= maxPrice
                  && (searchTerm == null || p.Name.Contains(searchTerm) || p.Description.Contains(searchTerm)),
        orderBy: q => q.OrderBy(p => p.Price).ThenBy(p => p.Name));
}
Multiple Sorting
public async Task<IEnumerable<Product>> GetProductsSorted()
{
    return await _productRepository.GetAsync(
        filter: p => p.Stock > 0,
        orderBy: q => q.OrderBy(p => p.Category)
                       .ThenByDescending(p => p.Price)
                       .ThenBy(p => p.Name));
}

📄 Pagination Examples

Basic Pagination
public async Task<PagedResult<Product>> GetProductsPaged(int pageNumber, int pageSize)
{
    return await _productRepository.GetPagedAsync(
        pageNumber: pageNumber,
        pageSize: pageSize,
        filter: p => p.IsActive,
        orderBy: q => q.OrderBy(p => p.Name));
}
public async Task<PagedResult<Product>> SearchProducts(
    string searchTerm,
    int page = 1,
    int pageSize = 10,
    string sortBy = "name",
    bool descending = false)
{
    var pagedResult = await _productRepository.GetPagedAsync(
        pageNumber: page,
        pageSize: pageSize,
        filter: p => p.IsActive && (
            p.Name.Contains(searchTerm) ||
            p.Description.Contains(searchTerm) ||
            p.Category.Contains(searchTerm)
        ),
        orderBy: sortBy.ToLower() switch
        {
            "price" => descending 
                ? q => q.OrderByDescending(p => p.Price)
                : q => q.OrderBy(p => p.Price),
            "date" => descending
                ? q => q.OrderByDescending(p => p.CreatedAt)
                : q => q.OrderBy(p => p.CreatedAt),
            _ => descending
                ? q => q.OrderByDescending(p => p.Name)
                : q => q.OrderBy(p => p.Name)
        });

    _logger.LogInformation(
        "Found {TotalCount} products. Showing page {Page} of {TotalPages}",
        pagedResult.TotalCount,
        pagedResult.PageNumber,
        pagedResult.TotalPages);

    return pagedResult;
}
Display Pagination Info
public async Task DisplayProductsPaged()
{
    var pagedResult = await _productRepository.GetPagedAsync(
        pageNumber: 1,
        pageSize: 10,
        filter: p => p.IsActive);

    Console.WriteLine($"Page {pagedResult.PageNumber} of {pagedResult.TotalPages}");
    Console.WriteLine($"Total items: {pagedResult.TotalCount}");
    Console.WriteLine($"Items per page: {pagedResult.PageSize}");
    Console.WriteLine($"Has previous: {pagedResult.HasPreviousPage}");
    Console.WriteLine($"Has next: {pagedResult.HasNextPage}");
    Console.WriteLine();

    foreach (var product in pagedResult.Items)
    {
        Console.WriteLine($"- {product.Name} (${product.Price})");
    }
}

✏️ Create Operations

Add Single Entity
public async Task<Product> CreateProduct(string name, decimal price, string category)
{
    var product = new Product
    {
        Id = Guid.NewGuid(),
        Name = name,
        Price = price,
        Category = category,
        IsActive = true,
        Stock = 0,
        CreatedAt = DateTime.UtcNow
    };

    await _productRepository.AddAsync(product);
    await _productRepository.SaveChangesAsync();

    _logger.LogInformation("Product {ProductName} created with ID {ProductId}", 
        product.Name, product.Id);

    return product;
}
Add Multiple Entities
public async Task<int> ImportProducts(List<ProductDto> productDtos)
{
    var products = productDtos.Select(dto => new Product
    {
        Id = Guid.NewGuid(),
        Name = dto.Name,
        Price = dto.Price,
        Category = dto.Category,
        IsActive = true,
        Stock = dto.Stock,
        CreatedAt = DateTime.UtcNow
    }).ToList();

    await _productRepository.AddRangeAsync(products);
    var savedCount = await _productRepository.SaveChangesAsync();

    _logger.LogInformation("Imported {Count} products", savedCount);

    return savedCount;
}

🔄 Update Operations

Update Single Entity
public async Task<bool> UpdateProductPrice(Guid productId, decimal newPrice)
{
    var product = await _productRepository.GetByIdAsync(productId);

    if (product == null)
    {
        _logger.LogWarning("Product {ProductId} not found", productId);
        return false;
    }

    product.Price = newPrice;

    await _productRepository.UpdateAsync(product);
    await _productRepository.SaveChangesAsync();

    _logger.LogInformation("Product {ProductId} price updated to {Price}", 
        productId, newPrice);

    return true;
}
Update Multiple Entities
public async Task<int> ApplyDiscountToCategory(string category, decimal discountPercent)
{
    var products = await _productRepository.GetAsync(
        filter: p => p.Category == category && p.IsActive);

    foreach (var product in products)
    {
        product.Price = product.Price * (1 - discountPercent / 100);
    }

    await _productRepository.UpdateRangeAsync(products);
    var updatedCount = await _productRepository.SaveChangesAsync();

    _logger.LogInformation(
        "Applied {Discount}% discount to {Count} products in category {Category}",
        discountPercent, updatedCount, category);

    return updatedCount;
}
Conditional Update
public async Task<int> DeactivateLowStockProducts()
{
    var lowStockProducts = await _productRepository.GetAsync(
        filter: p => p.Stock < 5 && p.IsActive);

    foreach (var product in lowStockProducts)
    {
        product.IsActive = false;
    }

    await _productRepository.UpdateRangeAsync(lowStockProducts);
    return await _productRepository.SaveChangesAsync();
}

🗑️ Delete Operations

Delete by ID
public async Task<bool> DeleteProduct(Guid productId)
{
    var product = await _productRepository.GetByIdAsync(productId);

    if (product == null)
    {
        return false;
    }

    await _productRepository.DeleteAsync(productId);
    await _productRepository.SaveChangesAsync();

    _logger.LogInformation("Product {ProductId} deleted", productId);

    return true;
}
Delete by Entity
public async Task DeleteInactiveProduct(string productName)
{
    var product = await _productRepository.GetFirstOrDefaultAsync(
        filter: p => p.Name == productName && !p.IsActive);

    if (product != null)
    {
        await _productRepository.DeleteAsync(product);
        await _productRepository.SaveChangesAsync();
    }
}
Delete Multiple Entities
public async Task<int> CleanupOldInactiveProducts(int daysOld)
{
    var cutoffDate = DateTime.UtcNow.AddDays(-daysOld);

    var oldProducts = await _productRepository.GetAsync(
        filter: p => !p.IsActive && p.CreatedAt < cutoffDate);

    if (oldProducts.Any())
    {
        await _productRepository.DeleteRangeAsync(oldProducts);
        var deletedCount = await _productRepository.SaveChangesAsync();

        _logger.LogInformation("Deleted {Count} old inactive products", deletedCount);
        return deletedCount;
    }

    return 0;
}

🔍 Query Helper Methods

Check Existence
public async Task<bool> ProductExists(string name)
{
    return await _productRepository.AnyAsync(
        filter: p => p.Name == name);
}

public async Task<bool> HasProductsInCategory(string category)
{
    return await _productRepository.AnyAsync(
        filter: p => p.Category == category && p.IsActive);
}
Count
public async Task<int> GetTotalProducts()
{
    return await _productRepository.CountAsync();
}

public async Task<int> GetActiveProductCount()
{
    return await _productRepository.CountAsync(
        filter: p => p.IsActive);
}

public async Task<Dictionary<string, int>> GetProductCountByCategory()
{
    var categories = await _productRepository.GetAsync(
        filter: p => p.IsActive);

    return categories
        .GroupBy(p => p.Category)
        .ToDictionary(g => g.Key, g => g.Count());
}
First or Default
public async Task<Product?> GetMostExpensiveProduct()
{
    var products = await _productRepository.GetAsync(
        filter: p => p.IsActive,
        orderBy: q => q.OrderByDescending(p => p.Price));

    return products.FirstOrDefault();
}

public async Task<Product?> FindProductByName(string name)
{
    return await _productRepository.GetFirstOrDefaultAsync(
        filter: p => p.Name == name && p.IsActive);
}

Using with Int Primary Keys

public class CategoryService
{
    private readonly IRepository<Category, int> _categoryRepository;

    public CategoryService(IRepository<Category, int> categoryRepository)
    {
        _categoryRepository = categoryRepository;
    }

    public async Task<Category> CreateCategory(string name)
    {
        var category = new Category
        {
            // No need to set Id for int (auto-increment)
            Name = name,
            IsActive = true
        };

        await _categoryRepository.AddAsync(category);
        await _categoryRepository.SaveChangesAsync();

        return category;
    }

    public async Task<Category?> GetCategoryById(int categoryId)
    {
        return await _categoryRepository.GetByIdAsync(categoryId);
    }
}

Advanced Scenarios

Using Unit of Work for Complex Transactions

The Unit of Work pattern is ideal when you need to coordinate multiple repositories in a single transaction. This ensures data consistency across multiple operations.

Simple Transaction Example
public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IUnitOfWork unitOfWork, ILogger<OrderService> logger)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(Order order, List<OrderItem> items)
    {
        return await _unitOfWork.ExecuteInTransactionAsync(async () =>
        {
            // Get repositories
            var orderRepo = _unitOfWork.Repository<Order, Guid>();
            var itemRepo = _unitOfWork.Repository<OrderItem, int>();
            var productRepo = _unitOfWork.Repository<Product, Guid>();

            // Create order
            await orderRepo.AddAsync(order);

            // Add order items and update stock
            foreach (var item in items)
            {
                var product = await productRepo.GetByIdAsync(item.ProductId);

                if (product == null)
                    throw new InvalidOperationException($"Product {item.ProductId} not found");

                if (product.Stock < item.Quantity)
                    throw new InvalidOperationException($"Insufficient stock for {product.Name}");

                // Update stock
                product.Stock -= item.Quantity;
                await productRepo.UpdateAsync(product);

                // Add order item
                item.OrderId = order.Id;
                await itemRepo.AddAsync(item);
            }

            // All operations are committed together
            // If any fails, all are rolled back automatically
            return order;
        });
    }
}
Manual Transaction Control
public async Task<bool> ProcessComplexOrderAsync(Order order)
{
    await using var transaction = await _unitOfWork.BeginTransactionAsync();

    try
    {
        var orderRepo = _unitOfWork.Repository<Order, Guid>();
        var inventoryRepo = _unitOfWork.Repository<Inventory, Guid>();

        // Step 1: Create order
        await orderRepo.AddAsync(order);
        await _unitOfWork.SaveChangesAsync();

        // Step 2: Update inventory
        // ... inventory operations
        await _unitOfWork.SaveChangesAsync();

        // Step 3: Process payment
        // ... payment operations
        await _unitOfWork.SaveChangesAsync();

        // Commit if everything succeeded
        await transaction.CommitAsync();
        return true;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing order");
        await transaction.RollbackAsync();
        return false;
    }
}
Multiple Repositories Coordination
public async Task<bool> TransferInventoryAsync(Guid fromWarehouseId, Guid toWarehouseId, Guid productId, int quantity)
{
    return await _unitOfWork.ExecuteInTransactionAsync(async () =>
    {
        var warehouseRepo = _unitOfWork.Repository<Warehouse, Guid>();
        var inventoryRepo = _unitOfWork.Repository<Inventory, Guid>();
        var logRepo = _unitOfWork.Repository<InventoryLog, int>();

        var fromWarehouse = await warehouseRepo.GetByIdAsync(fromWarehouseId);
        var toWarehouse = await warehouseRepo.GetByIdAsync(toWarehouseId);

        if (fromWarehouse == null || toWarehouse == null)
            throw new InvalidOperationException("Warehouse not found");

        // Deduct from source
        var sourceInventory = await inventoryRepo.GetFirstOrDefaultAsync(
            i => i.WarehouseId == fromWarehouseId && i.ProductId == productId);

        if (sourceInventory == null || sourceInventory.Quantity < quantity)
            throw new InvalidOperationException("Insufficient inventory");

        sourceInventory.Quantity -= quantity;
        await inventoryRepo.UpdateAsync(sourceInventory);

        // Add to destination
        var destInventory = await inventoryRepo.GetFirstOrDefaultAsync(
            i => i.WarehouseId == toWarehouseId && i.ProductId == productId);

        if (destInventory == null)
        {
            destInventory = new Inventory
            {
                Id = Guid.NewGuid(),
                WarehouseId = toWarehouseId,
                ProductId = productId,
                Quantity = quantity
            };
            await inventoryRepo.AddAsync(destInventory);
        }
        else
        {
            destInventory.Quantity += quantity;
            await inventoryRepo.UpdateAsync(destInventory);
        }

        // Log the transfer
        await logRepo.AddAsync(new InventoryLog
        {
            ProductId = productId,
            FromWarehouseId = fromWarehouseId,
            ToWarehouseId = toWarehouseId,
            Quantity = quantity,
            TransferDate = DateTime.UtcNow
        });

        return true;
    });
}

📚 Complete Unit of Work Documentation

For detailed examples and advanced usage patterns, see UNITOFWORK_USAGE.md which includes:

  • ✅ Basic setup and configuration
  • ✅ Transaction management strategies
  • ✅ Error handling patterns
  • ✅ Nested transactions
  • ✅ API controller examples
  • ✅ Best practices and performance tips

All repository methods support Include (Eager Loading) to load related entities and avoid N+1 query problems.

GetByIdAsync with Include
// Load Order with Items and Customer
var order = await _orderRepository.GetByIdAsync(
    orderId,
    includes: q => q.Include(o => o.Items)
                    .ThenInclude(i => i.Product)
                    .Include(o => o.Customer));
GetAsync with Include
// Load active orders with all details
var activeOrders = await _orderRepository.GetAsync(
    filter: o => o.Status == "Active",
    orderBy: q => q.OrderByDescending(o => o.OrderDate),
    includes: q => q.Include(o => o.Items)
                    .Include(o => o.Customer));
GetPagedAsync with Include
// Paginated orders with related data
var pagedOrders = await _orderRepository.GetPagedAsync(
    pageNumber: 1,
    pageSize: 10,
    filter: o => o.Status == "Pending",
    orderBy: q => q.OrderByDescending(o => o.OrderDate),
    includes: q => q.Include(o => o.Items)
                    .ThenInclude(i => i.Product));
Multiple Includes
// Load product with all related entities
var product = await _productRepository.GetByIdAsync(
    productId,
    includes: q => q.Include(p => p.Category)
                    .Include(p => p.Supplier)
                    .Include(p => p.Reviews)
                    .Include(p => p.Images));
📖 Complete Include Documentation

For comprehensive examples, performance tips, and best practices, see INCLUDE_USAGE.md which includes:

  • ✅ All methods with Include support
  • ✅ ThenInclude for nested relationships
  • ✅ Multiple includes examples
  • ✅ Performance optimization
  • ✅ Avoiding N+1 queries
  • ✅ Real-world scenarios

⚡ Performance Optimization with AsNoTracking

All query methods support AsNoTracking for 30-40% faster read-only queries. When entities don't need to be tracked for changes, use asNoTracking: true.

When to Use AsNoTracking
// ✅ RECOMMENDED: Lists and grids (read-only display)
var products = await _productRepository.GetAsync(
    filter: p => p.IsActive,
    orderBy: q => q.OrderBy(p => p.Name),
    includes: q => q.Include(p => p.Category),
    asNoTracking: true); // 30-40% faster!

// ✅ RECOMMENDED: Pagination (large result sets)
var pagedOrders = await _orderRepository.GetPagedAsync(
    pageNumber: 1,
    pageSize: 10,
    filter: o => o.Status == "Pending",
    orderBy: q => q.OrderByDescending(o => o.OrderDate),
    includes: q => q.Include(o => o.Items),
    asNoTracking: true); // Much faster for lists

// ✅ RECOMMENDED: API GET endpoints returning DTOs
var order = await _orderRepository.GetByIdAsync(
    orderId,
    includes: q => q.Include(o => o.Items)
                    .Include(o => o.Customer),
    asNoTracking: true);

var dto = new OrderDto
{
    Id = order.Id,
    OrderNumber = order.OrderNumber,
    CustomerName = order.Customer.Name
    // ... map to DTO
};
When NOT to Use AsNoTracking
// ❌ DON'T USE: When you need to update the entity
var product = await _productRepository.GetByIdAsync(
    productId,
    asNoTracking: false); // or omit parameter (default is false)

product.Price = newPrice;
await _productRepository.UpdateAsync(product);
await _productRepository.SaveChangesAsync();
Performance Comparison
┌────────────────────┬──────────────┬──────────────────┐
│ Operation          │ With Tracking│ AsNoTracking     │
├────────────────────┼──────────────┼──────────────────┤
│ Query 1000 records │ 250ms        │ 150ms (40%)     │
│ Memory usage       │ 15MB         │ 9MB (40%)       │
│ Query 10 records   │ 25ms         │ 18ms (28%)      │
└────────────────────┴──────────────┴──────────────────┘
📖 Complete AsNoTracking Documentation

For detailed examples and best practices, see ASNOTRACKING_USAGE.md which includes:

  • ✅ When to use vs when NOT to use
  • ✅ API controller examples
  • ✅ CQRS patterns
  • ✅ Performance benchmarks
  • ✅ Common pitfalls and solutions

🗑️ Soft Delete Support

Soft Delete allows marking records as deleted without physically removing them from the database. You can choose between two field naming conventions: IsDeleted or Deleted.

Quick Start

Option 1: Using IsDeleted (recommended):

using CVAMF.Repository.Entities;

public class Product : EntityBaseSoftDelete
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // IsDeleted, DeletedAt, DeletedBy are inherited
}

Option 2: Using Deleted (alternative):

using CVAMF.Repository.Entities;

public class Product : EntityBaseSoftDeleteAlt
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // Deleted, DeletedAt, DeletedBy are inherited
}
Basic Usage
// Soft delete a product
await _productRepository.SoftDeleteAsync(productId, "admin@example.com");
await _productRepository.SaveChangesAsync();

// Restore a soft deleted product
await _productRepository.RestoreAsync(productId);
await _productRepository.SaveChangesAsync();

// Soft delete multiple entities
var oldProducts = await _productRepository.GetAsync(
    filter: p => p.CreatedAt < DateTime.UtcNow.AddYears(-5));

var count = await _productRepository.SoftDeleteRangeAsync(oldProducts, "system");
await _productRepository.SaveChangesAsync();

Configure automatic filtering of soft deleted records:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // For ISoftDeletable (IsDeleted)
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);

    // For ISoftDeletableAlternative (Deleted)
    // modelBuilder.Entity<Product>()
    //     .HasQueryFilter(p => !p.Deleted);
}
📖 Complete Soft Delete Documentation

For comprehensive examples and migration guide, see SOFTDELETE_USAGE.md which includes:

  • ✅ Choosing field names (IsDeleted vs Deleted)
  • ✅ Base classes and interfaces available
  • ✅ Soft delete, restore, and bulk operations
  • ✅ Global query filters setup
  • ✅ When to use soft delete vs physical delete
  • ✅ Migration guide from physical to soft delete
  • ✅ Performance optimization with indexes
  • ✅ Complete examples with UnitOfWork

📝 Audit Fields Support (Optional)

Audit Fields automatically track who and when created or modified entities. This is completely optional and only applied when you use the overloads with audit parameters.

Quick Start

Option 1: Using base class with audit:

using CVAMF.Repository.Entities;

public class Product : EntityBaseAuditable
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // CreatedAt, CreatedBy, UpdatedAt, UpdatedBy are inherited
}

Option 2: Audit + Soft Delete (all together):

public class Product : EntityBaseAuditableSoftDelete
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // Audit: CreatedAt, CreatedBy, UpdatedAt, UpdatedBy
    // Soft Delete: IsDeleted, DeletedAt, DeletedBy
}
Basic Usage
// Create with audit
var product = new Product { Name = "Laptop", Price = 1299.99m };
await _productRepository.AddAsync(product, "admin@example.com");
await _productRepository.SaveChangesAsync();
// Result: product.CreatedAt and product.CreatedBy are set automatically

// Update with audit
product.Price = 999.99m;
await _productRepository.UpdateAsync(product, "manager@example.com");
await _productRepository.SaveChangesAsync();
// Result: product.UpdatedAt and product.UpdatedBy are set automatically

// Without audit (optional)
await _productRepository.AddAsync(product); // No audit info filled
Available Base Classes
  • EntityBaseAuditable (Guid) / EntityBaseAuditableInt (int) - Only audit fields
  • EntityBaseAuditableSoftDelete (Guid) / EntityBaseAuditableSoftDeleteInt (int) - Audit + Soft Delete (IsDeleted)
  • EntityBaseAuditableSoftDeleteAlt (Guid) / EntityBaseAuditableSoftDeleteAltInt (int) - Audit + Soft Delete (Deleted)
📖 Complete Audit Documentation

For comprehensive examples and ASP.NET Core integration, see AUDIT_USAGE.md which includes:

  • ✅ All available base classes and interfaces
  • ✅ Automatic vs manual audit field population
  • ✅ Combining with Soft Delete and other features
  • ✅ Audit queries and reporting
  • ✅ ASP.NET Core integration (getting current user)
  • ✅ DbContext configuration and indexes
  • ✅ Complete real-world examples

Transaction Example (Without Unit of Work)

public async Task<bool> TransferStock(Guid fromProductId, Guid toProductId, int quantity)
{
    var fromProduct = await _productRepository.GetByIdAsync(fromProductId);
    var toProduct = await _productRepository.GetByIdAsync(toProductId);

    if (fromProduct == null || toProduct == null || fromProduct.Stock < quantity)
    {
        return false;
    }

    try
    {
        fromProduct.Stock -= quantity;
        toProduct.Stock += quantity;

        await _productRepository.UpdateAsync(fromProduct);
        await _productRepository.UpdateAsync(toProduct);

        // Both updates are saved in a single transaction
        await _productRepository.SaveChangesAsync();

        return true;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error transferring stock");
        return false;
    }
}

Working with DTOs

public async Task<List<ProductListDto>> GetProductList(string category)
{
    var products = await _productRepository.GetAsync(
        filter: p => p.Category == category && p.IsActive,
        orderBy: q => q.OrderBy(p => p.Name));

    return products.Select(p => new ProductListDto
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price,
        InStock = p.Stock > 0
    }).ToList();
}

Soft Delete Pattern

// Add DeletedAt property to your entity
public class Product : EntityBase
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public bool IsActive { get; set; }
    public DateTime? DeletedAt { get; set; }
}

// Implement soft delete
public async Task SoftDeleteProduct(Guid productId)
{
    var product = await _productRepository.GetByIdAsync(productId);

    if (product != null)
    {
        product.DeletedAt = DateTime.UtcNow;
        product.IsActive = false;

        await _productRepository.UpdateAsync(product);
        await _productRepository.SaveChangesAsync();
    }
}

// Get only non-deleted products
public async Task<IEnumerable<Product>> GetActiveNonDeletedProducts()
{
    return await _productRepository.GetAsync(
        filter: p => p.IsActive && p.DeletedAt == null);
}

Best Practices

✅ DO

  • Always call SaveChangesAsync() after Add, Update, or Delete operations
  • Use pagination for large datasets
  • Use specific filters to reduce database load
  • Handle null returns from GetByIdAsync and GetFirstOrDefaultAsync
  • Use CancellationToken for long-running operations
  • Inject IRepository<TEntity, TKey> instead of concrete implementations

❌ DON'T

  • Don't forget to call SaveChangesAsync() - changes won't persist!
  • Don't load all data without filters unless necessary
  • Don't use magic strings - use constants or enums
  • Don't ignore exceptions - always log and handle errors

API Reference

Query Methods

Method Description Returns
GetByIdAsync(id) Get entity by primary key TEntity?
GetAllAsync() Get all entities IEnumerable<TEntity>
GetAsync(filter, orderBy) Get filtered/ordered entities IEnumerable<TEntity>
GetPagedAsync(page, size, filter, orderBy) Get paginated results PagedResult<TEntity>
GetFirstOrDefaultAsync(filter) Get first matching entity TEntity?
AnyAsync(filter) Check if any matches bool
CountAsync(filter) Count matching entities int

Command Methods

Method Description Returns
AddAsync(entity) Add new entity TEntity
AddRangeAsync(entities) Add multiple entities Task
UpdateAsync(entity) Update entity Task
UpdateRangeAsync(entities) Update multiple entities Task
DeleteAsync(entity) Delete entity Task
DeleteAsync(id) Delete by ID Task
DeleteRangeAsync(entities) Delete multiple entities Task
SaveChangesAsync() Persist changes to database int (affected rows)

Requirements

  • .NET 10.0 or higher
  • Entity Framework Core 10.0 or higher

License

MIT

Author

Carlos Filho

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

If you encounter any issues or have questions, please file an issue on the GitHub repository.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.8.0 94 6/1/2026
1.7.2 105 5/22/2026
1.7.1 94 5/18/2026
1.7.0 93 5/16/2026
1.6.0 91 5/16/2026
1.5.0 95 5/15/2026
1.4.3 99 5/14/2026
1.4.2 90 5/14/2026
1.4.1 98 5/13/2026
1.4.0 89 5/13/2026
1.3.0 91 5/13/2026
1.2.1 100 5/13/2026
1.2.0 94 5/13/2026
1.1.2 111 5/13/2026
1.1.1 89 5/13/2026
1.1.0 85 5/13/2026
1.0.0 92 5/13/2026

## v1.4.1
- 🎯 **Multi-Targeting Support**: Now supports .NET 9.0 and 10.0
 - Single package works across both .NET versions
 - Automatic version selection based on your project's target framework
 - EF Core 9.x for .NET 9.0, EF Core 10.x for .NET 10.0
 - Zero configuration required - NuGet handles everything automatically
 - Package contains optimized builds for each framework
 - Documentation: MULTITARGETING.md

## v1.4.0
- 📝 **Audit Fields Support**: Optional automatic tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
 - `IAuditable` interface for entities requiring audit tracking
 - Optional parameters in AddAsync, AddRangeAsync, UpdateAsync, UpdateRangeAsync
 - Base classes: EntityBaseAuditable, EntityBaseAuditableInt
 - Combined classes: EntityBaseAuditableSoftDelete, EntityBaseAuditableSoftDeleteInt, EntityBaseAuditableSoftDeleteAlt, EntityBaseAuditableSoftDeleteAltInt
 - Fully optional - use only when needed
 - Automatic timestamp and user tracking
 - ASP.NET Core integration examples
 - Documentation: AUDIT_USAGE.md

## v1.3.0
- ⚡ **AsNoTracking Support**: 30-40% performance improvement for read-only queries
 - All query methods (GetByIdAsync, GetAllAsync, GetAsync, GetPagedAsync, GetFirstOrDefaultAsync) support `asNoTracking` parameter
 - Ideal for lists, pagination, API GET endpoints, and display-only scenarios
 - Documentation: ASNOTRACKING_USAGE.md

- 🗑️ **Soft Delete Support**: Flexible field naming (IsDeleted or Deleted)
 - `ISoftDeletable` interface for entities using `IsDeleted` property
 - `ISoftDeletableAlternative` interface for entities using `Deleted` property
 - Base classes: EntityBaseSoftDelete, EntityBaseSoftDeleteInt, EntityBaseSoftDeleteAlt, EntityBaseSoftDeleteAltInt
 - Methods: SoftDeleteAsync, SoftDeleteRangeAsync, RestoreAsync
 - DeletedAt and DeletedBy tracking for audit purposes
 - Global Query Filter examples for automatic filtering
 - Documentation: SOFTDELETE_USAGE.md

## v1.2.1
- Improved release notes formatting with Markdown support
- Better documentation structure on NuGet.org

## v1.2.0
- Added Include (Eager Loading) support to all query methods
- GetByIdAsync, GetAllAsync, GetAsync, GetPagedAsync, and GetFirstOrDefaultAsync now support includes
- Support for ThenInclude for nested relationships
- Comprehensive documentation in INCLUDE_USAGE.md

## v1.1.1
- Added README.md to package for better documentation on NuGet.org

## v1.1.0
- Added Unit of Work pattern with comprehensive transaction support
- ExecuteInTransactionAsync for automatic transaction management
- Manual transaction control with BeginTransactionAsync, CommitAsync, and RollbackAsync
- Multiple repository coordination
- Comprehensive documentation in UNITOFWORK_USAGE.md

## v1.0.0
- Initial release with generic repository pattern
- Support for Guid and Int primary keys
- Filtering with Expression Functions
- Pagination support
- Full CRUD operations