ScheduleMany / ScheduleManyAsync

Batch-schedules multiple instances of a train from a collection. All manifests are created or updated in a single transaction. Supports automatic cleanup of stale manifests via prunePrefix.

ScheduleMany is used at startup configuration time; ScheduleManyAsync is used at runtime via ITraxScheduler.

Signatures

public SchedulerConfigurationBuilder ScheduleMany<TTrain>(
    string name,
    IEnumerable<ManifestItem> items,
    Schedule schedule,
    Action<ScheduleOptions>? options = null
)
    where TTrain : class

The name parameter automatically derives:

  • groupId = name
  • prunePrefix = "{name}-"
  • externalId = "{name}-{item.Id}" for each item

Each ManifestItem contains the item's ID and input. The input type is inferred from TTrain's IServiceTrain<TInput, TOutput> interface and validated at configuration time. The output type is not constrained — scheduled trains can return any output type, and the output is discarded for background jobs.

Startup: Unnamed with ManifestItem

public SchedulerConfigurationBuilder ScheduleMany<TTrain>(
    IEnumerable<ManifestItem> items,
    Schedule schedule,
    Action<ScheduleOptions>? options = null
)
    where TTrain : class

Each ManifestItem.Id is used as the full external ID (no name prefix applied).

ManifestItem

public sealed record ManifestItem(
    string Id,
    IManifestProperties Input,
    string? DependsOn = null
);
PropertyTypeDescription
IdstringThe item's identifier. In name-based overloads, this becomes the suffix (full external ID = "{name}-{Id}"). In unnamed overloads, this is the full external ID.
InputIManifestPropertiesThe train input for this item. Must match the expected input type of TTrain.
DependsOnstring?The external ID of the parent manifest this item depends on. Used by IncludeMany and ThenIncludeMany. See Dependent Scheduling.

Startup: Explicit Type Parameters (Legacy)

The four-type-parameter forms are still available for backward compatibility:

// Name-based
public SchedulerConfigurationBuilder ScheduleMany<TTrain, TInput, TOutput, TSource>(
    string name,
    IEnumerable<TSource> sources,
    Func<TSource, (string Suffix, TInput Input)> map,
    Schedule schedule,
    Action<ScheduleOptions>? options = null,
    Action<TSource, ManifestOptions>? configureEach = null
)
    where TTrain : IServiceTrain<TInput, TOutput>
    where TInput : IManifestProperties
 
// Explicit
public SchedulerConfigurationBuilder ScheduleMany<TTrain, TInput, TOutput, TSource>(
    IEnumerable<TSource> sources,
    Func<TSource, (string ExternalId, TInput Input)> map,
    Schedule schedule,
    Action<ScheduleOptions>? options = null,
    Action<TSource, ManifestOptions>? configureEach = null
)
    where TTrain : IServiceTrain<TInput, TOutput>
    where TInput : IManifestProperties

Runtime (ITraxScheduler)

Task<IReadOnlyList<Manifest>> ScheduleManyAsync<TTrain, TInput, TOutput, TSource>(
    IEnumerable<TSource> sources,
    Func<TSource, (string ExternalId, TInput Input)> map,
    Schedule schedule,
    Action<ScheduleOptions>? options = null,
    Action<TSource, ManifestOptions>? configureEach = null,
    CancellationToken ct = default
)
    where TTrain : IServiceTrain<TInput, TOutput>
    where TInput : IManifestProperties

Type Parameters

Type ParameterConstraintDescription
TTrainclassThe train interface type. Can implement IServiceTrain<TInput, TOutput> with any output type. The input type is inferred at configuration time. The output is discarded for background jobs.

Legacy / Runtime API

Type ParameterConstraintDescription
TTrainIServiceTrain<TInput, TOutput>The train interface type. All items in the batch execute the same train. Can have any output type — the output is discarded for background jobs.
TInputIManifestPropertiesThe input type for the train. Each item in the batch can have different input data.
TOutputThe output type of the train. Not constrained — any output type is accepted. The output is discarded when jobs complete.
TSourceThe type of elements in the source collection. Can be any type — it is transformed into (ExternalId, Input) pairs by the map function.

Parameters

ParameterTypeRequiredDefaultDescription
namestringYes (name-based)The batch name. Automatically derives groupId = name, prunePrefix = "{name}-", and each external ID = "{name}-{item.Id}".
itemsIEnumerable<ManifestItem>YesThe collection of items to create manifests from. Each item becomes one scheduled manifest.
scheduleScheduleYesThe schedule definition applied to all manifests in the batch. Use Every or Cron helpers.
optionsAction<ScheduleOptions>?NonullOptional callback to configure all scheduling options. The name-based overload pre-sets Group(name) and PrunePrefix("{name}-") before invoking your callback. See ScheduleOptions.

Legacy API Parameters

