A convention-based CRUD framework for .NET 10. Define entities, get endpoints.
- Auto-mapped REST endpoints (List, Get, Create, Update, Delete)
- Soft delete with cascade and restore
- Multi-tenant isolation (
ITenantContext+ 5 built-in resolvers + 3-layer cross-tenant protection +IDataFilter<T>for runtime filter control) - Audit trail (
[Audited]+UseAuditTrail()opt-in,[AuditIgnore]per-property,[Hashed]auto-masked) - Per-operation authorization (
Authorize()builder — role-based, permission-based, convention) - Custom endpoints via
.WithCustomEndpoints()under same route group - Optimistic concurrency
- Lifecycle hooks (
ICrudHooks<T>) + global hooks (IGlobalCrudHook) for cross-cutting concerns - Validation (FluentValidation priority, DataAnnotation fallback)
- State machine transitions (
IStateMachine<TState>) - Master-child relationships (fluent
.WithChild()or declarative[ChildOf]) - Idempotency via request header
- Bulk operations (
/bulk-delete,/bulk-update) - ReadOnly entities (List + Get only)
- CSV import/export (
[Exportable],[Importable], per-property control) - Auto-registration —
UseCrudKit()scans assemblies for[CrudEntity]types and registers endpoints automatically [CreateDtoFor]/[UpdateDtoFor]— manual DTOs discovered by auto-registrationOptional<T>for partial updates (distinguishes null from missing)- Property attributes:
[Hashed],[SkipResponse],[SkipUpdate],[Protected],[Unique],[Searchable] - Filter/sort control per entity and property (
[NotFilterable],[NotSortable]) - Modular monolith support (
IModulewith assembly scan, multi-DbContext auto-resolution) - Multi-database dialect (SQLite, PostgreSQL, SQL Server, MySQL/MariaDB) — auto-detected
- Entity base class hierarchy (
Entity,AuditableEntity,FullAuditableEntity— with<TUser>variants) - Enum properties stored as strings automatically
- Structured error responses (409 concurrency, dev/prod stack trace toggle)
- Domain events (
IDomainEvent,IHasDomainEvents,IDomainEventHandler<T>, automatic dispatch after SaveChanges) - Auto-sequence (
[AutoSequence]with{year},{month},{seq:N}tokens, per-tenant isolation,ISequenceCustomizer<T>) - AggregateRoot hierarchy (
AggregateRoot,AuditableAggregateRoot,FullAuditableAggregateRootwith domain event support) - Value object flattening (
[ValueObject]+[Flatten]for flat DTO properties) - Smart cascade restore (DeleteBatchId — only restores children deleted in same batch)
- Custom index support (
[CrudIndex]with tenant-aware composite indexes, custom names) IEndpointConfigurer<T>— auto-discovered custom endpoint configuration- Entity-as-DTO —
MapCrudEndpoints<T>()without DTOs, entity used directly [ResponseDtoFor]— custom response DTOsCrudKitDbContextDependencies— simplified 2-parameter DbContext constructorUseModuleSchema()— cross-provider schema support (PostgreSQL, SQL Server = schema; MySQL, SQLite = skipped)- Reflection metadata caching for performance
- Hook-aware bulk operations (entities loaded, hooks triggered)
- System fields hidden from responses (TenantId, DeleteBatchId, DomainEvents)
1. Register services
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlite("Data Source=app.db"));
builder.Services.AddCrudKitEf<AppDbContext>();
builder.Services.AddCrudKit(opts =>
{
opts.DefaultPageSize = 25;
opts.MaxPageSize = 100;
opts.UseAuditTrail();
opts.UseMultiTenancy().ResolveTenantFromHeader("X-Tenant-Id");
});2. Map endpoints
var app = builder.Build();
app.UseCrudKit(); // auto-registers all [CrudEntity] types
// Or register manually for full control:
// app.MapCrudEndpoints<Product, CreateProduct, UpdateProduct>();
app.Run();3. Define an entity
[CrudEntity]
[Audited]
[RequirePermissions] // auto-convention: products:read, products:create, ...
public class Product : FullAuditableEntity
{
[Required, MaxLength(200), Searchable]
public string Name { get; set; } = string.Empty;
[Range(0.01, 999_999.99)]
public decimal Price { get; set; }
[Unique, SkipUpdate]
public string Sku { get; set; } = string.Empty;
[Hashed, SkipResponse]
public string InternalToken { get; set; } = string.Empty;
}Generated endpoints
| Method | Route | Description |
|---|---|---|
| GET | /api/products |
List (paginated, filtered, sorted) |
| GET | /api/products/{id} |
Get by ID |
| POST | /api/products |
Create |
| PUT | /api/products/{id} |
Update (partial via Optional<T>) |
| DELETE | /api/products/{id} |
Soft-delete (ISoftDeletable) |
| DELETE | /api/products/{id}/purge |
Permanently delete a single soft-deleted record |
| DELETE | /api/products/purge?olderThan=30 |
Bulk purge soft-deleted records older than N days |
| POST | /api/products/{id}/restore |
Restore soft-deleted record |
| POST | /api/products/{id}/transition/{action} |
State transition (IStateMachine<TState> only) |
Mappers are optional. Without them, CrudKit uses reflection for DTO ↔ entity mapping and serializes entities directly for responses. Register IResponseMapper<T, TResponse> for custom response shapes.
| Class | Provides |
|---|---|
Entity |
Guid Id |
Entity<TKey> |
Custom key type (e.g. long, int) |
AuditableEntity |
Id + CreatedAt, UpdatedAt |
AuditableEntityWithUser<TUser> |
+ CreatedById, UpdatedById (auto-set from ICurrentUser) + navigations |
FullAuditableEntity |
AuditableEntity + DeletedAt (implements ISoftDeletable) |
FullAuditableEntityWithUser<TUser> |
+ CreatedById, UpdatedById, DeletedById (auto-set) + navigations |
AggregateRoot |
Id + domain events (IHasDomainEvents) |
AuditableAggregateRoot |
+ CreatedAt, UpdatedAt + domain events |
AuditableAggregateRootWithUser<TUser> |
+ CreatedById, UpdatedById (auto-set) + domain events |
FullAuditableAggregateRoot |
+ DeletedAt, DeleteBatchId + domain events |
FullAuditableAggregateRootWithUser<TUser> |
+ CreatedById, UpdatedById, DeletedById (auto-set) + domain events |
// Lookup table — Guid Id only
public class Currency : Entity { }
// Timestamps + soft delete
[CrudEntity]
[RequireAuth]
[AuthorizeOperation("Delete", "admin")]
public class Order : FullAuditableEntity
{
public string CustomerName { get; set; } = string.Empty;
public decimal Total { get; set; }
}
// Timestamps + user tracking + soft delete
[CrudEntity]
[RequireRole("admin")]
public class Invoice : FullAuditableEntityWithUser<AppUser>
{
public string InvoiceNumber { get; set; } = string.Empty;
public decimal Total { get; set; }
}
// Custom key type
public class LegacyProduct : AuditableEntity<long> { }
public class LegacyOrder : FullAuditableEntityWithUser<long, AppUser, int> { }Use [AutoSequence] on a string property to generate formatted sequential numbers automatically. Supports {year}, {month}, {seq:N} tokens, per-tenant isolation, and custom logic via ISequenceCustomizer<T>.
[CrudEntity]
public class Invoice : FullAuditableAggregateRoot, IMultiTenant, IStateMachine<InvoiceStatus>
{
[AutoSequence("INV-{year}-{seq:5}")]
public string InvoiceNumber { get; set; } = "";
[Protected]
public InvoiceStatus Status { get; set; } = InvoiceStatus.Draft;
public string TenantId { get; set; } = "";
public static IReadOnlyList<(InvoiceStatus From, InvoiceStatus To, string Action)> Transitions => [...];
}
// → INV-2026-00001 auto-generated, per-tenant sequencebuilder.Services.AddCrudKitEf<AppDbContext>();
builder.Services.AddCrudKit(opts =>
{
// Pagination
opts.DefaultPageSize = 25; // Default: 20
opts.MaxPageSize = 100; // Default: 100
// Routing
opts.ApiPrefix = "/api"; // Default: "/api"
// Bulk operations
opts.BulkLimit = 1_000; // Default: 1,000
// Idempotency
opts.EnableIdempotency = true; // Default: false
// Audit trail — opt entities in with [Audited]
opts.UseAuditTrail();
// or with custom writer:
opts.UseAuditTrail<ElasticAuditWriter>()
.EnableAuditFailedOperations();
// Import / Export
opts.UseExport(); // All entities exportable (opt-out with [NotExportable])
opts.MaxExportRows = 50_000; // Default: 50,000
opts.UseImport(); // All entities importable (opt-out with [NotImportable])
opts.MaxImportFileSize = 10 * 1024 * 1024; // Default: 10 MB
// Enum storage
opts.UseEnumAsString(); // Store enums as strings in DB
// Multi-tenancy
opts.UseMultiTenancy()
.ResolveTenantFromHeader("X-Tenant-Id")
.RejectUnresolvedTenant()
.CrossTenantPolicy(p => p.Allow("superadmin"));
// Global hooks
opts.UseGlobalHook<SearchIndexHook>();
// Module discovery
opts.ScanModulesFromAssembly = typeof(Program).Assembly;
});| Method | Returns | Description |
|---|---|---|
UseAuditTrail() |
AuditTrailOptions |
Enable audit trail, opt entities in with [Audited] |
UseAuditTrail<T>() |
AuditTrailOptions |
Same, with custom IAuditWriter implementation |
UseExport() |
CrudKitApiOptions |
Enable CSV export globally |
UseImport() |
CrudKitApiOptions |
Enable CSV import globally |
UseEnumAsString() |
CrudKitApiOptions |
Store all enum properties as strings |
UseMultiTenancy() |
MultiTenancyOptions |
Enable multi-tenancy, chain resolver method |
UseGlobalHook<T>() |
CrudKitApiOptions |
Register a global IGlobalCrudHook |
UseDomainEvents() |
CrudKitApiOptions |
Enable domain event dispatching after SaveChanges |
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<ICurrentUser, JwtCurrentUser>();
builder.Services.AddCrudKitEf<AppDbContext>();
builder.Services.AddCrudKit(opts =>
{
opts.UseMultiTenancy()
.ResolveTenantFromClaim("tenant_id")
.RejectUnresolvedTenant()
.CrossTenantPolicy(p => p.Allow("superadmin"));
});
var app = builder.Build();
app.UseCrudKit(); // auto-registers all [CrudEntity] types
app.Run();Entities implementing IMultiTenant are automatically scoped to the resolved tenant. Inject IDataFilter<ISoftDeletable> to temporarily disable the soft-delete filter within a request:
public class OrderService
{
private readonly IDataFilter<ISoftDeletable> _softDeleteFilter;
private readonly IRepo<Order> _repo;
public async Task<List<Order>> GetDeletedOrdersAsync()
{
using (_softDeleteFilter.Disable())
{
return (await _repo.List(new ListParams(), default)).Data;
}
}
}src/
├── CrudKit.Core/ # Attributes, interfaces, models
├── CrudKit.EntityFrameworkCore/ # EF Core integration, repository, query
├── CrudKit.Api/ # Minimal API layer, endpoint mapping, filters
└── CrudKit.Identity/ # ASP.NET Identity integration (CrudKitIdentityDbContext)
tests/
├── CrudKit.Core.Tests/
├── CrudKit.EntityFrameworkCore.Tests/
├── CrudKit.Api.Tests/
├── CrudKit.Identity.Tests/
└── CrudKit.Integration.Tests/ # Provider-agnostic tests (SQLite + PostgreSQL via Testcontainers)
samples/
└── CrudKit.Sample.Api/ # Working sample with Product, Category, Order, Unit
docs/
├── API-REFERENCE.md # Full feature reference
└── specs/ # Internal design specifications
Declare child entities with [ChildOf] and CrudKit generates nested endpoints automatically under the parent route.
[ChildOf(typeof(Order))]
public class OrderLine : AuditableEntity
{
public Guid OrderId { get; set; } // FK convention: {ParentType}Id
public string ProductName { get; set; } = string.Empty;
}
// Auto-generated: GET/DELETE /api/orders/{id}/order-lines, GET/POST /api/orders/{id}/order-lines/{id}
// Custom route + FK:
[ChildOf(typeof(Order), Route = "items", ForeignKey = "ParentOrderId")]For explicit control use the fluent API: app.MapCrudEndpoints<Order, ...>().WithChild<OrderLine, CreateOrderLine>("items", "OrderId");
When a DTO is annotated with [CreateDtoFor(typeof(T))] or [UpdateDtoFor(typeof(T))], auto-registration discovers and wires them up automatically.
[CreateDtoFor(typeof(Order))]
public record CreateOrder([Required] string CustomerName, decimal Total = 0);
[UpdateDtoFor(typeof(Order))]
public record UpdateOrder
{
public Optional<string?> CustomerName { get; init; }
}For ISoftDeletable entities, CrudKit exposes two purge endpoints for permanent (hard) deletion:
// Single item — must be soft-deleted first
DELETE /api/products/{id}/purge
// Response: 204 No Content
// Bulk — deletes all soft-deleted records older than N days
DELETE /api/products/purge?olderThan=30
// Response: { "purged": 15 }
Single purge requires the record to be soft-deleted already (returns 400 if active). Bulk purge requires olderThan (minimum 1 day). Both use ExecuteDeleteAsync — bypasses soft-delete interception. Respects tenant isolation for IMultiTenant entities.
Full API reference: docs/API-REFERENCE.md
MIT