Skip to content

C# business logic: the Net Operation Project

The Net Operation Project is the C# authoring surface for one Moltaro installation. The workspace owns a tree of C# source files; Moltaro compiles that source against a generated typed schema contract built from the current entity configuration, using the Moltaro .NET SDK programming model. You write operational logic — functions, services, integrations — while the platform keeps ownership of the schema, the project files, and the build toolchain. Its frontend counterpart is the Workspace UI Project, which publishes trusted custom pages built from workspace-owned Vue source.

The project consumes the generated Entity contract; it does not own Entity Manual Order configuration, protected rank storage, or positional Move APIs. Those remain governed Moltaro application behavior rather than workspace C# schema or business-logic surface. If Manual Order is active, inserting an entity through the generated context’s normal SaveChanges pipeline automatically allocates its canonical tail position in the same transaction. Workspace code must not calculate or submit a platform Manual Order rank.

In an explicit caller transaction, include every create, archive, restore, or delete for an ordered Entity in the first Entity SaveChanges. A later save cannot safely introduce the Manual Order lock after an Entity mutation lock has already been acquired. It fails before lock acquisition with MoltaroPreCommitSaveRejectedException and code moltaro.preCommit.manualOrderAfterEntityLockUnsupported. Inspect the typed error and either stage the complete unit in the first save or retry the lifecycle operation in a new transaction; never parse exception text.

The SDK’s policy-free LexicographicRankKeyGenerator may be used for a custom workspace-owned ordering algorithm. It generates standalone ordinal keys only; it neither reads nor changes the platform-owned Manual Order position.

Project source can contain every function shape Moltaro supports: entity validation, mutation, and trigger functions, global functions and actions, commands and API-enqueued jobs, inbound HTTP endpoints, and functions bound to CRON schedules. There is no separate outbound-webhook feature: outbound HTTP calls are ordinary C# code inside trigger or global functions.

API-first authoring targets the API host of one configured workspace. Resolve every /api/workspace/... route below against that installation’s WORKSPACE_API_BASE_URL, never against moltaro.com. The workspace API connection guide explains the human bootstrap, service key, and downloaded agent guide.

Authoring lives in the Constructor area under the Automation & logic group: the Development page (titled “Business Logic Development”) hosts both editing modes, next to the Function Catalog and Function Schedules pages.

  • Configuration API — the primary path for AI agents and automation: templates, unsaved-buffer Roslyn assistance, immutable revisions, checks, builds, and polling are all public operations. Start with the AI agent development quickstart.
  • Studio — in-browser editing over the stored source tree: a code editor with server-side Roslyn diagnostics, completions, hover, and signature help; source templates for common shapes; navigation over a source index; and save, save-and-check, and save-and-build actions.
  • NET Project — an optional human/local-IDE workflow: download a generated, ready-to-build solution ZIP, edit locally, and upload the result.

Both modes read and write the same versioned file tree stored in the workspace database. ZIP files are transport only: Moltaro generates the .sln, .csproj, props, and assembly references on every download, and on upload it imports only allowed source files (src/**/*.cs, Markdown docs) plus external NuGet PackageReference entries, ignoring generated project files and build outputs.

Source history is a sequence of immutable revisions. A Studio save or a ZIP upload never edits existing source — it creates a new revision and makes it the working revision, while the current revision and the active compiled artifact stay unchanged until a build succeeds. Draft, validated-draft, or failed revisions that never produced an artifact can be deleted; published revisions are retained for attribution.

Builds run in the background and follow one explicit pipeline:

  1. Generate the schema contract from the current entity configuration.
  2. Compile the working revision against it, storing full diagnostics.
  3. Discover function descriptors from the compiled assembly.
  4. Synchronize the function catalog — new functions appear, removed functions are retired and blocked from new invocations.
  5. Activate the new artifact. Exactly one artifact is active at a time.

A failed build never replaces the active artifact: the previous logic keeps running until a new build succeeds. A check uses the same compile path and diagnostics but activates nothing — it only validates the working revision.

