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
<PackageReference Include="RepletoryLib.Data.EntityFramework" Version="1.0.0" />
<PackageVersion Include="RepletoryLib.Data.EntityFramework" Version="1.0.0" />
<PackageReference Include="RepletoryLib.Data.EntityFramework" />
paket add RepletoryLib.Data.EntityFramework --version 1.0.0
#r "nuget: RepletoryLib.Data.EntityFramework, 1.0.0"
#:package RepletoryLib.Data.EntityFramework@1.0.0
#addin nuget:?package=RepletoryLib.Data.EntityFramework&version=1.0.0
#tool nuget:?package=RepletoryLib.Data.EntityFramework&version=1.0.0
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.
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 --
ToPagedResultAsyncextension forIQueryable<T> - Dynamic sorting -- Sort by property name at runtime
- No-tracking queries --
Query()returnsAsNoTracking()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 | Versions 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. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- RepletoryLib.Common (>= 1.0.0)
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 |