Parallel invoice processing and an optional feature
The report worker calls IReportRenderer. Its implementation reads invoice snapshots, calculates each invoice independently, then publishes one customer summary. The input belongs to one customer and one currency, EUR. Every line amount and paid amount has at most two fractional digits. Overpayment means zero outstanding in this report; it is not a credit-ledger implementation.
The ordinary application contracts define InvoiceSnapshot, StatementRow, CustomerReport and IInvoiceSource. The source returns independent values and releases its reader before processing starts. It must supply each invoice once: duplicate snapshots would be counted twice, so a joined query must group its rows correctly. IReadOnlyList alone does not make its objects immutable: the source implementation must also stop modifying the snapshots and their line lists.
Read once, then project independently
This report module uses root_namespace DirectSharp.Showcase. The host provides the scoped source and two borrowed Gates shared across report jobs: two permits for database reads and eight for projection. The class implements the ordinary IReportRenderer CLR contract through a named implementation block.
import Global.System
import Global.System.Collections.Generic
import Global.System.Linq
public class CustomerReportRenderer
private readonly source as IInvoiceSource
private readonly database as Gate
private readonly projection as Gate
public constructor(invoiceSource as IInvoiceSource, databaseGate as Gate, projectionGate as Gate)
require invoiceSource is not nothing
require databaseGate is not nothing
require projectionGate is not nothing
source = invoiceSource
database = databaseGate
projection = projectionGate
end constructor
implements IReportRenderer
public async action RenderAsync(customerId as Guid)
returns CustomerReport
let invoices as IReadOnlyList <of InvoiceSnapshot> = nothing
enter database
invoices = await source.ReadAsync(customerId, Cancellation.Token)
leave
if invoices.Count > 1000 then
throw new InvalidOperationException("Report exceeds the 1000-invoice batch limit")
end if
concurrent for_each invoice in invoices collects rows as StatementRow with limit: 4
enter projection
collect rows = await StatementMath.ProjectAsync(invoice, Cancellation.Token)
leave
end for_each
return new CustomerReport(customerId, invoices.Count, rows.Sum(.Outstanding))
end action
end implements
end class
RenderAsync's explicit async contract supplies the compatible optional CancellationToken parameter of IReportRenderer and its Cancellation context. The gate around the read ends before projection starts. Holding a database permit while waiting for CPU work would waste scarce database capacity.
Each iteration has its own invoice and result. Collect preserves input order despite differing completion times. There is no shared mutable List to append to from workers. A malformed invoice fails the report; this example does not silently publish partial totals. Empty input produces a zero-count, zero-total report. Decimal overflow remains an error.
The source contract bounds a report to 1000 invoices and must enforce its own row/payload limits during acquisition. The renderer's count check is a defensive boundary, not a cure for an unbounded source materialization. A larger reporting product needs explicit paging, checkpointing and stable-snapshot policy. A live database reader must never be shared between concurrent iterations; copy each row's needed values first.
Make CPU scheduling explicit
Concurrent async code overlaps waits; it does not automatically put ordinary arithmetic on separate threads. StatementMath.ProjectAsync makes the scheduling choice through an ordinary .NET Task.Run call. This DirectSharp method belongs beside the application's Compute action:
public static async action ProjectAsync(invoice as InvoiceSnapshot)
returns StatementRow
let token = Cancellation.Token
action Project()
returns StatementRow
return Compute(invoice, token)
end action
return await Global.System.Threading.Tasks.Task.Run <of StatementRow>(
action Project, token
)
end action
Compute loops through the invoice's line amounts, checks cancellation between items, validates the monetary inputs, sums with checked Decimal arithmetic and subtracts the paid amount. The complete reference implementation defines those operations. Passing a token to Task.Run alone cannot interrupt code already executing; the checks inside Compute provide cooperation.
The scheduling cost is worthwhile only for substantial CPU work. For a short invoice, call the synchronous Compute method directly. The explicit ProjectAsync method makes the tradeoff visible instead of making every asynchronous operation a background-thread dispatch. Its task is awaited inside the bounded iteration, so the permits remain held until that CPU operation settles.
Start with ordinary LINQ
For an export, use the projected rows rather than recalculating invoices. The following fragment assumes the collected rows and a caller-owned TextWriter. It filters zero balances, orders by unique invoice identifier and writes a small CSV:
let selected = rows.Where(.Outstanding > 0).OrderBy(.Id)
StatementRows.Write(selected, writer)
StatementRows.Write is an ordinary application method taking IEnumerable<StatementRow> and TextWriter. It writes InvoiceId and OutstandingEUR columns using Guid D format and invariant two-decimal formatting. The format has no free-text fields, so it does not pretend to be a general CSV escaping library. It borrows the writer and does not close it. Writer/enumeration failures propagate; a partial output is possible. Publish a temporary file only after successful completion when partial output must stay invisible.
Add a small feature for repeated export pipelines
If this shape repeats across many reports, the following feature exposes that composition. The business arithmetic still lives in StatementMath and ordinary application classes. The adapter uses the existing feature declaration mechanism and calls the same LINQ and writer methods:
feature statement_rows
target Global.System.Collections.Generic.IEnumerable <of Global.DirectSharp.Showcase.StatementRow>
operation where(predicate as captured expression)
calls Global.System.Linq.Enumerable.Where
sets current from result
end operation
operation sort_by(key as captured expression)
calls Global.System.Linq.Enumerable.OrderBy
sets current from result
end operation
operation export(writer as Global.System.IO.TextWriter)
calls Global.DirectSharp.Showcase.StatementRows.Write
end operation
end feature
Use this optional replacement for the preceding two statements:
statement_rows rows
where .Outstanding > 0
sort_by .Id
export writer
end statement_rows
Where and sort_by replace the current sequence; export consumes it through the ordinary method's source argument and the supplied writer. The captured predicate/key bind to LINQ delegate types, not a separate rule engine. Filtering and sorting retain their normal deferred behavior until Write enumerates them. Sorting buffers its selected input; the feature does not make it streaming or parallel. Source and output errors are not caught automatically.
The feature adds vocabulary only within its own block. It is discovered from the application's source/reference declarations; it does not install a library, add a runtime object or change cancellation/disposal semantics. Use the ordinary calls when they are already clearer.
Where the other language tools fit
| Application need | Appropriate tool |
|---|---|
| Repeated validation header | Validator class kind; normal FluentValidation rules |
| Independent dashboard workflows | Inline run blocks and structured joining |
| Repeated independent input items | Concurrent for_each and ordered collect |
| Work arriving over time | ChannelReader with concurrent consume |
| Shared scarce capacity | A borrowed Gate, owned and disposed by the host |
| Short compound state transition in the application registry | Ordinary private synchronization; no awaited work inside its lock |
| Scoped services and asynchronous cleanup | A per-job DI scope and using |
| Repeated pipeline composition | Optional statement_rows feature over ordinary APIs |
| Live UI progress stream | An ordinary observable/channel library if needed; no new event or observable language protocol |
Patterns, reconciliation and import policies remain useful separate recipes. They are not forced into every report, and an import's activation policy does not silently change interactive customer registration.
Previous: queued reports. Continue to reference C# and verification cases.