Schema changes do not trigger recompilation. Additive changes (new entities, new fields) are safe: the active artifact keeps running and the next build sees the new contract. Destructive or type-changing edits mark the project as needing validation while the active artifact continues to run; the Development page then offers an explicit Check project action that compiles the source against the new contract and reports exactly what broke.

  • GeneratedWorkspaceContext — a typed DbContext over the workspace’s runtime tables, with exact mappings to physical table and column names.
  • Typed entity classes and child-table row classes for every active entity definition, with typed accessors per field kind.
  • Stable constants for entity ids, table ids, field ids, field keys, and physical column names.
  • The current Entity Definition search-target declarations in the generated schema configuration, emitted as SearchField(...) and SearchSystem(...), so Check and Build use the same workspace contract.
  • A DI extension that registers the context and platform services.
  • XML documentation for supported Moltaro assemblies, used automatically by server-side hover and signature help and published in the curated .NET reference.

The contract is a build input generated by Moltaro, not user-owned source — you never download or maintain it in an API-driven workflow. Moltaro adds the current contract to every language-service, Check, and Build snapshot.

Presentation metadata, including responsive Card item spans, stays in Entity UI configuration. Net Operation Project code works with the generated typed records and must not treat the current Card column count as a business-logic contract; the runtime chooses it from the available page or drawer width.

A Date and time entity field is generated as nullable System.DateTimeOffset. The generated EF model maps it to one PostgreSQL timestamp(6) with time zone column. Moltaro normalizes writes to UTC and microsecond precision before saving, so project code may supply a non-zero offset but should compare and reason about the resulting instant rather than the original offset.

A String or Text field with the Markdown property label is generated as a nullable string. The property contains the original Markdown source. Moltaro maintains its display-safe plain-text projection internally and uses that projection for presentation, search, projections, and audit previews; links contribute their visible label, not their destination. Write the property and call the generated context’s normal SaveChanges method so source validation, the projection update, and the database writer contract remain atomic. Raw SQL writes to governed Markdown columns are unsupported and are rejected.

A String field with the TimeZone property label is generated as a nullable string containing the canonical IANA identifier stored by Moltaro. Its semantic role is retained in direct-database audit evidence. Treat the value as a closed catalog identifier. The generated context canonicalizes aliases and rejects unknown identifiers on SaveChanges and SaveChangesAsync, including owned table rows; raw SQL is not a supported mutation path.

The same generated-context save boundary enforces active Board status invariants for existing Entity records used as a Board Target or Board Data. Moltaro checks the complete tracked proposed root and owned-table state across all governing Boards before persistence. If any Board rejects the state, SaveChanges rolls back and throws MoltaroPreCommitSaveRejectedException. Catch it when the function must return a controlled failure and use its ordered Errors; the same contract also carries rejections from future non-Boards providers. Do not parse the exception message. Creating a new record has no pre-existing governing Board Item. Direct SQL bypasses this protection and is not a supported Entity mutation path.

Opaque moltaro-attachment://<FileReferenceId> values in Markdown source are not URLs or access grants. Net Operation Project code does not create Markdown draft sessions; use the product form workflow when a user must author an inline image. New ordinary attachments are queued for inspection. Downloads succeed only for verified Available content and return non-null streamed bytes; the SDK download result does not expose a backing-storage BlobUri.

Large Managed Cloud files use the provider-neutral IAttachmentService.UploadStreamAsync/UpsertStreamAsync contract. Supply the exact size and a stable idempotency key, retry a cancelled transfer from the start with the same key, and poll GetUploadAsync until IsTerminal. The key identifies the logical upload. An expired UploadSessionId stays terminal, but repeating UpsertStreamAsync with a fresh stream and the same actor, owner, metadata, key, and requested FileReferenceId returns the single successor physical session. Explicit abort and rejection remain final. Before opening the source, call GetUploadCapabilitiesAsync to inspect the effective storage type, maximum size, multipart and part policy, checksum policy, and extension-aware FileTypeRules. The longest matching extension and normalized declared MIME must occur in the same rule; the deprecated AllowedMimeTypes summary is not authoritative. This is a declared-metadata policy, not antivirus or malware inspection. A pre-session rejection is a MoltaroSdkException; branch on its stable FailureCode, not its bounded diagnostic message. Use FileTypePolicyVersion and FileTypePolicyHash from preflight and rejection metadata to correlate the effective normalized rule set across runtime processes. For a trusted scheduled import with no known historical author, leave SourceCreatedByUserId null. Moltaro preserves null on the descriptor, reference, and relations while the system actor remains separate audit and authorization provenance. A supplied source user must exist and never grants access. Failed with filesystem.upload.providerUnavailable remains resumable: reopen the source and repeat the same stream call; polling alone does not resume the provider command. The runtime keeps all S3 credentials and signed URLs outside project code. This ordinary-attachment path does not raise or bypass the Markdown image-draft limits.

