FBC.DBRepository 0.5.0

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

FBC.DBRepository

NuGet Badge NuGet Downloads


A lightweight, generic, async-first repository pattern implementation for Entity Framework Core. Supports .NET 8, 9 and 10.

📖 Behaviour notes — the parts that surprise people, and why they are that way. 📋 Changelog — what changed in each version, and what it fixed.

Features

Feature Description
Generic Repository EFRepositoryBase<TEntity, TEntityId, TContext> works with any entity and DbContext
Async-First All operations are fully async with CancellationToken support (writes included since 0.5.0)
Soft Delete Built-in soft delete via IEntityHasSoftDeleteFeature (automatically filtered from queries)
Restore Restore soft-deleted entities back to active state via RestoreAsync
Audit Tracking Automatic CreatedDateUTC, UpdatedDateUTC, DeletedDateUTC timestamps
User Audit Optional CreatedBy, UpdatedBy, DeletedBy tracking via interfaces
Pagination Built-in pagination with PaginateResponseModel<T>
Entity Validation Pre-operation async validation hooks via IEntityHasCheckDataFor<TEntity, TId>
Role-Based Access Control Entity-level role checks via IEntityRequiresRole + ICurrentUserProvider
Transactions BeginTransactionAsync / CommitTransactionAsync / RollbackTransactionAsync
Bulk Operations ApplyOperationRange for batch create, update, or delete
Bulk Query GetByIdsAsync for retrieving multiple entities by their IDs in a single query
Auto DI Registration RegisterRepositories() scans assemblies and registers repositories automatically
Deterministic Registration Ambiguous registrations are refused instead of resolved at random
Filtered Queryable GetActiveQueryable() for a raw query that still excludes soft-deleted rows

Installation

dotnet add package FBC.DBRepository

Quick Start

1. Define Your Entity

public class Product : Entity<int, Product>,
    IEntityHasSoftDeleteFeature,
    IEntityHasCreatedDate,
    IEntityHasUpdatedDate,
    IEntityHasCheckDataFor<Product, int>
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // Soft delete
    public bool IsDeleted { get; set; }

    // Audit dates
    public DateTime CreatedDateUTC { get; set; }
    public DateTime? UpdatedDateUTC { get; set; }

    // Validation hook
    public async Task CheckDataForAsync(EntityOperation operation, bool alsoValidate, IAsyncRepository<Product, int> repository)
    {
        if (alsoValidate)
        {
            if (string.IsNullOrWhiteSpace(Name))
                throw new ArgumentException("Product name is required.");

            if (operation == EntityOperation.Create || operation == EntityOperation.Update)
            {
                if (await repository.AnyAsync(p => p.Name == Name && !p.Id.Equals(Id)))
                    throw new ArgumentException("Product name must be unique.");
            }
        }
    }
}

2. Create Your Repository

public class ProductRepository : EFRepositoryBase<Product, int, AppDbContext>
{
    public ProductRepository(AppDbContext context) : base(context) { }
}

3. Register in DI

// Program.cs
builder.Services.RegisterRepositories(typeof(ProductRepository).Assembly);

4. Use in Your Services

public class ProductService(IAsyncRepository<Product, int> repo)
{
    public async Task<Product> CreateAsync(string name, decimal price)
    {
        var product = new Product { Name = name, Price = price };
        return await repo.ApplyOperation(EntityOperation.Create, product, alsoValidate: true);
    }

    public async Task<PaginateResponseModel<Product>> GetPagedAsync(int page, int size)
        => await repo.GetListAsync(pageNumber: page, itemsPerPage: size);

    public async Task SoftDeleteAsync(int id)
    {
        var product = await repo.GetByIdAsync(id)
            ?? throw new KeyNotFoundException();
        await repo.ApplyOperation(EntityOperation.Delete, product, alsoValidate: false);
    }

    public async Task RestoreAsync(int id)
        => await repo.RestoreAsync(id);
}

Core Concepts

Entity Base Class

All entities must inherit from Entity<TId, TEntity>:

public class Order : Entity<long, Order>
{
    public string OrderNumber { get; set; } = string.Empty;
    public decimal Total { get; set; }
}

