Hmz.Core.SharedKernel
0.1.17
dotnet add package Hmz.Core.SharedKernel --version 0.1.17
NuGet\Install-Package Hmz.Core.SharedKernel -Version 0.1.17
<PackageReference Include="Hmz.Core.SharedKernel" Version="0.1.17" />
<PackageVersion Include="Hmz.Core.SharedKernel" Version="0.1.17" />
<PackageReference Include="Hmz.Core.SharedKernel" />
paket add Hmz.Core.SharedKernel --version 0.1.17
#r "nuget: Hmz.Core.SharedKernel, 0.1.17"
#:package Hmz.Core.SharedKernel@0.1.17
#addin nuget:?package=Hmz.Core.SharedKernel&version=0.1.17
#tool nuget:?package=Hmz.Core.SharedKernel&version=0.1.17
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);
Combine permissions with AND and OR through PermissionRule. A permission
code converts to a rule implicitly; each RequirePermission... call adds one
requirement and every requirement must pass, so chained calls are AND:
endpoint.RequireAllPermissions(Users.Read, Brands.Read); // A and B
endpoint.RequireAnyPermission(Product.Read, Product.Manage); // A or B
endpoint.RequireAnyPermission(Quotes.Approve, // A or (B and C)
PermissionRule.All(Quotes.Manage, Prices.Read));
endpoint.RequirePermission(Users.Read).RequirePermission(Brands.Read); // A and B
// Outside endpoints, against the current snapshot:
if (permissionChecker.Satisfies(PermissionRule.Any(Product.Read, Product.Manage))) { ... }
Rules validate their codes when built, render as text (A or (B and C)) for
documentation, and add PermissionRuleMetadata to the endpoint. Prefer a
dedicated permission over deep nesting when a combination describes a real job.
Use application-owned constants at endpoints rather than string literals.
Resource policies (resource- and attribute-based rules)
Permission checks answer "may this user do this kind of action at all". A resource policy answers "may this user do it to this record", combining the permission snapshot with the record (author, owner, status, amount) and the current user. Write one policy per resource type, in the feature folder of the use case that owns the resource:
public sealed class StoryPostPolicy(IPermissionChecker permissions, ICurrentUser user)
: IResourcePolicy<StoryPost>
{
public ValueTask<AccessDecision> EvaluateAsync(StoryPost post, string action, CancellationToken ct) =>
ValueTask.FromResult(action switch
{
StoryPostActions.Delete when post.AuthorId == user.UserId => AccessDecision.Allow(),
StoryPostActions.Delete when permissions.Satisfies(Feeds.Moderate) => AccessDecision.Allow(),
StoryPostActions.Delete => AccessDecision.Deny("Only the author or a moderator.", StoryErrorCodes.NotAuthor),
_ => AccessDecision.DenyUnknownAction(action),
});
}
services.AddResourcePolicy<StoryPost, StoryPostPolicy>();
// In a handler, a bot command or a background job:
var decision = await access.CheckAsync(post, StoryPostActions.Delete, ct);
if (!decision.Allowed) return Result.Failure(decision.ToError(StoryErrorCodes.NotAuthor));
- Child resources check their parent first:
AccessDecision.AllAsync(() => access.CheckAsync(comment.Post, "read", ct), () => OwnRules(comment))stops at the first denial. IResourceCapabilities<TResource, TCapabilities>returns what one record allows (for exampleCanEdit,CanReadCost). Query handlers use it to mask response fields, and clients use it to show actions.- Business invariants that hold for every actor (status transitions, totals,
separation of duties written into business law) belong in the domain model.
Policies decide only who may act. A long
actionswitch usually means status rules sit in the policy instead of the aggregate. Dispatch each action to a private method, and split a policy per action only when one action grows large. - Lists are filtered in the database (data scopes and named query filters), never row by row through a policy.
IAuthorizationService.AuthorizeAsync(user, resource, new ResourceActionRequirement(action))evaluates the same policy.IAccessDecisionObserverreceives every decision, for audit logs. 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.
Every query filter is named. A specification can bypass single filters with Query.IgnoreQueryFilters([names]) (from Data.Specifications); the others still apply. Ardalis' parameterless IgnoreQueryFilters() still drops all of them. EfRepositoryBase<T> evaluates specifications with HmzSpecificationEvaluator, which applies the named form for lists, projections, CountAsync and AnyAsync:
public sealed class DeletedNotesSpecification : Specification<Note>
{
public DeletedNotesSpecification() =>
Query.IgnoreQueryFilters([QueryFilterNames.SoftDelete]).Where(note => note.IsDeleted);
}
The filter name belongs to whoever declares the filter: QueryFilterNames for SharedKernel's own, an application constant for its data scopes.
Named query filters and data scopes
The soft-delete filter is named QueryFilterNames.SoftDelete. Applications add their own named filters for an interface, once, and SharedKernel applies them to every root entity type that implements it:
modelBuilder.ApplyQueryFilter<IBrandOwned>(
"Brand",
owned => !BrandScopeEnforced || owned.BrandId == ActiveBrandId);
Read request state from the DbContext (its own members, or an interface it implements): EF Core swaps that reference for the running context on each query, while any other captured value is frozen into the cached model.
Data.Scoping describes a partition such as a tenant or brand: IDataScope<TKey> is the request scope (IsEnforced, ScopeId), IDataScopeKind<TKey> names the filter and key property, and an entity implements IScopedEntity<TScope, TKey> once per scope it belongs to. DataScopeWriteGuardInterceptor<TScope, TKey> refuses to save a scoped entity outside the enforced partition (DataScopeViolationException); register it before the audit, soft-delete and domain-event interceptors.
Persistence errors
Use cases depend on the provider-neutral IPersistenceErrorClassifier from Data.Errors and its checks: IsUniqueViolation(ex, constraint?), IsForeignKeyViolation(ex, constraint?), IsStringTooLong, IsConcurrencyConflict and IsTransient. Register the PostgreSQL implementation once with services.AddPostgresPersistenceErrorClassifier(). Modules add constraint-specific checks as their own extension members (for example IsCollectionCodeUniqueViolation(ex)) rather than defining classifier interfaces.
Save interceptors
AuditableInterceptorBase and SoftDeleteInterceptorBase handle both SaveChanges and SaveChangesAsync. Derived interceptors override the before-save pair together, or the after-save pair together, and let ChangeTracker.Entries() run DetectChanges.
Demo data seeds through Data.Seeding: context.SeedIfEmptyAsync(() => samples) inserts only into an empty table (soft-deleted and other-scope rows count), and context.HasAnyRowsAsync<T>() guards seeds that build dependent rows. Reference data the application depends on is seeded item by item instead.
Validation helpers
Hmz.Core.SharedKernel.Validation has two layers.
Domain invariants throw for the named parameter and return the value for inline use:
Code = code.RequireWithMaxLength(MaxCodeLength, nameof(code)).ToUpperInvariant();
Description = description.OptionalWithMaxLength(MaxDescriptionLength, nameof(description));
BrandId = brandId.RequirePositive(nameof(brandId));
Strings: IsBlank, TrimToNull, ExceedsMaxLength (after trimming), RequireWithMaxLength, OptionalWithMaxLength. Numbers (any INumber<T>): RequirePositive, RequireNonNegative, RequireInRange, and for nullable values OptionalNonNegative, OptionalInRange.
Request validation returns a validation Error with the field target, or null when the value is valid, so a validator reports every field at once:
var validation = FieldErrors.Collect(
request.Sku.RequiredError(code, "sku", "SKU"),
request.Name.MaxLengthError(MaxNameLength, code, "name", "Name"),
request.WidthMm.NonNegativeError(code, "widthMm", "Width"));
if (validation.IsFailure)
return Result.Failure<ProductDetails>(validation.Errors);
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), Npgsql (PostgreSQL error classification), 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.17, prepared for NuGet publication. 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)
- Npgsql (>= 10.0.3)
- 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 | 65 | 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 | 243 | 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 |