Canton.Ledger.Testing
0.7.1
Prefix Reserved
dotnet add package Canton.Ledger.Testing --version 0.7.1
NuGet\Install-Package Canton.Ledger.Testing -Version 0.7.1
<PackageReference Include="Canton.Ledger.Testing" Version="0.7.1" />
<PackageVersion Include="Canton.Ledger.Testing" Version="0.7.1" />
<PackageReference Include="Canton.Ledger.Testing" />
paket add Canton.Ledger.Testing --version 0.7.1
#r "nuget: Canton.Ledger.Testing, 0.7.1"
#:package Canton.Ledger.Testing@0.7.1
#addin nuget:?package=Canton.Ledger.Testing&version=0.7.1
#tool nuget:?package=Canton.Ledger.Testing&version=0.7.1
Canton.Ledger.Testing
In-memory test doubles and event/result builders for unit-testing business logic against the Canton Ledger API client surfaces without a live participant and without a mocking framework. Part of the Canton .NET SDK.
FakeLedgerClient implements the neutral Canton participant surface
Canton.Ledger.Abstractions.ICantonLedgerClient (and therefore the transport-neutral
Daml.Ledger.Abstractions.ILedgerClient it extends). FakeAdminClient, FakeTokenProvider
and FakePqsClient implement the equally neutral Canton.Ledger.Abstractions.IAdminClient,
Canton.Ledger.Abstractions.ITokenProvider and Canton.Ledger.Abstractions.IPqsClient.
Every fake therefore stands in for a contract declared in one neutral package, and this package
depends only on Canton.Ledger.Abstractions/Daml.Ledger.Abstractions/Daml.Runtime — no
transport, no Canton.Ledger.Kernel, no PostgreSQL driver.
Installation
dotnet add package Canton.Ledger.Testing --version 0.7.1
Every fake replays canned data staged ahead of time — none of them is a semantic ledger/PQS
simulator (no contract-key uniqueness or consuming-choice archival). FakePqsClient does evaluate the
PqsFilter it is given against the staged contracts, with the semantics of the SQL a live PQS runs.
The one exception is FakeLedgerClient's ledger end: it starts at the offset staged through
WithLedgerEnd and advances by one offset per committed write, so a test can read the end, write,
read it again, and get a bounded (fromOffset, toOffset] window that actually contains the write. The Canton participant surface (ICantonLedgerClient — fire-and-forget
submission, the completion stream, synchronizer/version discovery, offset/id point reads,
tree-shaped submission, traffic-cost estimation, interactive submission and package preference) lives
directly on FakeLedgerClient: seed completion events, connected synchronizers, the Ledger API
version, point-read transactions, transaction trees, the traffic-cost estimate, the prepared and executed interactive submissions and the preferred packages the same way, so tests swap Fake ⇆ REST ⇆ gRPC behind one
interface. SubmitAsync/SubmitReassignmentAsync echo the submission's command id (minting one
when omitted).
FakeLedgerClient.Submissions lists every write the client was handed, one RecordedSubmission per
call, in call order. A RecordedSubmission.Commands holds the command submission as the client puts
it on the wire (the submitter's ActAs/ReadAs applied, the effective command id, the workflow id
and the command; TryCreateAsync and TryExerciseAsync appear as the single-command submission they
stand for), a RecordedSubmission.Reassignment holds a reassignment submission, and a RecordedSubmission.Execution holds the signed submission handed to ExecuteSubmissionAsync, ExecuteSubmissionAndWaitAsync or ExecuteSubmissionAndWaitForTransactionAsync. Select a kind
with Submissions.OfType<RecordedSubmission.Commands>() and assert on your code's parties, payload,
choice arguments and sequence of writes. EstimateTrafficCostAsync submits nothing and is not
recorded.
Key types
| Type | Purpose |
|---|---|
FakeLedgerClient |
Configurable in-memory ICantonLedgerClient (and thus ILedgerClient). Build it with the fluent builder from FakeLedgerClient.Create(). Any member, Daml type, or Canton read you did not stage throws a descriptive NotSupportedException. |
FakeLedgerClientBuilder |
WithLedgerEnd (the ledger end before any write — stage an event for a bounded window opened around the nth write at that offset plus n), WithActiveContracts<T> (the staged snapshot must end on a single terminal Checkpoint or StreamError, as a participant's does — WithMalformedActiveContracts<T> stages one that deliberately does not), WithContractEvents<T>, WithLedgerEffects<T>, WithExerciseResult<TResult>, WithCreateResult<TTemplate>, WithSubmissionOutcome, WithSubmitAndWaitResult (the result plain SubmitAndWaitAsync returns, carrying the submission's command id), WithTransactionTree, WithReassignmentResult<T>, WithCompletionEvents, WithConnectedSynchronizers, WithLedgerApiVersion, WithTrafficCostEstimate (staging null replays a participant that served no estimation), WithPreparedSubmission, WithExecutedSubmission, WithExecutedTransaction, WithPreferredPackages, WithUpdateByOffset, WithUpdateById, WithUpdateTreeByOffset, WithContract<T>, WithContractLifecycle<T>, WithDisclosure<T> (the answer GetDisclosureAsync<T> returns for a contract id typed as a template or an interface; an id with no staged disclosure reads as null), WithActiveContractsPages<T>, WithUpdatesPages, WithPrunedOffsets, the interface counterparts WithActiveInterfaceContracts<TInterface, TView>, WithInterfaceEvents<TInterface, TView> and WithInterfaceLedgerEffects<TInterface, TView>, WithThrowingCallFailure (stage a LedgerOperationException that every throwing member raises from its task, as that same instance; Try* members and streams keep returning what you staged for them. The fake raises the instance as staged and does not derive its CommitState from the member called, so build it with the LedgerOperationException(message, status, commitState, category, errorId, metadata, innerException) constructor to carry a Status and the CommitState suited to the members under test: a failed read is NotCommitted), then Build(). |
LedgerEvents |
Factories for AcsSnapshotEntry<T> variants, all constrained where T : ITemplate, IDamlRecord<T>: Created (typed T payload plus a ContractKey? key), Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (a nullable LedgerOffset? and an UnclassifiedKind). |
ContractEvents |
Factories for ContractStreamEvent<T> variants, all constrained where T : ITemplate, IDamlRecord<T>: Created and Assigned (typed T payload plus a ContractKey? key), Archived, Unassigned, Exercised, Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (an UnclassifiedKind). |
CompletionEvents |
Factories for CompletionStreamEvent variants: CommandAccepted (a Completion and the update id), CommandRejected (a Completion and a CompletionStatus), Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message). |
InterfaceEvents |
Factories for InterfaceStreamEvent<TInterface, TView> variants, for both WithInterfaceEvents and WithInterfaceLedgerEffects; name both type arguments, TInterface : IDamlInterface, IHasView<TView> and TView : IDamlRecord<TView>: Created and Assigned (the TView payload plus a ContractKey? key), Archived, Unassigned, Exercised, Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (a nullable LedgerOffset? and an UnclassifiedKind). |
InterfaceLedgerEvents |
Factories for InterfaceAcsSnapshotEntry<TInterface, TView> variants, for WithActiveInterfaceContracts, under the same constraints as InterfaceEvents: Created (the TView payload plus a ContractKey? key), Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (a nullable LedgerOffset? and an UnclassifiedKind). |
LedgerOutcomes |
Factories for ExerciseOutcome<T> variants: One, None, Many, DamlError, InfraError (an optional DamlErrorCategory? before the source Exception?), CommittedUndecodable (the command committed but its response could not be decoded; carries the update id when one was read). |
LedgerResults |
Factories for TransactionResult, SubmitAndWaitResult, Contract<T>. |
FakeAdminClient |
Configurable in-memory IAdminClient. Build it with FakeAdminClient.Create(). Query-style members you did not stage throw NotSupportedException; the void command members (GrantUserRightsAsync, RevokeUserRightsAsync, UploadDarAsync, ValidateDarAsync) always succeed. |
FakeAdminClientBuilder |
WithParticipantId, WithAllocatedParty, WithParties, WithUser, WithUsers, WithUserRights, WithKnownPackages, WithPackage, WithVettedPackages, WithExternalPartyTopology, WithAllocatedExternalParty, then Build(). |
FakePqsClient |
Configurable in-memory IPqsClient. Build it with FakePqsClient.Create(). Query results are staged per Daml type; an unstaged type throws NotSupportedException. Filtered queries return only the staged contracts the filter matches, and QueryOneAsync returns null when none do. Paged queries order the staged contracts by contract id (ordinal comparison) before slicing, as PqsClient does, whatever order you staged them in; stage hex-like ids when a test depends on which contracts land on which page. Unpaged queries return contracts in staging order. |
FakePqsClientBuilder |
WithQueryResults<T>, WithInterfaceQueryResults<TInterface, TView>, then Build(). |
FakeTokenProvider |
In-memory ITokenProvider. FakeTokenProvider.WithToken(token) for the happy path, FakeTokenProvider.WithFailure(exception) to exercise auth-failure paths. No builder — both factories fully configure the fake. |
Usage
Stage the active-contract snapshot your business logic will read, keyed by Daml type, then drive it through the fake:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime;
using Daml.Runtime.Contracts;
using Daml.Runtime.Data;
var owner = new Party("bob");
var asset = new DemoAsset(new Party("issuer"), owner, "GOLD", 42m);
ILedgerClient client = FakeLedgerClient.Create()
.WithActiveContracts(
LedgerEvents.Created(
new ContractId<DemoAsset>("cid1"),
asset,
key: null,
LedgerOffset.At(1),
(SynchronizerId)"sync1",
[owner]),
LedgerEvents.Checkpoint<DemoAsset>(LedgerOffset.At(2)))
.Build();
// SubscribeActiveAsync<DemoAsset> now replays the staged entries:
await foreach (var entry in client.SubscribeActiveAsync<DemoAsset>(owner))
{
// ... exercise the code under test ...
}
// A member you never staged fails loudly instead of returning empty/null:
// client.TryExerciseAsync<string>(...) -> NotSupportedException("...WithExerciseResult<String>...")
Every unconfigured member and every unstaged Daml type throws a NotSupportedException whose
message names the builder call needed to stage it, so a test never silently exercises
unconfigured behaviour.
Seed the completion stream to drive fire-and-forget submission logic (SubmitAsync then observe
the completion), using the neutral CommandAccepted/CommandRejected/Checkpoint events:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime;
using Daml.Runtime.Commands;
using Daml.Runtime.Data;
var owner = new Party("bob");
var completion = new Completion(
new CommandId("cmd-1"), Offset: LedgerOffset.At(1), ActAs: [owner],
new SynchronizerTime("sync1", DateTimeOffset.UtcNow),
SubmissionId: null, UserId: null, DeduplicationPeriod: null,
PaidTrafficCost: 0, TraceContext: null);
ICantonLedgerClient client = FakeLedgerClient.Create()
.WithCompletionEvents(
CompletionEvents.Checkpoint(LedgerOffset.Begin),
CompletionEvents.CommandAccepted(completion, "update-1"))
.Build();
await foreach (var completionEvent in client.CompletionStreamAsync(owner))
{
// ... assert your code correlates the accepted completion by command id ...
}
Seed the submit-and-wait write path — the command shape every generated choice helper submits
through — with the ExerciseOutcome<TransactionResult> your code under test should observe.
TrySubmitAndWaitForTransactionAsync (both overloads) replies with the staged outcome for every
submission; stage a success or a failure to drive either branch:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime;
using Daml.Runtime.Commands;
using Daml.Runtime.Contracts;
using Daml.Runtime.Data;
using Daml.Runtime.Outcomes;
ILedgerClient happy = FakeLedgerClient.Create()
.WithSubmissionOutcome(LedgerOutcomes.One(LedgerResults.Transaction(
"update-1",
LedgerOffset.At(5),
[
new CreatedContract(
"event-1",
"cid1",
DemoAsset.TemplateId,
DamlRecord.Create(),
WitnessParties: [],
Signatories: [],
Observers: [])
{
PackageName = DemoAsset.PackageName,
},
],
archivedContractIds: [],
(CommandId)"cmd-1")))
.Build();
ILedgerClient failing = FakeLedgerClient.Create()
.WithSubmissionOutcome(LedgerOutcomes.DamlError<TransactionResult>(
DamlErrorCategory.InvalidGivenCurrentSystemStateOther,
"UNHANDLED_EXCEPTION", "assertion failed", new Dictionary<string, string>()))
.Build();
Stage an admin client the same way:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime.Data;
IAdminClient admin = FakeAdminClient.Create()
.WithParticipantId("participant1")
.WithUser(new UserDetails("alice", new Party("alice::1220")))
.Build();
var participantId = await admin.GetParticipantIdAsync();
var alice = await admin.GetUserAsync("alice"); // staged UserDetails
var bob = await admin.GetUserAsync("bob"); // null — never staged, not an error
Stage a PQS client's query results per Daml type:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime.Contracts;
IPqsClient pqs = FakePqsClient.Create()
.WithQueryResults(new Contract<Holding>(new ContractId<Holding>("cid1"), holding))
.Build();
var holdings = await pqs.QueryAsync<Holding>();
And a token provider for either the happy path or an auth-failure path:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
ITokenProvider ok = FakeTokenProvider.WithToken("test-bearer-token");
ITokenProvider failing = FakeTokenProvider.WithFailure(new InvalidOperationException("token endpoint unreachable"));
Stage a failed throwing call to test your failure handling. Both transports report a failed
call as a LedgerOperationException, so one staged failure covers either; build it with the
constructor that takes a TransportStatus and a CommitState when the test branches on them:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime.Outcomes;
var noAnswer = new LedgerOperationException(
"read got no answer",
new TransportStatus.NoResponse(),
CommitState.NotCommitted,
DamlErrorCategory.TransientServerFailure,
errorId: null,
metadata: null,
innerException: new TimeoutException("no response"));
ILedgerClient failing = FakeLedgerClient.Create()
.WithThrowingCallFailure(noAnswer)
.Build();
var act = () => failing.GetLedgerEndAsync(); // throws noAnswer from its task
Zero mocking-framework dependency
None of the fakes in this package pull in Moq / NSubstitute, so it composes with any (or no)
mocking framework in your own test project. Every fake — FakeLedgerClient, FakeAdminClient,
FakeTokenProvider and FakePqsClient — depends only on Canton.Ledger.Abstractions,
Daml.Ledger.Abstractions, and Daml.Runtime.
Excluding the package from your own code coverage
This package is test support, not code under test. When its PDB resolves next to the DLL, for
example in a build packed on your own machine, a coverage tool counts its lines in your report. Exclude
the module by path. With Microsoft.Testing.Extensions.CodeCoverage, put the exclusion in your
coverage settings file:
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<CodeCoverage>
<ModulePaths>
<Exclude>
<ModulePath>.*Canton\.Ledger\.Testing\.dll$</ModulePath>
</Exclude>
</ModulePaths>
</CodeCoverage>
</Configuration>
Save it as coverage.settings.xml and pass it with --coverage-settings coverage.settings.xml. If you
already have a settings file, add the ModulePath line to its existing ModulePaths Exclude list.
With coverlet, use its own filter instead: Exclude set to [Canton.Ledger.Testing]*.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Canton.Ledger.Abstractions (>= 0.7.1)
- Daml.Ledger.Abstractions (>= 0.7.1)
- Daml.Runtime (>= 0.7.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.7.1 | 40 | 10/23/2026 |
| 0.7.0 | 159 | 10/14/2026 |
| 0.7.0-preview.1 | 392 | 10/13/2026 |
| 0.6.0 | 255 | 10/7/2026 |
| 0.6.0-preview.4 | 153 | 10/7/2026 |
| 0.6.0-preview.3 | 176 | 10/1/2026 |
| 0.6.0-preview.2 | 409 | 9/30/2026 |
| 0.6.0-preview.1 | 207 | 9/28/2026 |
| 0.5.0-preview.3 | 215 | 9/24/2026 |
| 0.5.0-preview.2 | 131 | 9/22/2026 |
| 0.5.0-preview.1 | 215 | 9/8/2026 |
| 0.4.1-preview.1 | 113 | 8/29/2026 |