The base class provides:

  • Id property (primary key with [Key] attribute)
  • Automatic CreatedDateUTC initialization (if the entity implements IEntityHasCreatedDate)
  • Internal validation and audit pipeline via CheckEntityDataForAsync

Entity Operations

EntityOperation enum controls which operation is being performed:

public enum EntityOperation
{
    Create,   // Insert a new entity
    Update,   // Update an existing entity
    Delete    // Soft delete or permanent delete
}

Use ApplyOperation to perform any CRUD operation:

// Create
await repo.ApplyOperation(EntityOperation.Create, entity, alsoValidate: true);

// Update
await repo.ApplyOperation(EntityOperation.Update, entity, alsoValidate: true);

// Soft delete (sets IsDeleted = true)
await repo.ApplyOperation(EntityOperation.Delete, entity, alsoValidate: false);

// Permanent delete (removes from database)
await repo.ApplyOperation(EntityOperation.Delete, entity, alsoValidate: false, deletePermanent: true);

Features in Detail

Soft Delete

Implement IEntityHasSoftDeleteFeature to enable soft delete:

public class Customer : Entity<int, Customer>, IEntityHasSoftDeleteFeature
{
    public string Name { get; set; } = string.Empty;
    public bool IsDeleted { get; set; }
}

How it works:

  • When you call ApplyOperation(Delete, entity, ...) without deletePermanent: true, the entity's IsDeleted is set to true instead of being removed from the database.
  • All queries via GetAsync, GetListAsync, GetByIdAsync, AnyAsync, CountAsync automatically exclude soft-deleted records.
  • To include deleted records in queries, pass includeDeletedRecords: true.
// Normal query (excludes deleted)
var activeCustomers = await repo.GetListAsync();

// Include deleted records
var allCustomers = await repo.GetListAsync(includeDeletedRecords: true);

⚠️ GetQueryable() is not filtered — it returns deleted rows too. Use GetActiveQueryable() (0.5.0) when you want the raw queryable without them. Details

Restore (Soft Delete Recovery)

Restore a soft-deleted entity back to active state:

// Restore by ID
var restoredCustomer = await repo.RestoreAsync(customerId);

What RestoreAsync does:

  • Finds the entity (including deleted records)
  • Checks the roles required for EntityOperation.Delete (since 0.5.0)
  • Runs CheckDataForAsync — only if you pass alsoValidate: true (since 0.5.0)
  • Sets IsDeleted to false
  • Clears DeletedDateUTC and DeletedBy (if implemented)
  • Updates UpdatedDateUTC and UpdatedBy (if implemented)
  • Saves the changes
await repo.RestoreAsync(id, alsoValidate: true);   // validate before restoring

⚠️ Validation is opt-in on purpose. The row is loaded without any include, so an entity that validates its child collections would fail every restore. Ask for it when the entity validates only its own columns. Why, and why the role is Delete rather than Update

Audit Tracking

Implement audit interfaces to automatically track timestamps and users:

public class Invoice : Entity<int, Invoice>,
    IEntityHasCreatedDate,   // CreatedDateUTC set on Create
    IEntityHasUpdatedDate,   // UpdatedDateUTC set on Update
    IEntityHasDeletedDate,   // DeletedDateUTC set on Delete
    IEntityHasCreatedBy,     // CreatedBy set on Create
    IEntityHasUpdatedBy,     // UpdatedBy set on Update
    IEntityHasDeletedBy      // DeletedBy set on Delete
{
    public string InvoiceNumber { get; set; } = string.Empty;

    // Audit timestamps
    public DateTime CreatedDateUTC { get; set; }
    public DateTime? UpdatedDateUTC { get; set; }
    public DateTime? DeletedDateUTC { get; set; }

    // Audit users
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }
    public string? DeletedBy { get; set; }
}

Automatic behavior per operation:

Operation Fields Set
Create CreatedDateUTC = UtcNow, CreatedBy = current user
Update UpdatedDateUTC = UtcNow, UpdatedBy = current user
Soft Delete IsDeleted = true, DeletedDateUTC = UtcNow, DeletedBy = current user
Permanent Delete Only CheckDataForAsync is called (entity is removed)

User Audit (GetCurrentUser)

