RepletoryLib.Data.EntityFramework 1.0.0

dotnet add package RepletoryLib.Data.EntityFramework --version 1.0.0
                    
NuGet\Install-Package RepletoryLib.Data.EntityFramework -Version 1.0.0
                    
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="RepletoryLib.Data.EntityFramework" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepletoryLib.Data.EntityFramework" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepletoryLib.Data.EntityFramework" />
                    
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 RepletoryLib.Data.EntityFramework --version 1.0.0
                    
#r "nuget: RepletoryLib.Data.EntityFramework, 1.0.0"
                    
#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 RepletoryLib.Data.EntityFramework@1.0.0
                    
#: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=RepletoryLib.Data.EntityFramework&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepletoryLib.Data.EntityFramework&version=1.0.0
                    
Install as a Cake Tool

RepletoryLib.Data.EntityFramework

Generic EF Core repository pattern with unit of work, soft-delete, composite key support, and pagination.

Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

NuGet .NET 10 License: MIT


Overview

RepletoryLib.Data.EntityFramework provides a generic repository and unit of work implementation built on Entity Framework Core. It works with any entity that inherits from BaseEntity and provides built-in support for soft-delete, audit stamping, pagination, dynamic sorting, and transactional operations.

The repository exposes a clean IQueryable<T> for advanced queries while handling common operations like CRUD, soft-delete, and existence checks through a simple interface.

Key Features

  • Generic IRepository<T> -- Full CRUD with soft-delete, hard-delete, bulk operations, count, and existence checks
  • ICompositeKeyRepository<T> -- CRUD for entities with composite primary keys (join tables, many-to-many)
  • IUnitOfWork -- Transaction management with begin, commit, and rollback
  • Bulk operations -- AddRangeAsync, UpdateRangeAsync, SoftDeleteRangeAsync, HardDeleteRangeAsync
  • Soft-delete -- Automatic filtering of soft-deleted entities via global query filter
  • Pagination -- ToPagedResultAsync extension for IQueryable<T>
  • Dynamic sorting -- Sort by property name at runtime
  • No-tracking queries -- Query() returns AsNoTracking() queryable for read performance

Installation

dotnet add package RepletoryLib.Data.EntityFramework

Or add to your .csproj:

<PackageReference Include="RepletoryLib.Data.EntityFramework" Version="1.0.0" />

Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.

Dependencies

Package Type
RepletoryLib.Common RepletoryLib
Microsoft.EntityFrameworkCore NuGet (10.0.0)
Microsoft.EntityFrameworkCore.Relational NuGet (10.0.0)

Quick Start

1. Define your entity

using RepletoryLib.Common.Entities;

public class Product : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    public string Sku { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

2. Create your DbContext

using Microsoft.EntityFrameworkCore;

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

    public DbSet<Product> Products => Set<Product>();
}

3. Register services in Program.cs

using RepletoryLib.Data.EntityFramework;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services
    .AddRepletoryEF<AppDbContext>(builder.Configuration)
    .AddRepletoryRepository<Product, AppDbContext>()
    .AddRepletoryCompositeKeyRepository<OrderProduct, AppDbContext>(); // Composite key entity

4. Use in your service

using RepletoryLib.Data.EntityFramework.Interfaces;

public class ProductService
{
    private readonly IRepository<Product> _products;
    private readonly IUnitOfWork _unitOfWork;

    public ProductService(IRepository<Product> products, IUnitOfWork unitOfWork)
    {
        _products = products;
        _unitOfWork = unitOfWork;
    }

    public async Task<Product?> GetByIdAsync(Guid id)
    {
        return await _products.GetByIdAsync(id);
    }
}

Configuration

EFOptions

Property Type Default Description
EnableSoftDeleteFilter bool true Enables global query filter to exclude soft-deleted entities

Section name: "RepletoryEF"

{
  "RepletoryEF": {
    "EnableSoftDeleteFilter": true
  }
}

Usage Examples

CRUD Operations

