LoomKit.Requests.Crud 10.0.1

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

LoomKit.Requests.Crud

Generic CRUD base-handler classes for LoomKit.Requests: abstract Request/Response/Handler triads for Create, GetById, List, UpdateById, DeleteById, Import and "link/unlink related" use cases, plus a repository/unit-of-work contract and SQL-level field projection.

Status: early stage. The public API may still change between versions — pin a commit/tag if you depend on it.

Features

  • BaseCreateHandler, BaseGetByIdHandler, BaseListHandler, BaseUpdateByIdHandler, BaseDeleteByIdHandler, BaseImportHandler, BaseUpdateRelatedByIdHandler, BaseHandlerWithDto — each pairs a request/response record with a handler that runs an authorize → validate → ... → commit → build response pipeline, returning LoomKit.Esits' Esit<TResponse>.
  • SQL-level field selection: BaseGetByIdHandler and BaseListHandler prune a DTO's projection expression down to only the fields a caller asked for (Selects=Id,Name,User.Email), so EF Core (or any IQueryable provider) only selects what's needed.
  • BeforeCommitActivities/AfterCommitActivities hooks around every mutating handler's commit step, for side effects (publishing events, notifications, cache invalidation, ...).
  • Bring your own mapping (no bundled mapping library) and your own filtering/ordering (no bundled query-string parser) — see Extensibility below.

Requirements

  • .NET 10
  • LoomKit.Requests.Abstractions — IRequest<T>/IRequestHandler<,> contracts these handlers implement
  • LoomKit.Esits — the Esit/Esit<T> result type used throughout
  • Your own implementations of IBaseRepository<TEntity, TEntityId> and IUnitOfWork<TAuditable, TAudit> (e.g. backed by EF Core)
  • A working IRequestSender to actually dispatch requests to these handlers — add LoomKit.Requests (or any other IRequestSender implementation) to your composition-root project
  • If your response DTOs are shared with client code, add LoomKit.Requests.Crud.Abstractions to that shared project to implement IProjectable<TEntity, TDto> — see that package's README

Installation

dotnet add package LoomKit.Requests.Crud

Core concepts

Type Kind Purpose
BaseCreateHandler<...> abstract class Create-entity pipeline: materialize entity from DTO, validate, persist, commit, map to response DTO.
BaseGetByIdHandler<...> abstract class Fetch-by-id pipeline with SQL-level projection pruning based on Selects.
BaseListHandler<...> abstract class Filter/order/page/project pipeline returning a paged Data list plus counts.
BaseUpdateByIdHandler<...> abstract class Fetch-by-id, clone, mutate, validate, commit, map to response DTO.
BaseDeleteByIdHandler<...> abstract class Fetch-by-id, validate, delete, commit, map to response DTO.
BaseImportHandler<...> abstract class Bulk prepare/authorize/validate/clean/create pipeline with a single synthetic audit Change.
BaseUpdateRelatedByIdHandler<...> abstract class Reconciles a principal's related-entity set (link the new ones, unlink the removed ones).
BaseHandlerWithDto<...> abstract class Generic authorize → validate → process pipeline for use cases that don't fit strict CRUD.
IBaseRepository<TEntity, TEntityId> interface Minimal repository contract (ListAsync, GetByIdAsync, CreateAsync, DeleteAsync) every handler above depends on.
IBulkRepository<TEntity, TEntityId> interface Bulk create/delete contract for BaseImportHandler's CreateEntities/CleanEntities hooks.
IUnitOfWork<TAuditable, TAudit> interface Transactional boundary (SaveChangesAsync, CommitAsync with a per-change audit builder, RollbackAsync).
Change / ChangeSummary model Audit-trail unit-of-work-tracked mutation record.
ProjectionPruner static class Prunes a full projection expression down to the fields listed in Selects.

Quick start

using LoomKit.Esits;
using LoomKit.Requests.Crud.Abstracts;

public record CreateProductDto : BaseCreateResponse<ProductDto>;