Automatic save auditing is off for function code and cannot be switched on per save: field-level audit narrates what users change through the product surfaces, while your code tells its own story. The supported emit path is MoltaroDbContext.AddBusinessEventAsync(...), which appends a display-safe business event to a record’s audit trail; the event then appears in the record change feed and the WebApp history. The generated contract does not expose audit rows as queryable DbSets — read history through the API. The full picture, including the change-feed endpoint and audit settings, is in Record history and audit.

Project code uses constructor injection. Alongside the generated context, the runtime provides:

ServicePurpose
FunctionContextOriginal and actor user ids, operation, correlation id, and function run identity for the current invocation
MoltaroWorkspaceContextWorkspace metadata: display name, locale, and time zone
IDevLogDeveloper log entries visible in run diagnostics
IFunctionQueueEnqueue published functions for background execution
ISecretServiceResolve administrator-managed outbound credentials from the immutable invocation snapshot
IMoltaroRuntimeClockWorkspace time
IUserService, IRoleService, IGroupServiceUser, role, and responsibility-group list/get/create/update/delete, stable caller-supplied ids, memberships, permissions, blocked state, credentials, and non-authorizing extended data
IAttachmentService, ICommentService, ITagServiceCreate and change governed attachments, comments, and resource tags
IMoltaroResourceEventBusPublish supported durable resource events
IBusinessInvariantValidatorExplicitly validate the current existing resource and exact operation before function side effects
IDynamicEntityGraphQueryService, IDynamicEntityBusinessEventWriter, IDynamicEntityAuditTrailImportServiceSupported dynamic-entity traversal, business-event, and explicit import/restore operations

The lib/ assemblies ship with XML documentation, so every service member is documented in the IDE; the same documentation is readable from the raw .xml files for agents working outside an IDE.

The SDK exposes Business Invariant authoring, analyzer, and deterministic descriptor-discovery contracts. Administrators configure and enable targets; enabled compatible targets shape current object operations and guard admitted Entity mutations. Analyzer purity checks follow source-defined helper calls reached from Grant. Function execution is not intercepted automatically. Inject IBusinessInvariantValidator, pass the exact existing resource and operation with empty parameters, and stop before side effects when the ordinary ValidationResult is invalid.

Use the SDK operation constants rather than private strings. For example, single-item Board cycle assignment and whole-cycle rollover share BoardItemObjectOperationKeys.AssignCycle; a rollover is rejected before any item moves when one affected item is denied.

Inject ISecretService into any C# function constructor. Use GetRequired(key) when missing configuration must fail the invocation, or TryGet(key, out value) for optional integration configuration. Keys are case-insensitive. Null, empty, and whitespace-padded keys are programming errors. Unavailable required secrets throw SecretUnavailableException with stable Missing, Disabled, or Retired codes.

One invocation always reads one snapshot, so a rotation made while code is running becomes visible only to a later invocation. Never return or log the resolved value. Any trusted C# project that knows a key can resolve it; only allow trusted authors to publish project code.

For the owner/admin lifecycle, exact stable codes, safe outbound handling, and a compile-checked Job, see Managed secrets.

Short patterns for the services above; hover and signature help fill in the rest from the curated XML.

Resolve the current actor or another user (both return MoltaroUser?):

var actor = await users.GetActorUserAsync(cancellationToken);
var assignee = await users.GetUserAsync(assigneeId, cancellationToken);

Choose the right log target. IDevLog writes developer diagnostics for the current run; a durable, user-visible history entry is a business event on the record (see audit):

await devLog.LogInformationAsync("Recalculated credit snapshot", cancellationToken: cancellationToken);
await dbContext.AddBusinessEventAsync<Customer>(
customer.Id, new MoltaroBusinessEvent("Credit recalculated"), cancellationToken);

Enqueue a published global Job and poll it — EnqueueAsync returns a job id, not a result:

