GM.EntityFramework 1.5.0

dotnet add package GM.EntityFramework --version 1.5.0
                    
NuGet\Install-Package GM.EntityFramework -Version 1.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GM.EntityFramework" Version="1.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.EntityFramework" Version="1.5.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.EntityFramework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GM.EntityFramework --version 1.5.0
                    
#r "nuget: GM.EntityFramework, 1.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GM.EntityFramework@1.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GM.EntityFramework&version=1.5.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.EntityFramework&version=1.5.0
                    
Install as a Cake Tool

<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

CI NuGet License: MIT

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 (on PagingOptions<TKey>) → the PageSize rows 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 same OrderBy you'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 Sample aggregate root (SoftDeletableEntity<int>, IAggregateRoot) with child SampleItem entities, factory methods, and encapsulated mutations.
  • A custom ISampleRepository : IGenericRepository<Sample>, implemented by deriving from GenericRepository<Sample, ApplicationDbContext>.
  • An ApplicationDbContext : GenericDbContext that applies EF configurations from the assembly and gets automatic CreatedAt / UpdatedAt auditing 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.5.0 57 10/13/2026
1.4.0 83 10/7/2026
1.3.0 120 9/7/2026
1.2.2 104 8/31/2026
1.2.1 127 7/30/2026
1.2.0 125 7/28/2026
1.1.0 134 7/2/2026
1.0.1 313 6/13/2025
1.0.0 238 5/4/2025