ParameterTypeRequiredDefaultDescription
namestringYes (name-based)The batch name. Automatically derives groupId = name, prunePrefix = "{name}-", and each external ID = "{name}-{suffix}".
sourcesIEnumerable<TSource>YesThe collection of items to create manifests from. Each item becomes one scheduled manifest.
mapFunc<TSource, (string, TInput)>YesA function that transforms each source item into an ID/suffix and input pair.
scheduleScheduleYesThe schedule definition applied to all manifests in the batch.
optionsAction<ScheduleOptions>?NonullOptional callback to configure all scheduling options. See ScheduleOptions.
configureEachAction<TSource, ManifestOptions>?NonullOptional callback to set per-item manifest options. Receives both the source item and options, allowing per-item overrides of the base options set via options.
ctCancellationTokenNodefaultCancellation token (runtime API only).

Returns

  • Startup: SchedulerConfigurationBuilder — for continued fluent chaining.
  • Runtime: Task<IReadOnlyList<Manifest>> — all created or updated manifest records.

Examples

var tables = new[] { "customers", "orders", "products" };
 
services.AddTrax(trax => trax
    .AddScheduler(scheduler => scheduler
        .ScheduleMany<ISyncTableTrain>(
            "sync",
            tables.Select(table => new ManifestItem(
                table,
                new SyncTableInput { TableName = table }
            )),
            Every.Minutes(5))
    )
);
// Creates: sync-customers, sync-orders, sync-products
// groupId: "sync", prunePrefix: "sync-"

Each ManifestItem contains the item's ID (used as the suffix in name-based overloads) and the train input. No map function needed — the data is already structured.

Unnamed Batch Scheduling

var tables = new[] { "customers", "orders", "products" };
 
scheduler.ScheduleMany<ISyncTableTrain>(
    tables.Select(table => new ManifestItem(
        $"sync-{table}",
        new SyncTableInput { TableName = table }
    )),
    Every.Minutes(5));

With Pruning (Automatic Stale Cleanup)

The name-based overload includes pruning automatically (prunePrefix: "{name}-"):

// If "partners" was in a previous deployment but removed from this list,
// its manifest ("sync-partners") will be deleted because it starts with
// "sync-" but wasn't included in the current batch.
var tables = new[] { "customers", "orders" };
 
scheduler.ScheduleMany<ISyncTableTrain>(
    "sync",
    tables.Select(table => new ManifestItem(
        table,
        new SyncTableInput { TableName = table }
    )),
    Every.Minutes(5));

With Group Configuration

scheduler.ScheduleMany<ISyncTableTrain>(
    "sync",
    tables.Select(table => new ManifestItem(
        table,
        new SyncTableInput { TableName = table }
    )),
    Every.Minutes(5),
    options => options
        .Priority(10)
        .Group(group => group
            .MaxActiveJobs(5)
            .Priority(20)));

Legacy API with Map Function

The three-type-parameter form is still available and supports per-item configuration via configureEach:

scheduler.ScheduleMany<ISyncTableTrain, SyncTableInput, Unit, string>(
    tables,
    table => ($"sync-{table}", new SyncTableInput { TableName = table }),
    Every.Minutes(5),
    options => options.Priority(10),
    configureEach: (table, opts) =>
    {
        if (table == "orders")
        {
            opts.MaxRetries = 10;
            opts.Priority = 25;
        }
    });

Runtime Scheduling

public class TenantSyncService(ITraxScheduler scheduler)
{
    public async Task SyncTenants(IEnumerable<Tenant> tenants)
    {
        var manifests = await scheduler.ScheduleManyAsync<ISyncTenantTrain, SyncTenantInput, Unit, Tenant>(
            sources: tenants,
            map: tenant => (
                ExternalId: $"tenant-sync-{tenant.Id}",
                Input: new SyncTenantInput { TenantId = tenant.Id, Name = tenant.Name }
            ),
            schedule: Every.Hours(1),
            options => options
                .PrunePrefix("tenant-sync-")
                .Group("tenant-syncs"));
    }
}

Remarks

  • All manifests are created/updated in a single database transaction. If any manifest fails to save, the entire batch is rolled back.
  • Pruning runs in a separate database context after the main transaction commits. A prune failure does not roll back the upserted manifests — the failure is logged as a warning and retried on the next cycle.
  • The configureEach callback receives Action<TSource, ManifestOptions> (not Action<ManifestOptions> like Schedule) — this lets you customize options based on the source item. It applies per-item overrides on top of the base options from ScheduleOptions.
  • The source collection is materialized (.ToList()) internally to avoid multiple enumeration.
  • The group is configured via .Group(...) on ScheduleOptions. Per-group settings (MaxActiveJobs, Priority, IsEnabled) can be set from code or adjusted at runtime from the dashboard. See Per-Group Dispatch Controls.
  • ScheduleMany cannot be followed by .ThenInclude() — use IncludeMany (with dependsOn) instead for batch dependent scheduling.