.NET SDK for C# business logic
The Moltaro .NET SDK is the programming model for C# business logic in the workspace Net Operation Project: a function is a C# class that derives from an SDK base class and declares identity, bindings, and publication with attributes. Authoring happens in the Constructor area under Automation & logic > Development, or entirely through the Configuration API. AI agents should use the API-first authoring flow; downloading the NET Project ZIP is optional for a human using a local IDE.
Use the curated .NET reference together
with the installation’s GET .../net-operation-project/developer-surface
catalog. The catalog, not arbitrary public CLR visibility, defines supported
assemblies and injectable services.
Collectible runtime safety
Section titled “Collectible runtime safety”Package and Net Operation Project assemblies execute in collectible runtime
epochs. Keep work inside the invocation: use ordinary awaited async calls, and
use Moltaro schedules or IFunctionQueue when work must run later. Check and
Build reject detached, started, discarded, or unobserved stored tasks, threads
and ThreadPool work (MOLTARO040),
long-lived timers or callbacks (MOLTARO041), subscriptions to host events
(MOLTARO042), and explicit default-context/dynamic-load or GCHandle escapes
(MOLTARO044). CancellationToken.Register and UnsafeRegister are supported
only when the returned registration itself is disposed by using. A local
method-group delegate to either registration API is allowed only when every
invocation is disposed this way and the delegate does not escape.
Process-global ActivitySource.AddActivityListener registration and access to
DiagnosticListener.AllListeners are rejected. Creating, subscribing to, or
deriving from DiagnosticListener, and deriving from EventListener, are also
rejected because their process-global registration cannot be made fail-closed
in package code.
Call MeterListener.Start only on a receiver declared by using var or a
using declaration. A method-group delegate to Start must remain local and be
invoked directly.
A stored task must be directly awaited or returned by the very next statement;
for parallel work, store a combined Task.WhenAll(...) task and await it next.
MOLTARO043 warns on every user-authored non-const static/static-readonly
field. Review it as epoch-owned state: it must not escape to host state or
retain callbacks, host services, collectible CLR types, delegates, or
invocation values. Generated code and constants are excluded. Task.Delay and
ordinary async/await are supported. An already active artifact is not stopped
by a newly introduced analyzer rule, but its next Check or Build must pass the
current contract.
Reflective host-event subscriptions, AppDomain.SetData, AppContext.SetData,
process-principal replacement, named/thread-local data slots, and writes to
external static fields or properties are lifetime escapes and are rejected as
well. Process-global Encoding.RegisterProvider registration is also rejected.
The same applies to TypeDescriptor.AddProvider, AddProviderTransparent,
AddAttributes, AddEditorTable, and CreateAssociation.
The same rule rejects process-global Console.SetIn/SetOut/SetError
replacement and all Trace.Listeners collection access because those APIs can retain
package-owned readers, writers, or listeners after the runtime epoch. Taking a
method-group delegate to a forbidden process-global API is rejected too.
Structured result helpers for command and function data,
typed HTTP JSON, and data sources use the current runtime epoch’s serializer
context automatically. Existing installed artifacts that explicitly pass
process-singleton JSON options to those helpers remain runtime-safe because the
epoch context replaces them; their next Check/Build rejects that reference with
MOLTARO044, so omit the option. Direct JsonSerializer calls must pass a
freshly constructed inline options instance or explicit typed JSON metadata;
options obtained through parameters, fields, properties, or locals are
rejected because their ownership cannot be proven. A JsonSerializer method
group is allowed only when its selected overload requires JsonTypeInfo or
JsonSerializerContext. Do not put serializer
options, converters, runtime types, exceptions, or result objects into static
or other process-global storage.
Dynamic invocation is rejected because Check/Build cannot verify that the
runtime-selected target respects these lifetime rules.
Inline Activator.CreateInstance<T>() and CreateInstance(typeof(T)) remain
available for ordinary non-listener types. Type-erased activation and
Activator.CreateInstance method groups are rejected because the constructed
lifetime cannot be verified.
Parent assignment and reparenting use the same supported record write services and optimistic concurrency as any Reference. See the Parent Tree View developer guide.
Reference Eligibility is likewise configured on Entity Definition fields or through the Configuration API, not authored from public Net Operation Project schema code. The configuration workflow supports validation, impact preflight, activation, Form/Table pre-filtering, and authoritative save enforcement. See the Reference Eligibility guide.
Hierarchy Selector configuration is available from Entity Definition Form and Table-filter authoring and from the Configuration API. It supports fixed Reference paths and recursive indexed self-References while storing only the terminal Reference value. See the Hierarchy Selector guide.
Entity UI layout remains workspace configuration rather than Net Operation Project source. Configure responsive Card item width in the Entity Definition Surface library or through the Configuration API, as described in Card item layout. Workspace C# consumes the generated entity contract but does not author or override these UI spans.
Entity Manual Order is also platform-owned configuration. Net Operation Project source does not declare the capability, choose or write its protected rank storage, or receive a direct Manual Order move service. Generated entity writes continue through the normal Moltaro save pipeline. When Manual Order is active, that pipeline automatically gives each newly inserted active record a canonical tail position in the same transaction, so ordinary generated-context creates do not need special ordering code. Positional moves remain an authenticated application/UI operation with the same authorization and concurrency boundary as other Entity actions.
For workspace-specific algorithms that need their own ordered keys, the SDK
also exposes the policy-free
Moltaro.Package.NET.ModuleRuntime.Ordering.LexicographicRankKeyGenerator.
It returns typed generation statuses and evenly spaced sequences. Using that
primitive does not grant access to Moltaro’s protected Manual Order rank, and it
does not provide a Manual Order move endpoint or service.
Work Schedule application automation
Section titled “Work Schedule application automation”For calendar lifecycle, effective-dated Site and Worker assignments, schedule
exceptions, and effective-schedule reads, prefer
IWorkScheduleAutomationCommandService and
IWorkScheduleAutomationQueryService. The generated workspace contract
registers both interfaces, and developer-surface marks them
ApplicationAutomation and Preferred: true.
These calls run complete Work Schedule application orchestration in a separate
host-owned scope. Requests cannot supply an actor or authority tier. Every
command uses a stable caller-owned OperationId; calendar mutations additionally
use the current RowVersion, while assignment and exception mutations use the
last returned ChainVersion. Preserve an operation id only for an exact retry
after an unknown outcome.
The lower-level services in Moltaro.Package.NET.WorkSchedule.Runtime remain
available as advanced TrustedDirectDb, Preferred: false contracts for code
that explicitly owns authorization, transaction, audit, and side effects. Read
the Work Schedule C# guide before choosing
that boundary.
Generated entity search declarations
Section titled “Generated entity search declarations”The generated workspace contract reproduces each Entity Definition’s explicit PostgreSQL search targets through the SDK entity builder:
SearchField(...)declares a direct field or a path through Reference, Inverse Reference, or Table fields to a String, Text, Address, or File terminal;SearchSystem(EntitySearchSystemTargetEnum, ...)declares Display name, Number, Comments, or Attachments at the root or after a relation path.
These declarations are generated build input, not user-owned Net Operation Project source. Configure search targets in the Entity Definition Search tab or through the Configuration API search contract; the next generated contract then gives Check, Build, and server-side language services the same schema metadata.
Generated String and Text properties whose Entity Definition field is labeled
with the Markdown property label still expose the original Markdown source as
their typed property value. Moltaro separately maintains a display-safe
plain-text projection for search, presentation, projections, and audit
previews. Net Operation Project code should write Markdown through the generated
context and its normal SaveChanges pipeline; do not issue raw SQL against the
source or its internal companion column. The save is rejected when the source
exceeds the 512 KiB UTF-8 limit, and source plus projection are committed
together.
A tracked direct-database update through MoltaroDbContext.SaveChanges is
authoritative for every modified field. The save removes stored declarative
mutation-effect ownership only from those fields and preserves ownership of
unchanged fields. It does not execute configured mutation effects; those run
only through the application/API Entity save pipeline.
Markdown inline images use opaque
moltaro-attachment://<FileReferenceId> source tokens. Package code must not
construct raw download URLs or treat a token as an access grant. The public
Net Operation Project programming model does not expose draft-session adoption;
use product record forms for destination-scoped image authoring.
All new attachment uploads enter PendingInspection. The descriptor exposes
AvailabilityState, detected MIME, image dimensions, content generation, and
the checksum/generation covered by successful inspection. Only Available
content whose inspected generation and checksum still match can be downloaded
through IAttachmentService. MoltaroAttachmentDownloadResult.Content is now
a non-null verified Stream; dispose it after reading. Moltaro verifies the
immutable body into a bounded spool before exposing any bytes, and the result
does not expose BlobUri. Attachment downloads are intentionally rejected
inside caller-owned database transactions so a checksum mismatch can persist
its security invalidation independently. Code compiled against the previous
buffered result must be updated before moving to this SDK contract.
For large Managed Cloud attachments, use UploadStreamAsync or
UpsertStreamAsync with an exact declared size and a stable idempotency key.
Call GetUploadCapabilitiesAsync before opening or downloading the source. It
returns the effective storage type, maximum file size, multipart availability,
part policy, checksum policy, actor-local active-session/reserved-byte usage
and limits, any saturation retry delay, the 24-hour inactivity and 48-hour
absolute session lease, and authoritative extension-aware FileTypeRules plus
FileTypePolicyVersion and FileTypePolicyHash without
exposing storage credentials. A rule accepts a file
only when its longest matching extension and normalized declared MIME type are
present together. AllowedMimeTypes is a deprecated, conservative compatibility
projection and must not be used to make new upload decisions. It deliberately
omits application/octet-stream; a pre-rule runtime may reject DWG/FBD after a
rollback instead of allowing ambiguous bytes for every extension. A pre-session
policy rejection throws MoltaroSdkException; inspect its stable FailureCode
(also available as ReasonCode) instead of parsing the safe bounded message.
When FailureCode is filesystem.upload.activeLimitReached, the exception has
IsRetryable = true, exposes RetryAfterSeconds, and includes safe active
count/limit and reserved-byte usage/limit metadata. Keep the idempotency key
unchanged and retry only after the advertised delay.
filesystem.file.type.notAllowed metadata includes the normalized extension,
declared MIME, policy version, and policy hash so a caller can correlate the
decision with the exact preflight snapshot across API and Worker processes.
The rule check is not antivirus or malware inspection and cannot detect an
executable file renamed to an allowed ambiguous format. Workspace administrators
may deliberately configure executable extensions and own the consequences of
that policy.
The source may be non-seekable; Moltaro reads it in bounded parallel windows of
16 MiB parts up to the advertised MaximumParallelParts and does not expose S3
endpoints, buckets, object keys, credentials, or presigned URLs. Interactive
cancellation preserves the resumable session. Host
cancellation of a trusted background function aborts that physical attempt and
releases capacity; an exact retry with the same logical idempotency key creates
a successor. Retry the complete stream with the same key, poll
GetUploadAsync, and call AbortUploadAsync only when the upload should be
discarded. Uploaded means the provider accepted the
body, Verifying means background verification/publication is running, and
only Available returns the published reference and descriptor ids.
The idempotency key and requested file-reference id identify one logical
upload; UploadSessionId identifies its current physical attempt. If the
attempt expired, repeat the same stream call with a fresh stream and unchanged
actor, owner, metadata, key, and file-reference id. Moltaro returns one new
successor session after rechecking current policy and concurrency. Querying the
old session still returns terminal filesystem.upload.expired. A background
attempt aborted by host cancellation may restart; explicitly aborted ordinary
uploads and rejected uploads remain terminal. An already published result is
replayed without uploading again.
Failed with filesystem.upload.providerUnavailable is non-terminal; reopen
the source and repeat the same stream call with the same idempotency key because
polling does not retry a provider command.
Start, status, abort, and download operations must run outside an ambient
package database transaction because they can perform external storage I/O.
The legacy direct-database Database/Local upload and upsert methods remain
available and enforce the same workspace FileTypeRules before accepting
bytes.
For a migration backlog, use UpsertStreamsAsync rather than running concurrent
calls through one scoped service instance. A batch accepts at most 100 items;
MaximumConcurrency is 1 through 32 and defaults to 4, while current actor upload
capacity may reduce the effective concurrency. Ordinary actors remain capped at
8 active sessions; only host-established trusted system automation receives the
dedicated 32-session capacity. Each item owns an independent
runtime scope and transaction, results stay in input order, and one item failure
does not cancel successful siblings. Each failed result carries its stable code
and retry metadata. If a terminal item failure occurs after session creation,
the runtime aborts the physical attempt and releases its reservation before
returning the result; retryable failures retain their resumable attempt. Provide
a separate caller-owned readable stream for every
item and invoke the batch outside an ambient package transaction. A successful
item has entered the transfer lifecycle; poll its returned upload-session id to
Available before archiving the source migration task.
Trusted scheduled code may use either stream method when the historical author
is unknown. UploadStreamAsync has no source-author input; for
UpsertStreamAsync, leave SourceCreatedByUserId null. Moltaro preserves null
on the published descriptor, file reference, and relations while keeping the
runtime-owned moltaro-system-automation identity as the authorization and
audit actor. A non-null source creator must resolve to an existing workspace
user and never grants access. Direct-db upload/upsert follows the same unknown
creator rule for the reserved system actor; ordinary direct-db calls require a
real actor and attribute an omitted creator to that actor.
Use IAttachmentService.ReassignAsync to move one published attachment between
EntityInstance owners without changing its FileReferenceId or copying its
body. The request carries a stable OperationId, exact source and target owner
identities, and the row version returned by the attachment list. A committed
retry returns AlreadyApplied; the descriptor, provider object, bytes,
checksum, upload attribution, and relations stay unchanged. Moltaro rejects the
operation when access or Business Invariants deny detach/attach, the reference
is still used by a File or nested Table<File> field, its inspected content
is not currently downloadable, an upload/replacement or hold is active, or the
target already contains the normalized filename. Use the stable
filesystem.reassign.* failure code rather than parsing the safe message.
When a configured Business Invariant provides its own stable rejection code,
Moltaro preserves that code so the caller can identify the exact policy that
blocked the reassignment; filesystem.reassign.invariantRejected is the
fallback when the invariant provides no code.
Runtime functions require the Moltaro host gateway and never call their own HTTP
API or receive provider locators. A direct-db host must configure a canonical
workspace name and a real Business Invariant guard, and must call reassignment
outside any ambient package transaction.
This large-file contract does not change Markdown inline images. Markdown draft
uploads remain image-only, at most 25 items and 100 MiB in aggregate, and use
opaque moltaro-attachment:// tokens only after the existing draft workflow.
Do not place a presigned URL in Markdown source.
Comment imports and reassignment
Section titled “Comment imports and reassignment”Use ICommentService.UpsertAsync for an import or restore that must preserve a
comment id, source creation time, source author, and relations. It cannot move
an existing comment to another owner. If the source author is omitted in a
host-authenticated CRON or Trigger function, Moltaro records
moltaro-system-automation; an explicit source author must be an existing
workspace user. The reserved actor id by itself is not trusted provenance.
CRON and Trigger functions can use ReassignAsync to atomically move one active
comment between existing EntityInstance or BoardItem owners. The comment id,
text, parent, source attribution/time, and relations remain unchanged. Supply a
stable operation id and the current comment row version. Replaying that exact
request returns AlreadyApplied without duplicate writes or events. User-action
functions and external direct-db hosts cannot use this operation.
var current = await comments.GetAsync(commentId, cancellationToken) ?? throw new InvalidOperationException("The source comment no longer exists.");
var moved = await comments.ReassignAsync( new MoltaroCommentReassignRequest { OperationId = operationId, CommentId = current.Id, SourceOwner = MoltaroResourceIdentity.BoardItem(sourceBoardId, sourceItemId), TargetOwner = MoltaroResourceIdentity.BoardItem(targetBoardId, targetItemId), ExpectedRowVersion = current.RowVersion }, cancellationToken);Keep the operation id only for the exact retry. On stale input, re-read and
start a new operation. Handle MoltaroSdkException.ReasonCode using
MoltaroCommentErrorCodes; do not parse error messages.
Principal CRUD and application automation services
Section titled “Principal CRUD and application automation services”For workspace C# that changes principals, use the existing principal CRUD services. Boards and Entitlement Operations retain their curated application-level services:
IUserService,IRoleService, andIGroupService;IBoardAutomationCommandServiceandIBoardAutomationQueryService;IEntitlementAutomationCommandServiceandIEntitlementAutomationQueryService.
Principal mutations create their own transaction, serialize with authoritative Entity authorization through the workspace access-state lock, and atomically commit the principal rows, cache invalidation version, and applicable user-index outbox row. They are not supported inside an already active package transaction.
Entity application automation uses IEntityAutomationCommandService. Its exact
15 commands are CreateAsync, UpdateAsync, ArchiveAsync, RestoreAsync,
DeleteAsync, MergeAsync, ImportAsync, BulkUpdateAsync,
ReplaceResponsibilitiesAsync, ReplaceTagsAsync, CreateCommentAsync,
UpdateCommentAsync, DeleteCommentAsync, UploadAttachmentAsync, and
DeleteAttachmentAsync. Every request carries an OperationKey; exact replay
returns the stored authorized result, while reusing the key for different input
returns an idempotency conflict. Import and bulk update accept at most 100 items
per captured transaction.
CreateAsync takes the five-member EntityCreateCommandRequest: a non-empty
UUID OperationKey, the matching EntityDefinitionKey, a Values field map,
the latest ExpectedDefinitionRowVersion, and a non-null
ResponsibilityCandidates list. Use
Array.Empty<ResponsibilityCandidatePayload>() when the caller proposes no
StageOnCreate assignments. Preserve the operation key only for an exact retry;
re-read the Entity Definition before rebuilding a request after a schema
concurrency conflict.
Code written for the earlier create shape must replace Fields with Values
and replace string MutationIdempotencyKey values with caller-generated UUID
OperationKey values. Rebuild against the current SDK rather than maintaining
an adapter for the removed request shape.
Each method returns SecurityMutationCommandResult<TResult>. Success values are
the documented authorized-detail, safe identity/receipt, responsibility, tag,
comment, attachment, import, or bulk result unions. Current Entity View and
field visibility are rechecked before replay output, so a retry cannot reveal a
record or field that is no longer readable. This facade is host-enforced and
always requires the current Entity Security configuration and dependency
guards. Its host-authenticated system actor bypasses end-user permission
statements but not configuration existence, lifecycle invariants, concurrency,
or idempotency; the reserved actor id without trusted provenance grants no
access. It is distinct from trusted direct-database services, which must never
be substituted behind an application-facade call.
For record lifecycle automation, inject IEntityAutomationCommandService and
pass EntityIdentityCommandRequest to ArchiveAsync or RestoreAsync:
var request = new EntityIdentityCommandRequest( operationKey, "service_ticket", ticketId, currentRowVersion, "Closed by retention policy");
var result = await entities.ArchiveAsync(request, cancellationToken);if (!result.IsSuccess){ var codes = result.Errors.Select(error => error.Code).ToArray(); return FunctionResult.Failed(string.Join(", ", codes));}
var receipt = (EntityLifecycleMutationReceipt)result.Result!;var nextRowVersion = receipt.Data.RowVersion;var replayed = receipt.IsIdempotentReplay;Preserve operationKey only for an exact retry and use the record’s current
RowVersion. moltaro.instances.notFound is the safe missing-or-not-visible
failure, moltaro.instances.conflict means the row version is stale, and
moltaro.instances.archive.alreadyArchived / moltaro.instances.restore.notArchived
report lifecycle-state conflicts. Success always returns an
EntityLifecycleMutationReceipt; its identity-only Data contains the new
committed RowVersion without disclosing Entity field values. The receipt
exposes IsIdempotentReplay, and a replay does not emit another audit row or
resource event. The effective actor is always
moltaro-system-automation. Scheduled Jobs may run without an original user;
when an interactive user exists, that identity and the function, run,
correlation, and Schedule/Trigger/Function origin remain audit attribution.
Board Statements are workspace configuration authored through the Boards Configuration API, not a new C# evaluator or Net Operation Project service. Their public Boards package schema exists for trusted direct-database module integration, while supported workspace logic should use the documented configuration endpoints and server-owned DSL authoring profile.
Board automation facade mutations enforce enabled transition, entry, exit, and
destination status-invariant Constraints through the same application
transaction as the Runtime API.
BoardAutomationMoveValidationResult exposes ordered Errors,
CapturedAtUtc, and ConstraintDecisions in addition to compatibility
ErrorCodes; callers must handle every returned error and must not parse the
workspace-authored message. The advanced direct-database Boards package also
exposes IBoardConstraintRuntimeEvaluator and
AddMoltaroBoardsConstraintRuntimeEvaluator<TEvaluator>() for trusted hosts.
This public custom-evaluator contract handles Board events only. It cannot opt
a standalone host into proposed-state StatusInvariant evaluation, which
remains fail-closed outside Moltaro’s host-provided integration. Its default
event evaluator fails closed if an enabled Binding applies without a registered
evaluator. Destination pickers should call
IBoardRuntimeCommandService.ValidateMoveOptionsAsync; it evaluates candidates
for one item and actor as one request-local batch. Custom high-volume evaluators
should override EvaluateBatchAsync. Authoritative mutations call
EvaluateAuthoritativeBatchAsync only after acquiring governance locks. An
evaluator backed by a configuration or compiled-plan cache must override
that method and reload the applicable configuration inside that call; it may
delegate to EvaluateBatchAsync only when its evaluation source is uncached and
already authoritative. Custom definition providers must implement the remaining
set-based Board definition lookups. BoardRuntimeActor carries only the user id;
roles, groups, responsibilities, Statements, and exact transition permissions
come from the host’s captured SQL authorization scope.
Direct package move validation also returns ordered transition
LinkPrerequisites with current and missing counts. A trusted host can set
MoveBoardRuntimeItemRequest.PrerequisiteLinks to existing Board item ids;
the package derives link kind/direction from the transition, applies ordinary
link permissions and invariants, and commits any new link with the move. Inline
target creation remains an application/Runtime API concern rather than a
direct-package contract.
Generated-context transaction writes
Section titled “Generated-context transaction writes”When code opens a generated-context transaction directly, call
AcquireMoltaroDynamicSchemaReadLockAsync immediately after
BeginTransactionAsync and before any query, row lock, or mutation. Use the
synchronous pair for synchronous transactions. The lock prevents a NetPackage
schema update from changing the meaning of the generated EF model during the
unit of work. ExecuteInTransactionIfNotExistsAsync takes this lock
automatically before it invokes package code.
Normal generated-context SaveChanges and SaveChangesAsync also enforce
status invariants when an existing Entity is the Target or Board Data of a
governing Board Item. Moltaro evaluates the complete tracked proposed root and
owned-table state, including owned Address and Money changes, before persistence.
Single Select values use one option key and Multi-Select values use an array of
option keys. The generated workspace contract maps a sanitized C# enum member
back to the actual workspace option key through a platform-owned EF converter;
for example, member InProgress can represent key in_progress. Board DSL and
Runtime API values use the workspace option key, not the sanitized member name.
Generated String fields carrying the TimeZone semantic role retain that role in
direct-database audit evidence. Normal generated-context SaveChanges and
SaveChangesAsync canonicalize IANA aliases and reject values outside the
platform catalog before persistence, including owned table rows. Raw SQL bypasses
this protection and is not a supported mutation path.
File values use stable string ids. Money uses an Amount/CurrencyCode object:
a missing Amount makes the whole value canonical null, while an omitted,
null, empty, or whitespace CurrencyCode uses the field’s normalized uppercase
BaseCurrencyCode. An Address with no meaningful text or coordinate component
is canonical null. Persisted and proposed values use the same carriers as the
Runtime API. A rejection rolls back the save and
throws MoltaroPreCommitSaveRejectedException; its Errors collection
contains the safe, ordered failures from every registered provider, including
all governing active Boards. Catch that typed exception when function code
needs to translate a rejected save into its own result.
The exception is public from
Moltaro.Package.NET.ModuleRuntime.Runtime. The following fragment is intended
for an OnRunAsync method that returns FunctionResult:
using System.Linq;using Moltaro.Package.NET.Functions;using Moltaro.Package.NET.ModuleRuntime.Runtime;
try{ await dbContext.SaveChangesAsync(cancellationToken);}catch (MoltaroPreCommitSaveRejectedException exception){ return FunctionResult.OkData(new { Accepted = false, Errors = exception.Errors.Select(error => new { error.Code, error.Target, error.Metadata, }), });}Branch on Errors[].Code, not Message. A run that does not catch the
exception receives the general run-level failure reason
moltaro.preCommit.rejected; the provider-specific reasons remain the ordered
codes in Errors because one save may be rejected for more than one reason.
BoardConstraintSaveRejectedException is a separate exception type used only
by direct Boards runtime commands and protected direct Board mutations; code
that handles both boundaries must catch both types explicitly. Multiple tracked saves inside
one explicit transaction retain the latest proposed Entity state from earlier
successful saves. The first Entity mutation lock batch fixes the transaction’s
complete Entity identity set, including a Board-command batch or a save later
rejected by validation. Every later batch must be a subset. A new identity fails
before lock acquisition with
moltaro.preCommit.newEntityIdentityAfterLockBatchUnsupported; stage all
records in a cross-Entity unit of work and save them in one call.
This lock-batch rule applies to every generated-context Entity transaction,
even when none of the records is used by a Board. The complete sorted identity
set is fixed before locks are acquired so concurrent transactions cannot each
hold one Entity lock while waiting for the other.
Manual Order adds the same fail-closed transaction rule for its separate lock.
If an Entity mutation lock has already been acquired, a later save cannot first
introduce a create, archive, restore, or delete that needs the Entity’s Manual
Order lock. The SDK throws MoltaroPreCommitSaveRejectedException before lock
acquisition with code
moltaro.preCommit.manualOrderAfterEntityLockUnsupported. Stage that lifecycle
operation in the first save, or roll back/complete the current work and retry it
in a new transaction. Catch the typed exception and inspect Errors; do not
parse its message.
New generated Entity ids are application-assigned strings. For an atomic
multi-record import, assign every id first, connect References/foreign keys in
memory, add the complete graph to the context, and call SaveChangesAsync
once. A dependent record does not need an intermediate save merely to discover
its parent’s id. Prefer source-stable ids or a durable source-id mapping when
the import must be safely repeatable. See
Create a related Entity graph in one atomic save.
An ordinary single-savepoint rollback restores that snapshot: Entity state
saved before the savepoint remains visible, while state saved after it is
discarded. If nested savepoint history is ambiguous to the runtime, every later
Entity save fails closed with moltaro.preCommit.transactionOverlayInvalidated; roll back
the transaction and retry the complete unit of work in a new transaction.
MoltaroDbContext.SaveChanges[Async] creates that savepoint before the
OnBeforeSaveChanges[Async] hook. Database work performed by the hook, runtime
preflight, the Entity write, mutation-effect ownership release, audit, and
runtime outbox changes therefore share the same rollback boundary.
An owned table row’s OwnerEntityInstanceId is immutable after insertion.
Do not parse exception text or expose evaluated field values.
The same save boundary validates newly assigned File fields on Entity roots and
table rows. The id must resolve to a non-deleted file reference and descriptor
whose content is Available or LegacyUninspected. Pending, rejected,
soft-deleted, and nonexistent references are rejected through
MoltaroPreCommitSaveRejectedException with the safe code
filesystem.file.content.unavailable. An unchanged existing value does not
block an unrelated edit.
ExecuteUpdate[Async] and ExecuteDelete[Async] over a MoltaroEntity or
one of its owned table-row types produce analyzer error MOLTARO039: EF bulk
operations bypass interceptors and therefore cannot participate in pre-commit
validation, audit, presentation, or runtime side effects. Direct SQL that bypasses the generated
MoltaroDbContext is unsupported.
Do not add, edit, or delete BoardItem rows through a generated context.
Tracked direct writes are rejected at save time; ExecuteUpdate[Async] and
ExecuteDelete[Async] over BoardItem produce analyzer error
MOLTAROBOARD001. Use IBoardRuntimeCommandService so locking, Constraints,
history, audit, and outbox behavior stay atomic. Raw SQL against Board runtime
tables is outside the supported Package SDK contract.
Inside one explicit package transaction, call a Board runtime command before
staging or saving any Entity or owned-table mutation. A direct Boards runtime
service call throws BoardConstraintSaveRejectedException, and a Net Operation
Project Board automation facade returns an error, with
moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported when this
ordering is violated. The facade performs this check before opening its
separate application scope, so it cannot wait on locks held by the calling
transaction. Internal Moltaro orchestration may use configuration-prelocked
paths that are not part of the Package SDK contract.
Board configuration is admin-owned. Tracked direct writes to Board,
Statement, Constraint, Binding, Status, Transition, Target Definition, and
related configuration rows are rejected at save time. Bulk writes over those
types produce MOLTAROBOARD002. Use the Board Configuration API so advisory
locking, DSL validation, active-board health checks, and cache invalidation are
part of the same transaction.
Historical Board import and reconciliation do not replay transition, entry, or exit Constraints against the current actor or clock. Only the resulting status invariant governs that path, and it is enforced before commit.
Principal identities
Section titled “Principal identities”Principal CRUD accepts caller-supplied stable string ids. Use an external
source id as MoltaroCreateUserRequest.Id, MoltaroCreateRoleRequest.Id, or
MoltaroCreateGroupRequest.Id, call the matching Get*Async, then choose
Create*Async or Update*Async. These ids are the persisted Moltaro ids, not
separate external keys. There is no separate synchronization facade, upsert
result family, or preflight service.
The exact Unicode UserName is stored without trimming or rewriting; lookup
uniqueness uses trim + Unicode NFC + invariant uppercase. User create/update
can apply profile, type, email confirmation, blocked state, complete role
membership, and metadata in one operation. MoltaroUpdateUserRequest.RoleIds
is applied literally — an explicit list revokes every role missing from it,
including Admin and Configurator, while null preserves the current
memberships and Everyone is always retained. UserType is nullable and null
keeps the stored type. Credential activation remains explicit. Role and group
create/update use the same stable-id CRUD pattern, and built-in system role ids
cannot be created, renamed, or deleted.
These services signal failure with ordinary .NET exceptions —
ArgumentException for malformed input and InvalidOperationException for a
rejected operation such as a duplicate id, a protected role, or an Owner
change. They carry English messages and no stable code, so check state with the
matching Get*Async before writing instead of parsing exception text. The
stable ApiError codes documented for users and roles belong to the workspace
HTTP API.
ITagService.ReplaceAsync uses the current execution actor for newly added
tags. Scheduled automation is therefore attributed to
moltaro-system-automation without creating a synthetic workspace user. Set
SourceCreatedByUserId only when an import or restore preserves an existing
workspace user as the historical author. An unknown source user throws
MoltaroSdkException with the stable
MoltaroTagErrorCodes.SourceCreatorNotFound reason code; function runs persist
that value as FailureReasonCode and do not expose database constraint details.
Any other effective actor must identify a real workspace user or the operation
fails with MoltaroTagErrorCodes.ActorNotFound. Concurrent replacements for
the same resource are serialized, retries are idempotent, and tag changes emit
TagAdded, TagUpdated, or TagDeleted resource events in the same
transaction. Moltaro uses those events to retain function origin metadata,
update entity activity, and produce Board item audit/outbox side effects.
Scheduled automation is authorized through the platform’s owner/admin system
scope only while the runtime-owned tag mutation guard is active and the
function invocation carries platform-issued trusted-system provenance. Actor
strings from HTTP calls or persisted resource events cannot create that
provenance. The actor id is reserved from workspace-user creation and by a
database constraint, so supplying the string alone does not grant access.
Upgrades reject a legacy user row with that id until the conflicting user is
renamed or removed. OriginalUserId is attribution and does not grant access.
Direct-db tag events retain original-user and correlation metadata as a
non-function Moltaro.Tags source.
MoltaroTag.CreatedByUserId can be null when reading legacy rows created
before author attribution was required.
Entity and Board tag flags are deliberately separate. An EntityInstance owner
uses EntityDefinition.TagsEnabled; a BoardItem owner uses
Board.TagsEnabled. Ordinary access failures expose stable
MoltaroTagErrorCodes for disabled Entity tags, disabled Board tags, terminal
Board items that require historical reconciliation, access denial, unknown
owners, and unsupported owner kinds. The workspace Tags HTTP API returns the
same safe code and owner metadata in correlated Problem Details.
While a user has a current external-identity link, its identity-profile state
remains provider-owned; blocked state, role membership, and ExtendedData
remain mutable. DeleteUserAsync archives the current external-identity link
before soft-deleting the user so that the external subject can be linked again.
User, role, and responsibility-group DTOs expose ExtendedData for small
non-secret integration metadata. Keys should be source-namespaced, for example
ventcontrol.source-id. A null request value preserves existing metadata; an
empty dictionary clears it. The supported limit is 32 string entries, key
length 128, and value length 2048. moltaro.* and moltaro:* keys are
reserved. Extended data is read back by list/get calls, is not a permission
grant, and must not contain credentials, tokens, secrets, or full source
payloads.
The Board and Entitlement application services execute complete Moltaro
application orchestration in a separate scope as moltaro-system-automation,
while preserving original-user, function-run, and correlation metadata.
Command DTOs do not accept ActorUserId. Entitlement command DTOs require a
stable IdempotencyKey. AddBoardAutomationItemRequest requires a caller-owned
UUID OperationKey: preserve it for an exact retry, which returns the original
item without another write; changed-payload reuse conflicts. Other ordinary
Boards commands mirror Runtime API concurrency and repeat semantics without a
separate receipt. Rebuild existing consumers and pass the UUID as the first
AddBoardAutomationItemRequest constructor argument; the older constructor
without an operation key is no longer available. Historical
Board migration is the exception: ImportHistoricalItemsAsync uses durable,
case-sensitive per-Board source keys and
GetHistoricalImportAsync resolves unknown outcomes. Batches contain 1–500
items, commit per item, return ordered partial results, and require the latest
ExpectedRowVersion only when reconciling changed state. Exact replay returns
Unchanged without additional writes. Reconciliation updates the existing
Board Data record; its creation-time DataRecordId cannot be replaced. The
separate ReconcileHistoricalItemDataAsync operation repairs only
Subject/Description/Part values on terminal, non-removed, source-identified
historical items. It requires exactly one BoardItemId or case-sensitive
SourceKey locator plus ExpectedRowVersion; changed stale patches roll back,
while an exact replay is write-free and returns the current token. Omitting a
BoardHistoricalTextPatch preserves that text value and wrapping null clears
it. The operation preserves Board lifecycle, target, history, and timestamps;
ordinary closed-item UpdateDataAsync remains forbidden. A pre-existing Entity Definition Board
item uses the explicit GetHistoricalRecoveryStateAsync then
RecoverHistoricalItemAsync flow with the returned exact item id, row version,
and recommended adoption or post-removal mode. Self-contained, Entitlement,
and ambiguous multi-pass targets are rejected by recovery. These services are
supported only in Action, TriggerHandler, Command, Job, and HttpEndpoint
functions. Validation and BeforeSaveMutation receive the stable
moltaro.automation.executionPhase.unsupported error.
ReconcileHistoricalItemTagsAsync is the terminal-only tag counterpart. Its
1-500 item batch commits per owner and returns ordered partial results with
structured MoltaroRuntimeError code, field, target, and metadata. Every item
uses exactly one Board item id or historical source key and a row-version
guard. Add creates only missing names. ReplaceManagedSet changes only the
explicit ManagedTagNames set, preserving all manual/unrelated tags; an empty
desired tag list clears that managed set. Exact
replay is write-free; lifecycle, Board Data, completion/history, and item row
version are unchanged. Normal tag resource events carry the system actor and
function/run/correlation origin into Board audit and outbox processing.
Ordinary SDK tag mutation rejection throws MoltaroSdkException with the same
stable owner-specific ReasonCode/FailureCode, optional Field, and safe
Metadata used by the HTTP and automation contracts. Callers should branch on
the code and metadata, never on the diagnostic message.
The *.Runtime module services remain advanced trusted direct-DB APIs. They
are useful when package-style code deliberately owns all application side
effects, but they are not the default for triggers or jobs. See the
C# runtime recipes and
read ServiceKind/Preferred in the installation’s developer-surface
response.
Advanced direct-DB code that uses
ExecuteInTransactionIfNotExistsAsync receives one atomic boundary in both
hosting modes. The helper opens and owns a transaction when none exists and
acquires the shared dynamic-schema lock before invoking the action. When the
caller already owns a transaction, it acquires the same lock before creating a
nested savepoint;
an exception or a result rejected by shouldCommit restores both persisted
changes and the EF tracked state to that savepoint. The caller may then decide
whether to commit or roll back its outer transaction.
Managed secrets
Section titled “Managed secrets”Moltaro.Package.NET.Functions.ISecretService is available through constructor
injection in Job, Command, Action, TriggerHandler, Validation,
BeforeSaveMutation, and HttpEndpoint functions:
public sealed class SendToPartner(ISecretService secrets) : GlobalFunction{ public override Task<FunctionResult> OnRunAsync(CancellationToken cancellationToken) { var token = secrets.GetRequired("integrations.partner.api-token"); // Use token only for the outbound call. Do not log or return it. return Task.FromResult(FunctionResult.Ok("Partner call completed.")); }}GetRequired throws SecretUnavailableException; inspect Code against
SecretUnavailableCodes.Missing, Disabled, and Retired. TryGet returns
false with a null value for those states. Keys are case-insensitive, but null,
empty, whitespace-padded, or null-character-containing keys throw
ArgumentException.
The runtime captures one immutable snapshot before it creates the function. Rotation affects the next invocation, not repeated lookups in the current one. The service is a trusted-code boundary: any published trusted C# that knows a key can resolve it. Never place a value in arguments, results, DevLog, diagnostics, audit data, exports, or exception messages.
The canonical Managed secrets guide covers provisioning, lifecycle, audit, stable codes, safe token handling, agent rules, and a compile-checked Job example.
Function base classes
Section titled “Function base classes”ValidationFunction<TEntity>— rejects an entity save with validation issues. Bound to save operations (create, update, archive, restore, delete) with[EntitySaveBinding(...)]. See entity-scoped logic.MutationFunction<TEntity>— changes a record before create/update save, or runs after an operation when bound as a trigger with[EntityTrigger(...)]; covered on the same page.GlobalFunction/GlobalFunction<TArgs>— reusable logic without an entity binding. See global logic.CommandFunction<TResult>/CommandFunction<TArgs, TResult>— synchronous calls through the authenticated commands API. See commands and API functions.GlobalActionFunction<TInput>,EntityInstanceActionFunction<TEntity, TInput>,EntitySelectionActionFunction<TEntity, TInput>, andEntityCollectionActionFunction<TEntity, TInput>— user-triggered UI actions with typed input. See actions.HttpEndpointFunction/HttpEndpointFunction<TBody>— inbound HTTP endpoints served under/api/workspace/inbound/{endpointKey}, declared with[MoltaroHttpEndpoint]. See HTTP endpoints.
TEntity is the workspace entity class the function works with; argument,
input, and result types are JSON-serializable records you define.
For generated workspace entities, a Date and time field is a nullable
System.DateTimeOffset property backed by one PostgreSQL
timestamp(6) with time zone column. SDK SaveChanges normalizes values to
UTC and truncates precision beyond microseconds. The original numeric offset
is not a persisted attribute of the field.
Business Invariants
Section titled “Business Invariants”The SDK exposes the supported object-level Business Invariant contract under
Moltaro.Package.NET.BusinessInvariants. It defines
the generic BusinessInvariant<TEvaluationData> authoring base, the no-data
BusinessInvariant base, canonical resource and actor inputs, operation-key
constants, denial results, and the explicit IBusinessInvariantValidator
contract for function code.
Build analysis validates stable keys, typed targets, exact/category operation
scopes and exceptions, declared denial messages, parameter schema, supported
dependencies, and pure Grant code, including side effects reached through
source-defined helper methods. Package and Net Operation Project publication
produce equivalent deterministic descriptors and resolve embedded default text
without creating configuration.
Discovery alone does not enable an invariant; administrators own target
configuration. Enabled compatible targets shape current object-operation
decisions and automatically guard admitted Entity mutation owners. Function
execution is not intercepted automatically. Inject
IBusinessInvariantValidator, validate the exact existing resource and
function.{BusinessFunction.Key} with empty parameters, and stop before side
effects when the ordinary ValidationResult is invalid.
When an owning runtime adapter returns an invariant denial, its public API error
keeps the exact operation key in Target and the stable invariant source key in
Metadata.SourceKey.
Use the published constants instead of private operation strings. Board cycle
assignment and rollover both use BoardItemObjectOperationKeys.AssignCycle;
rollover validates every affected open item before moving any of them.
Identity and metadata
Section titled “Identity and metadata”Every function class carries [MoltaroFunction(id)]. The id is the durable
function id; an optional Key overrides the callable key (by default the id
is the key). Name and Description are shown to administrators and
configurators — set Description on any published function so its purpose is
clear in the Function Catalog. EnabledByDefault controls whether the
function starts enabled when it is first applied.
Publishing commands and enqueueable functions
Section titled “Publishing commands and enqueueable functions”[MoltaroCommand] publishes a command function at
POST /api/workspace/commands/{functionKey}. [MoltaroApiEnqueue] publishes
a Job-contract global function for asynchronous enqueue at
POST /api/workspace/functions/{functionKey}/enqueue; callers with access can
also observe and cancel the resulting queued job.
Both attributes are fail-closed: exactly one of PermissionKey or
AllowAnyAuthenticatedUser = true must be configured; a publication without
an access declaration is rejected at build and apply time, and anonymous
invocation is never supported. A permission key is a lowercase dotted
identifier such as acme.commands.recalculate — the segment after the last
dot becomes the catalog code, the leading segments the catalog group, so
administrators can assign it to roles.
MoltaroCommand.TimeoutSeconds optionally overrides the synchronous timeout,
from 1 through 86400 seconds. When unset, the runtime action timeout setting
applies — and the effective timeout never exceeds that installation setting.
MoltaroApiEnqueue.TimeoutSeconds optionally declares the worker timeout for
the queued job, also from 1 through 86400 seconds. Moltaro captures the value
when the API request creates the job, so every retry uses the same timeout even
if the publication or runtime defaults change. When unset, the runtime Job
timeout setting applies.
[MoltaroFunction("recalculate-credit", Name = "Recalculate credit", Description = "Recomputes the customer credit snapshot.")][MoltaroCommand(PermissionKey = "acme.commands.recalculate", TimeoutSeconds = 120)]public sealed class RecalculateCredit : CommandFunction<RecalculateResult>{ public override async Task<CommandResult<RecalculateResult>> OnRunAsync( CancellationToken cancellationToken) { // ... business logic ... return CommandResult<RecalculateResult>.Ok(result); }}Failure results
Section titled “Failure results”A message passed to CommandResult.Failed(...) is returned to the caller as
user-facing text: keep it actionable and free of secrets or internal
diagnostics. Use CommandResult.ValidationFailed(...) to reject an
invocation with field-level validation issues instead of a generic failure.
For choosing between C# and declarative logic, project layout, and the build and apply workflow, continue with C# business logic.