public class ProductService
{
    private readonly IRepository<Product> _products;

    public ProductService(IRepository<Product> products) => _products = products;

    // Create
    public async Task<Product> CreateAsync(string name, decimal price)
    {
        var product = new Product { Name = name, Price = price, Sku = $"SKU-{Guid.NewGuid():N}" };
        return await _products.AddAsync(product);
    }

    // Read
    public async Task<Product?> GetAsync(Guid id) => await _products.GetByIdAsync(id);

    public async Task<IReadOnlyList<Product>> GetAllAsync() => await _products.GetAllAsync();

    // Update
    public async Task UpdatePriceAsync(Guid id, decimal newPrice)
    {
        var product = await _products.GetByIdAsync(id)
            ?? throw new NotFoundException($"Product {id} not found");
        product.Price = newPrice;
        await _products.UpdateAsync(product);
    }

    // Soft Delete (sets IsDeleted = true)
    public async Task DeactivateAsync(Guid id) => await _products.SoftDeleteAsync(id);

    // Hard Delete (permanently removes)
    public async Task RemoveAsync(Guid id) => await _products.HardDeleteAsync(id);
}

Finding and Filtering

// Find by predicate
var expensiveProducts = await _products.FindAsync(p => p.Price > 1000);

// Check existence
bool hasSku = await _products.ExistsAsync(p => p.Sku == "SKU-001");

// Count with optional predicate
int total = await _products.CountAsync();
int inStock = await _products.CountAsync(p => p.StockQuantity > 0);

Advanced Queries with IQueryable

// Query() returns IQueryable with AsNoTracking and soft-delete filter
var results = await _products.Query()
    .Where(p => p.Price > 50)
    .OrderByDescending(p => p.CreatedAt)
    .Take(10)
    .ToListAsync();

Pagination with PagedResult

using RepletoryLib.Common.Models;
using RepletoryLib.Data.EntityFramework.Extensions;

public async Task<PagedResult<Product>> GetPagedAsync(PagedRequest request)
{
    return await _products.Query()
        .Where(p => p.StockQuantity > 0)
        .ToPagedResultAsync(request);
}

// Usage:
var page = await productService.GetPagedAsync(new PagedRequest
{
    Page = 1,
    PageSize = 20,
    SortBy = "Price",
    SortDirection = SortDirection.Descending
});

// page.Items       -- List<Product> for the current page
// page.TotalCount  -- Total matching products
// page.TotalPages  -- Computed number of pages
// page.HasNextPage -- Whether more pages exist

Unit of Work and Transactions

public class OrderService
{
    private readonly IRepository<Order> _orders;
    private readonly IRepository<Product> _products;
    private readonly IUnitOfWork _unitOfWork;

    public OrderService(
        IRepository<Order> orders,
        IRepository<Product> products,
        IUnitOfWork unitOfWork)
    {
        _orders = orders;
        _products = products;
        _unitOfWork = unitOfWork;
    }

    public async Task PlaceOrderAsync(Guid productId, int quantity)
    {
        await using var transaction = await _unitOfWork.BeginTransactionAsync();

        try
        {
            var product = await _products.GetByIdAsync(productId)
                ?? throw new NotFoundException("Product not found");

            if (product.StockQuantity < quantity)
                throw new AppException("Insufficient stock", 409);

            product.StockQuantity -= quantity;
            await _products.UpdateAsync(product);

            var order = new Order
            {
                ProductId = productId,
                Quantity = quantity,
                TotalPrice = product.Price * quantity
            };
            await _orders.AddAsync(order);

            await _unitOfWork.SaveChangesAsync();
            await _unitOfWork.CommitAsync();
        }
        catch
        {
            await _unitOfWork.RollbackAsync();
            throw;
        }
    }
}

Bulk Operations

// Add multiple entities
var products = Enumerable.Range(1, 100).Select(i => new Product
{
    Name = $"Product {i}",
    Sku = $"SKU-{i:D5}",
    Price = i * 10.99m
});

