Skip to content

Import records and retain history

Choose the write path before implementing an import. Running C# inside Moltaro supplies invocation identity and services; it does not automatically add field history to every database write. All /api/workspace/... routes below use your WORKSPACE_API_BASE_URL, not the public documentation host.

RequirementSupported pathWhat is recorded
Create or update records through the complete Entity application flowInject IEntityAutomationCommandService; use CreateAsync, UpdateAsync, ImportAsync or BulkUpdateAsyncNormal field/table/system history when the definition enables audit, with the function run that issued the command as the change source
Perform a trusted direct-EF operationUse the generated context and its supported transaction/save rulesAutomatic SaveChanges audit is off in the hosted generated context; a direct save does not get a second field-audit pass from the function host
Explain a meaningful custom operationMoltaroDbContext.AddBusinessEventAsync<TEntity>An explicit event in the same record history; it does not calculate old/new field values
Import an existing, trustworthy external historyIDynamicEntityAuditTrailImportService.ImportAsyncExplicit historical entries with supplied source event identity, time, actor and field changes; it does not reconstruct history from current values

Read the installed GET /api/workspace/admin/net-operation-project/developer-surface and the installed SDK XML/reference before choosing a service. Entity application automation is supported in Action, Command, Job, TriggerHandler and HttpEndpoint functions; Validation and BeforeSaveMutation reject it. Verify the installation’s SupportedFunctionContracts, rather than inferring support from a public page. These services are already registered: inject them; do not replace platform DI registrations or edit generated code to enable auditing.

The hosted Net Operation Project’s invocation audit context supplies actor/correlation metadata, not an audit enable switch. A before-save contribution to the record being saved by the Entity application flow is included in that operation’s final audit. This does not extend that audit to arbitrary other records saved directly by the function.

Import one record with normal field history

Section titled “Import one record with normal field history”

First enable audit for the Entity Definition and read its current RowVersion. The example uses a String field named Name; substitute the installed field key and valid values. Use the result of your own source-to-record mapping to choose Create or Update. For Update, obtain the current record RowVersion through a protected record read. A missing or unreadable record is not proof that Create is appropriate.

This helper belongs in project-owned code and can be called from an allowed function with an injected IEntityAutomationCommandService. It intentionally imports one item, so callers cannot overlook partial batch outcomes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Moltaro.Package.NET;
using Moltaro.Package.NET.Automation;
namespace Moltaro.Operational.Examples;
public static class EntityHistoryImport
{
public static async Task<EntityImportItemResult> ApplyAsync(
IEntityAutomationCommandService entities,
Guid operationKey,
string definitionKey,
Guid definitionRowVersion,
string sourceKey,
string name,
string? existingRecordId,
Guid? existingRowVersion,
CancellationToken cancellationToken)
{
if ((existingRecordId is null) != (existingRowVersion is null))
throw new ArgumentException("Update requires both record id and row version.");
var request = new EntityImportCommandRequest(
OperationKey: operationKey,
EntityDefinitionKey: definitionKey,
Items:
[
new EntityImportCommandItem(
InputOrdinal: 0,
Mode: existingRecordId is null ? "Create" : "Update",
CallerIdentityKey: sourceKey,
EntityInstanceId: existingRecordId,
RowVersion: existingRowVersion,
Values: new Dictionary<string, JsonElement>
{
["Name"] = JsonSerializer.SerializeToElement(
name, new JsonSerializerOptions())
},
ResponsibilityCandidates: [])
],
ExpectedDefinitionRowVersion: definitionRowVersion);
var response = await entities.ImportAsync(request, cancellationToken);
response.ThrowIfFailed(); // Checks the outer command, not each item.
var item = response.Result!.Items.Single();
if (item is SafeFailure failure)
throw new MoltaroSdkException(failure.Code, "The import item failed.", failure.Field);
// Preserve the union: a committed receipt does not imply readable data.
return item;
}
}

For AuthorizedEntityIdentity, persist the returned Data.EntityInstanceId in your source mapping and use the returned RowVersion for subsequent decisions. For EntityRecordMutationReceipt, retain the receipt and reconcile through an authorized read; do not create a replacement because result data is hidden. Its CallerKnownIdentity holds only what you sent: the definition, the record and the row version of an Update item. A replay of a Create item whose record you can no longer read answers SafeFailure with moltaro.instances.replay.resultNotViewable: the record exists, so do not import it again under a new key. Do not assume every successful item contains readable identity or field data.