To enable user tracking, either override GetCurrentUser() or inject ICurrentUserProvider:

Option 1: Override in repository

public class InvoiceRepository : EFRepositoryBase<Invoice, int, AppDbContext>
{
    private readonly IHttpContextAccessor _http;

    public InvoiceRepository(AppDbContext context, IHttpContextAccessor http) : base(context)
    {
        _http = http;
    }

    protected override string? GetCurrentUser()
        => _http.HttpContext?.User?.FindFirst("UserId")?.Value;
}

Option 2: Use ICurrentUserProvider (recommended)

// Implement the provider
public class CurrentUserProvider(IHttpContextAccessor http) : ICurrentUserProvider
{
    public string? GetUserId()
        => http.HttpContext?.User?.FindFirst("UserId")?.Value;

    public string? GetUserName()
        => http.HttpContext?.User?.Identity?.Name;

    public string[] GetRoles()
        => http.HttpContext?.User?.Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => c.Value)
            .ToArray() ?? [];

    public bool IsInRole(string role)
        => http.HttpContext?.User?.IsInRole(role) ?? false;
}

// Register in DI
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();

// Use in repository (provider is injected via constructor)
public class InvoiceRepository : EFRepositoryBase<Invoice, int, AppDbContext>
{
    public InvoiceRepository(AppDbContext context, ICurrentUserProvider provider)
        : base(context, provider) { }
}

When ICurrentUserProvider is passed to the repository, GetCurrentUser() automatically returns the provider's GetUserId() value. You can still override GetCurrentUser() for custom logic.

Entity Validation (CheckDataForAsync)

Implement IEntityHasCheckDataFor<TEntity, TId> to add pre-operation validation and data adjustment:

public class Category : Entity<int, Category>,
    IEntityHasCheckDataFor<Category, int>
{
    public string Name { get; set; } = string.Empty;
    public string? Slug { get; set; }

    public async Task CheckDataForAsync(EntityOperation operation, bool alsoValidate, IAsyncRepository<Category, int> repository)
    {
        // Data adjustment (always runs)
        Slug = Name.ToLower().Replace(" ", "-");

        // Validation (only when alsoValidate is true)
        if (alsoValidate)
        {
            if (string.IsNullOrWhiteSpace(Name))
                throw new ArgumentException("Category name is required.");

            switch (operation)
            {
                case EntityOperation.Create:
                case EntityOperation.Update:
                    if (await repository.AnyAsync(c => c.Name == Name && !c.Id.Equals(Id)))
                        throw new ArgumentException("Category name must be unique.");
                    break;

                case EntityOperation.Delete:
                    // GetActiveQueryable(), not GetQueryable(): the raw one includes soft-deleted
                    // products, so a category whose products were all deleted would refuse to go.
                    var hasProducts = await repository.GetActiveQueryable()
                        .Where(c => c.Id.Equals(Id))
                        .SelectMany(c => c.Products)
                        .AnyAsync();
                    if (hasProducts)
                        throw new InvalidOperationException("Cannot delete a category with products.");
                    break;
            }
        }
    }
}

Key points:

  • CheckDataForAsync is called automatically by ApplyOperation before the database operation.
  • Use it for both data adjustment (normalizing values, syncing fields) and validation (uniqueness checks, business rules).
  • The repository parameter gives you access to the full repository capabilities (AnyAsync, GetAsync, GetListAsync, CountAsync, etc.) for cross-entity checks.
  • If you need raw IQueryable<TEntity> access, you can call repository.GetQueryable().
  • Recommended: Prefer using the repository methods (AnyAsync, GetAsync, GetListAsync) over GetQueryable() — especially if your entity implements IEntityHasSoftDeleteFeature, because repository methods automatically filter out soft-deleted records from every query.
  • The alsoValidate parameter lets you skip validation when you only want data adjustment.

Role-Based Access Control (RBAC)

Enforce entity-level role checks using IEntityRequiresRole + ICurrentUserProvider:

Step 1: Define required roles on your entity

public class Device : Entity<int, Device>, IEntityRequiresRole
{
    public string Name { get; set; } = string.Empty;

