Loading independent service data
A customer overview may need a profile, order history and account balance. None of those requests depends on another's result, so they can overlap. The caller needs all three before it can publish the overview.
The example uses ordinary HttpClient calls and two levels of coordination: a shared request Gate limits outgoing HTTP requests, and a concurrent enumeration limits active customer workflows.
One customer, three requests
The host supplies an HttpClient whose BaseAddress identifies the application gateway, along with a shared request Gate. It owns both and keeps them alive until the client's work has settled. Each relative path returns JSON text. Keeping the response as String here makes the coordination visible without introducing a serialization abstraction.
import Global.System
import Global.System.Net.Http
public class AccountOverview
public CustomerId as Guid = Guid.Empty
public ProfileJson as String = ""
public OrdersJson as String = ""
public BalanceJson as String = ""
end class
public class AccountOverviewClient
private readonly http as HttpClient
private readonly requests as Gate
public constructor(client as HttpClient, requestGate as Gate)
require client is not nothing
require requestGate is not nothing
http = client
requests = requestGate
end constructor
public async action LoadAsync(customerId as Guid)
returns AccountOverview
require customerId <> Guid.Empty
Cancellation.ThrowIfCanceled()
concurrent all
returns profile as String, orders as String, balance as String
profile = run FetchAsync("customers/{{customerId}}/profile")
orders = run FetchAsync("customers/{{customerId}}/orders")
balance = run FetchAsync("customers/{{customerId}}/balance")
end concurrent
let overview = new AccountOverview()
overview.CustomerId = customerId
overview.ProfileJson = profile
overview.OrdersJson = orders
overview.BalanceJson = balance
return overview
end action
private async action FetchAsync(path as String)
returns String
enter requests
return await http.GetStringAsync(path, Cancellation.Token)
leave
end action
end class
run starts child work owned by the concurrent scope. Profile, orders and balance become values after end concurrent; a sibling run cannot consume one of those not-yet-published results. If order history needed a profile-derived identifier, that part would need sequential code or a nested workflow.
The Gate's capacity limits outgoing requests across all calls that share it. FetchAsync holds a permit through the HTTP operation and releases it on every exit, including failure. Both dependencies are borrowed, so this class disposes neither the HttpClient nor the Gate. HttpClient supports concurrent requests; custom headers or shared mutable request state should not be changed while requests are running.
Successful completion publishes a coherent overview containing all three values. An HTTP failure remains a failure, rather than an empty JSON string manufactured by try_get. Application logic here does not depend on an ordering of simultaneous failures or on which exception represents several failed children.
Several customers
Add this action inside AccountOverviewClient. Each iteration owns its LoadAsync call. The collection is published after the loop; completion order does not reorder it.
public async action PrintManyAsync(ids as Global.System.Collections.Generic.IEnumerable <of Guid>)
require ids is not nothing
Cancellation.ThrowIfCanceled()
concurrent for_each id in ids collects overviews as AccountOverview with limit: 3
collect overviews = await LoadAsync(id)
end for_each
for_each overview in overviews
Global.System.Console.WriteLine("Customer {{overview.CustomerId}}")
Global.System.Console.WriteLine(overview.BalanceJson)
end for_each
end action
The integer limit belongs to this enumeration and permits three whole customer iterations at once. With the four-permit request Gate supplied below, at most four HTTP calls can run across those iterations. These limits protect different resources. Increasing the outer limit does not increase the shared Gate's capacity.
Iteration-local variables require no coordination. Writing to an outer List from several iterations would introduce shared mutation; collect expresses the intended result publication directly. If counters or other shared state are necessary, choose explicit atomic updates or a Gate rather than relying on ordinary assignment.
An empty ids source completes with an empty output. A positive limit of one serializes whole iterations; a zero or negative limit is invalid. Direct return from a concurrent iteration body is prohibited because it cannot synchronously exit the enclosing action from a child operation.
Use this loop for independent work whose successful results all matter. Keep any policy for partial results, retries and failure classification explicit in the application's operations rather than assuming that concurrent completion chooses it for you.
Own the shared Gate
An owning caller can set the request capacity and keep the Gate alive around the complete batch. This class uses the same System and System.Net.Http imports as the client:
public class AccountOverviewExample
public static async action RunAsync(
client as HttpClient,
ids as Global.System.Collections.Generic.IEnumerable <of Guid>
)
require client is not nothing
require ids is not nothing
using requests = new Gate(4)
let overviewClient = new AccountOverviewClient(client, requests)
await overviewClient.PrintManyAsync(ids)
end using
end action
end class
The call to PrintManyAsync is explicitly awaited. Its structured loops/scopes join their owned work before it returns or propagates a failure. The using scope then disposes the Gate after those uses have settled. It does not dispose the caller's HttpClient. Call this action with a suitably configured client and customer identifiers; RunAsync is an ordinary action, not a special entry point.
When a host shares one Gate across several client instances or batches, the host must join all those users before disposing it. Gate.Dispose refuses while acquisitions are pending or held; it does not cancel or wait for the users. See Gate ownership.
Choosing a completion mode
| Scope | Meaning | Suitable scenario |
|---|---|---|
concurrent all |
All required child operations complete | Profile, orders and balance are all needed |
concurrent first |
First successful producer wins | Competing replicas where a later success can outlive an earlier failure |
concurrent any |
First completed producer wins, including a failed completion | A deliberate race where the earliest completion itself matters |
This overview uses all. Switching it to first would change the application requirement, not merely improve its speed. A concurrent limit can also be written as with limit: 4 or with limit: sharedGate on the scope itself. Use a shared Gate when several operations need to respect the same external quota.