Skip to content

Commit 4f5b99c

Browse files
committed
Refactor documentation and code for EfCore.Extensions
- Updated installation instructions to reflect new package names and structure. - Changed references from EfCoreKit to EfCore.Extensions throughout the documentation. - Revised DbContext inheritance and registration in dependency injection. - Enhanced entity configuration examples and clarified optional features. - Improved multi-tenancy setup and usage instructions. - Added repository and unit of work patterns with examples. - Updated soft delete implementation details and added restore functionality. - Introduced specification pattern for reusable query logic. - Enhanced pagination strategies and examples. - Added query helper methods for streamlined data access.
1 parent b136faa commit 4f5b99c

13 files changed

Lines changed: 1207 additions & 289 deletions

README.md

Lines changed: 203 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div align="center">
22

3-
# EfCoreKit
3+
# EfCore.Extensions
44

55
**EF Core extensions that eliminate boilerplate — so you can focus on building features.**
66

7-
[![NuGet](https://img.shields.io/nuget/v/EfCoreKit?logo=nuget&label=NuGet)](https://www.nuget.org/packages/EfCoreKit)
8-
[![NuGet Downloads](https://img.shields.io/nuget/dt/EfCoreKit?logo=nuget&label=Downloads)](https://www.nuget.org/packages/EfCoreKit)
7+
[![NuGet](https://img.shields.io/nuget/v/EfCore.Extensions?logo=nuget&label=NuGet)](https://www.nuget.org/packages/EfCore.Extensions)
8+
[![NuGet Downloads](https://img.shields.io/nuget/dt/EfCore.Extensions?logo=nuget&label=Downloads)](https://www.nuget.org/packages/EfCore.Extensions)
99
[![Build](https://img.shields.io/github/actions/workflow/status/Clifftech123/EfCoreKit/ci.yml?branch=develop&logo=github&label=Build)](https://github.com/Clifftech123/EfCoreKit/actions)
1010
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
1111

@@ -15,109 +15,254 @@
1515

1616
---
1717

18-
## Why EfCoreKit?
18+
## Why EfCore.Extensions?
1919

20-
Every .NET project with EF Core ends up writing the same plumbing: soft delete filters, audit timestamps, tenant isolation, pagination helpers, bulk imports. EfCoreKit packages all of that into a single `AddEfCoreKit()` call.
20+
Every .NET project with EF Core ends up writing the same plumbing: soft delete filters, audit timestamps, tenant isolation, pagination helpers, generic repositories, transaction wrappers. EfCore.Extensions packages all of that into a single `AddEfCoreExtensions()` call.
2121

2222
**Design goals:**
2323

24-
- **Zero lock-in**EfCoreKit uses standard EF Core interceptors and global query filters. Your entities stay plain C# classes, your `DbContext` stays a normal `DbContext`, and you can remove EfCoreKit at any time without rewriting your data layer.
24+
- **Zero lock-in**Uses standard EF Core interceptors and global query filters. Your entities stay plain C# classes, your `DbContext` stays a normal `DbContext`, and you can remove EfCore.Extensions at any time without rewriting your data layer.
2525
- **Opt-in everything** — Enable only the features you need. Nothing runs unless you turn it on.
26-
- **No custom ORM** — This is not a repository layer or a replacement for EF Core. It's a set of extensions that plug into the pipeline you already use.
26+
- **No custom ORM** — This is not a replacement for EF Core. It's a set of extensions that plug into the pipeline you already use.
2727

2828
---
2929

3030
## Features
3131

32-
All core features use EF Core interceptors and global query filters — they work with **any database** EF Core supports.
32+
| Feature | Description |
33+
|---------|-------------|
34+
| **Base Entity Hierarchy** | Ready-made base classes: `BaseEntity`, `AuditableEntity`, `SoftDeletableEntity`, `FullEntity` |
35+
| **Entity Configuration Bases** | Fluent config base classes that auto-wire keys, indexes, and soft-delete defaults |
36+
| **Soft Delete** | Mark records as deleted with automatic global query filters; restore or hard-delete on demand |
37+
| **Audit Trail** | Auto-stamp `CreatedAt/By`, `UpdatedAt/By`; optional field-level `AuditLog` history |
38+
| **Multi-Tenancy** | Automatic tenant filtering so each tenant only sees their own data |
39+
| **Repository + Unit of Work** | Generic `IRepository<T>` / `IReadRepository<T>` backed by `IUnitOfWork` |
40+
| **Specification Pattern** | Composable query specs with `And()` / `Or()` combinators, projection, and multi-column ordering |
41+
| **Pagination** | Offset (`ToPagedAsync`) and keyset/cursor (`ToKeysetPagedAsync`) pagination with `PagedResult<T>` |
42+
| **Dynamic Filters** | Apply runtime filter arrays (eq, ne, gt, lt, contains, in, between, isnull…) via `ApplyFilters` |
43+
| **Query Helpers** | `ExistsAsync`, `GetByIdOrThrowAsync`, `WhereIf`, `OrderByDynamic`, and more |
44+
| **DbContext Utilities** | `ExecuteInTransactionAsync`, `DetachAll`, `TruncateAsync<T>` |
45+
| **Slow Query Logging** | Logs warnings for queries exceeding a configurable threshold |
46+
| **Structured Exceptions** | `ConcurrencyConflictException`, `DuplicateEntityException`, `TenantMismatchException` |
47+
48+
---
49+
50+
## Installation
3351

34-
| Feature | Status | Description |
35-
|---------|--------|-------------|
36-
| **Soft Delete** || Mark records as deleted with automatic global query filters |
37-
| **Audit Trail** || Auto-stamp `CreatedAt`, `CreatedBy`, `UpdatedAt`, `UpdatedBy` on every save |
38-
| **Multi-Tenancy** || Automatic tenant filtering so each tenant only sees their own data |
39-
| **Pagination** || Offset-based and keyset/cursor-based pagination with `PagedResult<T>` |
40-
| **Query Helpers** || `ExistsAsync`, `GetByIdOrThrowAsync`, `WhereIf`, `OrderByDynamic`, and more |
41-
| **Specification Pattern** || Reusable, composable query logic in strongly-typed classes |
42-
| **Slow Query Logging** || Logs warnings for queries exceeding a configurable threshold |
43-
| **Bulk Operations** | 🚧 | Insert, update, delete, or upsert thousands of rows in one call *(coming soon)* |
52+
```bash
53+
dotnet add package EfCore.Extensions
54+
```
55+
56+
Or install only what you need:
57+
58+
| Package | Description |
59+
|---------|-------------|
60+
| `EfCore.Extensions` | Meta-package — installs everything |
61+
| `EfCore.Extensions.Core` | Core implementation (interceptors, repositories, extensions) |
62+
| `EfCore.Extensions.Abstractions` | Interfaces, base entities, and models only |
4463

4564
---
4665

47-
## Quick Look
66+
## Quick Start
67+
68+
### 1. Register services
4869

4970
```csharp
50-
// One-line setup — pick only what you need
51-
builder.Services.AddEfCoreKit<AppDbContext>(
71+
builder.Services.AddEfCoreExtensions<AppDbContext>(
5272
options => options.UseSqlServer(connectionString),
5373
kit => kit
5474
.EnableSoftDelete()
55-
.EnableAuditTrail()
75+
.EnableAuditTrail() // basic timestamps
76+
.EnableAuditTrail(fullLog: true) // + field-level AuditLog records
5677
.EnableMultiTenancy()
5778
.UseUserProvider<HttpContextUserProvider>()
58-
);
79+
.UseTenantProvider<HttpContextTenantProvider>()
80+
.LogSlowQueries(TimeSpan.FromSeconds(1)));
5981
```
6082

83+
### 2. Inherit a base entity
84+
6185
```csharp
62-
// Query helpers
63-
var exists = await context.Customers.ExistsAsync(x => x.Email == email);
64-
var customer = await context.Customers.GetByIdOrThrowAsync(id);
65-
66-
// Pagination
67-
var page = await context.Customers
68-
.Where(c => c.IsActive)
69-
.ToPagedAsync(page: 1, pageSize: 20);
70-
71-
// Conditional filtering + dynamic sort
72-
var results = await context.Orders
73-
.WhereIf(hasStatus, x => x.Status == status)
74-
.OrderByDynamic("CreatedAt", ascending: false)
75-
.ToListAsync();
86+
// Plain entity with int PK
87+
public class Product : BaseEntity { }
88+
89+
// Audited entity with Guid PK
90+
public class Order : AuditableEntity<Guid> { }
91+
92+
// Soft-deletable + audited
93+
public class Customer : SoftDeletableEntity { }
94+
95+
// Full — soft-delete + audit + tenant + row version
96+
public class Invoice : FullEntity { }
97+
```
98+
99+
### 3. Configure with base classes
100+
101+
```csharp
102+
public class CustomerConfiguration : SoftDeletableEntityConfiguration<Customer>
103+
{
104+
protected override void ConfigureEntity(EntityTypeBuilder<Customer> builder)
105+
{
106+
builder.Property(c => c.Name).HasMaxLength(200).IsRequired();
107+
}
108+
}
109+
```
110+
111+
### 4. Use the repository
112+
113+
```csharp
114+
public class OrderService(IRepository<Order> repo, IUnitOfWork uow)
115+
{
116+
public async Task<Order> CreateAsync(Order order)
117+
{
118+
await repo.AddAsync(order);
119+
await uow.CommitAsync();
120+
return order;
121+
}
122+
123+
public async Task<IReadOnlyList<Order>> GetRecentAsync()
124+
=> await repo.FindAsync(o => o.CreatedAt > DateTime.UtcNow.AddDays(-7));
125+
}
126+
```
127+
128+
### 5. Use specifications
129+
130+
```csharp
131+
public class ActiveOrdersSpec : Specification<Order>
132+
{
133+
public ActiveOrdersSpec(int customerId)
134+
{
135+
AddCriteria(o => o.CustomerId == customerId && !o.IsDeleted);
136+
AddInclude(o => o.Items);
137+
ApplyOrderByDescending(o => o.CreatedAt);
138+
ApplyPaging(skip: 0, take: 20);
139+
ApplyAsNoTracking();
140+
}
141+
}
142+
143+
// Compose specs with And/Or
144+
var spec = new ActiveOrdersSpec(customerId).And(new HighValueOrdersSpec(500));
145+
var orders = await dbSet.FindAsync(spec);
76146
```
77147

78-
### What happens behind the scenes
148+
---
79149

80-
Once configured, EfCoreKit hooks into EF Core's pipeline automatically:
150+
## What Happens Behind the Scenes
81151

82-
| You do this | EfCoreKit does this |
83-
|-------------|---------------------|
152+
| You do this | EfCore.Extensions does this |
153+
|-------------|------------------------------|
84154
| Call `SaveChangesAsync()` | Stamps `CreatedAt`/`UpdatedAt`, sets `CreatedBy`/`UpdatedBy` from your user provider |
85-
| Delete an entity | Converts to a soft delete — sets `IsDeleted`, `DeletedAt`, `DeletedBy` instead of removing the row |
155+
| Delete an entity implementing `ISoftDeletable` | Converts to a soft delete — sets `IsDeleted`, `DeletedAt`, `DeletedBy` instead of removing the row |
86156
| Query any `DbSet` | Automatically filters out soft-deleted rows and scopes to the current tenant |
87157
| Add a new tenant entity | Auto-assigns `TenantId` from your tenant provider |
88158
| Modify a tenant entity you don't own | Throws `TenantMismatchException` before hitting the database |
89-
| Run a slow query | Logs a warning with the SQL and duration so you can catch performance issues early |
159+
| Save with a stale row version | Throws `ConcurrencyConflictException` wrapping `DbUpdateConcurrencyException` |
160+
| Run a slow query | Logs a warning with the SQL and duration |
161+
| Save `IFullAuditable` entities with `fullLog: true` | Writes an `AuditLog` row for every changed property |
90162

91163
---
92164

93-
## Installation
165+
## Soft Delete Lifecycle
94166

95-
```bash
96-
dotnet add package EfCoreKit
167+
```csharp
168+
// Default queries automatically exclude soft-deleted rows
169+
var customers = await context.Customers.ToListAsync();
170+
171+
// Include soft-deleted rows alongside active ones
172+
var all = await context.Customers.IncludeDeleted().ToListAsync();
173+
174+
// Only soft-deleted rows
175+
var trash = await context.Customers.OnlyDeleted().ToListAsync();
176+
177+
// Restore a soft-deleted record
178+
context.Customers.Restore(customer);
179+
await context.SaveChangesAsync();
180+
181+
// Permanently remove a record (regardless of soft-delete settings)
182+
context.Customers.HardDelete(customer);
183+
await context.SaveChangesAsync();
97184
```
98185

99-
Or install only what you need:
186+
---
100187

101-
| Package | Description |
102-
|---------|-------------|
103-
| `EfCoreKit.Core` | Core implementation (interceptors, filters, extensions) |
104-
| `EfCoreKit.Abstractions` | Interfaces and models only |
188+
## Pagination
189+
190+
```csharp
191+
// Offset pagination
192+
var page = await context.Orders
193+
.Where(o => o.CustomerId == id)
194+
.OrderBy(o => o.CreatedAt)
195+
.ToPagedAsync(page: 2, pageSize: 25);
196+
197+
Console.WriteLine($"Page {page.CurrentPage} of {page.TotalPages} ({page.TotalCount} total)");
198+
199+
// Keyset / cursor pagination (no OFFSET — scales to millions of rows)
200+
var first = await context.Orders
201+
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id)
202+
.ToKeysetPagedAsync(pageSize: 25, afterId: null);
203+
204+
var next = await context.Orders
205+
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id)
206+
.ToKeysetPagedAsync(pageSize: 25, afterId: first.NextCursor);
207+
```
105208

106209
---
107210

108-
## Documentation
211+
## Dynamic Filters
212+
213+
```csharp
214+
var filters = new[]
215+
{
216+
new FilterDescriptor("Status", "eq", "Active"),
217+
new FilterDescriptor("CreatedAt", "gte", DateTime.UtcNow.AddDays(-30)),
218+
new FilterDescriptor("Tags", "in", new[] { "VIP", "Premium" }),
219+
new FilterDescriptor("Score", "between", new object[] { 10, 100 }),
220+
};
221+
222+
var results = await context.Customers
223+
.ApplyFilters(filters)
224+
.ToListAsync();
225+
```
226+
227+
Supported operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `startswith`, `endswith`, `isnull`, `isnotnull`, `in`, `between`.
228+
229+
---
109230

110-
Each feature has a dedicated guide with full examples and configuration options:
231+
## DbContext Utilities
232+
233+
```csharp
234+
// Run work inside a transaction (respects EF Core execution strategy / retry)
235+
var result = await context.ExecuteInTransactionAsync(async () =>
236+
{
237+
await DoWorkA(context);
238+
await DoWorkB(context);
239+
return await context.SaveChangesAsync();
240+
});
241+
242+
// Detach all tracked entities (useful after bulk imports)
243+
context.DetachAll();
244+
245+
// Truncate a table by entity type (uses EF Core metadata for table name)
246+
await context.TruncateAsync<AuditLog>();
247+
```
248+
249+
---
250+
251+
## Documentation
111252

112253
| Guide | What You'll Learn |
113254
|-------|-------------------|
114255
| [Getting Started](docs/getting-started.md) | Installation, DbContext setup, DI registration |
115-
| [Soft Delete](docs/soft-delete.md) | ISoftDeletable, cascade delete, restoring records |
116-
| [Audit Trail](docs/audit-trail.md) | IAuditable, auto-stamping, CreatedAt/CreatedBy protection |
256+
| [Base Entities](docs/base-entities.md) | Entity hierarchy, configuration base classes |
257+
| [Soft Delete](docs/soft-delete.md) | ISoftDeletable, lifecycle methods, restoring records |
258+
| [Audit Trail](docs/audit-trail.md) | IAuditable, IFullAuditable, field-level AuditLog |
117259
| [Multi-Tenancy](docs/multi-tenancy.md) | ITenantEntity, automatic filtering, tenant validation |
118-
| [Pagination](docs/pagination.md) | Offset and keyset pagination, PagedResult, KeysetPagedResult |
119-
| [Query Helpers](docs/query-helpers.md) | WhereIf, OrderByDynamic, specifications, DbSet extensions |
120-
| [Bulk Operations](docs/bulk-operations.md) | BulkInsert/Update/Delete/Upsert, BulkConfig tuning |
260+
| [Repository & Unit of Work](docs/repository-uow.md) | IRepository, IReadRepository, IUnitOfWork |
261+
| [Specification Pattern](docs/specifications.md) | Spec classes, And/Or combinators, projection specs |
262+
| [Pagination](docs/pagination.md) | Offset and keyset pagination, PagedResult |
263+
| [Dynamic Filters](docs/dynamic-filters.md) | FilterDescriptor, all supported operators |
264+
| [Query Helpers](docs/query-helpers.md) | WhereIf, OrderByDynamic, DbSet extensions |
265+
| [DbContext Utilities](docs/dbcontext-utilities.md) | Transactions, DetachAll, TruncateAsync |
121266

122267
---
123268

0 commit comments

Comments
 (0)