await _products.AddRangeAsync(products);
await _unitOfWork.SaveChangesAsync();

// Update multiple entities
var outdatedProducts = await _products.FindAsync(p => p.Price < 5);
foreach (var p in outdatedProducts) p.Price = 5;
await _products.UpdateRangeAsync(outdatedProducts);
await _unitOfWork.SaveChangesAsync();

// Soft-delete multiple entities by IDs
var expiredIds = new List<Guid> { id1, id2, id3 };
await _products.SoftDeleteRangeAsync(expiredIds);
await _unitOfWork.SaveChangesAsync();

// Hard-delete multiple entities by IDs (permanent)
await _products.HardDeleteRangeAsync(expiredIds);
await _unitOfWork.SaveChangesAsync();

Composite Key Entities

For entities with composite primary keys (e.g., join tables), use ICompositeKeyRepository<T>:

// 1. Define composite key entity (does NOT inherit from BaseEntity)
public class OrderProduct
{
    public Guid OrderId { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }

    public Order Order { get; set; } = null!;
    public Product Product { get; set; } = null!;
}

// 2. Configure composite key in DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderProduct>()
        .HasKey(op => new { op.OrderId, op.ProductId });
}

// 3. Register in DI
builder.Services.AddRepletoryCompositeKeyRepository<OrderProduct, AppDbContext>();

// 4. Use in your service
public class OrderProductService
{
    private readonly ICompositeKeyRepository<OrderProduct> _orderProducts;
    private readonly IUnitOfWork _unitOfWork;

    public OrderProductService(
        ICompositeKeyRepository<OrderProduct> orderProducts,
        IUnitOfWork unitOfWork)
    {
        _orderProducts = orderProducts;
        _unitOfWork = unitOfWork;
    }

    // Lookup by composite key (order matters -- must match HasKey configuration)
    public async Task<OrderProduct?> GetAsync(Guid orderId, Guid productId)
    {
        return await _orderProducts.GetByKeysAsync(new object[] { orderId, productId });
    }

    // Add line items
    public async Task AddLineItemsAsync(Guid orderId, IEnumerable<(Guid ProductId, int Qty, decimal Price)> items)
    {
        var entities = items.Select(i => new OrderProduct
        {
            OrderId = orderId,
            ProductId = i.ProductId,
            Quantity = i.Qty,
            UnitPrice = i.Price
        });

        await _orderProducts.AddRangeAsync(entities);
        await _unitOfWork.SaveChangesAsync();
    }

    // Query with LINQ joins
    public async Task<IReadOnlyList<OrderProduct>> GetByOrderAsync(Guid orderId)
    {
        return await _orderProducts.FindAsync(op => op.OrderId == orderId);
    }

    // Delete by composite key
    public async Task RemoveLineItemAsync(Guid orderId, Guid productId)
    {
        await _orderProducts.DeleteAsync(new object[] { orderId, productId });
        await _unitOfWork.SaveChangesAsync();
    }

    // Bulk delete
    public async Task ClearOrderAsync(Guid orderId)
    {
        var items = await _orderProducts.FindAsync(op => op.OrderId == orderId);
        await _orderProducts.DeleteRangeAsync(items);
        await _unitOfWork.SaveChangesAsync();
    }
}

API Reference

IRepository<T>

Method Returns Description
GetByIdAsync(id) T? Get by ID, excludes soft-deleted
GetAllAsync() IReadOnlyList<T> Get all non-deleted entities
FindAsync(predicate) IReadOnlyList<T> Find by predicate
AddAsync(entity) T Add entity, stamps CreatedAt
AddRangeAsync(entities) -- Add multiple entities
UpdateAsync(entity) -- Update entity, stamps UpdatedAt
UpdateRangeAsync(entities) -- Update multiple entities
SoftDeleteAsync(id) -- Set IsDeleted = true, stamp DeletedAt
HardDeleteAsync(id) -- Permanently remove from database
SoftDeleteRangeAsync(ids) -- Soft-delete multiple entities by IDs
HardDeleteRangeAsync(ids) -- Permanently remove multiple entities by IDs
ExistsAsync(predicate) bool Check if any entity matches
CountAsync(predicate?) int Count matching entities
Query() IQueryable<T> No-tracking queryable with soft-delete filter

