Vorn.EntityManagement 4.4.0

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

Vorn.EntityManagement

Vorn.EntityManagement provides a thin domain layer for CRUD-heavy .NET applications that rely on Entity Framework Core and MediatR. The suite ships with audited entity base types, descriptor-driven queries, repository and service abstractions, cache invalidation helpers, a Roslyn source generator, and optional SignalR packages so your application can focus on domain logic instead of wiring.

Table of contents

Features

  • Audited aggregate roots – derive from Entity to get identifiers, created/updated/deleted timestamps, soft-delete helpers, and change notifications out of the box.
  • Descriptor-based filtering & DTOs – use EntityDescriptor/EntityDescriptorDto records to express filters, paging, search, and soft-delete options that can be projected across boundaries.
  • Repository abstraction – reuse the generic EntityRepository base for Entity Framework Core DbContexts or implement IEntityRepository manually for other persistence stores.
  • Mediator-powered CRUD – ready-made MediatR commands, queries, and handlers cover add, update, delete, list, count, and paged scenarios so you can orchestrate persistence through a single IMediator dependency.
  • Application servicesEntityService streamlines mapping between entities, descriptors, and DTOs and pairs with the IEntityService interface so higher layers can work with simple models.
  • Cache supportEntityMemoryCache, EntityCacheInvalidator, and EntityCacheInvalidationService keep entities warm in IMemoryCache while automatically evicting stale data during SaveChanges operations.
  • SaveChanges interceptionEntityManagementInterceptor stamps audit fields, converts hard deletes into soft deletes, invalidates caches, and publishes EntityNotification messages after each successful save.
  • Source generator registration – annotate your assembly and descriptors and the Roslyn generator will emit an EntityManagementRegistration helper that registers handlers, caches, interceptors, and ambient services in the DI container automatically.
  • SignalR endpoints – the optional Vorn.EntityManagement.SignalR.Server and .Client packages expose the same CRUD surface over persistent SignalR connections for real-time applications.

Packages

  • Vorn.EntityManagement – core runtime library with entities, repositories, services, caching, notifications, and the bundled source generator attributes.
  • Vorn.EntityManagement.SignalR.Server – SignalR Hub base class that exposes every IEntityService operation to connected clients.
  • Vorn.EntityManagement.SignalR.Client – client-side base class that mirrors the hub contract and manages hub connections for DTO-driven CRUD flows.

The generator analyzer is linked from the core package so consumers only need the packages above.

Installation

Install the NuGet packages that match your scenario:

dotnet add package Vorn.EntityManagement --version 4.0.0
# Optional packages when exposing the service over SignalR
dotnet add package Vorn.EntityManagement.SignalR.Server --version 4.0.0
dotnet add package Vorn.EntityManagement.SignalR.Client --version 4.0.0

Getting started

1. Declare entities, descriptors, and DTOs

Derive your domain entity from Entity and create descriptor/DTO types. Apply the generator attributes so the source generator can discover the relationship between the two types and register all handlers automatically:

using Vorn.EntityManagement;
using Vorn.EntityManagement.Generators;

[assembly: GenerateEntityManagementRegistration]

[EntityDescriptorFor(typeof(Customer))]
public sealed record CustomerDescriptor : EntityDescriptor
{
    public string? Name { get; init; }
}

public sealed record CustomerDescriptorDto : EntityDescriptorDto
{
    public string? Name { get; init; }

    public static CustomerDescriptor ToDescriptor(CustomerDescriptorDto dto) => new()
    {
        Name = dto.Name,
        Skip = dto.Skip,
        Take = dto.Take,
        Search = dto.Search,
        IsDeleted = dto.IsDeleted,
        CreatedFrom = dto.CreatedFrom,
        CreatedTo = dto.CreatedTo,
        UpdatedFrom = dto.UpdatedFrom,
        UpdatedTo = dto.UpdatedTo,
        DeletedFrom = dto.DeletedFrom,
        DeletedTo = dto.DeletedTo
    };
}

public sealed record CustomerDto : EntityDto
{
    public Guid Id { get; init; }
    public string Name { get; init; } = string.Empty;

    public static CustomerDto FromEntity(Customer entity) => new()
    {
        Id = entity.Id,
        Name = entity.Name,
        CreatedAt = entity.CreatedAt,
        CreatedBy = entity.CreatedBy,
        UpdatedAt = entity.UpdatedAt,
        UpdatedBy = entity.UpdatedBy,
        DeletedAt = entity.DeletedAt,
        DeletedBy = entity.DeletedBy
    };