    public string[] GetRequiredRolesFor(EntityOperation operation) => operation switch
    {
        EntityOperation.Create => ["Edit.Devices"],
        EntityOperation.Update => ["Edit.Devices"],
        EntityOperation.Delete => ["Edit.Devices", "SysAdmin"],
        _ => []
    };
}

Step 2: Ensure ICurrentUserProvider is injected into your repository

public class DeviceRepository : EFRepositoryBase<Device, int, AppDbContext>
{
    public DeviceRepository(AppDbContext context, ICurrentUserProvider provider)
        : base(context, provider) { }
}

How it works:

  • Before every ApplyOperation / ApplyOperationRange, the repository checks if the entity implements IEntityRequiresRole.
  • If it does and an ICurrentUserProvider is available, it calls GetRequiredRolesFor(operation).
  • If the user has any of the required roles, the operation proceeds.
  • If the user has none of the required roles, an UnauthorizedAccessException is thrown.
  • If no ICurrentUserProvider is injected, role checks are skipped (backward compatible).

This provides defense-in-depth: even if an API endpoint accidentally lacks authorization attributes, the repository layer catches unauthorized operations.

⚠️ With no ICurrentUserProvider, nothing is checked and nothing is said. That default has to stay — seeders and migrations have no user — but it is fail-open. Since 0.5.0 CheckRoleRequirement is protected virtual, so an application can override it and refuse. The override, and why the default is what it is

Pagination

All list queries return PaginateResponseModel<T>:

// Paginated query (page 0, 10 items per page)
var result = await repo.GetListAsync(
    predicate: p => p.IsActive,
    orderBy: q => q.OrderBy(p => p.Name),
    pageNumber: 0,
    itemsPerPage: 10,
    enableTracking: false
);

// Access pagination metadata
int totalItems = result.TotalFilteredCount;
int totalPages = result.TotalPages;
bool hasNext = result.HasNext;
bool hasPrevious = result.HasPrevious;
IList<Product> items = result.Items;

Special cases:

  • itemsPerPage = 0 returns all records in a single page, and pageNumber is ignored.
  • pageNumber is zero-based (0 = first page).

⚠️ Cap the page size yourself. Nothing here limits it. An unclamped size arriving from a request reads the whole table into memory in one call. Why the library does not cap it

Transactions

Wrap multiple operations in a single atomic unit:

public async Task TransferDeviceAsync(int deviceId, int newGroupId)
{
    await repo.BeginTransactionAsync();
    try
    {
        var device = await repo.GetByIdAsync(deviceId)
            ?? throw new KeyNotFoundException();

        device.DeviceGroupId = newGroupId;
        await repo.ApplyOperation(EntityOperation.Update, device, alsoValidate: true);

        // More operations within the same transaction...

        await repo.CommitTransactionAsync();
    }
    catch
    {
        await repo.RollbackTransactionAsync();
        throw;
    }
}

Key points:

  • CommitTransactionAsync saves all changes; RollbackTransactionAsync discards them.
  • An InvalidOperationException is thrown if you try to begin a second transaction or commit/rollback without an active transaction.

⚠️ A transaction belongs to the DbContext, not to the repository. Repositories sharing a context share one transaction, and only the one that began it can commit or roll it back — which is exactly what you want for writing two entities atomically. How that works, and the messages you get when it does not

Bulk Operations

Use ApplyOperationRange for batch operations:

// Batch create
var newProducts = new List<Product> { product1, product2, product3 };
await repo.ApplyOperationRange(EntityOperation.Create, newProducts, alsoValidate: true);

// Batch delete
var toDelete = existingProducts.Where(p => p.IsExpired).ToList();
await repo.ApplyOperationRange(EntityOperation.Delete, toDelete, alsoValidate: false, deletePermanent: true);

Bulk Query (GetByIdsAsync)

Retrieve multiple entities by their IDs in a single database query:

var ids = new[] { 1, 5, 12, 23 };
var products = await repo.GetByIdsAsync(ids, enableTracking: false);

Supports all standard options: include, enableTracking, includeDeletedRecords.

Eager Loading (Include)

Use the include parameter to load related entities:

// Single include
var device = await repo.GetByIdAsync(id,
    include: q => q.Include(d => d.DeviceType));

