From 362d93d218f66c673ffe11af4e7fda4206534a2f Mon Sep 17 00:00:00 2001 From: Isaiah Clifford Opoku Date: Thu, 9 Apr 2026 14:20:06 +0000 Subject: [PATCH] docs: update contributing guide and documentation for clarity and accuracy --- CONTRIBUTING.md | 164 ++++++++++++++++++++++++++++++++++++++++ README.md | 37 +++------ docs/base-entities.md | 9 +-- docs/exceptions.md | 28 ------- docs/getting-started.md | 26 +------ docs/soft-delete.md | 4 - 6 files changed, 181 insertions(+), 87 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b13789..64b9d18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,165 @@ +# Contributing to EfCoreKit +Thank you for taking the time to contribute! This guide covers everything you need to go from idea to merged pull request. + +--- + +## Table of Contents + +- [Ways to Contribute](#ways-to-contribute) +- [Before You Start](#before-you-start) +- [Development Setup](#development-setup) +- [Branch Strategy](#branch-strategy) +- [Making Changes](#making-changes) +- [Running the Tests](#running-the-tests) +- [Code Style](#code-style) +- [Commit Messages](#commit-messages) +- [Pull Request Process](#pull-request-process) +- [Versioning and Releases](#versioning-and-releases) + +--- + +## Ways to Contribute + +- **Report a bug** — [Open an issue](https://github.com/Clifftech123/EfCoreKit/issues) with steps to reproduce, expected behaviour, and actual behaviour. +- **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. +- **Fix a bug or implement a feature** — Fork the repo, make changes on a branch, and submit a pull request. +- **Improve documentation** — Typos, missing examples, unclear wording — all fixes are welcome. + +--- + +## Before You Start + +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. + +--- + +## Development Setup + +### Prerequisites + +| Tool | Version | +|------|---------| +| .NET SDK | 10.0 or later | +| Git | Any recent version | + +### Getting the code + +```bash +# Fork the repo on GitHub, then clone your fork +git clone https://github.com//EfCoreKit.git +cd EfCoreKit + +# Add the upstream remote so you can pull future changes +git remote add upstream https://github.com/Clifftech123/EfCoreKit.git +``` + +### Build + +```bash +dotnet restore +dotnet build +``` + +--- + +## Branch Strategy + +| Branch | Purpose | +|--------|---------| +| `master` | Latest stable release — never commit here directly | +| `develop` | Integration branch — all PRs target this branch | +| `feature/` | New features | +| `fix/` | Bug fixes | +| `docs/` | Documentation-only changes | + +**Always branch off `develop`, and open your PR against `develop`.** + +```bash +git fetch upstream +git checkout -b fix/soft-delete-cascade upstream/develop +``` + +--- + +## Making Changes + +1. Create your branch off `develop` (see above). +2. Make focused, minimal changes — one concern per PR. +3. Keep the public API backwards-compatible unless you've discussed a breaking change in an issue first. +4. Do not add features, refactor surrounding code, or clean up unrelated areas as part of a bug fix PR. +5. Update the relevant `docs/` page if your change affects documented behaviour. + +--- + +## Running the Tests + +The project uses integration tests (no mocks — tests run against a real in-memory or SQLite database): + +```bash +dotnet test tests/EfCoreKit.Tests.Integration/EfCoreKit.Tests.Integration.csproj --configuration Release +``` + +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. + +--- + +## Code Style + +- Follow the conventions already in the codebase — consistency matters more than any individual preference. +- Use `var` where the type is obvious from the right-hand side. +- Prefer expression-bodied members for single-line methods/properties. +- Use `async`/`await` throughout — no `.Result` or `.Wait()`. +- No unused `using` directives. +- No commented-out code. +- XML doc comments (`///`) are not required unless you are adding a new public API surface. + +The project does not currently enforce a formatter tool, so use your judgement to match the surrounding code. + +--- + +## Commit Messages + +Use the conventional commits style: + +``` +: + +[Optional longer description explaining *why*, not what] +``` + +| Type | Use when | +|------|----------| +| `feat` | Adding a new feature | +| `fix` | Fixing a bug | +| `docs` | Documentation changes only | +| `test` | Adding or updating tests | +| `refactor` | Code change that is neither a fix nor a feature | +| `chore` | Build system, CI, or tooling changes | + +Examples: + +``` +feat: add WhereIfNotEmpty extension method +fix: restore clears DeletedBy when soft-delete interceptor is enabled +docs: add cascade soft delete example to soft-delete guide +``` + +--- + +## Pull Request Process + +1. Ensure your branch is up to date with `upstream/develop` before opening the PR. +2. Fill in the PR description — what changed, why, and how to test it. +3. All CI checks (build + tests) must pass. +4. At least one maintainer review is required before merge. +5. Squash commits if the history is noisy — a clean history per PR is preferred. +6. Once approved, a maintainer will merge into `develop`. + + + +## Code of Conduct + +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. + +If you experience or witness unacceptable behaviour, please open a private issue or contact the maintainer directly. diff --git a/README.md b/README.md index 4e54daf..7dd55b1 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Every .NET project with EF Core ends up writing the same plumbing: soft delete f **Design goals:** -- **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. -- **Opt-in everything** — Enable only the features you need. Nothing runs unless you turn it on. -- **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. +- **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. +- **Opt-in everything** Enable only the features you need. Nothing runs unless you turn it on. +- **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. --- @@ -42,7 +42,7 @@ Every .NET project with EF Core ends up writing the same plumbing: soft delete f | **Query Helpers** | `ExistsAsync`, `GetByIdOrThrowAsync`, `WhereIf`, `OrderByDynamic`, and more | | **DbContext Utilities** | `ExecuteInTransactionAsync`, `DetachAll`, `TruncateAsync` | | **Slow Query Logging** | Logs warnings for queries exceeding a configurable threshold | -| **Structured Exceptions** | `ConcurrencyConflictException`, `DuplicateEntityException`, `TenantMismatchException` | +| **Structured Exceptions** | `EntityNotFoundException`, `ConcurrencyConflictException`, `DuplicateEntityException`, `InvalidFilterException` | --- @@ -67,9 +67,7 @@ builder.Services.AddEfCoreExtensions( .EnableSoftDelete() .EnableAuditTrail() // basic: stamps CreatedAt/By, UpdatedAt/By // .EnableAuditTrail(fullLog: true) // alternative: also writes field-level AuditLog rows - .EnableMultiTenancy() .UseUserProvider() - .UseTenantProvider() .LogSlowQueries(TimeSpan.FromSeconds(1))); ``` @@ -85,7 +83,7 @@ public class Order : AuditableEntity { } // Soft-deletable + audited public class Customer : SoftDeletableEntity { } -// Full — soft-delete + audit + tenant + row version +// Full — soft-delete + audit + row version public class Invoice : FullEntity { } ``` @@ -140,21 +138,6 @@ var orders = await dbSet.FindAsync(spec); --- -## What Happens Behind the Scenes - -| You do this | EfCoreKit does this | -|-------------|------------------------------| -| Call `SaveChangesAsync()` | Stamps `CreatedAt`/`UpdatedAt`, sets `CreatedBy`/`UpdatedBy` from your user provider | -| Delete an entity implementing `ISoftDeletable` | Converts to a soft delete — sets `IsDeleted`, `DeletedAt`, `DeletedBy` instead of removing the row | -| Query any `DbSet` | Automatically filters out soft-deleted rows and scopes to the current tenant | -| Add a new tenant entity | Auto-assigns `TenantId` from your tenant provider | -| Modify a tenant entity you don't own | Throws `TenantMismatchException` before hitting the database | -| Save with a stale row version | Throws `ConcurrencyConflictException` wrapping `DbUpdateConcurrencyException` | -| Run a slow query | Logs a warning with the SQL and duration | -| Save `IFullAuditable` entities with `fullLog: true` | Writes an `AuditLog` row for every changed property | - ---- - ## Soft Delete Lifecycle ```csharp @@ -187,16 +170,16 @@ var page = await context.Orders .OrderBy(o => o.CreatedAt) .ToPagedAsync(page: 2, pageSize: 25); -Console.WriteLine($"Page {page.CurrentPage} of {page.TotalPages} ({page.TotalCount} total)"); +Console.WriteLine($"Page {page.Page} of {page.TotalPages} ({page.TotalCount} total)"); // Keyset / cursor pagination (no OFFSET — scales to millions of rows) var first = await context.Orders - .OrderBy(o => o.CreatedAt).ThenBy(o => o.Id) - .ToKeysetPagedAsync(pageSize: 25, afterId: null); + .OrderBy(o => o.Id) + .ToKeysetPagedAsync(o => o.Id, cursor: null, pageSize: 25); var next = await context.Orders - .OrderBy(o => o.CreatedAt).ThenBy(o => o.Id) - .ToKeysetPagedAsync(pageSize: 25, afterId: first.NextCursor); + .OrderBy(o => o.Id) + .ToKeysetPagedAsync(o => o.Id, cursor: int.Parse(first.NextCursor!), pageSize: 25); ``` --- diff --git a/docs/base-entities.md b/docs/base-entities.md index 7d8d5fc..e264a04 100644 --- a/docs/base-entities.md +++ b/docs/base-entities.md @@ -8,7 +8,7 @@ EfCoreKit provides a hierarchy of ready-made base classes so you don't have to r BaseEntity └── AuditableEntity (+ CreatedAt/By, UpdatedAt/By) └── SoftDeletableEntity (+ IsDeleted, DeletedAt/By) - └── FullEntity (+ TenantId, RowVersion) + └── FullEntity (+ RowVersion) ``` Each level adds the interface properties for the corresponding feature. All levels have an `int`-key convenience alias (e.g. `BaseEntity` = `BaseEntity`). @@ -71,13 +71,12 @@ public class Customer : SoftDeletableEntity { } ### FullEntity<TKey> / FullEntity -Implements everything: `IAuditable`, `ISoftDeletable`, `ITenantEntity`, and `IConcurrencyAware`. +Implements `IAuditable`, `ISoftDeletable`, and `IConcurrencyAware`. ```csharp -public abstract class FullEntity : SoftDeletableEntity, ITenantEntity, IConcurrencyAware +public abstract class FullEntity : SoftDeletableEntity, IConcurrencyAware { - public string? TenantId { get; set; } - public byte[] RowVersion { get; set; } = []; + public byte[] RowVersion { get; set; } = []; } ``` diff --git a/docs/exceptions.md b/docs/exceptions.md index 18794f1..d40328b 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -10,7 +10,6 @@ Exception ├── EntityNotFoundException ├── ConcurrencyConflictException ├── DuplicateEntityException - ├── TenantMismatchException └── InvalidFilterException ``` @@ -135,33 +134,6 @@ Messages produced: --- -## TenantMismatchException - -Thrown by `TenantInterceptor` when a save is attempted on an entity that belongs to a different tenant than the current request. - -```csharp -public sealed class TenantMismatchException : EfCoreException -{ - public string? ExpectedTenant { get; } // current tenant from ITenantProvider - public string? ActualTenant { get; } // tenant on the entity -} -``` - -```csharp -try -{ - await context.SaveChangesAsync(); -} -catch (TenantMismatchException ex) -{ - // ex.ExpectedTenant == "tenant-abc" - // ex.ActualTenant == "tenant-xyz" - return Forbid(); -} -``` - ---- - ## InvalidFilterException Thrown by `ApplyFilters` when a `FilterDescriptor` is invalid. diff --git a/docs/getting-started.md b/docs/getting-started.md index c5cf103..5d6fc92 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -38,9 +38,7 @@ builder.Services.AddEfCoreExtensions( .EnableSoftDelete() .EnableAuditTrail() // basic: stamps CreatedAt/By, UpdatedAt/By // .EnableAuditTrail(fullLog: true) // alternative: also writes field-level AuditLog rows - .EnableMultiTenancy() .UseUserProvider() - .UseTenantProvider() .LogSlowQueries(TimeSpan.FromSeconds(1))); ``` @@ -64,14 +62,14 @@ public class Order : AuditableEntity { } // Soft-deletable + audited, int PK public class Customer : SoftDeletableEntity { } -// Full — soft-delete + audit + tenant + row version +// Full — soft-delete + audit + row version public class Invoice : FullEntity { } ``` You can also implement interfaces directly if you prefer to control your own hierarchy: ```csharp -public class Customer : IAuditable, ISoftDeletable, ITenantEntity +public class Customer : IAuditable, ISoftDeletable { public int Id { get; set; } public string Name { get; set; } = string.Empty; @@ -84,8 +82,6 @@ public class Customer : IAuditable, ISoftDeletable, ITenantEntity public bool IsDeleted { get; set; } public DateTime? DeletedAt { get; set; } public string? DeletedBy { get; set; } - - public string? TenantId { get; set; } } ``` @@ -129,20 +125,6 @@ public class HttpContextUserProvider : IUserProvider } ``` -## 6. Implement ITenantProvider (if using multi-tenancy) - -```csharp -public class HttpContextTenantProvider : ITenantProvider -{ - private readonly IHttpContextAccessor _accessor; - - public HttpContextTenantProvider(IHttpContextAccessor accessor) => _accessor = accessor; - - public string? GetCurrentTenantId() - => _accessor.HttpContext?.User?.FindFirst("tenant_id")?.Value; -} -``` - ## What Happens Automatically Once configured, EfCoreKit handles the following via EF Core interceptors: @@ -151,8 +133,7 @@ Once configured, EfCoreKit handles the following via EF Core interceptors: |---------|-------------|------| | **Audit Trail** | Sets `CreatedAt`/`CreatedBy` on insert, `UpdatedAt`/`UpdatedBy` on update | Every `SaveChanges` / `SaveChangesAsync` | | **Soft Delete** | Converts `DELETE` to `UPDATE SET IsDeleted = true` | When deleting an `ISoftDeletable` entity | -| **Multi-Tenancy** | Auto-assigns `TenantId` on insert, validates ownership on update | Every `SaveChanges` / `SaveChangesAsync` | -| **Query Filters** | Hides soft-deleted rows and scopes queries to the current tenant | Every LINQ query | +| **Query Filters** | Hides soft-deleted rows | Every LINQ query | | **Slow Query Logging** | Logs a warning for queries exceeding the threshold | After each database command | | **Concurrency** | Throws `ConcurrencyConflictException` on stale row version conflicts | Every `SaveChanges` / `SaveChangesAsync` | @@ -161,7 +142,6 @@ Once configured, EfCoreKit handles the following via EF Core interceptors: - [Base Entities](base-entities.md) — Entity class hierarchy and configuration bases - [Soft Delete](soft-delete.md) — Lifecycle methods, restoring records, cascade delete - [Audit Trail](audit-trail.md) — Timestamps, user tracking, field-level AuditLog -- [Multi-Tenancy](multi-tenancy.md) — Tenant isolation and filtering - [Repository & Unit of Work](repository-uow.md) — Generic repository and transaction management - [Specification Pattern](specifications.md) — Composable, reusable query logic - [Pagination](pagination.md) — Offset and keyset/cursor pagination diff --git a/docs/soft-delete.md b/docs/soft-delete.md index 4277942..3e357de 100644 --- a/docs/soft-delete.md +++ b/docs/soft-delete.md @@ -162,10 +162,6 @@ await context.SaveChangesAsync(); When an entity implements both `IAuditable` and `ISoftDeletable` (as `SoftDeletableEntity` does), a soft delete triggers a `Modified` state change — so `UpdatedAt` and `UpdatedBy` are also stamped at the moment of deletion. -## Combining with Multi-Tenancy - -Soft-deleted rows from other tenants remain invisible. The tenant filter and soft-delete filter are both applied independently. - --- [← Base Entities](base-entities.md) | [Audit Trail →](audit-trail.md)