ExtendedCRUD 1.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package ExtendedCRUD --version 1.0.1
                    
NuGet\Install-Package ExtendedCRUD -Version 1.0.1
                    
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="ExtendedCRUD" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ExtendedCRUD" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="ExtendedCRUD" />
                    
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 ExtendedCRUD --version 1.0.1
                    
#r "nuget: ExtendedCRUD, 1.0.1"
                    
#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 ExtendedCRUD@1.0.1
                    
#: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=ExtendedCRUD&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=ExtendedCRUD&version=1.0.1
                    
Install as a Cake Tool

CRUD+

CRUD repository framework, designed for rapid development with Entity Framework Core.

Note:

Haters will say: 'Tis but a wrapper! I will say: Indeed, most of it is. Now hush ye little scallywags.

Key Feature

Highly generic and composable CRUD repository pattern with rich extension points for Entity Framework Core.
This project allows you to quickly create repositories for any entity type, leveraging a clean interface structure and asynchronous patterns. You can easily extend, customize, and add advanced data access logic.

Prerequisites

Usage Example

// Example: Register and use the generic repository for your entity
public class User { /* ... */ }

// In your DI setup:
services.AddScoped<IExtendedCrud<User>, CrudRepository<User>>();

// Usage in your service:
public class UserService
{
    private readonly IExtendedCrud<User> _repo;
    public UserService(IExtendedCrud<User> repo) { _repo = repo; }

    public async Task CreateUser(User user)
    {
        await _repo.Create(user);
    }
}

Authorization

CrudControllerAPI<T> no longer hardcodes a role for reads or writes. Instead it uses two ASP.NET Core authorization policies that the consuming application must register:

  • CrudRead — required by GET (list and by-id)
  • CrudWrite — required by POST, PUT, DELETE

Registering them in the consuming app's Program.cs. To reproduce the library's previous default behavior (any authenticated user can read, only the admin role can write):

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CrudRead", policy => policy.RequireAuthenticatedUser());
    options.AddPolicy("CrudWrite", policy => policy.RequireRole("admin"));
});

Defining stricter, looser, or per-entity-type policies (e.g. by writing a custom IAuthorizationHandler/IAuthorizationRequirement that inspects the route/resource and registering it under the same policy names)

CrudControllerAPI<T>.Find accepts only single-property equality ({ "PropertyName": "Name", "Value": "Alpha" }) — an arbitrary predicate can't be safely bound from an HTTP request body, so broader filtering needs IQueryableCrudExtensions<T>.Find called directly from server-side code instead.

Excluding sensitive properties from find: the HTTP find endpoint resolves PropertyName against any public property of the entity by default, which can turn it into a value/existence oracle for a sensitive field (e.g. a password hash, an internal flag). Mark any property that shouldn't be filterable over HTTP with [CrudNotFilterable]:

using CRUD.Attributes;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    [CrudNotFilterable]
    public string PasswordHash { get; set; } = string.Empty;
}

A request filtering on an excluded property gets the exact same 400 Bad Request "unknown property" response as a genuinely nonexistent property name → the response never reveals which case applies. This restriction is HTTP-layer-only: IQueryableCrudExtensions<T>.Find called directly from your own server-side code with a compiled predicate is unaffected, since that predicate comes from trusted code, not raw client input.

Bulk operations

CreateRange/DeleteRange issue exactly one SaveChangesAsync for the whole batch instead of one per entity:

await repo.CreateRange(newUsers);
await repo.DeleteRange(usersToRemove);

Exposed over HTTP as POST api/[controller]/bulk and DELETE api/[controller]/bulk (the latter takes a JSON array of ids, resolved to entities via a single FindByIdsAsync query — not a per-id Read loop), both under the CrudWrite policy.

Both bulk operations (and FindByIdsAsync) reject — rather than silently truncate — a batch larger than 500 items, returning 400 Bad Request from the HTTP endpoints. Rejecting instead of clamping is deliberate here: silently dropping entities from a create/delete batch would be data loss with no client-visible signal, unlike clamping a read page size.

Even with that check in place, a client can still send an oversized request body before the count is ever inspected. Configure Kestrel:Limits:MaxRequestBodySize (or a [RequestSizeLimit] attribute on your concrete controller subclass) to bound this at the host level — this is host-specific configuration the library itself can't and shouldn't impose.

Deterministic paging and sorting

Pageineering now always returns a stable, defined order. Without an explicit orderBy it falls back to ordering by the entity's conventional int "Id" property instead of leaving the order up to the provider:

await repo.Pageineering(page: 2, pageSize: 20, orderBy: u => u.LastName, descending: true);

Concurrency

Update, Delete, and DeleteRange let DbUpdateConcurrencyException propagate instead of flattening it into false. CrudControllerAPI<T>.Put/Delete/DeleteRange catch it explicitly and return 409 Conflict rather than a generic 500.

This only ever activates if your entity has a configured concurrency token. DbUpdateConcurrencyException is EF Core's mechanism for detecting a conflicting concurrent write, and EF Core only checks for one when a property is marked as a concurrency token — a [Timestamp]/RowVersion property, or any property configured via .IsConcurrencyToken(). Without one, two concurrent updates to the same row silently overwrite each other ("last write wins") like normal EF Core behavior; the 409/conflict handling described above simply never triggers, because EF Core never throws.

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    [Timestamp]
    public byte[] RowVersion { get; set; } = [];
}

SQLite note: unlike SQL Server, SQLite has no native auto-generated rowversion column type. [Timestamp] alone marks the property as database-generated, which on SQLite means every insert fails with a NOT NULL constraint violation instead of ever reaching the concurrency check. If you're on SQLite, configure the property as not store-generated and bump its value yourself on every save:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property(u => u.RowVersion).ValueGeneratedNever();
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
    foreach (var entry in ChangeTracker.Entries<User>())
    {
        if (entry.State is EntityState.Added or EntityState.Modified)
        {
            entry.Entity.RowVersion = Guid.NewGuid().ToByteArray();
        }
    }
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

This exact pattern is used by the CRUDimplementation sample host (see AppDbContext.cs, Product.cs, Note.cs) and was verified end-to-end against a real SQLite database — a stale PUT genuinely returns 409 Conflict, not just a theoretical code path.

Soft delete

Entities that implement ISoftDeletable (bool IsDeleted { get; set; }) are flagged instead of removed by Delete. This library does not filter soft-deleted rows out of GetAll/Pageineering/Find itself — configure that in your own DbContext:

modelBuilder.Entity<MyEntity>().HasQueryFilter(e => !e.IsDeleted);

Read(id) respects that filter like any other query (a soft-deleted entity is "not found", same as a hard-deleted one). Use IEfCoreCrudExtensions<T>.ReadIncludingDeleted(id) when you deliberately need to see a soft-deleted row (e.g. an admin "show deleted" view or a recovery flow) — note that it bypasses all global query filters configured on the entity, not only a soft-delete one.

Roadmap

  • Extending this beautiful mess

  • Adding some Documentation

Author


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 was computed.  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 was computed.  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.1.0 101 8/8/2026
1.0.1 105 7/28/2026