BB84.EntityFrameworkCore.Repositories.Abstractions 5.0.916

dotnet add package BB84.EntityFrameworkCore.Repositories.Abstractions --version 5.0.916
                    
NuGet\Install-Package BB84.EntityFrameworkCore.Repositories.Abstractions -Version 5.0.916
                    
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="BB84.EntityFrameworkCore.Repositories.Abstractions" Version="5.0.916" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BB84.EntityFrameworkCore.Repositories.Abstractions" Version="5.0.916" />
                    
Directory.Packages.props
<PackageReference Include="BB84.EntityFrameworkCore.Repositories.Abstractions" />
                    
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 BB84.EntityFrameworkCore.Repositories.Abstractions --version 5.0.916
                    
#r "nuget: BB84.EntityFrameworkCore.Repositories.Abstractions, 5.0.916"
                    
#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 BB84.EntityFrameworkCore.Repositories.Abstractions@5.0.916
                    
#: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=BB84.EntityFrameworkCore.Repositories.Abstractions&version=5.0.916
                    
Install as a Cake Addin
#tool nuget:?package=BB84.EntityFrameworkCore.Repositories.Abstractions&version=5.0.916
                    
Install as a Cake Tool

net100 NuGet

BB84.EntityFrameworkCore.Repositories.Abstractions

This package provides the repository interface definitions and the IDbContext abstraction.

Installation

dotnet add package BB84.EntityFrameworkCore.Repositories.Abstractions

IDbContext

A thin abstraction over DbContext that exposes only what repositories need: Set<TEntity>(), SaveChanges, SaveChangesAsync, ChangeTracker, Database, and the save-events. Your application DbContext should implement this interface so repositories remain decoupled from the concrete EF context type.

public class AppDbContext : DbContext, IDbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    // DbSet properties ...
}

// DI registration
services.AddDbContext<AppDbContext>(...);
services.AddScoped<IDbContext>(sp => sp.GetRequiredService<AppDbContext>());

Query<TEntity>

Every read method takes its options as one immutable record instead of a positional parameter list. All options are optional, so passing nothing reads everything.

Property Type Default
Where Expression<Func<TEntity, bool>>? null
QueryFilter Func<IQueryable<TEntity>, IQueryable<TEntity>>? null
Include IReadOnlyList<Expression<Func<TEntity, object?>>>? null
OrderBy Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? null
Skip / Take int? null
TrackChanges bool false
IgnoreQueryFilters bool false

Options apply in a fixed order: Where, QueryFilter, IgnoreQueryFilters, Include, OrderBy, Skip, Take — so ordering always precedes paging.

IReadOnlyList<Person> people = await repository.GetListAsync(
    new()
    {
        Where = x => x.IsActive,
        OrderBy = q => q.OrderBy(x => x.LastName),
        Include = [x => x.Jobs],
        Take = 50,
    },
    cancellationToken);

Being a record, a base query can be varied with with instead of rebuilt:

Query<Person> active = new() { Where = x => x.IsActive };

int total = repository.Count(active);
IReadOnlyList<Person> firstPage = repository.GetList(active with { Take = 25 });

Include names navigations as expressions, so a rename is a compile error rather than a runtime one. Use QueryFilter for anything the properties cannot express — a nested ThenInclude, a join, a grouping.

Read / write split

Each repository abstraction exists as two halves plus a composed alias:

Read Write Composed
IReadRepository<TEntity> IWriteRepository<TEntity> IGenericRepository<TEntity>
IReadIdentityRepository<TEntity, TKey> IWriteIdentityRepository<TEntity, TKey> IIdentityRepository<TEntity, TKey>
IReadEnumeratorRepository<TEntity, TKey> — IEnumeratorRepository<TEntity, TKey>

Depend on a half wherever a component only reads or only writes — a query handler, a reporting service, an importer, either side of a CQRS split. The dependency then states in its type what it is allowed to do, and a hand written test double only has to implement the half it needs.

public sealed class PriceReport(IReadRepository<Product> products)
{
    // Cannot delete anything. Not by convention — there is no such member.
    public Task<IReadOnlyList<Product>> GetActiveAsync(CancellationToken cancellationToken)
        => products.GetListAsync(new() { Where = p => p.IsActive }, cancellationToken);
}

The composed interfaces add no members of their own, so nothing is reachable only through them. GenericRepository<TEntity> and its derivatives still implement the composed interfaces, and existing IGenericRepository<TEntity> consumers are unaffected.

