On this page

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.