ICompositeKeyRepository<T>

Method Returns Description
GetByKeysAsync(keyValues) T? Get by composite key values
GetAllAsync() IReadOnlyList<T> Get all entities
FindAsync(predicate) IReadOnlyList<T> Find by predicate
AddAsync(entity) T Add entity
AddRangeAsync(entities) -- Add multiple entities
UpdateAsync(entity) -- Update entity
UpdateRangeAsync(entities) -- Update multiple entities
DeleteAsync(keyValues) -- Delete by composite key values
DeleteRangeAsync(entities) -- Delete multiple entities
ExistsAsync(predicate) bool Check if any entity matches
CountAsync(predicate?) int Count matching entities
Query() IQueryable<T> No-tracking queryable

IUnitOfWork

Method Returns Description
SaveChangesAsync() int Persist all pending changes
BeginTransactionAsync() IDbContextTransaction Start a new transaction
CommitAsync() -- Commit the active transaction
RollbackAsync() -- Rollback the active transaction

Integration with Other RepletoryLib Packages

Package Relationship
RepletoryLib.Common BaseEntity, PagedResult<T>, PagedRequest
RepletoryLib.Data.Interceptors Attribute-driven encryption, validation, normalization on entities
RepletoryLib.Data.Migrations Migration runner and data seeding for EF Core
RepletoryLib.Utilities.Pagination Extended pagination utilities for IQueryable
RepletoryLib.Testing InMemoryDbContextFactory<T> for unit testing

Testing

Use InMemoryDbContextFactory<T> from RepletoryLib.Testing:

using RepletoryLib.Testing;

public class ProductRepositoryTests
{
    private readonly InMemoryDbContextFactory<AppDbContext> _factory = new();

    [Fact]
    public async Task AddAsync_persists_product()
    {
        var context = _factory.Create();
        var repository = new Repository<Product, AppDbContext>(context);

        var product = new Product { Name = "Widget", Price = 9.99m };
        await repository.AddAsync(product);
        await context.SaveChangesAsync();

        var found = await repository.GetByIdAsync(product.Id);
        found.Should().NotBeNull();
        found!.Name.Should().Be("Widget");
    }

    [Fact]
    public async Task SoftDeleteAsync_hides_entity()
    {
        var context = _factory.Create();
        var repository = new Repository<Product, AppDbContext>(context);

        var product = await repository.AddAsync(new Product { Name = "Temp" });
        await context.SaveChangesAsync();

        await repository.SoftDeleteAsync(product.Id);
        await context.SaveChangesAsync();

        var found = await repository.GetByIdAsync(product.Id);
        found.Should().BeNull(); // Filtered by soft-delete
    }
}

Troubleshooting

Issue Solution
Soft-deleted entities still appear in queries Ensure EnableSoftDeleteFilter is true in EFOptions and your DbContext applies the global query filter
SaveChangesAsync not persisting Remember to call _unitOfWork.SaveChangesAsync() after repository operations
Query() returns stale data Query() uses AsNoTracking() -- if you need to update results, use FindAsync() instead
Transaction not rolling back Ensure RollbackAsync() is called in the catch block

License

This project is licensed under the MIT License.

Copyright (c) 2024-2026 Repletory.


For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.

Product Compatible and additional computed target framework versions.
.NET 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 (1)

Showing the top 1 NuGet packages that depend on RepletoryLib.Data.EntityFramework:

Package Downloads
RepletoryLib.Caching.Repository

Cache-aside decorator for RepletoryLib repositories with cross-service entity caching, pagination, and streaming

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 81 3/2/2026