There is no IWriteEnumeratorRepository: an enumerator repository adds only name based reads on top of the identity repository, so its write half is exactly the inherited one.

IGenericRepository<TEntity>

The base repository interface, composing IReadRepository<TEntity> and IWriteRepository<TEntity>. Every Async variant takes CancellationToken cancellationToken last.

An Async variant exists only where there is something to await. Marking an entity in the change tracker is a synchronous in-memory operation, so Delete(entity) and Update(entity) have no asynchronous counterpart — call them and then await the save. CreateAsync is the exception, because DbSet.AddAsync genuinely awaits when the entity uses a value generator that queries the store, such as HiLo.

Create — declared on IWriteRepository<TEntity>

  • Create(entity) / Create(entities) — marks entity/entities as Added
  • CreateAsync(entity) / CreateAsync(entities)

Read — declared on IReadRepository<TEntity>

  • Count(query) — number of matching rows
  • GetSingle(query) — one entity or null; throws when more than one matches
  • GetSingle<TResult>(selector, query) — projected single
  • GetList(query) — matching entities, buffered
  • GetList<TResult>(selector, query) — projected list
  • Stream(query, cancellationToken) — matching entities as IAsyncEnumerable<TEntity>, not buffered
  • Stream<TResult>(selector, query, cancellationToken) — projected stream

There is no separate "all" method: an absent Where already means everything, so GetList() reads the whole set.

Count deliberately ignores OrderBy, Skip, Take and Include, because none of them changes how many rows match.

Streaming yields rows as they arrive. The sequence is lazy, so it has to be enumerated within the lifetime of the IDbContext, only one stream may be live per context at a time, and TrackChanges should stay off or the change tracker grows with every entity yielded.

Update — declared on IWriteRepository<TEntity>

  • Update(entity) / Update(entities) — marks entity/entities as Modified; no Async variant
  • ExecuteUpdate(expression, setPropertyCalls) — immediate bulk UPDATE (bypasses change tracker)

Delete — declared on IWriteRepository<TEntity>

  • Delete(entity) / Delete(entities) — marks entity/entities as Deleted; no Async variant
  • ExecuteDelete(expression) — immediate bulk DELETE (bypasses change tracker)

The Execute prefix marks the immediate operations, matching EF Core's own IQueryable.ExecuteDelete() / ExecuteUpdate(). They run against the database there and then, so no save changes interceptor fires for them — auditing is skipped, and ExecuteDelete deletes permanently even for soft deletable entities.

IIdentityRepository<TEntity, TKey> / IIdentityRepository<TEntity>

Extends IGenericRepository<TEntity> with ID-based operations. The non-generic overload defaults TKey to Guid.

Additional read methods — declared on IReadIdentityRepository<TEntity, TKey>

  • GetById(id, query) / GetById<TResult>(id, selector, query)
  • GetByIds(ids, query) / GetByIds<TResult>(ids, selector, query)
  • Async variants of all four

The key condition is added to the query rather than replacing it, so a Where in the query keeps applying alongside it.

Additional delete methods — declared on IWriteIdentityRepository<TEntity, TKey>

  • ExecuteDelete(id) / ExecuteDelete(ids) — immediate bulk DELETE by key(s)

Additional update methods — declared on IWriteIdentityRepository<TEntity, TKey>

  • ExecuteUpdate(id, setPropertyCalls) / ExecuteUpdate(ids, setPropertyCalls) — immediate bulk UPDATE by key(s)

Every method on IWriteIdentityRepository<TEntity, TKey> is of the immediate kind — there is no key-based deferred operation.

IEnumeratorRepository<TEntity, TKey> / IEnumeratorRepository<TEntity>

Extends IIdentityRepository with name-based lookups, declared on IReadEnumeratorRepository<TEntity, TKey>. The non-generic overload defaults TKey to int.

  • GetByName(name, query) / GetByNames(names, query)
  • Async variants of both

Migrating from 4.x

