BB84.EntityFrameworkCore.Repositories 5.0.916

There is a newer version of this package available.
See the version list below for details.
dotnet add package BB84.EntityFrameworkCore.Repositories --version 5.0.916
                    
NuGet\Install-Package BB84.EntityFrameworkCore.Repositories -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" 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" Version="5.0.916" />
                    
Directory.Packages.props
<PackageReference Include="BB84.EntityFrameworkCore.Repositories" />
                    
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 --version 5.0.916
                    
#r "nuget: BB84.EntityFrameworkCore.Repositories, 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@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&version=5.0.916
                    
Install as a Cake Addin
#tool nuget:?package=BB84.EntityFrameworkCore.Repositories&version=5.0.916
                    
Install as a Cake Tool

net100 NuGet

BB84.EntityFrameworkCore.Repositories

This package provides the default repository implementations, the provider-agnostic entity type configuration base classes, and the save changes interceptors for auditing and soft delete.

Moved in 5.0. The configuration base classes and the interceptors came here from BB84.EntityFrameworkCore.Repositories.SqlServer, where almost nothing about them was SQL Server specific. DatabaseFacadeExtensions went the other way, into that package, because it emits T-SQL.

Installation

dotnet add package BB84.EntityFrameworkCore.Repositories

Repository implementations

GenericRepository<TEntity>

Abstract base for all repositories. Accepts IDbContext as a constructor parameter. Every read method takes a single Query<TEntity> and delegates to the protected PrepareQuery(query, forCount), which composes Where, QueryFilter, IgnoreQueryFilters, Include, OrderBy, Skip, Take and AsNoTracking into one IQueryable<TEntity>. Projections go through the protected ApplyProjection(query, selector).

Two protected helpers are worth knowing when writing a repository method the interface does not cover:

  • PrepareQuery(query, forCount) — the single place a Query<TEntity> becomes a query. Pass forCount: true to skip the options that cannot change how many rows match.
  • WithCondition(query, condition) — appends a condition to a query without discarding what the caller already set. This is how GetById and GetByName add their key or name filter.
Streaming reads

Stream returns IAsyncEnumerable<T> instead of IReadOnlyList<T>. It takes the same Query<TEntity> as GetListAsync and goes through the same composition, but the result set is not buffered — rows are yielded as they arrive. Use it for exports, batch jobs and other unbounded reads:

await foreach (Product product in repository.Stream(
    new()
    {
        Where = p => p.IsActive,
        OrderBy = q => q.OrderBy(p => p.Name),
    },
    cancellationToken))
{
    await writer.WriteAsync(product, cancellationToken);
}

Two things to keep in mind:

  • The sequence is lazy. It must be enumerated within the lifetime of the IDbContext, and EF Core allows only one active stream per context at a time.
  • TrackChanges defaults to false and should stay that way for large reads — with tracking enabled the change tracker grows with every yielded entity, which defeats the purpose of streaming.

IdentityRepository<TEntity, TKey> / IdentityRepository<TEntity>

Extends GenericRepository<TEntity> with key-based GetById, GetByIds, and immediate bulk ExecuteDelete/ExecuteUpdate by ID. The non-generic overload defaults TKey to Guid.

EnumeratorRepository<TEntity, TKey> / EnumeratorRepository<TEntity>

Extends IdentityRepository with GetByName and GetByNames. The non-generic overload defaults TKey to int.

Usage

Inherit from the appropriate base class and inject IDbContext:

public class ProductRepository : IdentityRepository<Product>, IProductRepository
{
    public ProductRepository(IDbContext dbContext) : base(dbContext) { }

    public async Task<IReadOnlyList<Product>> GetByCategoryAsync(
        int categoryId, CancellationToken cancellationToken = default)
        => await GetListAsync(
            new()
            {
                Where = p => p.CategoryId == categoryId,
                OrderBy = q => q.OrderBy(p => p.Name),
            },
            cancellationToken);
}

public class CategoryRepository : EnumeratorRepository<ProductCategory>, ICategoryRepository
{
    public CategoryRepository(IDbContext dbContext) : base(dbContext) { }
    // GetByName / GetByNames inherited
}

DI registration:

services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();

Calling save changes is the responsibility of the caller (unit-of-work pattern):

repository.Create(entity);
await dbContext.SaveChangesAsync(cancellationToken);

Delete(entity) and Update(entity) are synchronous on purpose — they only mark the entity in the change tracker, and the database work happens in the save:

repository.Delete(entity);
await dbContext.SaveChangesAsync(cancellationToken);

Configuration base classes

Provider-agnostic IEntityTypeConfiguration<TEntity> base classes in BB84.EntityFrameworkCore.Repositories.Configurations. They apply key declaration, column ordering, the concurrency token, audit columns and — for enumerator entities — the name/description constraints, unique index and soft delete query filter. Everything they do is EF Core or EF Core Relational, so they work on PostgreSQL, SQLite, MySQL and Oracle.