var jobId = await functionQueue.EnqueueAsync("acme.nightly-rollup", new { Scope = "eu" }, cancellationToken);
// then poll GET /api/workspace/functions/jobs/{jobId} until a terminal state

Add governed content to a record through the generated entity type — comments.CreateAsync<ServiceTicket>(ticket.Id, request, cancellationToken), and attachments/tags follow the same <TEntity>(entityId, …) shape.

Scheduled jobs attribute new tags to the reserved moltaro-system-automation actor without requiring a workspace user record. Set SourceCreatedByUserId only for import or restore when preserving an existing workspace user as the historical author. If that user does not exist, ITagService throws MoltaroSdkException with MoltaroTagErrorCodes.SourceCreatorNotFound; job diagnostics retain that stable reason code without exposing database constraint details. Moltaro validates tag permissions with the effective actor. Scheduled automation uses the platform’s owner/admin system envelope only when the function invocation carries platform-issued trusted-system provenance; the reserved actor string cannot grant that envelope to HTTP or resource-event callers. Workspace migrations also reject a real user with the reserved id. OriginalUserId is retained only as attribution and is never substituted for authorization. A repeated complete-set replacement is idempotent, concurrent replacements for one resource are serialized, and tag mutation events preserve the function run, version, correlation, and original-user origin used by entity activity and Board audit/outbox processing.

EntityInstance tags and BoardItem tags have independent feature flags: EntityDefinition.TagsEnabled and Board.TagsEnabled. Ordinary BoardItem tag writes are open-item operations. For a terminal BoardItem, use the trusted IBoardAutomationCommandService.ReconcileHistoricalItemTagsAsync batch. Add preserves existing names; ReplaceManagedSet changes only an explicit managed-name allow-list, so manual tags stay intact. The batch commits each owner separately and returns structured partial outcomes; it does not reopen, move, or change Board Data, completion, history, or the item row version.

In-app notifications are not a Net Operation Project surface: IMoltaroNotificationPublisher is intentionally not injectable — use IDevLog and business events instead.

Application automation vs trusted direct-DB runtime

Section titled “Application automation vs trusted direct-DB runtime”

Principals, Boards, and Entitlement Operations expose two deliberate C# levels. Choose the application automation interfaces for normal Net Operation Project code:

LevelUse it whenWhat it owns
ApplicationAutomation (preferred)A function must perform the same complete operation as the Moltaro applicationTarget and module validation, SQL authorization, system actor provenance, module-native concurrency/idempotency semantics, audit and origin metadata, business/resource events, outbox, and attention projections
TrustedDirectDb (advanced)Trusted package-style code intentionally owns database and application orchestrationPackage domain rules and direct database work only; the caller owns every missing application side effect

The machine-readable distinction is in each entry returned by GET /api/workspace/admin/net-operation-project/developer-surface: inspect ServiceKind, Preferred, SupportedFunctionContracts, ExecutionSemantics, and DocumentationUrl. Public CLR visibility by itself is not a support promise.

Preferred Entity service (Moltaro.Package.NET.Automation):

ServicePurpose
IEntityAutomationCommandServiceGoverned create, update, archive, restore, delete, merge, import, bulk update, responsibilities, tags, comments, and attachments with durable OperationKey replay, optimistic concurrency, lifecycle audit, and application side effects

Preferred Boards services (Moltaro.Package.NET.Boards.Automation):

ServicePurpose
IBoardAutomationCommandServiceUI-equivalent add, movement, removal, cycle assignment, links, responsibilities, Board Data, due dates, durable 1–500 item historical import, terminal-safe historical Board Data and managed-tag reconciliation, and guarded historical identity recovery with the complete application flow
IBoardAutomationQueryServiceBoard/target discovery, open-item lookup, historical source-key and recovery-state lookup, move validation, current item, and current RowVersion

Principal CRUD services (Moltaro.Package.NET):

ServicePurpose
IUserServiceUser list/get/create/update/delete, caller-supplied stable ids, exact-Unicode names, blocked state, role membership, password activation, and extended data
IRoleServiceRole list/get/create/update/delete, caller-supplied stable ids, user/permission replacement, and extended data
IGroupServiceResponsibility-group list/get/create/update/delete, user membership, and extended data

Principal mutations own their transaction, serialize with authoritative Entity authorization, and atomically commit the principal rows, cache invalidation version, and applicable user-index outbox row. They cannot run inside an active package transaction.

