diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a45c8b..746197d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,6 +56,13 @@ jobs: --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + - name: Publish to GitHub Packages + run: > + dotnet nuget push ./artifacts/*.nupkg + --source https://nuget.pkg.github.com/Clifftech123/index.json + --api-key ${{ secrets.GITHUB_TOKEN }} + --skip-duplicate + create-release: name: Create GitHub Release needs: publish-nuget diff --git a/README.md b/README.md index c64767d..5dca0d0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ -
- # EfCoreKit -**EF Core extensions that eliminate boilerplate — so you can focus on building features.** +**EF Core extensions that eliminate boilerplate so you can focus on building features.** [![NuGet](https://img.shields.io/nuget/v/EfCoreKit?logo=nuget&label=NuGet)](https://www.nuget.org/packages/EfCoreKit) [![NuGet Downloads](https://img.shields.io/nuget/dt/EfCoreKit?logo=nuget&label=Downloads)](https://www.nuget.org/packages/EfCoreKit) [![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) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -**Works with any EF Core-supported database** - -
+*Works with any EF Core-supported database** --- diff --git a/README.nuget.md b/README.nuget.md new file mode 100644 index 0000000..4e0cf75 --- /dev/null +++ b/README.nuget.md @@ -0,0 +1,166 @@ +# EfCoreKit + +EF Core extensions that eliminate boilerplate, so you can focus on building features. + +**.NET 8 / 9 / 10** · **EF Core 8.x / 9.x / 10.x** · **Works with any EF Core-supported database** + +--- + +## Why EfCoreKit? + +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. + +- **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. +- **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. + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Base Entity Hierarchy** | Ready-made base classes: `BaseEntity`, `AuditableEntity`, `SoftDeletableEntity`, `FullEntity` | +| **Entity Configuration Bases** | Fluent config base classes that auto-wire keys, indexes, and soft-delete defaults | +| **Soft Delete** | Mark records as deleted with automatic global query filters; restore or hard-delete on demand | +| **Audit Trail** | Auto-stamp `CreatedAt/By`, `UpdatedAt/By`; optional field-level `AuditLog` history | +| **Repository + Unit of Work** | Generic `IRepository` / `IReadRepository` backed by `IUnitOfWork` | +| **Specification Pattern** | Composable query specs with `And()` / `Or()` combinators, projection, and multi-column ordering | +| **Pagination** | Offset (`ToPagedAsync`) and keyset/cursor (`ToKeysetPagedAsync`) pagination with `PagedResult` | +| **Dynamic Filters** | Apply runtime filter arrays (eq, ne, gt, lt, contains, in, between, isnull…) via `ApplyFilters` | +| **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** | `EntityNotFoundException`, `ConcurrencyConflictException`, `DuplicateEntityException`, `InvalidFilterException` | + +--- + +## Installation + +```bash +dotnet add package EfCoreKit +``` + +One package — everything is included. No separate installs needed. + +--- + +## Quick Start + +### 1. Register services + +```csharp +builder.Services.AddEfCoreExtensions( + options => options.UseSqlServer(connectionString), + kit => kit + .EnableSoftDelete() + .EnableAuditTrail() + .UseUserProvider() + .LogSlowQueries(TimeSpan.FromSeconds(1))); +``` + +### 2. Inherit a base entity + +```csharp +public class Product : BaseEntity { } // int PK +public class Order : AuditableEntity { } // audited, Guid PK +public class Customer : SoftDeletableEntity { } // soft-deletable + audited +public class Invoice : FullEntity { } // soft-delete + audit + row version +``` + +### 3. Use the repository + +```csharp +public class OrderService(IRepository repo, IUnitOfWork uow) +{ + public async Task CreateAsync(Order order) + { + await repo.AddAsync(order); + await uow.CommitAsync(); + return order; + } +} +``` + +### 4. Use specifications + +```csharp +public class ActiveOrdersSpec : Specification +{ + public ActiveOrdersSpec(int customerId) + { + AddCriteria(o => o.CustomerId == customerId); + AddInclude(o => o.Items); + ApplyOrderByDescending(o => o.CreatedAt); + ApplyPaging(skip: 0, take: 20); + ApplyAsNoTracking(); + } +} + +var orders = await repo.FindAsync(new ActiveOrdersSpec(customerId)); +``` + +--- + +## Soft Delete + +```csharp +var active = await context.Customers.ToListAsync(); // deleted rows excluded +var all = await context.Customers.IncludeDeleted().ToListAsync(); +var deleted = await context.Customers.OnlyDeleted().ToListAsync(); + +context.Customers.Restore(customer); // un-delete +context.Customers.HardDelete(customer); // permanent remove +await context.SaveChangesAsync(); +``` + +--- + +## Pagination + +```csharp +// Offset pagination +var page = await context.Orders + .OrderBy(o => o.CreatedAt) + .ToPagedAsync(page: 2, pageSize: 25); + +// Keyset / cursor pagination +var first = await context.Orders + .OrderBy(o => o.Id) + .ToKeysetPagedAsync(o => o.Id, cursor: null, pageSize: 25); + +var next = await context.Orders + .OrderBy(o => o.Id) + .ToKeysetPagedAsync(o => o.Id, cursor: int.Parse(first.NextCursor!), pageSize: 25); +``` + +--- + +## Dynamic Filters + +```csharp +var filters = new[] +{ + new FilterDescriptor { Field = "Status", Operator = "eq", Value = "Active" }, + new FilterDescriptor { Field = "CreatedAt", Operator = "gte", Value = DateTime.UtcNow.AddDays(-30) }, + new FilterDescriptor { Field = "Tags", Operator = "in", Value = new[] { "VIP", "Premium" } }, + new FilterDescriptor { Field = "Score", Operator = "between", Value = new object[] { 10, 100 } }, +}; + +var results = await context.Customers.ApplyFilters(filters).ToListAsync(); +``` + +Supported operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `startswith`, `endswith`, `isnull`, `isnotnull`, `in`, `between`. + +--- + +## Full Documentation + +Complete guides, API reference, and examples are available on GitHub: +https://github.com/Clifftech123/EfCoreKit + +--- + +## License + +MIT — free for personal and commercial use, forever. diff --git a/src/EfCoreKit/EfCoreKit.csproj b/src/EfCoreKit/EfCoreKit.csproj index ba02ef9..deda962 100644 --- a/src/EfCoreKit/EfCoreKit.csproj +++ b/src/EfCoreKit/EfCoreKit.csproj @@ -4,13 +4,18 @@ EfCoreKit - EF Core extensions that eliminate boilerplate — soft delete, audit trail, multi-tenancy, + EF Core extensions that eliminate boilerplate — soft delete, audit trail, repository pattern, unit of work, specification pattern, offset and keyset pagination, dynamic filters, DbContext utilities, and structured exceptions. Install one package and everything is included. - README.md + efcore;entity-framework-core;entityframeworkcore;soft-delete;softdelete;audit-trail;audit;repository-pattern;repository;unit-of-work;specification-pattern;specification;pagination;keyset-pagination;cursor-pagination;dynamic-filters;query-helpers;data-access;orm-extensions;dotnet;csharp;boilerplate + README.nuget.md true + portable + true + true + snupkg @@ -21,7 +26,8 @@ - + +