4.x 5.0
GetAll(ignoreQueryFilters, trackChanges) GetList(new() { IgnoreQueryFilters = …, TrackChanges = … })
GetAll<TResult>(selector, fieldSelector, …) GetList(selector, query)
GetByCondition(expression, …) GetSingle(new() { Where = … })
GetByCondition(queryFilter, …) GetSingle(new() { QueryFilter = … })
GetByCondition<TResult>(expression, selector, …) GetSingle(selector, new() { Where = … })
GetManyByCondition(expression, …, orderBy, skip, take, …) GetList(new() { Where = …, OrderBy = …, Skip = …, Take = … })
GetManyByCondition<TResult>(…) GetList(selector, query)
CountAll(ignoreQueryFilters) Count(new() { IgnoreQueryFilters = … })
CountByCondition(expression, …) Count(new() { Where = … })
StreamAll(…) Stream(query, cancellationToken)
StreamByCondition(expression, …) Stream(new() { Where = … }, cancellationToken)
StreamByCondition<TResult>(…) Stream(selector, query, cancellationToken)
GetById(id, ignoreQueryFilters, trackChanges, includes) GetById(id, query)
GetByName(name, ignoreQueryFilters, trackChanges) GetByName(name, query)
includeProperties: [nameof(X.Nav)] Include = [x => x.Nav]
fieldSelector: … removed — compose it into selector
token: cancellationToken:
Delete(expression) ExecuteDelete(expression)
DeleteAsync(expression, …) ExecuteDeleteAsync(expression, …)
Delete(id) / Delete(ids) ExecuteDelete(id) / ExecuteDelete(ids)
DeleteAsync(id, …) / DeleteAsync(ids, …) ExecuteDeleteAsync(id, …) / ExecuteDeleteAsync(ids, …)
Update(expression, setPropertyCalls) ExecuteUpdate(expression, setPropertyCalls)
UpdateAsync(expression, setPropertyCalls, …) ExecuteUpdateAsync(expression, setPropertyCalls, …)
Update(id, …) / Update(ids, …) ExecuteUpdate(id, …) / ExecuteUpdate(ids, …)
UpdateAsync(id, …) / UpdateAsync(ids, …) ExecuteUpdateAsync(id, …) / ExecuteUpdateAsync(ids, …)
await DeleteAsync(entity) / await DeleteAsync(entities) removed — Delete(entity) / Delete(entities)
await UpdateAsync(entity) / await UpdateAsync(entities) removed — Update(entity) / Update(entities)

Every Async method now takes its cancellation token last. Call sites that passed arguments positionally around the old token position need checking, not just recompiling.

The four removed entity overloads were Async in name only: they checked the token, mutated the change tracker synchronously and returned a completed task. Migration is dropping the await and the Async suffix. Nothing is lost, because the database work always happened in the save operation, not in these methods.

The Execute renames cover the immediate operations only; the entity based Delete(entity), Delete(entities), Update(entity) and Update(entities) keep their names. Both sets used to share a name while differing in whether they defer to a save operation and whether interceptors run — the rename makes that difference visible at the call site. Each rename is mechanical, but it breaks every call site of the immediate overloads; there are no [Obsolete] forwarders.

ICurrentUserProvider<TUser> / ICurrentUserProvider

Supplies the identity that UserAuditedInterceptor records in the CreatedBy / EditedBy columns. One member:

  • GetCurrentUser()

The identity source is application specific, so the library asks for it rather than deciding it. EnvironmentUserProvider in BB84.EntityFrameworkCore.Repositories covers the process-user case; implement the interface yourself to resolve the identity from an HTTP request, a job context, or anywhere else. The non-generic alias fixes TUser to string, matching the default audit columns.

Usage

Define custom repository interfaces against these abstractions:

public interface IProductRepository : IIdentityRepository<Product>
{
    Task<IReadOnlyList<Product>> GetByCategoryAsync(int categoryId, CancellationToken token = default);
}

public interface ICategoryRepository : IEnumeratorRepository<ProductCategory>
{
    // GetByName / GetByNames already provided
}
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 (1)

Showing the top 1 NuGet packages that depend on BB84.EntityFrameworkCore.Repositories.Abstractions:

Package Downloads
BB84.EntityFrameworkCore.Repositories

This package contains frequently used repository implementations of their respective abstractions, along with provider agnostic entity type configurations and save changes interceptors.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.916 349 9/16/2026
5.0.818 280 8/18/2026
4.5.725 218 7/25/2026
4.4.724 155 7/24/2026
4.4.625 246 6/25/2026
4.3.520 340 5/20/2026
4.3.513 205 5/13/2026
4.3.507 169 5/7/2026
4.3.419 256 4/19/2026
4.3.327 296 3/27/2026
4.2.322 194 3/22/2026
4.1.319 153 3/19/2026
4.1.222 242 2/22/2026
4.1.122 237 1/22/2026
4.0.1026 436 10/26/2025
4.0.916 503 9/16/2025
4.0.817 328 8/17/2025
3.1.617 431 6/17/2025
3.1.518 278 5/18/2025
3.1.409 654 4/9/2025
Loading failed