AgileTech.EfCore.Base
1.0.0
dotnet add package AgileTech.EfCore.Base --version 1.0.0
NuGet\Install-Package AgileTech.EfCore.Base -Version 1.0.0
<PackageReference Include="AgileTech.EfCore.Base" Version="1.0.0" />
<PackageVersion Include="AgileTech.EfCore.Base" Version="1.0.0" />
<PackageReference Include="AgileTech.EfCore.Base" />
paket add AgileTech.EfCore.Base --version 1.0.0
#r "nuget: AgileTech.EfCore.Base, 1.0.0"
#:package AgileTech.EfCore.Base@1.0.0
#addin nuget:?package=AgileTech.EfCore.Base&version=1.0.0
#tool nuget:?package=AgileTech.EfCore.Base&version=1.0.0
AgileTech.EfCore.Base
A small EF Core base library that gives you, for free:
- Auditable entities —
CreatedAt/CreatedBy/ModifiedAt/ModifiedBystamped automatically, plus a fullAuditLogschange history. - Soft delete —
Remove()becomes a soft delete automatically; deleted rows are hidden from every query. - DbSet query helpers —
GetAllAsync,GetPagedAsync,FindOneAsync,ExistsAsync,Save(upsert) — no repository layer needed. - Automatic caching — flip one setting and
GetAllAsync/GetPagedAsync/FindOneAsyncare transparently cached, with the cache kept correct on every write. No cache parameter to pass anywhere.
Targets net10.0.
Install
Add a project/package reference, then:
dotnet add package Microsoft.AspNetCore.Http.Abstractions
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.Extensions.Caching.Hybrid
dotnet add package Microsoft.Extensions.Hosting.Abstractions
(These are pulled in transitively when you reference the library as a project/package — listed here so dotnet restore has everything it needs.)
1. Entities
Inherit BaseAuditableEntity and you're done — no boilerplate:
public class Product : BaseAuditableEntity
{
public string Name { get; set; } = default!;
public decimal Price { get; set; }
}
You get: Id (int), CreatedAt, CreatedBy, ModifiedAt, ModifiedBy, IsDeleted, DeletedAt, DeletedBy.
Other base types available:
| Base type | Use when |
|---|---|
BaseEntity / BaseEntity<TKey> |
You just want an Id, no audit fields |
BaseAuditableEntity / BaseAuditableEntity<TKey> |
You want audit fields and soft delete (the common case) |
BaseAuditableEntityHardDelete / BaseAuditableEntityHardDelete<TKey> |
You want audit fields but Remove() should be a real (hard) delete |
TKey defaults to int on the non-generic versions; use the generic version for Guid, long, string, etc.
2. DbContext
Inherit AuditableDbContext (or AuditableIdentityDbContext<TUser> if you also need ASP.NET Core Identity):
public class AppDbContext : AuditableDbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options, ICurrentUserService currentUser)
: base(options, currentUser) { }
public DbSet<Product> Products => Set<Product>();
}
ICurrentUserService is how the library reads "who made this change" without depending on ASP.NET Core directly. Register the built-in implementation, which reads the logged-in user from HttpContext:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
That's it — every SaveChanges/SaveChangesAsync now:
- Stamps
CreatedAt/CreatedByorModifiedAt/ModifiedByon any changedIAuditableEntity. - Converts deletes on
ISoftDeletableentities into a soft delete (IsDeleted = true,DeletedAt,DeletedBy) instead of a realDELETE. - Writes one
AuditLogrow per changed entity, with old/new values and changed columns as JSON. - Invalidates the cache for anything just written (see Caching below), if caching is enabled.
Soft-deleted rows are automatically excluded from every query (Products.Where(...), GetAllAsync(), etc.) — you never need to write !IsDeleted yourself.
3. Query extensions
Available directly on any DbSet<T> / IQueryable<T>:
var all = await db.Products.GetAllAsync();
var active = await db.Products.GetAllAsync(p => p.IsActive);
var page = await db.Products.GetPagedAsync(
pageNumber: 1,
pageSize: 20,
filter: p => p.IsActive,
orderBy: q => q.OrderByDescending(p => p.CreatedAt));
var product = await db.Products.FindOneAsync(p => p.Sku == sku);
var exists = await db.Products.ExistsAsync(p => p.Sku == sku);
// Upsert: updates if a row matches the predicate, otherwise adds
await db.Products.Save(product, p => p.Sku == product.Sku, db);
await db.SaveChangesAsync();
Add / Update / Remove are EF Core's own DbSet<T> methods — not re-wrapped here.
4. Caching
GetAllAsync, GetPagedAsync, and FindOneAsync can be transparently cached — same method calls, no new API, no cache object to pass around.
Turn it on
// Program.cs
builder.Services.AddCaching(o =>
{
o.Enabled = true; // the one switch — off by default
o.DefaultExpiration = TimeSpan.FromMinutes(5); // optional, this is the default
});
That's the entire setup. From this point on:
var product = await db.Products.FindOneAsync(p => p.Id == id); // served from cache on repeat calls
var all = await db.Products.GetAllAsync(); // cached
var page = await db.Products.GetPagedAsync(1, 20); // cached
db.Products.Update(product);
await db.SaveChangesAsync(); // automatically evicts the cache for Product
Nothing else changes — no constructor changes, no cache parameter, no separate "cached" method names.
How it works
- Built on HybridCache — a fast in-process (L1) cache by default, with built-in protection against cache stampedes (100 concurrent requests for the same cold key trigger exactly 1 database query, not 100).
- The cache key is generated automatically from the query's own SQL (
IQueryable.ToQueryString(), hashed) — it already encodes the entity type, filter, ordering, and paging, so you never construct a key and two different queries can never collide. - Every write through
AuditableDbContext/AuditableIdentityDbContextalready knows which entity types changed (for the audit log);SaveChanges/SaveChangesAsyncreuses that to evict the cache for exactly those types — nothing else is touched. - If caching is disabled (default), or a provider can't produce a query string, calls fall straight through to the database — this never breaks a query that used to work.
Scaling across multiple instances (Redis)
Out of the box the cache is in-process only — fastest option, but each instance/container has its own copy. If you run more than one instance (e.g. several ECS tasks behind a load balancer) and need writes on one instance to invalidate the cache on all of them, register Redis before AddAgileTechCaching:
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "your-redis-endpoint:6379");
builder.Services.AddCaching(o => o.Enabled = true);
HybridCache automatically uses Redis as an L2 behind the in-process L1 — no other code changes, and invalidation now propagates to every instance sharing that Redis.
When to use it, and when not to
Good fit: reference/lookup tables, dashboards, lists that get read far more often than they change.
Skip it (or accept the small, harmless overhead) for tables that are written to almost as often as they're read — you'll pay a tiny invalidation cost on every save without getting much benefit from the cache.
If running multiple instances without Redis: a write on instance A won't invalidate instance B's cache — B can serve slightly stale data (up to DefaultExpiration) until its own entry expires or is naturally refreshed. Add Redis (above) if that window matters for a given table.
Project layout
Entities/ BaseEntity, BaseAuditableEntity, AuditLog
Interfaces/ IAuditableEntity, ISoftDeletable, ICurrentUserService
Context/ AuditableDbContext, AuditableIdentityDbContext<TUser>, soft-delete query filter, audit interceptor
Extensions/ DbSet query helpers (GetAllAsync, GetPagedAsync, FindOneAsync, ExistsAsync, Save) + CurrentUserService
Repository/ PagedResult<T>
Caching/ HybridCache wiring, automatic caching, cache invalidation on save
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.11)
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 10.0.10)
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.Extensions.Caching.Hybrid (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 115 | 7/15/2026 |
Initial release: AuditableDbContext / AuditableIdentityDbContext with automatic audit logging and soft delete, DbSet query extensions (GetAllAsync, GetPagedAsync, FindOneAsync, ExistsAsync, Save), and opt-in automatic caching via HybridCache with SaveChanges-triggered invalidation.