// Multiple includes with ThenInclude
var device = await repo.GetByIdAsync(id,
    include: q => q
        .Include(d => d.DeviceType)
        .Include(d => d.DeviceGroup)
        .Include(d => d.Addresses).ThenInclude(a => a.AddrType));

Change Tracking Control

Disable EF Core change tracking for read-only queries to improve performance:

// Read-only (no tracking)
var products = await repo.GetListAsync(enableTracking: false);

// Tracked (needed if you plan to update)
var product = await repo.GetByIdAsync(id, enableTracking: true);

Auto DI Registration

RegisterRepositories scans assemblies and registers all repository implementations:

// Program.cs
builder.Services.RegisterRepositories(typeof(ProductRepository).Assembly);

This registers both:

  • IAsyncRepository<Product, int> → ProductRepository (base interface)
  • IProductRepository → ProductRepository (custom interface, if exists)

Custom interface example:

public interface IProductRepository : IAsyncRepository<Product, int>
{
    Task<IList<Product>> GetExpensiveProductsAsync(decimal minPrice);
}

public class ProductRepository : EFRepositoryBase<Product, int, AppDbContext>, IProductRepository
{
    public ProductRepository(AppDbContext context) : base(context) { }

    public async Task<IList<Product>> GetExpensiveProductsAsync(decimal minPrice)
    {
        var result = await GetListAsync(predicate: p => p.Price >= minPrice);
        return result.Items;
    }
}

Injecting the concrete type. A repository with no interface of its own is only registered as IAsyncRepository<Product, int>. To inject ProductRepository itself (0.5.0):

builder.Services.RegisterRepositories(includeConcreteTypes: true, typeof(ProductRepository).Assembly);

Also since 0.5.0: two unrelated implementations of one repository interface are refused instead of picked at random, a derived repository wins over its base, open generic repositories are skipped, and an assembly whose types fail to load no longer stops the scan. What each of those means


API Reference

IAsyncRepository<TEntity, TEntityId>

Method Description
GetByIdAsync(id, ...) Get a single entity by its primary key
GetByIdsAsync(ids, ...) Get multiple entities by their primary keys in a single query
GetAsync(predicate, ...) Get a single entity matching a predicate
GetListAsync(predicate, orderBy, include, pageNumber, itemsPerPage, ...) Get a paginated list of entities
GetListAsync(query, ...) Get a paginated list from a pre-built IQueryable
AnyAsync(predicate, ...) Check if any entity matches a predicate
CountAsync(predicate, ...) Count entities matching a predicate
GetQueryable() Raw IQueryable — not filtered for soft delete
GetActiveQueryable() The same queryable with the soft-delete filter applied (0.5.0)
ApplyOperation(operationType, entity, alsoValidate, deletePermanent) Create, update, or delete a single entity
ApplyOperation(…, cancellationToken) The same, with a cancellation token (0.5.0)
ApplyOperationRange(operationType, entities, alsoValidate, deletePermanent) Batch create, update, or delete multiple entities
ApplyOperationRange(…, cancellationToken) The same, with a cancellation token (0.5.0)
RestoreAsync(id) Restore a soft-deleted entity — checks roles since (0.5.0)
RestoreAsync(id, alsoValidate) The same, optionally validating first (0.5.0)
BeginTransactionAsync() Begin a database transaction on the repository's DbContext
CommitTransactionAsync() Commit the transaction this repository began
RollbackTransactionAsync() Roll back the transaction this repository began

EFRepositoryBase adds two members that are not on the interface: CurrentTransaction and HasActiveTransaction (0.5.0), which report the transaction running on the DbContext whoever started it.

Entity Marker Interfaces

Interface Property Auto-Set On
IEntityHasSoftDeleteFeature bool IsDeleted Delete (soft)
IEntityHasCreatedDate DateTime CreatedDateUTC Create
IEntityHasUpdatedDate DateTime? UpdatedDateUTC Update
IEntityHasDeletedDate DateTime? DeletedDateUTC Delete (soft)
IEntityHasCreatedBy string? CreatedBy Create
IEntityHasUpdatedBy string? UpdatedBy Update
IEntityHasDeletedBy string? DeletedBy Delete (soft)

