Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<div align="center">

# 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**

</div>
*Works with any EF Core-supported database**

---

Expand Down
166 changes: 166 additions & 0 deletions README.nuget.md
Original file line number Diff line number Diff line change
@@ -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<T>` / `IReadRepository<T>` 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<T>` |
| **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<T>` |
| **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<AppDbContext>(
options => options.UseSqlServer(connectionString),
kit => kit
.EnableSoftDelete()
.EnableAuditTrail()
.UseUserProvider<HttpContextUserProvider>()
.LogSlowQueries(TimeSpan.FromSeconds(1)));
```

### 2. Inherit a base entity

```csharp
public class Product : BaseEntity { } // int PK
public class Order : AuditableEntity<Guid> { } // 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<Order> repo, IUnitOfWork uow)
{
public async Task<Order> CreateAsync(Order order)
{
await repo.AddAsync(order);
await uow.CommitAsync();
return order;
}
}
```

### 4. Use specifications

```csharp
public class ActiveOrdersSpec : Specification<Order>
{
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.
12 changes: 9 additions & 3 deletions src/EfCoreKit/EfCoreKit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
<PackageId>EfCoreKit</PackageId>
<!-- Version is managed by MinVer via git tags (e.g. v1.0.0) -->
<Description>
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.
</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>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</PackageTags>
<PackageReadmeFile>README.nuget.md</PackageReadmeFile>
<IsPackable>true</IsPackable>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
Expand All @@ -21,7 +26,8 @@
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="" />
<None Include="..\..\README.md" Pack="false" />
<None Include="..\..\README.nuget.md" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
Loading