Skip to content

Enterprise and Multiple Accounts

Joseph Namey edited this page Jul 30, 2026 · 1 revision

Enterprise and Multiple Accounts

An Emma enterprise account owns subaccounts — one per department, campus, brand, whatever the organisation divides on. You authenticate once with the enterprise credentials and then address each subaccount by id.

Discover the subaccounts

public sealed class QuotaSweep(
    IEmmaEnterpriseProvider enterprise,
    IEmmaAccountScopeFactory scopeFactory,
    ILogger<QuotaSweep> logger)
{
    public async Task RunAsync(CancellationToken ct)
    {
        IReadOnlyList<Subaccount> subaccounts = await enterprise.ListSubaccounts(cancellationToken: ct);

        foreach (Subaccount sub in subaccounts)
        {
            IEmmaAccountScope scope = scopeFactory.ForAccount(sub.AccountId!);

            int active = await scope.Members.GetMemberCount(
                status: MemberStatusShort.Active,
                cancellationToken: ct);

            logger.LogInformation("{Name} ({Id}): {Active} active", sub.AccountName, sub.AccountId, active);
        }
    }
}

ListSubaccounts sends all four statuses — active, trial, pending_retirement, retired — unless you narrow it:

await enterprise.ListSubaccounts(SubaccountStatusFilter.Active | SubaccountStatusFilter.Trial, ct);

What a scope is

ForAccount(id) returns the full provider set bound to that account. It reuses the same credentials and the same pooled HttpClient — only the account segment of the request path changes. Creating one is a handful of field assignments; it opens no connection.

IEmmaAccountScope scope = scopeFactory.ForAccount("1234");

scope.AccountId      // "1234"
scope.Members        // IEmmaMemberProvider, targeting 1234
scope.Groups         // IEmmaGroupProvider, targeting 1234
scope.Account        // IEmmaAccountProvider, targeting 1234
// ...every provider, same shape as the injected ones

Scopes are independent, so making 47 of them is fine.

Running in parallel

Scopes share the pooled client, which is safe to use concurrently. Bound the parallelism — Emma rate-limits, and more concurrency mostly buys you more 403s:

await Parallel.ForEachAsync(
    subaccounts,
    new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = ct },
    async (sub, token) =>
    {
        IEmmaAccountScope scope = scopeFactory.ForAccount(sub.AccountId!);
        await ProcessAsync(scope, token);
    });

A degree of 4 is a reasonable starting point for a sweep of tens of subaccounts. Raise it only with resilience configured, and watch for throttling.

If you resolve scoped services inside the loop — an IServiceScope, a DbContext — create a new scope per task. DbContext is not thread-safe, and sharing one across parallel tasks produces "A second operation was started on this context instance."

Users on a subaccount

IReadOnlyList<AccountUser> users = await scope.Account.ListUsers(ct);

foreach (AccountUser user in users)
{
    // CreatedAt and LastLoginAttempt are DateTime?, not string - so this sorts.
    Console.WriteLine($"{user.Email} {user.Role} last seen {user.LastLoginAttempt:d}");
}

Fields Emma returns that aren't modelled

Subaccount and AccountUser both carry AdditionalData, so any property Emma returns that this library does not model is available rather than discarded:

if (sub.AdditionalData?.TryGetValue("contact_limit", out JsonElement limit) == true)
{
    int contactLimit = limit.GetInt32();
}

If you find a field that is consistently present, please open an issue so it can be modelled properly.

Clone this wiki locally