    public static Customer ToEntity(CustomerDto dto) =>
        new(dto.Id == Guid.Empty ? Guid.NewGuid() : dto.Id)
        {
            Name = dto.Name
        };
}

public sealed class Customer : Entity
{
    public Customer() => Id = Guid.NewGuid();
    public Customer(Guid id) => Id = id;

    public string Name { get; set; } = string.Empty;
}

2. Implement a repository

For Entity Framework Core scenarios subclass EntityRepository<TEntity, TDescriptor, TDbContext> and override behavior as needed. Alternatively, implement IEntityRepository manually for non-EF persistence; the in-memory test double illustrates the required methods.

3. Register infrastructure

Call the generated registration helper when building your service provider, register MediatR, and add your repository implementation. Register your DTO-facing service so presentation layers can consume it:

services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CustomerDescriptor>());
EntityManagementRegistration.AddGeneratedEntityHandlers(services);
services.AddScoped<IEntityRepository<Customer, CustomerDescriptor>, CustomerRepository>();
services.AddScoped<CustomerService>();
services.AddScoped<IEntityService<CustomerDto, CustomerDescriptorDto>>(sp => sp.GetRequiredService<CustomerService>());

EntityManagementRegistration wires the cache invalidation service, interceptor, and ambient user/time services. Override the default IEntityUserService/IEntityTimeService implementations if you need custom behavior.

4. Issue CRUD requests

With DI configured you can route persistence through IMediator. Use the generated commands and queries to add, update, fetch, list, or remove entities—setting Skip and Take on descriptors yields simple paging:

Customer customer = new() { Name = "Initial" };
Customer? added = await mediator.Send(new AddEntityCommand<Customer, CustomerDescriptor>(customer));
Customer? fetched = await mediator.Send(new GetEntityByIdQuery<Customer, CustomerDescriptor>(customer.Id));
customer.Name = "Updated";
await mediator.Send(new UpdateEntityCommand<Customer, CustomerDescriptor>(customer));
await mediator.Send(new RemoveEntityCommand<Customer, CustomerDescriptor>(customer));
int count = await mediator.Send(new CountEntitiesQuery<Customer, CustomerDescriptor>(new CustomerDescriptor()));
IReadOnlyList<Customer> page = await mediator.Send(
    new GetEntitiesQuery<Customer, CustomerDescriptor>(new CustomerDescriptor { Skip = 0, Take = 20 }));

The integration tests demonstrate the same end-to-end flow against the in-memory repository.

5. Wrap handlers in an application service

EntityService wraps the mediator pipeline with mapping delegates so application layers can work with DTOs instead of entities, handle bulk operations, convert descriptor DTOs, and retrieve paged results from a single abstraction:

public sealed class CustomerService
    : EntityService<Customer, CustomerDescriptor, CustomerDto, CustomerDescriptorDto>,
      IEntityService<CustomerDto, CustomerDescriptorDto>
{
    public CustomerService(IMediator mediator)
        : base(mediator, CustomerDto.FromEntity, CustomerDto.ToEntity, CustomerDescriptorDto.ToDescriptor)
    {
    }
}

6. Plug into Entity Framework Core

Register the EntityManagementInterceptor as a SaveChangesInterceptor for your DbContext. The interceptor stamps audit fields, converts hard deletes into soft deletes, triggers cache invalidation, and publishes notifications after the save succeeds. It also clears pending notifications when a transaction fails.

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString);
    options.AddInterceptors(sp.GetRequiredService<EntityManagementInterceptor>());
});

7. Cache entities

Use the provided EntityMemoryCache<TEntity> to warm frequently accessed aggregates and pair it with EntityCacheInvalidator<TEntity> (or your own IEntityCacheInvalidator implementation) to keep cache entries synchronized with persistence operations. The generated registration already wires the default implementations—override them if you need a distributed cache.

8. React to domain notifications

Every intercepted save emits EntityNotification<TId> instances describing the affected entity, operation type, and captured field deltas. Subscribe to them with MediatR notification handlers to orchestrate downstream workflows or integration events.

Working with the presentation layer

The presentation layer can expose IEntityService operations over HTTP endpoints or SignalR hubs. Set the current user through IEntityUserService before executing commands so audit fields capture the correct identity.

