How-to guide

Add your own trains to a template

Source content/

The steps below assume a project made from one of the project templates. A query train needs trax-hub or trax-api (they serve GraphQL); a scheduled train needs trax-hub or trax-scheduler (they run the scheduler).

Put each train in its own folder under Trains/, with its interface, input, output and a Junctions/ folder, laid out as HelloWorld/ is. AddMediator(typeof(Program).Assembly) in Program.cs finds it; nothing else is registered.

Add a query train

  1. Add the train and its interface. [TraxQuery] serves it under discover, and every train the API exposes carries [TraxAuthorize] or [TraxAllowAnonymous]; the template's demo key holds the User role.

    using Trax.Core.Functional;
    using Trax.Effect.Attributes;
    using Trax.Effect.Services.ServiceTrain;
     
    namespace MyCompany.Hub.Trains.GetCustomer;
     
    public interface IGetCustomerTrain : IServiceTrain<GetCustomerInput, CustomerOutput>;
     
    [TraxAuthorize(Roles = "User")]
    [TraxQuery(Description = "Fetches a customer by ID")]
    public class GetCustomerTrain
        : ServiceTrain<GetCustomerInput, CustomerOutput>, IGetCustomerTrain
    {
        protected override Task<Either<Exception, CustomerOutput>> Junctions() =>
            Chain<FetchCustomerJunction>().Resolve();
    }
  2. Add its input, output and junction. Derive EffectJunction<TIn, TOut>, as the template's junctions do, so AddJunctionProgress() reports it.

    using Trax.Effect.Services.EffectJunction;
     
    namespace MyCompany.Hub.Trains.GetCustomer;
     
    public record GetCustomerInput
    {
        public required string Id { get; init; }
    }
     
    public record CustomerOutput
    {
        public required string Id { get; init; }
        public required string Name { get; init; }
    }
     
    public class FetchCustomerJunction : EffectJunction<GetCustomerInput, CustomerOutput>
    {
        public override Task<CustomerOutput> Run(GetCustomerInput input) =>
            Task.FromResult(new CustomerOutput { Id = input.Id, Name = $"Customer {input.Id}" });
    }
  3. Run the host and call the field. It is getCustomer under query { discover { ... } }: the interface name without the I and the Train, in camelCase.

    curl -X POST http://localhost:5400/trax/graphql \
      -H 'Content-Type: application/json' \
      -H 'X-Api-Key: demo-key-do-not-use-in-production' \
      -d '{"query":"{ discover { getCustomer(input: { id: \"42\" }) { id name } } }"}'

The host refuses to start if an exposed train has neither [TraxAuthorize] nor [TraxAllowAnonymous], and if a junction needs an input that nothing before it produces.

Add a scheduled train

  1. Add the train. A scheduled train's input implements IManifestProperties, so the scheduler can store it on the manifest.

    using Trax.Core.Functional;
    using Trax.Effect.Models.Manifest;
    using Trax.Effect.Services.ServiceTrain;
     
    namespace MyCompany.Scheduler.Trains.SyncCustomers;
     
    public record SyncCustomersInput : IManifestProperties
    {
        public string Region { get; init; } = "us-east";
    }
     
    public interface ISyncCustomersTrain : IServiceTrain<SyncCustomersInput, Unit>;
     
    public class SyncCustomersTrain : ServiceTrain<SyncCustomersInput, Unit>, ISyncCustomersTrain
    {
        protected override Task<Either<Exception, Unit>> Junctions() =>
            Chain<FetchCustomersJunction>()
                .Chain<UpsertCustomersJunction>()
                .Resolve();
    }
  2. Add FetchCustomersJunction and UpsertCustomersJunction under the train's Junctions/ folder, as in step 2 above: the first takes the SyncCustomersInput, the last returns Unit.

  3. In Program.cs, add the schedule after the hello-world one, inside AddScheduler:

    scheduler
        .Schedule<ISyncCustomersTrain>(
            "sync-customers",
            new SyncCustomersInput { Region = "us-east" },
            Every.Hours(1)
        );

The first argument is the manifest's id. Schedule updates the manifest with that id at every startup, so renaming it creates a new manifest and leaves the old one in place. The dashboard shows the new manifest and each run.

See Scheduling for cron schedules, dependent trains, bulk scheduling and dead letters, and Trains & Junctions for what a junction can do.

SDK Reference

AddMediator | TraxQuery / TraxMutation | TraxAuthorize | Schedule | EffectJunction | ServiceTrain