Vorn.EntityManagement
4.4.0
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
<PackageReference Include="Vorn.EntityManagement" Version="4.4.0" />
<PackageVersion Include="Vorn.EntityManagement" Version="4.4.0" />
<PackageReference Include="Vorn.EntityManagement" />
paket add Vorn.EntityManagement --version 4.4.0
#r "nuget: Vorn.EntityManagement, 4.4.0"
#:package Vorn.EntityManagement@4.4.0
#addin nuget:?package=Vorn.EntityManagement&version=4.4.0
#tool nuget:?package=Vorn.EntityManagement&version=4.4.0
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
- Packages
- Installation
- Getting started
- Working with the presentation layer
- Descriptor-driven querying
- Development
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 implementIEntityRepository
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 services –
EntityService
streamlines mapping between entities, descriptors, and DTOs and pairs with theIEntityService
interface so higher layers can work with simple models. - Cache support –
EntityMemoryCache
,EntityCacheInvalidator
, andEntityCacheInvalidationService
keep entities warm inIMemoryCache
while automatically evicting stale data during SaveChanges operations. - SaveChanges interception –
EntityManagementInterceptor
stamps audit fields, converts hard deletes into soft deletes, invalidates caches, and publishesEntityNotification
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 everyIEntityService
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 | Versions 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. |
-
net8.0
- MediatR (>= 12.5.0)
- Microsoft.EntityFrameworkCore (>= 8.0.19)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Vorn.EntityManagement.Common (>= 4.4.0)
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 |