Hmz.Core.SharedKernel
0.1.15
See the version list below for details.
dotnet add package Hmz.Core.SharedKernel --version 0.1.15
NuGet\Install-Package Hmz.Core.SharedKernel -Version 0.1.15
<PackageReference Include="Hmz.Core.SharedKernel" Version="0.1.15" />
<PackageVersion Include="Hmz.Core.SharedKernel" Version="0.1.15" />
<PackageReference Include="Hmz.Core.SharedKernel" />
paket add Hmz.Core.SharedKernel --version 0.1.15
#r "nuget: Hmz.Core.SharedKernel, 0.1.15"
#:package Hmz.Core.SharedKernel@0.1.15
#addin nuget:?package=Hmz.Core.SharedKernel&version=0.1.15
#tool nuget:?package=Hmz.Core.SharedKernel&version=0.1.15
Hmz.Core.SharedKernel
Hmz.Core.SharedKernel 0.1.14 targets .NET 10 and provides reusable application, domain-support, EF Core, advanced-search, authorization, and ASP.NET Core boundary contracts.
Results and errors
Use Result and Result<T> for expected application failures:
using Hmz.Core.SharedKernel.Results;
public Result<Order> CreateOrder(string customerId)
{
if (string.IsNullOrWhiteSpace(customerId))
{
return Result.Failure<Order>(Error.Validation(
"order.customer-required",
"Customer is required.",
"customerId"));
}
return Result.Success(new Order(customerId));
}
Errors carry a stable code, safe message, transport-neutral category, and optional validation target. Failed results may contain multiple errors of one category. Use Match, Map, and Bind for composition. Unexpected failures remain exceptions and are handled at the application boundary.
Generate diagnostic codes with ErrorCode.Create:
public static readonly string ProductItemsNotFound = ErrorCode.Create(
"CM",
ErrorType.NotFound,
"product-items");
// CM.NOTF.PRODUCT-ITEMS
Use a short, stable application/module prefix. Do not repeat the error type in the semantic suffix.
Pagination
using Hmz.Core.SharedKernel.Contracts.Pagination;
return new PagedResponse<OrderResponse>(items, page, pageSize, totalCount);
Messaging and identity
Hmz.Core.SharedKernel.Messaging provides ICommand, IQuery, INotification, and corresponding handler contracts over Mediator abstractions.
Hmz.Core.SharedKernel.Identity.Abstractions.ICurrentUser is the shared current-user boundary. Applications own authentication, claims mapping, profile lookup, and authorization policy.
Integration events and messages
The Messaging sub-namespaces add cross-module communication for modular
monoliths. They distinguish two kinds of communication:
| Integration event | Message | |
|---|---|---|
| Meaning | A fact that already happened in one module | A notification that only matters now |
| Durability | Written to the producer's outbox in the same transaction as the state change; kept and replayable | Not persisted; lost if the process stops |
| Delivery | At least once, with retry, backoff, and dead-lettering; consumers are idempotent through an inbox | At most once, no retry |
| Use for | Keeping modules consistent | Cache invalidation, live notifications, telemetry |
| Namespace | Messaging.IntegrationEvents, .Outbox, .Inbox, .Transport |
Messaging.Messages |
Domain events (EventDispatcherInterceptor) remain in-process, after-commit
side effects inside one module and are not a substitute for either.
Defining and publishing an integration event
[IntegrationEventName("media.attachments-changed.v1")] // stable wire name; version it, never rename it
public sealed record MediaAttachmentsChanged(...) : IntegrationEvent;
// UseCase: each module exposes its own outbox interface, like its unit of work.
public interface ICatalogOutbox : IIntegrationEventOutbox;
outbox.Add(new MediaAttachmentsChanged(...)); // tracked only
await unitOfWork.SaveChangesAsync(ct); // commits the row change and the outbox row together
Infrastructure wires the outbox into the module DbContext:
// OnModelCreating
modelBuilder.AddOutboxMessages(); // producer: <schema>.OutboxMessages
modelBuilder.AddInboxMessages(); // consumer: <schema>.InboxMessages
// DbContext options
options.AddOutboxNotifications(serviceProvider); // wakes the processor right after commit
// Services
services.AddIntegrationEventOutbox<AppDbContext>(configuration);
services.AddScoped<ICatalogOutbox, CatalogOutbox>(); // : EfIntegrationEventOutbox<AppDbContext>
services.AddIntegrationEventHandler<MediaAttachmentsChanged, Handler, MediaDbContext>(configuration);
Delivery flow
- A request handler changes state and calls
outbox.Add(event); oneSaveChangesAsynccommits both rows. OutboxNotificationInterceptorsignals the module'sOutboxProcessor<TDbContext>after the commit (it also polls everyMessaging:Outbox:PollInterval).- The processor leases a batch in one short statement
(
UPDATE … SET "LockedUntil" … WHERE "Id" IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING *) and commits. No transaction stays open during delivery, any number of hosts can run processors, and rows of a crashed host become claimable when their lease expires. - Events are delivered outside any transaction, up to
Messaging:Outbox:DeliveryConcurrencyat a time, through the transport selected byMessaging:Transport. The built-inInMemoryIntegrationEventTransportis a bounded channel that completes only when every subscriber acknowledges; broker transports complete on the broker's publisher confirm. Each outcome is written back individually and releases the lease; a late failure never overwrites another host's success. IntegrationEventDispatcherruns each subscription in its own DI scope. The inbox wrapper opens a transaction on the consumer's DbContext, skips events already in its inbox, runs the handler, and records the inbox row in the same transaction.- On success the outbox row is marked processed. On failure it is retried
with exponential backoff up to
Messaging:Outbox:MaxAttempts, then dead-lettered. Processed rows are kept forProcessedRetentionandOutboxReplayer<TDbContext>can requeue them; consumers that already handled a replayed event skip it.
Handlers run in background scopes without an HTTP user, so they must not
depend on ICurrentUser; carry the actor in the event if it matters.
Events published while a handler runs inherit its correlation id and record
the handled event as their CausationId (IntegrationEventScope); outside a
handler the correlation id is the current trace id.
Transports and scaling out
// A broker package registers its transport and its own consumer host:
services.AddIntegrationEventTransport<RabbitMqIntegrationEventTransport>("RabbitMq");
// The consumer host delivers each queue to one consumer, so queues retry independently:
await dispatcher.DispatchToConsumerAsync(envelope, consumerName, cancellationToken);
Switch environments with Messaging:Transport (InMemory by default); an
unregistered name fails at startup resolution with a clear error. Broker
consumer hosts can enumerate IntegrationEventRegistry.All to declare one
queue per consumer.
Observability
Register MessagingTelemetry.Name (Hmz.Core.Messaging) with OpenTelemetry
AddSource and AddMeter. Activities: deliver {event} (producer) and
handle {event} (consumer) tagged with message id, correlation id, causation
id, consumer, and attempt. Metrics: hmz.messaging.outbox.delivered,
.failed, .dead_lettered, hmz.messaging.outbox.delivery.duration (ms),
hmz.messaging.inbox.handled, and hmz.messaging.inbox.duplicates.
Fire-and-forget messages
services.AddMessageHandler<ProductCacheInvalidated, InvalidateProductCache>();
messageBus.Publish(new ProductCacheInvalidated(productId)); // returns false when dropped
Configuration
| Key | Default | Purpose |
|---|---|---|
Messaging:Enabled |
true |
Runs processors, transports, and housekeeping; disable for tooling-only hosts. |
Messaging:Transport |
InMemory |
Registered transport name to use. |
Messaging:Outbox:LeaseDuration |
00:02:00 |
Claim lease; keep above the slowest delivery. |
Messaging:Outbox:DeliveryConcurrency |
1 |
Deliveries in flight per processor; 1 keeps per-instance order. |
Messaging:Inbox:Retention |
45.00:00:00 |
Inbox purge age; must be at least ProcessedRetention. |
Messaging:Outbox:PollInterval |
00:00:05 |
Fallback poll when no commit signal arrives. |
Messaging:Outbox:BatchSize |
50 |
Rows claimed per batch. |
Messaging:Outbox:MaxAttempts |
10 |
Attempts before dead-lettering. |
Messaging:Outbox:MaxBackoff |
00:05:00 |
Backoff cap. |
Messaging:Outbox:ProcessedRetention |
30.00:00:00 |
Replay window before processed rows are purged. |
Messaging:TransportCapacity / TransportConcurrency |
256 / 4 |
In-memory transport queue and consumers. |
Messaging:MessageBusCapacity |
10000 |
Fire-and-forget queue length before messages are dropped. |
The outbox claim query uses PostgreSQL SKIP LOCKED syntax. Applications own
the outbox and inbox migrations in each module schema.
Known limitations (deferred)
Recorded during the 0.1.15 review and intentionally not implemented
yet; none can deadlock, and the first only matters with several instances.
- Lease per batch. A batch shares one lease taken at claim time. With
several instances and sequential delivery, rows late in a large batch can
outlive
LeaseDurationand be redelivered by another instance (the inbox absorbs it). Planned: renew the lease per message before delivery. - Claim sort under backlog. The pending index leads with
NextAttemptAtwhile the claim orders byOccurredAt, so a large backlog is sorted on each poll. Planned: a pending partial index on(OccurredAt, Id). - Unbatched retention deletes. Outbox and inbox purges run one delete per sweep. Planned: bounded batch deletes.
Secret comparison
Use FixedTimeSecretComparer.Equals(expected, actual) for application-owned
API keys, webhook signatures, and similar shared-secret checks. The helper
hashes both values to fixed-size buffers before using
CryptographicOperations.FixedTimeEquals, so differing input lengths do not
reintroduce an early-exit comparison.
Keep secret resolution and rotation in the consuming application. This helper does not read configuration, log values, define an authentication scheme, or replace a real identity provider when user identity and authorization are required.
Permission authorization
Hmz.Core.SharedKernel.Authorization provides transport-neutral permission
contracts. Keep ICurrentUser focused on identity and populate a
request-scoped IPermissionSnapshotAccessor from application data after the
external identity has been resolved.
var snapshot = new PermissionSnapshot(
userId,
permissions.ToHashSet(StringComparer.Ordinal),
userPermissionStamp,
rolePermissionStamps);
permissionSnapshotAccessor.Set(snapshot);
PermissionChecker grants an exact permission or a deliberately configured
wildcard such as USER-MANAGEMENT.ROLES.*. Missing and empty snapshots deny
access. Applications should combine permissions from every assigned role
before constructing the snapshot.
The ASP.NET Core integration is under
Hmz.Core.SharedKernel.Http.Authorization:
services.AddPermissionAuthorization();
endpoint.RequirePermission(UserManagementPermissions.Roles.Manage);
Use application-owned constants at endpoints rather than string literals. Permission catalogs, role assignments, persistence, cache invalidation, and version-stamp resolution remain responsibilities of the owning application. Do not make JWT permission claims or a claims transformer the application permission source of truth.
Advanced search
Hmz.Core.SharedKernel.AdvancedSearch provides a validated, allow-listed query pipeline. Clients never supply raw Dynamic LINQ.
services.AddAdvancedSearch();
A feature owns its ISearchConfiguration, source query, projection, and optional external resolvers:
public ValueTask<PagedResponse<ProductListItem>> ExecuteAsync(
AdvancedSearchRequest request,
CancellationToken cancellationToken = default) =>
advancedSearch.ExecutePageAsync(
dbContext.Products.AsNoTracking(),
product => new ProductListItem(product.Id, product.Name, product.Price),
request,
ProductSearchConfiguration.Instance,
cancellationToken: cancellationToken);
The projection overload projects the source query before applying filters,
global search, ordering, counting, and paging. Therefore, MatchKey resolves
against the projected result type, not the persistence entity. This supports
client-facing field names, renamed fields, SmartEnum .Value members, and
aggregate values exposed by the projection while keeping the query
provider-translatable.
Important behavior:
- fields inside one global-search item are OR-ed;
- separate search items use the requested
SearchLogic; - filters default to AND;
- multiple positive values inside one filter are OR-ed;
- negative multi-value operators are AND-ed;
- external no-match results remain match-nothing;
- default ordering should end with a unique field;
- validation errors use exact request paths;
- scalar conversion and enum parsing use bounded, safe messages.
- filter
searchValuesaccepts JSON strings or numbers and normalizes both forms before typed validation.
TODO: Consider optimizing multi-value scalar equality filters to an SQL
IN-style value-collection Contains expression (and NOT IN for negative
filters). Keep the current expanded equality clauses as the compatibility
baseline until translation is verified across supported EF Core providers and
projection shapes.
AdvancedSearch.OpenApi supplies reusable metadata and a schema transformer. Endpoint-specific examples remain in the API feature that owns the search contract.
EF Core data support
Hmz.Core.SharedKernel.Data includes:
- auditable, soft-deletable, and row-version entity bases;
EfRepositoryBase<T>;IUnitOfWorkandEfUnitOfWork;- entity-configuration helpers;
- audit, soft-delete, and domain-event interceptors.
Repository mutation methods may only update the DbContext change tracker. Commit through the owning module's unit of work:
await repository.AddAsync(entity, cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);
Keep provider-specific constraint interpretation and cross-context transaction policy in application Infrastructure.
HTTP boundary support
Hmz.Core.SharedKernel.Http currently provides:
- Result-to-Problem-Details mapping;
- validation problem results;
- global and request-body exception handlers;
- strong ETag parsing/formatting;
- precondition results and conditional-request OpenAPI helpers;
- streamed single-file multipart uploads (
Http.Uploads).
These types use ASP.NET Core and belong only at HTTP boundaries. Domain and UseCase projects should remain transport neutral.
ETags use quoted invariant row versions such as "42". Clients should treat the complete quoted value as opaque and return it unchanged through If-Match or If-None-Match.
Register the shared Problem Details behavior in an API host:
services.AddHmzProblemDetails();
Malformed request binding is sanitized. Only messages represented by Serialization.SafeJsonException may expose bounded actionable details.
Streamed file uploads
MultipartUploads.ReadSingleFileAsync reads a multipart/form-data request
without buffering it and hands the first file section to a callback. The
section content is a BoundedReadStream that throws UploadTooLargeException
one byte past MaxFileBytes, so oversized files fail instead of being
truncated.
app.MapPost("files", (HttpContext context, ISender sender, CancellationToken ct) =>
MultipartUploads.ReadSingleFileAsync(
context,
new MultipartUploadOptions(maxFileBytes),
async (file, token) =>
{
var result = await sender.Send(
new UploadFileCommand(file.Content, file.FileName, file.DeclaredLength),
token);
return result.IsFailure ? result.ToProblem(context) : TypedResults.Ok(result.Value);
},
ct))
.DisableAntiforgery();
| Condition | Response | Code |
|---|---|---|
Not multipart/form-data |
415 problem | http.upload-unsupported-media-type |
| Missing/invalid boundary, truncated or malformed body | 400 validation problem (request) |
http.upload-invalid-boundary, http.upload-malformed |
Content-Length above MaxFileBytes + MultipartHeadroomBytes (default 1 MiB), file above MaxFileBytes, or any UploadTooLargeException from the callback |
413 problem | http.upload-too-large |
| No file section | 400 validation problem keyed by FileField |
http.upload-file-required |
Rules:
- The callback must consume
Contentbefore returning. FileNameandContentTypeare client-supplied: detect the real type from content and never use the name as a storage path.- Application exceptions may derive from
UploadTooLargeException(for example a per-purpose limit enforced after storage) and still map to 413. - Other exceptions, including storage
IOExceptions, propagate to the global exception handler.
Additional dependencies
The current package references Mediator abstractions, ASP.NET Core/OpenAPI, EF Core (including EF Core Relational for messaging persistence), System.Linq.Dynamic.Core, and temporary Ardalis building blocks for guards, shared entities, specifications, SmartEnum, repositories, and service introspection. Versions are centrally managed in Directory.Packages.props.
Build, test, and pack
dotnet build Hmz.Core.SharedKernel/Hmz.Core.SharedKernel.csproj `
--configuration Release
dotnet run `
--project tests/Hmz.Core.SharedKernel.UnitTests/Hmz.Core.SharedKernel.UnitTests.csproj `
--configuration Release `
--no-build `
-- --minimum-expected-tests 1
dotnet pack Hmz.Core.SharedKernel/Hmz.Core.SharedKernel.csproj `
--configuration Release `
--output <package-output>
Current version: 0.1.15, published to NuGet. Inspect the package before
publishing and never republish the same version; advance the project version
and changelog for any subsequent package-content change.
Extension rules
- Add only cross-project contracts with demonstrated reuse.
- Keep feature-specific models and policies in the owning application.
- Keep HTTP and EF Core dependencies in their explicit namespaces and out of Domain code.
- Add tests for compatibility-sensitive behavior and public contracts.
- Preserve existing comments; add or update comments only when behavior changes make them inaccurate or clarification is important.
| Product | Versions 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. |
-
net10.0
- Ardalis.GuardClauses (>= 5.0.0)
- Ardalis.ListStartupServices (>= 1.1.4)
- Ardalis.SharedKernel (>= 5.0.0)
- Ardalis.SmartEnum (>= 8.2.0)
- Ardalis.Specification (>= 9.3.1)
- Ardalis.Specification.EntityFrameworkCore (>= 9.3.1)
- Mediator.Abstractions (>= 3.0.2)
- Microsoft.AspNetCore.OpenApi (>= 10.0.11)
- Microsoft.EntityFrameworkCore (>= 10.0.11)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.11)
- Microsoft.OpenApi (>= 2.12.2)
- System.Linq.Dynamic.Core (>= 1.7.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Hmz.Core.SharedKernel:
| Package | Downloads |
|---|---|
|
Hmz.Core.CrudKit
CRUD toolkit for Hmz projects, built on Hmz.Core.SharedKernel |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.17 | 68 | 10/21/2026 |
| 0.1.16 | 91 | 10/19/2026 |
| 0.1.15 | 82 | 10/13/2026 |
| 0.1.14 | 95 | 10/9/2026 |
| 0.1.13 | 87 | 10/9/2026 |
| 0.1.13-pre03 | 80 | 10/9/2026 |
| 0.1.13-pre01 | 79 | 10/9/2026 |
| 0.1.12 | 89 | 10/8/2026 |
| 0.1.11 | 257 | 9/26/2026 |
| 0.1.10 | 128 | 9/21/2026 |
| 0.1.9 | 138 | 9/12/2026 |
| 0.1.8 | 134 | 9/7/2026 |
| 0.1.7 | 144 | 5/8/2026 |
| 0.1.6 | 134 | 5/7/2026 |
| 0.1.5 | 125 | 5/6/2026 |
| 0.1.4 | 129 | 4/23/2026 |
| 0.1.3 | 139 | 4/22/2026 |
| 0.1.1 | 134 | 4/20/2026 |
| 0.1.0 | 135 | 3/22/2026 |