Repeatable integrations call Get*Async and then Create*Async or Update*Async. Update requests are complete desired state: RoleIds is applied literally and revokes any role missing from an explicit list, while null preserves the current memberships. Moltaro deliberately does not expose a parallel principal synchronization facade or batch preflight service.

Preferred Entitlement services (Moltaro.Package.NET.EntitlementOperations.Automation):

ServicePurpose
IEntitlementAutomationCommandServiceGrant, renew, suspend, resume, revoke, expire, consume, reverse, adjust, and renewal-operation lifecycle with durable idempotency
IEntitlementAutomationQueryServiceGrant preview, access evaluation, history and ledger snapshots, limit state, and renewal-operation state

Application automation has fixed semantics:

  • It is supported in Action, TriggerHandler, Command, Job, and HttpEndpoint functions. Validation and BeforeSaveMutation return moltaro.automation.executionPhase.unsupported.
  • The application call persists as moltaro-system-automation. The original user, function run, function/version, and correlation are retained in origin and audit metadata; request DTOs intentionally have no ActorUserId.
  • Entity ArchiveAsync and RestoreAsync take EntityIdentityCommandRequest. Reuse its OperationKey only for an exact retry and supply the current RowVersion. Success returns an EntityLifecycleMutationReceipt with the next row version, no Entity field values, and IsIdempotentReplay. Scheduled Jobs do not require an original user, and Schedule/Trigger/Function origin is retained without duplicating lifecycle audit or resource events on replay.
  • Entry to an interactive function is authorized before its code runs. The module call does not impersonate or re-authorize that original user.
  • Each call creates a separate application scope and transaction. It does not join arbitrary pending changes in an injected DbContext. Save those changes first, or invoke the facade from an after-commit trigger.
  • A successful return means the facade’s main transaction committed. Ordinary Boards commands intentionally use normal Runtime API semantics and do not add a separate idempotency receipt; query current Board state before retrying an unknown outcome. Historical import instead uses a durable, case-sensitive SourceKey: query it with GetHistoricalImportAsync or safely replay the batch. When one legacy Entity Definition target already has a Board pass, read GetHistoricalRecoveryStateAsync, use exactly its recommended mode and item/row-version guards with RecoverHistoricalItemAsync, and inspect the outcome. After an unknown recovery result, retry that guarded request or query the source identity. Entitlement commands require a stable IdempotencyKey, and Replayed = true means their native durable result was returned. Use the returned RowVersion for the next mutation.
  • FollowUpOperationIds contains only operations that were actually queued. When a function job id is returned, poll GET /api/workspace/functions/jobs/{jobId}. Do not infer completion from the C# Task alone.

The older IBoardRuntimeCommandService, IBoardRuntimeDefinitionProvider, IEntitlementRuntimeCommandService, IEntitlementRuntimeGrantPreviewService, IEntitlementRuntimeAccessService, IEntitlementRuntimeHistoryQueryService, IEntitlementRuntimeLimitWindowService, IEntitlementRenewalPolicyService, and IEntitlementRenewalOperationService remain supported advanced TrustedDirectDb interfaces. They are not the recommended route for a C# trigger because they do not promise the WebApp/application orchestration listed above.

This distinction includes transition link prerequisites: the advanced Boards package can create configured links to existing Board items during a move, while atomic creation of a new target item plus its link and move is provided by the Runtime API application adapter.

Currency Rates (Moltaro.Package.NET.CurrencyRates) exposes ICurrencyRatesRuntime. It reads settings, provider readiness, and local cache only, then resolves same/direct/inverse/cross rates with provider priority and historical fallback. It never performs a network refresh from project code.

The complete machine-readable allow-list is GET /api/workspace/admin/net-operation-project/developer-surface. See the runtime recipes for Boards, Entitlement Operations, Currency Rates, and explicit audit events.

Project-owned services are registered from a startup class declared with assembly attributes:

using Microsoft.Extensions.DependencyInjection;
using Moltaro.Operational.Generated;
using Moltaro.Package.NET.Functions;
[assembly: MoltaroNetOperationProject(typeof(GeneratedWorkspaceContext))]
[assembly: MoltaroNetOperationProjectStartup(typeof(Moltaro.Operational.MoltaroStartup))]
namespace Moltaro.Operational;
public sealed class MoltaroStartup : IMoltaroNetOperationProjectStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ITicketRoutingService, TicketRoutingService>();
}
}