Minimal API example

app.Use(async (context, next) =>
{
    var entityUserService = context.RequestServices.GetRequiredService<IEntityUserService>();
    entityUserService.Set(context.User.FindFirstValue(ClaimTypes.NameIdentifier));
    await next(context);
});

var customers = app.MapGroup("/customers");
customers.MapGet("/{id:guid}", (CustomerService service, Guid id, CancellationToken ct)
        => service.GetAsync(id, ct));
customers.MapPost("/", (CustomerService service, CustomerDto dto, CancellationToken ct)
        => service.AddAsync(dto, ct));
customers.MapGet("/paged", (CustomerService service, CustomerDescriptorDto descriptor, CancellationToken ct)
        => service.GetPagedAsync(descriptor, ct));

CustomerService exposes the full CRUD surface so you can map the remaining endpoints with the same pattern.

SignalR real-time endpoints

Derive a hub from EntityServer to surface the same DTO-based contract to connected clients:

using Vorn.EntityManagement.SignalR.Server;

public sealed class CustomerHub : EntityServer<CustomerDto, CustomerDescriptorDto, CustomerService>
{
    public CustomerHub(CustomerService service, IEntityUserService entityUserService)
        : base(service, entityUserService)
    {
    }
}

builder.Services.AddSignalR();
app.MapHub<CustomerHub>("/hubs/customers");

Create a matching client by inheriting from EntityClient and configuring the connection builder:

using Microsoft.AspNetCore.SignalR.Client;
using Vorn.EntityManagement.SignalR.Client;

public sealed class CustomerClient : EntityClient<CustomerDto, CustomerDescriptorDto>
{
    public CustomerClient(IEntityUserService entityUserService, Uri hubUrl)
    {
        EntityUserService = entityUserService;
        Connection = new HubConnectionBuilder()
            .WithUrl(hubUrl)
            .WithAutomaticReconnect()
            .Build();
    }

    protected override async Task EnsureConnectionAsync(CancellationToken cancellationToken)
    {
        if(Connection.State == HubConnectionState.Disconnected)
        {
            await Connection.StartAsync(cancellationToken).ConfigureAwait(false);
            await SetUser().ConfigureAwait(false);
        }
    }
}

Consumers can now call await customerClient.AddAsync(dto, ct) or await customerClient.GetPagedAsDtoAsync(descriptor, ct) to interact with the hub. The base class automatically mirrors every CRUD method exposed by the server hub.

Descriptor-driven querying

Descriptors double as request models for complex filters. Populate properties like CreatedFrom, UpdatedBy, IsDeleted, Skip, and Take, then pass them into any GetEntitiesQuery or repository call. The shared QueryExtensions.Apply routine applies the filters, paging, and soft-delete rules to the EF Core queryable, automatically toggling IgnoreQueryFilters when deleted records are requested. Use EntityDescriptorDto when binding from HTTP or SignalR clients and convert it back with the mapping delegate you supplied to EntityService.

Development

  • Run the automated test suite with dotnet test to validate repository, cache, interceptor, and SignalR behavior.
  • Create a NuGet package locally via dotnet pack -c Release, which respects the project metadata and bundles the analyzer bits alongside the library.
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 (1)

Showing the top 1 NuGet packages that depend on Vorn.EntityManagement:

Package Downloads
Vorn.EntityManagement.SignalR.Server

This library provides SignalR entity server base class.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.6.0-beta 17 10/5/2025
4.6.0-alpha 20 10/5/2025
4.5.0 42 10/4/2025
4.4.0 230 9/24/2025
4.3.0 234 9/23/2025
4.0.0 245 9/22/2025
4.0.0-rc3 162 9/21/2025
4.0.0-rc2 141 9/21/2025
4.0.0-rc1 149 9/21/2025
4.0.0-gamma 164 9/20/2025
4.0.0-delta 160 9/20/2025
4.0.0-beta 158 9/20/2025
4.0.0-alpha 178 9/20/2025
3.5.0 328 9/17/2025
3.4.0 305 9/17/2025
3.3.0 310 9/17/2025
3.0.0 344 9/16/2025
2.3.0 178 9/8/2025
2.2.1 119 9/6/2025
2.2.0 88 9/6/2025
2.1.1 81 9/6/2025
2.1.0 93 9/6/2025
2.0.0 234 9/1/2025
1.0.0 223 8/27/2025