Skip to content

Commit 9b8c0be

Browse files
committed
Enhance documentation across multiple files with detailed guides on features, installation, pagination, multi-tenancy, soft delete, and query helpers; improve clarity and usability for users.
1 parent 0cce302 commit 9b8c0be

9 files changed

Lines changed: 1033 additions & 2 deletions

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,5 +362,3 @@ MigrationBackup/
362362
# Fody - auto-generated XML schema
363363
FodyWeavers.xsd
364364

365-
# Personal implementation notes
366-
IMPLEMENTATION_GUIDE.md

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ Install just what you need, or grab the umbrella package:
8080
dotnet add package EfCoreKit
8181
```
8282

83+
## Documentation
84+
85+
Each feature has a dedicated guide with full examples, configuration options, and best practices:
86+
87+
| Guide | What You'll Learn |
88+
|-------|-------------------|
89+
| [Getting Started](docs/getting-started.md) | Installation, DbContext setup, DI registration |
90+
| [Soft Delete](docs/soft-delete.md) | ISoftDeletable, cascade delete, restoring records |
91+
| [Audit Trail](docs/audit-trail.md) | IAuditable, auto-stamping, CreatedAt/CreatedBy protection |
92+
| [Multi-Tenancy](docs/multi-tenancy.md) | ITenantEntity, automatic filtering, tenant validation |
93+
| [Pagination](docs/pagination.md) | Offset and keyset pagination, PagedResult, KeysetPagedResult |
94+
| [Query Helpers](docs/query-helpers.md) | WhereIf, OrderByDynamic, specifications, DbSet extensions |
95+
| [Bulk Operations](docs/bulk-operations.md) | BulkInsert/Update/Delete/Upsert, BulkConfig tuning |
96+
8397
## License
8498

8599
MIT — free for personal and commercial use, forever.

docs/audit-trail.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,110 @@
1+
# Audit Trail
2+
3+
EfCoreKit automatically timestamps entity creation and modification, and records which user made the change.
4+
5+
## How It Works
6+
7+
The `AuditInterceptor` hooks into every `SaveChanges` / `SaveChangesAsync` call:
8+
9+
| State | What happens |
10+
|-------|-------------|
11+
| `Added` | Sets `CreatedAt` to `DateTime.UtcNow` and `CreatedBy` to the current user ID |
12+
| `Modified` | Sets `UpdatedAt` to `DateTime.UtcNow` and `UpdatedBy` to the current user ID. Protects `CreatedAt` and `CreatedBy` from being overwritten |
13+
14+
## Setup
15+
16+
```csharp
17+
builder.Services.AddEfCoreKit<AppDbContext>(
18+
options => options.UseSqlServer(connectionString),
19+
kit => kit
20+
.EnableAuditTrail()
21+
.UseUserProvider<HttpContextUserProvider>()
22+
);
23+
```
24+
25+
## Implement the Interface
26+
27+
Add `IAuditable` to any entity you want to track:
28+
29+
```csharp
30+
public class Product : IAuditable
31+
{
32+
public int Id { get; set; }
33+
public string Name { get; set; } = string.Empty;
34+
public decimal Price { get; set; }
35+
36+
// IAuditable
37+
public DateTime CreatedAt { get; set; }
38+
public string? CreatedBy { get; set; }
39+
public DateTime? UpdatedAt { get; set; }
40+
public string? UpdatedBy { get; set; }
41+
}
42+
```
43+
44+
## Usage
45+
46+
No extra code needed — just save normally:
47+
48+
```csharp
49+
// Insert — CreatedAt and CreatedBy are set automatically
50+
var product = new Product { Name = "Widget", Price = 9.99m };
51+
context.Products.Add(product);
52+
await context.SaveChangesAsync();
53+
// product.CreatedAt == DateTime.UtcNow
54+
// product.CreatedBy == "user-123" (from IUserProvider)
55+
56+
// Update — UpdatedAt and UpdatedBy are set automatically
57+
product.Price = 12.99m;
58+
await context.SaveChangesAsync();
59+
// product.UpdatedAt == DateTime.UtcNow
60+
// product.UpdatedBy == "user-123"
61+
// product.CreatedAt remains unchanged (protected)
62+
```
63+
64+
## IUserProvider
65+
66+
EfCoreKit resolves the current user through `IUserProvider`. You provide the implementation:
67+
68+
```csharp
69+
public interface IUserProvider
70+
{
71+
string? GetCurrentUserId();
72+
string? GetCurrentUserName();
73+
}
74+
```
75+
76+
### ASP.NET Core Example
77+
78+
```csharp
79+
public class HttpContextUserProvider : IUserProvider
80+
{
81+
private readonly IHttpContextAccessor _accessor;
82+
83+
public HttpContextUserProvider(IHttpContextAccessor accessor) => _accessor = accessor;
84+
85+
public string? GetCurrentUserId()
86+
=> _accessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
87+
88+
public string? GetCurrentUserName()
89+
=> _accessor.HttpContext?.User?.Identity?.Name;
90+
}
91+
```
92+
93+
### Console App / Background Service Example
94+
95+
```csharp
96+
public class SystemUserProvider : IUserProvider
97+
{
98+
public string? GetCurrentUserId() => "system";
99+
public string? GetCurrentUserName() => "Background Service";
100+
}
101+
```
102+
103+
## CreatedAt / CreatedBy Protection
104+
105+
The audit interceptor marks `CreatedAt` and `CreatedBy` as `IsModified = false` on updates. This means even if your code accidentally sets these properties during an update, the original values are preserved in the database.
106+
107+
## Combining with Soft Delete
108+
109+
When an entity implements both `IAuditable` and `ISoftDeletable`, soft delete triggers a `Modified` state change, which means `UpdatedAt` and `UpdatedBy` are also set when an entity is soft-deleted.
1110

docs/bulk-operations.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,143 @@
1+
# Bulk Operations
2+
3+
EfCoreKit provides high-performance batch operations that execute in a single database round trip instead of one command per row.
4+
5+
## Supported Operations
6+
7+
| Method | Description |
8+
|--------|-------------|
9+
| `BulkInsertAsync<T>` | Insert thousands of rows in one call |
10+
| `BulkUpdateAsync<T>` | Update many rows by primary key |
11+
| `BulkDeleteAsync<T>` | Delete many rows by primary key |
12+
| `BulkUpsertAsync<T>` | Insert or update (merge) based on key match |
13+
14+
## Supported Databases
15+
16+
Each database has its own provider package with an optimized implementation:
17+
18+
| Package | Database | Registration |
19+
|---------|----------|-------------|
20+
| `EfCoreKit.SqlServer` | SQL Server 2016+ | `services.AddEfCoreKitSqlServer()` |
21+
| `EfCoreKit.PostgreSql` | PostgreSQL 12+ | `services.AddEfCoreKitPostgreSql()` |
22+
| `EfCoreKit.MySql` | MySQL 8.0+ | `services.AddEfCoreKitMySql()` |
23+
| `EfCoreKit.Sqlite` | SQLite 3.x | `services.AddEfCoreKitSqlite()` |
24+
25+
Or install `EfCoreKit` (umbrella package) to get all providers at once.
26+
27+
## Setup
28+
29+
```csharp
30+
builder.Services.AddEfCoreKit<AppDbContext>(
31+
options => options.UseSqlServer(connectionString));
32+
33+
// Register the bulk operations provider
34+
builder.Services.AddEfCoreKitSqlServer();
35+
```
36+
37+
## Usage
38+
39+
Inject `IBulkExecutor` and use it with your `DbContext`:
40+
41+
```csharp
42+
public class OrderService
43+
{
44+
private readonly AppDbContext _context;
45+
private readonly IBulkExecutor _bulk;
46+
47+
public OrderService(AppDbContext context, IBulkExecutor bulk)
48+
{
49+
_context = context;
50+
_bulk = bulk;
51+
}
52+
53+
public async Task ImportOrders(List<Order> orders)
54+
{
55+
await _bulk.BulkInsertAsync(_context, orders);
56+
}
57+
}
58+
```
59+
60+
### Insert
61+
62+
```csharp
63+
var customers = Enumerable.Range(1, 10_000)
64+
.Select(i => new Customer { Name = $"Customer {i}" })
65+
.ToList();
66+
67+
await bulk.BulkInsertAsync(context, customers);
68+
```
69+
70+
### Update
71+
72+
```csharp
73+
// Load entities, modify them, then bulk update
74+
var products = await context.Products.Where(p => p.Category == "Sale").ToListAsync();
75+
foreach (var p in products) p.Price *= 0.9m; // 10% off
76+
77+
await bulk.BulkUpdateAsync(context, products);
78+
```
79+
80+
### Delete
81+
82+
```csharp
83+
var expired = await context.Orders.Where(o => o.ExpiresAt < DateTime.UtcNow).ToListAsync();
84+
await bulk.BulkDeleteAsync(context, expired);
85+
```
86+
87+
### Upsert (Insert or Update)
88+
89+
```csharp
90+
// Inserts new rows, updates existing ones (matched by primary key)
91+
await bulk.BulkUpsertAsync(context, incomingProducts);
92+
```
93+
94+
## BulkConfig Options
95+
96+
All operations accept an optional `BulkConfig` for fine-tuning:
97+
98+
```csharp
99+
await bulk.BulkInsertAsync(context, customers, new BulkConfig
100+
{
101+
BatchSize = 5000, // Rows per batch (default: 1000)
102+
Timeout = 60, // Seconds (default: 30)
103+
PreserveInsertOrder = true, // Maintain list order (default: true)
104+
SetOutputIdentity = true, // Populate generated IDs after insert
105+
UseTransaction = true, // Wrap in transaction (default: true)
106+
TrackEntities = false, // Add to EF change tracker after operation
107+
108+
// Column control
109+
PropertiesToInclude = ["Name", "Email"], // Only update these columns
110+
PropertiesToExclude = ["CreatedAt"], // Skip these columns
111+
112+
// Upsert key
113+
UpdateByProperties = ["ExternalId"], // Match on this instead of PK
114+
115+
// Progress reporting
116+
OnProgress = (processed, total) =>
117+
Console.WriteLine($"{processed}/{total}")
118+
});
119+
```
120+
121+
### BulkConfig Properties
122+
123+
| Property | Default | Description |
124+
|----------|---------|-------------|
125+
| `BatchSize` | 1000 | Number of rows per batch |
126+
| `Timeout` | 30 | Command timeout in seconds |
127+
| `PreserveInsertOrder` | `true` | Maintain the order of the input list |
128+
| `SetOutputIdentity` | `false` | Populate auto-generated keys after insert |
129+
| `UpdateByProperties` | `null` | Columns to match on for upsert (defaults to PK) |
130+
| `PropertiesToInclude` | `null` | Only include these columns |
131+
| `PropertiesToExclude` | `null` | Exclude these columns |
132+
| `UseTransaction` | `true` | Wrap operation in a transaction |
133+
| `TrackEntities` | `false` | Add entities to the change tracker after the operation |
134+
| `OnProgress` | `null` | Callback for progress reporting `(processed, total)` |
135+
136+
## Performance Notes
137+
138+
- Bulk operations bypass the EF Core change tracker — they go directly to the database
139+
- `BulkInsertAsync` with 10,000 rows is typically **10-50x faster** than `AddRange` + `SaveChanges`
140+
- Set `BatchSize` based on your row size — larger rows benefit from smaller batches
141+
- `SetOutputIdentity = true` adds overhead (an extra round trip) but populates generated keys
142+
- Interceptors (audit, soft delete) do **not** apply to bulk operations since they bypass `SaveChanges`
1143

0 commit comments

Comments
 (0)