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.
Choose the required history
Section titled “Choose the required history”| Requirement | Supported path | What is recorded |
|---|---|---|
| Create or update records through the complete Entity application flow | Inject IEntityAutomationCommandService; use CreateAsync, UpdateAsync, ImportAsync or BulkUpdateAsync | Normal 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 operation | Use the generated context and its supported transaction/save rules | Automatic 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 operation | MoltaroDbContext.AddBusinessEventAsync<TEntity> | An explicit event in the same record history; it does not calculate old/new field values |
| Import an existing, trustworthy external history | IDynamicEntityAuditTrailImportService.ImportAsync | Explicit 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.
Transaction, batch and actor boundaries
Section titled “Transaction, batch and actor boundaries”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.
Verify the result
Section titled “Verify the result”What the history shows
Section titled “What the history shows”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:
- Create one record and persist its returned identity. Read the record and
GET /api/workspace/entity/{entityIdOrKey}/instances/{instanceId}/changes. Confirm the Create entry, themoltaro-system-automationactor and the Source: itsFunctionRunIdis the run of your function on the Function operations page (Administration, group Monitoring). - Change
Nameusing Update with a new operation key and current versions. Read back the value and confirm the old/new change in history. - 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.
- 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.
- 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.