BB84.EntityFrameworkCore.Repositories
5.0.916
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
<PackageReference Include="BB84.EntityFrameworkCore.Repositories" Version="5.0.916" />
<PackageVersion Include="BB84.EntityFrameworkCore.Repositories" Version="5.0.916" />
<PackageReference Include="BB84.EntityFrameworkCore.Repositories" />
paket add BB84.EntityFrameworkCore.Repositories --version 5.0.916
#r "nuget: BB84.EntityFrameworkCore.Repositories, 5.0.916"
#:package BB84.EntityFrameworkCore.Repositories@5.0.916
#addin nuget:?package=BB84.EntityFrameworkCore.Repositories&version=5.0.916
#tool nuget:?package=BB84.EntityFrameworkCore.Repositories&version=5.0.916
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.DatabaseFacadeExtensionswent 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 aQuery<TEntity>becomes a query. PassforCount: trueto 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 howGetByIdandGetByNameadd 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. TrackChangesdefaults tofalseand 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→ setsCreatedAtEntityState.Modified→ setsEditedAt
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→ setsCreatedByEntityState.Modified→ setsEditedBy
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 | 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
- BB84.EntityFrameworkCore.Entities (>= 5.0.916)
- BB84.EntityFrameworkCore.Repositories.Abstractions (>= 5.0.916)
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 |