GM.EntityFramework
1.5.0
dotnet add package GM.EntityFramework --version 1.5.0
NuGet\Install-Package GM.EntityFramework -Version 1.5.0
<PackageReference Include="GM.EntityFramework" Version="1.5.0" />
<PackageVersion Include="GM.EntityFramework" Version="1.5.0" />
<PackageReference Include="GM.EntityFramework" />
paket add GM.EntityFramework --version 1.5.0
#r "nuget: GM.EntityFramework, 1.5.0"
#:package GM.EntityFramework@1.5.0
#addin nuget:?package=GM.EntityFramework&version=1.5.0
#tool nuget:?package=GM.EntityFramework&version=1.5.0
<p align="center"> <img src="https://raw.githubusercontent.com/gmetskhvarishvili/GM.EntityFramework/master/icon.png" alt="GM.EntityFramework" width="140" height="140" /> </p>
GM.EntityFramework
A lightweight DDD + Entity Framework Core toolkit for .NET: base entities, value objects, the specification pattern, a generic repository, and a unit of work with transactions and automatic auditing. Targets .NET 10.
Packages
The three packages version and release together (lockstep):
| Package | What it gives you |
|---|---|
GM.EntityFramework.Domain |
Entities, value objects, specifications, results, domain-event and repository abstractions (no EF dependency to reference in your domain layer beyond EF Core primitives). |
GM.EntityFramework.Persistence |
The EF Core implementation: GenericRepository, GenericUnitOfWork, and an auditing GenericDbContext. |
GM.EntityFramework |
Meta-package that pulls in both of the above. |
dotnet add package GM.EntityFramework
Quick start
1. Define entities
using GM.EntityFramework.Domain.Base;
public class Product : AuditableEntity<Guid> // adds CreatedAt / UpdatedAt (set automatically)
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Base classes: Entity<TKey> → AuditableEntity<TKey> (timestamps) →
SoftDeletableEntity<TKey> (adds IsActive / IsDeleted / IsHidden with SoftRemove(),
RestoreAll(), etc.). There's also ValueObject for equality-by-value types.
2. Create a DbContext
Inherit GenericDbContext — it stamps CreatedAt/UpdatedAt on save automatically.
using GM.EntityFramework.Persistence;
using Microsoft.EntityFrameworkCore;
public class AppDbContext(DbContextOptions options) : GenericDbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}
3. Register and use the repository + unit of work
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));
services.AddScoped<IGenericUnitOfWork, GenericUnitOfWork<AppDbContext>>();
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<,>));
await repository.AddAsync(new Product { Name = "Book", Price = 9.99m });
await unitOfWork.SaveChangesAsync();
var cheap = await repository.FindAsync(p => p.Price < 10);
var exists = await repository.ExistsAsync(p => p.Name == "Book");
4. Specifications
Encapsulate query logic and compose it with And / Or / Not:
using GM.EntityFramework.Domain.Specifications;
public class CheapProductsSpec : BaseSpecification<Product>
{
public CheapProductsSpec()
{
AddCriteria(p => p.Price < 10);
ApplyOrdering(nameof(Product.Name));
ApplyPaging(currentPage: 1, pageSize: 20);
}
}
var page = await repository.ListAsync(new CheapProductsSpec());
Paging by page number or by last key value
With paging enabled, what you pass decides how the page is fetched:
LastKeyValue(onPagingOptions<TKey>) → thePageSizerows after the row with that key, in the query's ordering (see below). Cost doesn't grow with depth and concurrent inserts/deletes don't shift the window — good for infinite scroll.CurrentPage(1-based) → skip/take to that page, in the query's ordering. Lets clients jump to any page and show totals.- Neither → page 1 by number, in the query's ordering. To walk a list by
LastKeyValue, request page 1 with the sameOrderByyou'll page with, then send the last row's key.
If both are passed, LastKeyValue wins. A default LastKeyValue (0, Guid.Empty, null) counts as not
passed.
LastKeyProperty (Id by default, must be unique) + LastKeyValue identify the last row the caller has;
the page is the rows after it in the query's ordering — one column with an optional direction (e.g.
"Id desc", "CreatedAt desc"), defaulting to LastKeyProperty ascending. Ties in the order column are
broken by LastKeyProperty. When the order column is the key the filter is just Key > @value;
otherwise the last row is looked up in the same query (WHERE EXISTS (… WHERE a.Key = @value AND (x.Order < a.Order OR (x.Order = a.Order AND x.Key < @value)))), and if no row has that key the page is
empty. LastKeyValue can be of the key's type or a string converted to it (invariant culture; dates
without an offset are read as UTC), so an API that lets the caller pick the column can take
PagingOptions<string>. A bad key, value or ordering throws ArgumentException.
public class ProductsSpec : BaseSpecification<Product>
{
public ProductsSpec(PagingOptions paging)
{
AddVisibilityFilter();
ApplyListQuery(new AuditDateRange(), paging, new OrderingOptions { OrderBy = "Name asc" }); // ordering: CurrentPage only
}
}
var paging = new PagingOptions<int> { LastKeyValue = 40, PageSize = 20 }; // or { CurrentPage = 3, PageSize = 20 }
var items = await repository.ListAsync(new ProductsSpec(paging));
var nextLastKeyValue = items.Count > 0 ? items[^1].Id : 0; // send back as LastKeyValue for the next page
var hasMore = items.Count == paging.PageSize;
int? total = paging.IncludeTotalCount // true by default; callers set false to skip the count query
? await repository.CountAsync(new ProductsSpec(PagingOptions.None))
: null;
5. Transactions
await unitOfWork.ExecuteInTransactionAsync(async () =>
{
await repository.AddAsync(product);
// ... more work; commits on success, rolls back on exception
});
Concurrency conflicts surface as a ConcurrencyException.
6. Multi-tenancy
Mark tenant-owned aggregates with IHasTenant, then apply the ambient-tenant filter and write-stamp
(from ICurrentActor.TenantId) in your context:
public class Order : SoftDeletableEntity<int>, IHasTenant
{
public Guid? TenantId { get; private set; }
public void AssignTenant(Guid? tenantId) => TenantId = tenantId;
}
public class AppDbContext(DbContextOptions options, ICurrentActor actor) : GenericDbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyTenantQueryFilters(actor); // global query filter + index per IHasTenant entity
}
public override int SaveChanges()
{
ChangeTracker.StampTenants(actor); // stamp the ambient tenant on insert
return base.SaveChanges();
}
}
Reads then return only the caller's tenant (a null tenant — a system job or a cross-tenant admin — sees
only global, null-tenant rows). The actor is read per query, so use a request-scoped or
IHttpContextAccessor-backed actor with a context created per scope.
Sample application
A complete, runnable DDD + CQRS example built on these packages lives in a separate repo: GM.EntityFramework.Samples. It's a layered ASP.NET Core Web API (Domain / Application / Persistence / API) that shows how the pieces fit together in a real project:
- A
Sampleaggregate root (SoftDeletableEntity<int>,IAggregateRoot) with childSampleItementities, factory methods, and encapsulated mutations. - A custom
ISampleRepository : IGenericRepository<Sample>, implemented by deriving fromGenericRepository<Sample, ApplicationDbContext>. - An
ApplicationDbContext : GenericDbContextthat applies EF configurations from the assembly and gets automaticCreatedAt/UpdatedAtauditing for free. - A
UnitOfWork : GenericUnitOfWork<ApplicationDbContext>that exposes the aggregate repository. - CQRS commands (create / update / delete) and queries (details / list), EF Core migrations, and a PostgreSQL (Npgsql) provider.
Clone it to see the library used end-to-end.
Repository layout
GM.EntityFramework/
├── GM.EntityFramework.Domain/ # abstractions (entities, specifications, results, events)
├── GM.EntityFramework.Persistence/ # EF Core implementation (repository, UoW, DbContext)
├── GM.EntityFramework/ # meta-package
└── tests/GM.EntityFramework.Tests/ # xUnit tests (SQLite in-memory for persistence)
Building & testing
dotnet build -c Release
dotnet test -c Release
Releasing
Versioning is automated from Conventional Commits —
see CONTRIBUTING.md. All three packages share one version
(Directory.Build.props) and publish together to nuget.org on each release.
License
MIT — see LICENSE.
| 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
- GM.EntityFramework.Domain (>= 1.5.0)
- GM.EntityFramework.Persistence (>= 1.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.