Skip to content

Commit 362d93d

Browse files
committed
docs: update contributing guide and documentation for clarity and accuracy
1 parent 4b56a72 commit 362d93d

6 files changed

Lines changed: 181 additions & 87 deletions

File tree

CONTRIBUTING.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,165 @@
1+
# Contributing to EfCoreKit
12

3+
Thank you for taking the time to contribute! This guide covers everything you need to go from idea to merged pull request.
4+
5+
---
6+
7+
## Table of Contents
8+
9+
- [Ways to Contribute](#ways-to-contribute)
10+
- [Before You Start](#before-you-start)
11+
- [Development Setup](#development-setup)
12+
- [Branch Strategy](#branch-strategy)
13+
- [Making Changes](#making-changes)
14+
- [Running the Tests](#running-the-tests)
15+
- [Code Style](#code-style)
16+
- [Commit Messages](#commit-messages)
17+
- [Pull Request Process](#pull-request-process)
18+
- [Versioning and Releases](#versioning-and-releases)
19+
20+
---
21+
22+
## Ways to Contribute
23+
24+
- **Report a bug**[Open an issue](https://github.com/Clifftech123/EfCoreKit/issues) with steps to reproduce, expected behaviour, and actual behaviour.
25+
- **Request a feature**[Open an issue](https://github.com/Clifftech123/EfCoreKit/issues) describing the use case and what you'd like the API to look like.
26+
- **Fix a bug or implement a feature** — Fork the repo, make changes on a branch, and submit a pull request.
27+
- **Improve documentation** — Typos, missing examples, unclear wording — all fixes are welcome.
28+
29+
---
30+
31+
## Before You Start
32+
33+
For anything beyond a small bug fix or documentation change, **open an issue first**. This lets us agree on the approach before you invest time writing code, and avoids situations where a well-written PR cannot be merged because the design doesn't fit the project's direction.
34+
35+
---
36+
37+
## Development Setup
38+
39+
### Prerequisites
40+
41+
| Tool | Version |
42+
|------|---------|
43+
| .NET SDK | 10.0 or later |
44+
| Git | Any recent version |
45+
46+
### Getting the code
47+
48+
```bash
49+
# Fork the repo on GitHub, then clone your fork
50+
git clone https://github.com/<your-username>/EfCoreKit.git
51+
cd EfCoreKit
52+
53+
# Add the upstream remote so you can pull future changes
54+
git remote add upstream https://github.com/Clifftech123/EfCoreKit.git
55+
```
56+
57+
### Build
58+
59+
```bash
60+
dotnet restore
61+
dotnet build
62+
```
63+
64+
---
65+
66+
## Branch Strategy
67+
68+
| Branch | Purpose |
69+
|--------|---------|
70+
| `master` | Latest stable release — never commit here directly |
71+
| `develop` | Integration branch — all PRs target this branch |
72+
| `feature/<name>` | New features |
73+
| `fix/<name>` | Bug fixes |
74+
| `docs/<name>` | Documentation-only changes |
75+
76+
**Always branch off `develop`, and open your PR against `develop`.**
77+
78+
```bash
79+
git fetch upstream
80+
git checkout -b fix/soft-delete-cascade upstream/develop
81+
```
82+
83+
---
84+
85+
## Making Changes
86+
87+
1. Create your branch off `develop` (see above).
88+
2. Make focused, minimal changes — one concern per PR.
89+
3. Keep the public API backwards-compatible unless you've discussed a breaking change in an issue first.
90+
4. Do not add features, refactor surrounding code, or clean up unrelated areas as part of a bug fix PR.
91+
5. Update the relevant `docs/` page if your change affects documented behaviour.
92+
93+
---
94+
95+
## Running the Tests
96+
97+
The project uses integration tests (no mocks — tests run against a real in-memory or SQLite database):
98+
99+
```bash
100+
dotnet test tests/EfCoreKit.Tests.Integration/EfCoreKit.Tests.Integration.csproj --configuration Release
101+
```
102+
103+
All tests must pass before a PR can be merged. If you're adding a feature or fixing a bug, add a test that covers the new behaviour.
104+
105+
---
106+
107+
## Code Style
108+
109+
- Follow the conventions already in the codebase — consistency matters more than any individual preference.
110+
- Use `var` where the type is obvious from the right-hand side.
111+
- Prefer expression-bodied members for single-line methods/properties.
112+
- Use `async`/`await` throughout — no `.Result` or `.Wait()`.
113+
- No unused `using` directives.
114+
- No commented-out code.
115+
- XML doc comments (`///`) are not required unless you are adding a new public API surface.
116+
117+
The project does not currently enforce a formatter tool, so use your judgement to match the surrounding code.
118+
119+
---
120+
121+
## Commit Messages
122+
123+
Use the conventional commits style:
124+
125+
```
126+
<type>: <short summary in present tense>
127+
128+
[Optional longer description explaining *why*, not what]
129+
```
130+
131+
| Type | Use when |
132+
|------|----------|
133+
| `feat` | Adding a new feature |
134+
| `fix` | Fixing a bug |
135+
| `docs` | Documentation changes only |
136+
| `test` | Adding or updating tests |
137+
| `refactor` | Code change that is neither a fix nor a feature |
138+
| `chore` | Build system, CI, or tooling changes |
139+
140+
Examples:
141+
142+
```
143+
feat: add WhereIfNotEmpty extension method
144+
fix: restore clears DeletedBy when soft-delete interceptor is enabled
145+
docs: add cascade soft delete example to soft-delete guide
146+
```
147+
148+
---
149+
150+
## Pull Request Process
151+
152+
1. Ensure your branch is up to date with `upstream/develop` before opening the PR.
153+
2. Fill in the PR description — what changed, why, and how to test it.
154+
3. All CI checks (build + tests) must pass.
155+
4. At least one maintainer review is required before merge.
156+
5. Squash commits if the history is noisy — a clean history per PR is preferred.
157+
6. Once approved, a maintainer will merge into `develop`.
158+
159+
160+
161+
## Code of Conduct
162+
163+
Be respectful. Constructive criticism of code and design is welcome; personal criticism is not. We want this to be a project where everyone feels comfortable contributing.
164+
165+
If you experience or witness unacceptable behaviour, please open a private issue or contact the maintainer directly.

README.md

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ Every .NET project with EF Core ends up writing the same plumbing: soft delete f
2121

2222
**Design goals:**
2323

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 EfCoreKit at any time without rewriting your data layer.
25-
- **Opt-in everything** Enable only the features you need. Nothing runs unless you turn it on.
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.
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 EfCoreKit at any time without rewriting your data layer.
25+
- **Opt-in everything** Enable only the features you need. Nothing runs unless you turn it on.
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

@@ -42,7 +42,7 @@ Every .NET project with EF Core ends up writing the same plumbing: soft delete f
4242
| **Query Helpers** | `ExistsAsync`, `GetByIdOrThrowAsync`, `WhereIf`, `OrderByDynamic`, and more |
4343
| **DbContext Utilities** | `ExecuteInTransactionAsync`, `DetachAll`, `TruncateAsync<T>` |
4444
| **Slow Query Logging** | Logs warnings for queries exceeding a configurable threshold |
45-
| **Structured Exceptions** | `ConcurrencyConflictException`, `DuplicateEntityException`, `TenantMismatchException` |
45+
| **Structured Exceptions** | `EntityNotFoundException`, `ConcurrencyConflictException`, `DuplicateEntityException`, `InvalidFilterException` |
4646

4747
---
4848

@@ -67,9 +67,7 @@ builder.Services.AddEfCoreExtensions<AppDbContext>(
6767
.EnableSoftDelete()
6868
.EnableAuditTrail() // basic: stamps CreatedAt/By, UpdatedAt/By
6969
// .EnableAuditTrail(fullLog: true) // alternative: also writes field-level AuditLog rows
70-
.EnableMultiTenancy()
7170
.UseUserProvider<HttpContextUserProvider>()
72-
.UseTenantProvider<HttpContextTenantProvider>()
7371
.LogSlowQueries(TimeSpan.FromSeconds(1)));
7472
```
7573

@@ -85,7 +83,7 @@ public class Order : AuditableEntity<Guid> { }
8583
// Soft-deletable + audited
8684
public class Customer : SoftDeletableEntity { }
8785

88-
// Full — soft-delete + audit + tenant + row version
86+
// Full — soft-delete + audit + row version
8987
public class Invoice : FullEntity { }
9088
```
9189

@@ -140,21 +138,6 @@ var orders = await dbSet.FindAsync(spec);
140138

141139
---
142140

143-
## What Happens Behind the Scenes
144-
145-
| You do this | EfCoreKit does this |
146-
|-------------|------------------------------|
147-
| Call `SaveChangesAsync()` | Stamps `CreatedAt`/`UpdatedAt`, sets `CreatedBy`/`UpdatedBy` from your user provider |
148-
| Delete an entity implementing `ISoftDeletable` | Converts to a soft delete — sets `IsDeleted`, `DeletedAt`, `DeletedBy` instead of removing the row |
149-
| Query any `DbSet` | Automatically filters out soft-deleted rows and scopes to the current tenant |
150-
| Add a new tenant entity | Auto-assigns `TenantId` from your tenant provider |
151-
| Modify a tenant entity you don't own | Throws `TenantMismatchException` before hitting the database |
152-
| Save with a stale row version | Throws `ConcurrencyConflictException` wrapping `DbUpdateConcurrencyException` |
153-
| Run a slow query | Logs a warning with the SQL and duration |
154-
| Save `IFullAuditable` entities with `fullLog: true` | Writes an `AuditLog` row for every changed property |
155-
156-
---
157-
158141
## Soft Delete Lifecycle
159142

160143
```csharp
@@ -187,16 +170,16 @@ var page = await context.Orders
187170
.OrderBy(o => o.CreatedAt)
188171
.ToPagedAsync(page: 2, pageSize: 25);
189172

190-
Console.WriteLine($"Page {page.CurrentPage} of {page.TotalPages} ({page.TotalCount} total)");
173+
Console.WriteLine($"Page {page.Page} of {page.TotalPages} ({page.TotalCount} total)");
191174

192175
// Keyset / cursor pagination (no OFFSET — scales to millions of rows)
193176
var first = await context.Orders
194-
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id)
195-
.ToKeysetPagedAsync(pageSize: 25, afterId: null);
177+
.OrderBy(o => o.Id)
178+
.ToKeysetPagedAsync(o => o.Id, cursor: null, pageSize: 25);
196179

197180
var next = await context.Orders
198-
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id)
199-
.ToKeysetPagedAsync(pageSize: 25, afterId: first.NextCursor);
181+
.OrderBy(o => o.Id)
182+
.ToKeysetPagedAsync(o => o.Id, cursor: int.Parse(first.NextCursor!), pageSize: 25);
200183
```
201184

202185
---

docs/base-entities.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ EfCoreKit provides a hierarchy of ready-made base classes so you don't have to r
88
BaseEntity<TKey>
99
└── AuditableEntity<TKey> (+ CreatedAt/By, UpdatedAt/By)
1010
└── SoftDeletableEntity<TKey> (+ IsDeleted, DeletedAt/By)
11-
└── FullEntity<TKey> (+ TenantId, RowVersion)
11+
└── FullEntity<TKey> (+ RowVersion)
1212
```
1313

1414
Each level adds the interface properties for the corresponding feature. All levels have an `int`-key convenience alias (e.g. `BaseEntity` = `BaseEntity<int>`).
@@ -71,13 +71,12 @@ public class Customer : SoftDeletableEntity { }
7171

7272
### FullEntity&lt;TKey&gt; / FullEntity
7373

74-
Implements everything: `IAuditable`, `ISoftDeletable`, `ITenantEntity`, and `IConcurrencyAware`.
74+
Implements `IAuditable`, `ISoftDeletable`, and `IConcurrencyAware`.
7575

7676
```csharp
77-
public abstract class FullEntity<TKey> : SoftDeletableEntity<TKey>, ITenantEntity, IConcurrencyAware
77+
public abstract class FullEntity<TKey> : SoftDeletableEntity<TKey>, IConcurrencyAware
7878
{
79-
public string? TenantId { get; set; }
80-
public byte[] RowVersion { get; set; } = [];
79+
public byte[] RowVersion { get; set; } = [];
8180
}
8281
```
8382

docs/exceptions.md

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Exception
1010
├── EntityNotFoundException
1111
├── ConcurrencyConflictException
1212
├── DuplicateEntityException
13-
├── TenantMismatchException
1413
└── InvalidFilterException
1514
```
1615

@@ -135,33 +134,6 @@ Messages produced:
135134

136135
---
137136

138-
## TenantMismatchException
139-
140-
Thrown by `TenantInterceptor` when a save is attempted on an entity that belongs to a different tenant than the current request.
141-
142-
```csharp
143-
public sealed class TenantMismatchException : EfCoreException
144-
{
145-
public string? ExpectedTenant { get; } // current tenant from ITenantProvider
146-
public string? ActualTenant { get; } // tenant on the entity
147-
}
148-
```
149-
150-
```csharp
151-
try
152-
{
153-
await context.SaveChangesAsync();
154-
}
155-
catch (TenantMismatchException ex)
156-
{
157-
// ex.ExpectedTenant == "tenant-abc"
158-
// ex.ActualTenant == "tenant-xyz"
159-
return Forbid();
160-
}
161-
```
162-
163-
---
164-
165137
## InvalidFilterException
166138

167139
Thrown by `ApplyFilters` when a `FilterDescriptor` is invalid.

docs/getting-started.md

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ builder.Services.AddEfCoreExtensions<AppDbContext>(
3838
.EnableSoftDelete()
3939
.EnableAuditTrail() // basic: stamps CreatedAt/By, UpdatedAt/By
4040
// .EnableAuditTrail(fullLog: true) // alternative: also writes field-level AuditLog rows
41-
.EnableMultiTenancy()
4241
.UseUserProvider<HttpContextUserProvider>()
43-
.UseTenantProvider<HttpContextTenantProvider>()
4442
.LogSlowQueries(TimeSpan.FromSeconds(1)));
4543
```
4644

@@ -64,14 +62,14 @@ public class Order : AuditableEntity<Guid> { }
6462
// Soft-deletable + audited, int PK
6563
public class Customer : SoftDeletableEntity { }
6664

67-
// Full — soft-delete + audit + tenant + row version
65+
// Full — soft-delete + audit + row version
6866
public class Invoice : FullEntity { }
6967
```
7068

7169
You can also implement interfaces directly if you prefer to control your own hierarchy:
7270

7371
```csharp
74-
public class Customer : IAuditable, ISoftDeletable, ITenantEntity
72+
public class Customer : IAuditable, ISoftDeletable
7573
{
7674
public int Id { get; set; }
7775
public string Name { get; set; } = string.Empty;
@@ -84,8 +82,6 @@ public class Customer : IAuditable, ISoftDeletable, ITenantEntity
8482
public bool IsDeleted { get; set; }
8583
public DateTime? DeletedAt { get; set; }
8684
public string? DeletedBy { get; set; }
87-
88-
public string? TenantId { get; set; }
8985
}
9086
```
9187

@@ -129,20 +125,6 @@ public class HttpContextUserProvider : IUserProvider
129125
}
130126
```
131127

132-
## 6. Implement ITenantProvider (if using multi-tenancy)
133-
134-
```csharp
135-
public class HttpContextTenantProvider : ITenantProvider
136-
{
137-
private readonly IHttpContextAccessor _accessor;
138-
139-
public HttpContextTenantProvider(IHttpContextAccessor accessor) => _accessor = accessor;
140-
141-
public string? GetCurrentTenantId()
142-
=> _accessor.HttpContext?.User?.FindFirst("tenant_id")?.Value;
143-
}
144-
```
145-
146128
## What Happens Automatically
147129

148130
Once configured, EfCoreKit handles the following via EF Core interceptors:
@@ -151,8 +133,7 @@ Once configured, EfCoreKit handles the following via EF Core interceptors:
151133
|---------|-------------|------|
152134
| **Audit Trail** | Sets `CreatedAt`/`CreatedBy` on insert, `UpdatedAt`/`UpdatedBy` on update | Every `SaveChanges` / `SaveChangesAsync` |
153135
| **Soft Delete** | Converts `DELETE` to `UPDATE SET IsDeleted = true` | When deleting an `ISoftDeletable` entity |
154-
| **Multi-Tenancy** | Auto-assigns `TenantId` on insert, validates ownership on update | Every `SaveChanges` / `SaveChangesAsync` |
155-
| **Query Filters** | Hides soft-deleted rows and scopes queries to the current tenant | Every LINQ query |
136+
| **Query Filters** | Hides soft-deleted rows | Every LINQ query |
156137
| **Slow Query Logging** | Logs a warning for queries exceeding the threshold | After each database command |
157138
| **Concurrency** | Throws `ConcurrencyConflictException` on stale row version conflicts | Every `SaveChanges` / `SaveChangesAsync` |
158139

@@ -161,7 +142,6 @@ Once configured, EfCoreKit handles the following via EF Core interceptors:
161142
- [Base Entities](base-entities.md) — Entity class hierarchy and configuration bases
162143
- [Soft Delete](soft-delete.md) — Lifecycle methods, restoring records, cascade delete
163144
- [Audit Trail](audit-trail.md) — Timestamps, user tracking, field-level AuditLog
164-
- [Multi-Tenancy](multi-tenancy.md) — Tenant isolation and filtering
165145
- [Repository & Unit of Work](repository-uow.md) — Generic repository and transaction management
166146
- [Specification Pattern](specifications.md) — Composable, reusable query logic
167147
- [Pagination](pagination.md) — Offset and keyset/cursor pagination

0 commit comments

Comments
 (0)