Behavior Interfaces

Interface Purpose
IEntityHasCheckDataFor<TEntity, TId> Pre-operation data adjustment and validation
IEntityRequiresRole Entity-level role-based access control
ICurrentUserProvider Provides current user identity and roles
IQuery<T> Declares GetQueryable(); IAsyncRepository derives from it

Exceptions

Type Thrown when
UnauthorizedAccessException The current user lacks a role the entity requires
KeyNotFoundException RestoreAsync was given an id that does not exist
InvalidOperationException A programming error: transaction misuse, ambiguous registration, restoring an entity without soft delete
EntityValidationException Yours to throw from CheckDataForAsync (0.5.0). Nothing in the library throws it — it exists so applications and endpoints can agree on one type. Why not ValidationException

PaginateResponseModel<T>

Property Type Description
Items IList<T> Items for the current page
PageIndex int Zero-based current page index
ItemsPerPage int Number of items per page
TotalFilteredCount int Total matching items across all pages
TotalPages int Total number of pages
HasPrevious bool True if there is a previous page
HasNext bool True if there is a next page

Breaking Changes

See the changelog for the full history and the reasoning behind each change.

V.0.5.0: behaviour changes — no source changes required

Nothing needs editing to move from 0.4.0, but three things behave differently. Each one turns a silent wrong answer into a visible one, which is the point.

Change What used to happen What happens now
RestoreAsync checks roles Any caller could restore, even one who could not have deleted the row Requires the roles GetRequiredRolesFor(EntityOperation.Delete) returns
RestoreAsync can validate A row that had become invalid while deleted came back, and the database constraint threw Opt in with alsoValidate: true; the default is unchanged
Registration ambiguity Two unrelated implementations: one won, chosen by Reflection's type order Throws and names both. Inheritance chains are not ambiguous — the derived type wins

The first only affects code that calls RestoreAsync on entities implementing IEntityRequiresRole. The second is opt-in. The third only affects a solution that already had two unrelated repositories for one interface, where the registration was already arbitrary.

V.0.4.0: IEntityHasCheckDataFor<TEntity, TId>.CheckDataForAsync — parameter change

The third parameter of CheckDataForAsync has been changed from IQueryable<TEntity> to IAsyncRepository<TEntity, TId>.

Before:

public async Task CheckDataForAsync(EntityOperation operation, bool alsoValidate, IQueryable<Product> query)
{
    if (await query.AnyAsync(p => p.Name == Name && !p.Id.Equals(Id)))
        throw new ArgumentException("Product name must be unique.");
}

After:

public async Task CheckDataForAsync(EntityOperation operation, bool alsoValidate, IAsyncRepository<Product, int> repository)
{
    if (await repository.AnyAsync(p => p.Name == Name && !p.Id.Equals(Id)))
        throw new ArgumentException("Product name must be unique.");
}

Migration guide:

  • Replace IQueryable<TEntity> with IAsyncRepository<TEntity, TId> in your CheckDataForAsync implementations.
  • If you were using query.AnyAsync(...), query.Where(...), etc., prefer using the equivalent repository methods (repository.AnyAsync(...), repository.GetAsync(...), repository.GetListAsync(...)) instead. This is especially recommended if your entity implements IEntityHasSoftDeleteFeature, because repository methods automatically exclude soft-deleted records from every query.
  • If you still need raw IQueryable<TEntity> access, call repository.GetQueryable().

Requirements

  • .NET 8.0, 9.0, or 10.0
  • Entity Framework Core (version matched to your .NET target)

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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
0.5.0 182 9/6/2026
0.4.0 398 3/29/2026
0.3.0 122 3/22/2026
0.2.0 119 3/21/2026

0.5.0 — RestoreAsync now checks the roles required for Delete, and can validate on request. Automatic registration refuses ambiguity instead of choosing at random, skips open generics, and survives an assembly that fails to load. New: GetActiveQueryable(), EntityValidationException, cancellation tokens on the write path, RegisterRepositories(includeConcreteTypes: true). No source changes are needed to upgrade. Full notes: https://github.com/fatihbahceci/FBC.DBRepository/blob/main/CHANGELOG.md