|
| 1 | +# EfCoreKit |
| 2 | + |
| 3 | +EF Core extensions that eliminate boilerplate, so you can focus on building features. |
| 4 | + |
| 5 | +**.NET 8 / 9 / 10** · **EF Core 8.x / 9.x / 10.x** · **Works with any EF Core-supported database** |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Why EfCoreKit? |
| 10 | + |
| 11 | +Every .NET project with EF Core ends up writing the same plumbing: soft delete filters, audit timestamps, pagination helpers, generic repositories, transaction wrappers. EfCoreKit packages all of that into a single `AddEfCoreExtensions()` call. |
| 12 | + |
| 13 | +- **Zero lock-in** Uses standard EF Core interceptors and global query filters. Your entities stay plain C# classes and you can remove EfCoreKit at any time without rewriting your data layer. |
| 14 | +- **Opt-in everything** Enable only the features you need. Nothing runs unless you turn it on. |
| 15 | +- **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. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Features |
| 20 | + |
| 21 | +| Feature | Description | |
| 22 | +|---------|-------------| |
| 23 | +| **Base Entity Hierarchy** | Ready-made base classes: `BaseEntity`, `AuditableEntity`, `SoftDeletableEntity`, `FullEntity` | |
| 24 | +| **Entity Configuration Bases** | Fluent config base classes that auto-wire keys, indexes, and soft-delete defaults | |
| 25 | +| **Soft Delete** | Mark records as deleted with automatic global query filters; restore or hard-delete on demand | |
| 26 | +| **Audit Trail** | Auto-stamp `CreatedAt/By`, `UpdatedAt/By`; optional field-level `AuditLog` history | |
| 27 | +| **Repository + Unit of Work** | Generic `IRepository<T>` / `IReadRepository<T>` backed by `IUnitOfWork` | |
| 28 | +| **Specification Pattern** | Composable query specs with `And()` / `Or()` combinators, projection, and multi-column ordering | |
| 29 | +| **Pagination** | Offset (`ToPagedAsync`) and keyset/cursor (`ToKeysetPagedAsync`) pagination with `PagedResult<T>` | |
| 30 | +| **Dynamic Filters** | Apply runtime filter arrays (eq, ne, gt, lt, contains, in, between, isnull…) via `ApplyFilters` | |
| 31 | +| **Query Helpers** | `ExistsAsync`, `GetByIdOrThrowAsync`, `WhereIf`, `OrderByDynamic`, and more | |
| 32 | +| **DbContext Utilities** | `ExecuteInTransactionAsync`, `DetachAll`, `TruncateAsync<T>` | |
| 33 | +| **Slow Query Logging** | Logs warnings for queries exceeding a configurable threshold | |
| 34 | +| **Structured Exceptions** | `EntityNotFoundException`, `ConcurrencyConflictException`, `DuplicateEntityException`, `InvalidFilterException` | |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Installation |
| 39 | + |
| 40 | +```bash |
| 41 | +dotnet add package EfCoreKit |
| 42 | +``` |
| 43 | + |
| 44 | +One package — everything is included. No separate installs needed. |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Quick Start |
| 49 | + |
| 50 | +### 1. Register services |
| 51 | + |
| 52 | +```csharp |
| 53 | +builder.Services.AddEfCoreExtensions<AppDbContext>( |
| 54 | + options => options.UseSqlServer(connectionString), |
| 55 | + kit => kit |
| 56 | + .EnableSoftDelete() |
| 57 | + .EnableAuditTrail() |
| 58 | + .UseUserProvider<HttpContextUserProvider>() |
| 59 | + .LogSlowQueries(TimeSpan.FromSeconds(1))); |
| 60 | +``` |
| 61 | + |
| 62 | +### 2. Inherit a base entity |
| 63 | + |
| 64 | +```csharp |
| 65 | +public class Product : BaseEntity { } // int PK |
| 66 | +public class Order : AuditableEntity<Guid> { } // audited, Guid PK |
| 67 | +public class Customer : SoftDeletableEntity { } // soft-deletable + audited |
| 68 | +public class Invoice : FullEntity { } // soft-delete + audit + row version |
| 69 | +``` |
| 70 | + |
| 71 | +### 3. Use the repository |
| 72 | + |
| 73 | +```csharp |
| 74 | +public class OrderService(IRepository<Order> repo, IUnitOfWork uow) |
| 75 | +{ |
| 76 | + public async Task<Order> CreateAsync(Order order) |
| 77 | + { |
| 78 | + await repo.AddAsync(order); |
| 79 | + await uow.CommitAsync(); |
| 80 | + return order; |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### 4. Use specifications |
| 86 | + |
| 87 | +```csharp |
| 88 | +public class ActiveOrdersSpec : Specification<Order> |
| 89 | +{ |
| 90 | + public ActiveOrdersSpec(int customerId) |
| 91 | + { |
| 92 | + AddCriteria(o => o.CustomerId == customerId); |
| 93 | + AddInclude(o => o.Items); |
| 94 | + ApplyOrderByDescending(o => o.CreatedAt); |
| 95 | + ApplyPaging(skip: 0, take: 20); |
| 96 | + ApplyAsNoTracking(); |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +var orders = await repo.FindAsync(new ActiveOrdersSpec(customerId)); |
| 101 | +``` |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +## Soft Delete |
| 106 | + |
| 107 | +```csharp |
| 108 | +var active = await context.Customers.ToListAsync(); // deleted rows excluded |
| 109 | +var all = await context.Customers.IncludeDeleted().ToListAsync(); |
| 110 | +var deleted = await context.Customers.OnlyDeleted().ToListAsync(); |
| 111 | + |
| 112 | +context.Customers.Restore(customer); // un-delete |
| 113 | +context.Customers.HardDelete(customer); // permanent remove |
| 114 | +await context.SaveChangesAsync(); |
| 115 | +``` |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Pagination |
| 120 | + |
| 121 | +```csharp |
| 122 | +// Offset pagination |
| 123 | +var page = await context.Orders |
| 124 | + .OrderBy(o => o.CreatedAt) |
| 125 | + .ToPagedAsync(page: 2, pageSize: 25); |
| 126 | + |
| 127 | +// Keyset / cursor pagination |
| 128 | +var first = await context.Orders |
| 129 | + .OrderBy(o => o.Id) |
| 130 | + .ToKeysetPagedAsync(o => o.Id, cursor: null, pageSize: 25); |
| 131 | + |
| 132 | +var next = await context.Orders |
| 133 | + .OrderBy(o => o.Id) |
| 134 | + .ToKeysetPagedAsync(o => o.Id, cursor: int.Parse(first.NextCursor!), pageSize: 25); |
| 135 | +``` |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +## Dynamic Filters |
| 140 | + |
| 141 | +```csharp |
| 142 | +var filters = new[] |
| 143 | +{ |
| 144 | + new FilterDescriptor { Field = "Status", Operator = "eq", Value = "Active" }, |
| 145 | + new FilterDescriptor { Field = "CreatedAt", Operator = "gte", Value = DateTime.UtcNow.AddDays(-30) }, |
| 146 | + new FilterDescriptor { Field = "Tags", Operator = "in", Value = new[] { "VIP", "Premium" } }, |
| 147 | + new FilterDescriptor { Field = "Score", Operator = "between", Value = new object[] { 10, 100 } }, |
| 148 | +}; |
| 149 | + |
| 150 | +var results = await context.Customers.ApplyFilters(filters).ToListAsync(); |
| 151 | +``` |
| 152 | + |
| 153 | +Supported operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `startswith`, `endswith`, `isnull`, `isnotnull`, `in`, `between`. |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## Full Documentation |
| 158 | + |
| 159 | +Complete guides, API reference, and examples are available on GitHub: |
| 160 | +https://github.com/Clifftech123/EfCoreKit |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## License |
| 165 | + |
| 166 | +MIT — free for personal and commercial use, forever. |
0 commit comments