public class CreateProductHandler
    : BaseCreateHandler<IProductRepository, Product, int, CreateProductRequest, ProductInput, CreateProductResponse, ProductDto, MyAuditable, MyAudit>
{
    public CreateProductHandler(IProductRepository repository, IUnitOfWork<MyAuditable, MyAudit> unitOfWork)
        : base(repository, unitOfWork) { }

    protected override Task<Esit> AuthorizeRequest(CreateProductRequest request) => Esit.Success().AsTask();
    protected override Task<Esit> ValidateRequest(CreateProductRequest request) => Esit.Success().AsTask();
    protected override Task<Esit> ValidateBusinessRules(Product entity, CreateProductRequest request) => Esit.Success().AsTask();

    protected override Task<Esit<Product>> CreateEntity(ProductInput? data) =>
        new Product { Name = data!.Name }.ToEsit().AsTask();

    protected override ProductDto MapToDto(Product entity) => new() { Id = entity.Id, Name = entity.Name };
}

BaseGetByIdHandler/BaseListHandler read a DTO's own projection expression instead of mapping in memory:

public class GetProductByIdHandler : BaseGetByIdHandler<IProductRepository, Product, int, GetProductByIdRequest, GetProductByIdResponse, ProductDto>
{
    protected override Expression<Func<Product, ProductDto>> BuildProjection() => ProductDto.Projection;
    protected override string[] DefaultSelects => ProductDto.DefaultSelects;
    // AuthorizeRequest / ValidateRequest ...
}

Selects and field projection

BaseGetByIdHandler and BaseListHandler don't map a fully-loaded entity to a DTO in memory — they prune a projection expression down to only the requested fields, then let the query provider (e.g. EF Core) translate that into a SELECT with only those columns.

A DTO declares its full projection and, optionally, a default field set:

using System.Linq.Expressions;

public record ProductDto
{
    public int Id { get; init; }
    public string Name { get; init; }
    public UserDto? User { get; init; }

    // EF Core translates this into an optimized SQL SELECT.
    // Use property-initializer syntax, not a positional record constructor.
    public static Expression<Func<Product, ProductDto>> Projection =>
        e => new ProductDto
        {
            Id = e.Id,
            Name = e.Name,
            User = new UserDto
            {
                Email = e.User.Email,
                Roles = e.User.Roles.Select(r => new RoleDto
                {
                    Id = r.Id,
                    Name = r.Name
                }).ToList()
            }
        };

    // Fields included by default when the caller doesn't request specific ones.
    // Omit (or return []) to always include every field.
    public static string[] DefaultSelects => ["Id", "Name"];
}

The Selects request field accepts a comma-separated list of dot-nested paths following the DTO's own property structure (not the entity's):

Id,Name,User.Email,User.Roles.Id,User.Roles.Name
  • Simple path: "Name"
  • Nested object path: "User.Email"
  • Nested collection path: "User.Roles.Name"

If Selects is null, the DTO's DefaultSelects are used. If DefaultSelects is [], the full projection is returned. Path matching is case-insensitive.

ProjectionPruner only supports property-initializer DTO syntax (new Dto { Prop = ... }), not positional record constructors.

Implementing IProjectable<TEntity, TDto> from LoomKit.Requests.Crud.Abstractions on the DTO is optional — BuildProjection()/DefaultSelects just need to return the right shapes — but it's a convenient way to keep the convention explicit, especially when the DTO lives in a project shared with client code.

Extensibility

  • Mapping (MapToDto): BaseCreateHandler, BaseDeleteByIdHandler and BaseUpdateByIdHandler call an abstract MapToDto(TEntity entity) to build the response DTO after a mutation. There's no bundled mapping library — implement it with whatever you already use (a mapping library, a source-generated mapper, or plain manual code).
  • Filtering and ordering (BaseListHandler): ApplyRequestFilteringToQueryable/ApplyRequestOrderingToQueryable are abstract — there's no bundled query-string parser. Implement them with whatever filtering/ordering approach you use, or return the queryable unchanged if you don't support it. ApplyRequestPagingToQueryable has a working default (Skip/Take) you can override if needed.
  • Commit-time side effects: every mutating handler exposes BeforeCommitActivities/AfterCommitActivities around its commit step — publish domain events, invalidate caches, etc. from there.
  • Exception handling: override HandleException on any handler to customize the error returned on an unhandled exception (the default wraps it as a generic "unexpected" error).

License

MIT

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

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
10.0.1 181 8/25/2026