Registered services can then be injected into any function or data source in the project.

Startup is only for project-owned service types. Services listed by the installation-local developer-surface endpoint are already registered by the generated contract and Moltaro host; inject them instead of adding, removing, replacing, or decorating their descriptors. Check and Build apply the same reserved-service validation used by runtime execution.

For common core platform types in the analyzer catalog, statically identifiable, unconditional AddSingleton, AddScoped, and AddTransient registrations are reported as MOLTARO016, including keyed and non-generic overloads. The complete developer surface is installation- and module-specific, so the exact runtime diff is authoritative for every generated and host descriptor. It also covers conditional TryAdd* calls, raw descriptors, removal, replacement, decoration, and registrations hidden behind extension methods. Check/Build fail when those operations mutate the host baseline, with reason code netOperationProject.startup.reservedPlatformServiceMutation and safe metadata identifying the service, mutation, descriptor, and startup type.

The Net Operation Project status response includes ActiveArtifactRuntimeStatus, an activation preflight with the active revision/build/artifact ids and a stable compatibility reason code. Function run and queued-job operation records include FailureReasonCode or LastFailureReasonCode together with their captured Net Operation Project revision, build, and artifact ids, so startup activation failures are distinct from failures inside user code.

Every failed terminal run has a non-empty stable reason code. Moltaro preserves the exact code from a typed SDK or platform failure; an ordinary unhandled runtime error uses moltaro.functionRuntime.unhandled. Queued retries and the terminal failed job retain the same code in LastFailureReasonCode. Treat the code as the machine-readable diagnostic contract and do not parse localized or administrative error text.

Everything the Development page does is available through the Configuration API under /api/workspace/admin/net-operation-project:

EndpointPurpose
GET /statusProject status; bootstraps the starter revision if needed
GET /source-treeSource tree of the working or requested revision
GET /source-filesOne source file by revision and path
POST /source-revisions/manual-editCreate a manual-edit revision from file changes
GET /source-templatesList template ids, default folders, and required inputs
POST /source-templates/previewPreview generated C# without creating a revision
GET /developer-surfaceSupported assemblies, versions, injectable services, generated-contract behavior, and reference links
GET /builds, GET /builds/{buildId}Build and check history with diagnostics
POST /buildsQueue a build or a check of the working revision
POST /source-language/completionsC# completions for an unsaved source buffer
POST /source-language/diagnosticsC# diagnostics for a source buffer
POST /source-language/hoverSymbol signature and XML documentation for a source buffer
POST /source-language/signature-helpCall signature and parameter documentation for a source buffer
GET /source-language/source-indexRead-only classification and navigation index
GET /downloadGenerated project ZIP for the current or requested revision
POST /uploadUpload a project ZIP as a new immutable revision

For API-driven authoring, use all four source-language operations on the unsaved Content buffer before creating a revision. Include the current SchemaContractHash from GET /status; a stale value returns HTTP 409 with netOperationProject.language.schemaContract.stale. Diagnostics use the same generated-contract project reference, controlled assemblies, C# options, and analyzers as Check. Every diagnostic identifies its Source or GeneratedContract origin and includes a stable code and Roslyn location when available. Diagnostics do not save. Successful diagnostics and manual-edit responses publish effective source processing Limits. The current contract supports 500 files, 512 KiB per UTF-8 file, 5 MiB total source, and 500 changes per manual edit. Diagnostics deadline failure is HTTP 503 with netOperationProject.language.analysis.timeout; manual-edit deadline or bounded project-lock failure is HTTP 503 with netOperationProject.source.manualEdit.timeout and creates no revision. Size errors report safe maximum and actual byte counts without source content. Manual edit creates an immutable revision with optimistic concurrency; ExpectedContentHash mismatch is HTTP 409. Net Operation Project numeric values are Build = 0 and Check = 1; Check never activates, while a successful Build automatically activates and a failed Build keeps the previous active artifact.

Deployed logic is monitored in the Administration area under the Monitoring group — API Functions, Function operations, and HTTP Endpoints — covered in Runs, jobs, and diagnostics.