Configuration class For entity type
IdentityConfiguration<TEntity, TKey, TToken> IIdentityEntity<TKey, TToken>
AuditedConfiguration<TEntity, TKey, TCreator, TEditor, TToken> IAuditedEntity<TKey, TCreator, TEditor, TToken>
FullAuditedConfiguration<TEntity, TKey, TCreator, TEditor, TToken> IFullAuditedEntity<TKey, TCreator, TEditor, TToken>
CompositeConfiguration<TEntity, TToken> ICompositeEntity<TToken>
AuditedCompositeConfiguration<TEntity, TCreator, TEditor, TToken> IAuditedCompositeEntity<TCreator, TEditor, TToken>
EnumeratorConfiguration<TEntity, TKey, TToken> IEnumeratorEntity<TKey, TToken>

Each ladder has narrower generic aliases: one that defaults the concurrency token to byte[], one that supplies the key and defaults the creator/editor to string, and one that supplies nothing and defaults the key to Guid (int for the enumerator) as well. There is deliberately no alias that defaults the key alone — pick the full form and name every parameter when the creator or editor type is not string.

The token rung is the one to reach for on a provider without an eight byte row version. It marks the property as a concurrency token generated on add or update, which suits rowversion and PostgreSQL's xmin; for a token the application rotates itself, override the generation strategy after the base.Configure(builder) call.

On SQL Server, use the same type names from BB84.EntityFrameworkCore.Repositories.SqlServer.Configurations instead — those derive from these and add clustering, NEWID() defaults and sysname audit columns.

Interceptors

Register on the DbContextOptionsBuilder:

services.AddSingleton<ICurrentUserProvider, EnvironmentUserProvider>();
services.AddSingleton(TimeProvider.System);

services.AddSingleton<SoftDeletableInterceptor>();
services.AddSingleton<TimeAuditedInterceptor>();
services.AddSingleton<UserAuditedInterceptor>();

services.AddDbContext<AppDbContext>((sp, options) =>
{
    options
        .UseSqlServer(connectionString) // any provider
        .AddInterceptors(
            sp.GetRequiredService<SoftDeletableInterceptor>(),
            sp.GetRequiredService<TimeAuditedInterceptor>(),
            sp.GetRequiredService<UserAuditedInterceptor>());
});

SoftDeletableInterceptor

Fires on SavingChanges/SavingChangesAsync. For every entity tracked as Deleted that implements ISoftDeletable, it sets IsDeleted = true and changes the state to Modified — preventing a physical DELETE from being issued.

Pair it with a configuration deriving from EnumeratorConfiguration<TEntity, TKey>, which applies the matching HasQueryFilter(e => !e.IsDeleted). Without that filter the flag is set but never read.

TimeAuditedInterceptor

Fires on SavingChanges/SavingChangesAsync. For every entity implementing ITimeAudited:

  • EntityState.Added → sets CreatedAt
  • EntityState.Modified → sets EditedAt

The clock comes from an optional TimeProvider constructor parameter, defaulting to TimeProvider.System. It is read once per save, so every entity in one save shares a timestamp. Supply a fake provider to freeze time:

FakeTimeProvider timeProvider = new(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
TimeAuditedInterceptor interceptor = new(timeProvider);

UserAuditedInterceptor

Fires on SavingChanges/SavingChangesAsync. For every entity implementing IUserAudited:

  • EntityState.Added → sets CreatedBy
  • EntityState.Modified → sets EditedBy

The identity comes from ICurrentUserProvider (in BB84.EntityFrameworkCore.Repositories.Abstractions), asked once per save. EnvironmentUserProvider ships as the default and reports MachineName\UserName, which suits desktop applications and services. A web application implements the interface over the current request instead:

public sealed class HttpCurrentUserProvider(IHttpContextAccessor accessor) : ICurrentUserProvider
{
    public string GetCurrentUser()
        => accessor.HttpContext?.User.Identity?.Name ?? "anonymous";
}

For audit columns that are not string, use the generic UserAuditedInterceptor<TUser> with a matching ICurrentUserProvider<TUser>.

All three interceptors run on save. The ExecuteDelete / ExecuteUpdate repository methods execute immediately and bypass the change tracker, so none of them fire for those.

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:

Package Downloads
BB84.EntityFrameworkCore.Repositories.SqlServer

This package contains the SqlServer specific tuning of the entity type configurations, column type extensions and DatabaseFacade extensions for the use of EntityFrameworkCore with SqlServer.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.1020 88 10/20/2026
5.0.916 390 9/16/2026
5.0.818 302 8/18/2026
4.5.725 223 7/25/2026
4.4.724 140 7/24/2026
4.4.625 229 6/25/2026
4.3.520 316 5/20/2026
4.3.513 198 5/13/2026
4.3.507 154 5/7/2026
4.3.419 249 4/19/2026
4.3.327 288 3/27/2026
4.2.322 179 3/22/2026
4.1.319 161 3/19/2026
4.1.222 246 2/22/2026
4.1.122 219 1/22/2026
4.0.1026 407 10/26/2025
4.0.916 471 9/16/2025
4.0.817 315 8/17/2025
3.1.617 393 6/17/2025
3.1.518 279 5/18/2025
Loading failed