Persist the caller-generated UUID operationKey and the exact request inputs before invoking the helper. After an unknown outcome, retry that same operation with unchanged inputs. Do not generate a new key or replace a version on an exact retry. A genuine next change uses a new key and freshly read state. CallerIdentityKey identifies an input item; it is not a global source-key upsert index and does not choose a Create record id. Repeating Create with a different operation key can create another record. No-op detection across different operations belongs to the importer; compare the intended source state first.

Application commands execute in a separate host-owned scope. They do not join your injected context’s transaction. Do not wrap the helper in an EF transaction and assume that rolling it back will undo a successful application command.

ImportAsync accepts 1–100 items for one Entity Definition and returns a completed EntityImportResult. Its JobId does not mean another background import was queued. Inspect every Items outcome: an outer success can contain SafeFailure items alongside committed items. Do not treat the batch as an all-or-nothing import or retry its successful items with new keys.

This API does not provide a single transaction for a new Beneficiary → Case → Assistance graph spanning several definitions. A Command or HTTP endpoint can save such records in one transaction as its caller, up to the host’s step limit (100 by default), with IEntityCompositeMutationService, which keeps Entity Security, field history and replay but is not available in Jobs, triggers or schedules. If whole-graph atomicity and caller-assigned record ids are required, evaluate that requirement explicitly. The direct-EF graph recipe remains supported with its documented history boundary; this example is not an equivalent replacement for every importer.

Hosted Entity application commands use the trusted moltaro-system-automation actor. Source attribution is separate: every history entry names the function run that issued the command, and the user who started that run when there is one. The host still enforces configuration existence, lifecycle invariants, concurrency, idempotency and audit; writing the reserved actor id yourself grants no privilege. Verify record history as an ordinary authorized reader: root access, ReadChangesHistory and field visibility remain relevant.

Create, Update, Import and BulkUpdate, like Archive, Restore and Delete, do not start the Entity trigger functions of the records they write; MergeAsync and ReplaceResponsibilitiesAsync still do. If an import must be followed by work that a trigger performs for interactive saves, call that logic explicitly, for example as a Job the import enqueues after its items succeed.

Each Create or Update entry of an import item carries the change source of the function run that called ImportAsync: SourceType is Function, Trigger for a trigger function or Schedule for a scheduled run, SourceLabel is the function key, and FunctionRunId and FunctionVersion identify the run and the version it executed. For a Job enqueued through the API or started from an Action, OriginalUserId is the user who enqueued it; a scheduled run has none. A Job that another Job, trigger or scheduled run enqueued through IFunctionQueue currently records the system automation account as its original user. The entry is not labelled as an anonymous import. Entries written by builds before this change keep Source: null, which means unknown origin; they are not updated. A business event can explain an older operation, but it does not change the source of an existing field-history row.

On a disposable fixture with audit enabled:

  1. Create one record and persist its returned identity. Read the record and GET /api/workspace/entity/{entityIdOrKey}/instances/{instanceId}/changes. Confirm the Create entry, the moltaro-system-automation actor and the Source: its FunctionRunId is the run of your function on the Function operations page (Administration, group Monitoring).
  2. Change Name using Update with a new operation key and current versions. Read back the value and confirm the old/new change in history.
  3. Retry each exact request with its original key and verify that neither another record nor another history entry appears, and that the existing entry still names the first run.
  4. Exercise an invalid item and a stale record version. Inspect item failures, then read back data/history; never infer success from the outer result alone.
  5. Repeat the history read with a restricted reader and verify that unavailable fields/records are not disclosed.

For a direct-EF custom operation, add a business event inside the same explicit transaction as the save if they must commit together. AddBusinessEventAsync writes immediately and has no operation-key parameter; your operation/replay guard must prevent duplicate events. Keep its text and values safe for every reader entitled to see the event: arbitrary narrative values are not automatically field-level redacted.

Historical import requires evidence for the supplied old/new values, source time and actor. It is not a way to invent missing past changes. No history rows can mean disabled audit, retention, an unaudited write or no recorded changes; show a neutral empty state unless the server knows the reason.