11.3 Fulfilment is a train
Packing happens in the warehouse, which, like the payment provider and the supplier, is somebody else's system behind an interface.
Shopfront.Core/Warehouse/IWarehouse.cs:
namespace Shopfront.Warehouse;
public interface IWarehouse
{
// Packs the order's items and hands back the parcel's tracking number.
Task<string> Pack(Guid orderId, int items, CancellationToken cancellationToken);
}Shopfront.Core/Warehouse/FakeWarehouse.cs:
namespace Shopfront.Warehouse;
// Takes ten seconds to pack anything, long enough to kill the worker while it does.
public class FakeWarehouse : IWarehouse
{
public TimeSpan PackingTime { get; init; } = TimeSpan.FromSeconds(10);
public async Task<string> Pack(Guid orderId, int items, CancellationToken cancellationToken)
{
await Task.Delay(PackingTime, cancellationToken);
return $"PKG-{orderId.ToString("N")[..8].ToUpperInvariant()}";
}
}The train takes what packing needs: the order and how many items are in it. It carries no
[TraxAuthorize]: it is queued by PlaceOrder, not by anyone outside.
Shopfront.Core/Trains/FulfilOrder/FulfilOrderInput.cs:
namespace Shopfront.Trains.FulfilOrder;
public record FulfilOrderInput
{
public required Guid OrderId { get; init; }
public required int Items { get; init; }
}Shopfront.Core/Trains/FulfilOrder/FulfilOrderOutput.cs:
namespace Shopfront.Trains.FulfilOrder;
public record FulfilOrderOutput(Guid OrderId, string TrackingNumber);Shopfront.Core/Trains/FulfilOrder/IFulfilOrderTrain.cs:
using Trax.Effect.Services.ServiceTrain;
namespace Shopfront.Trains.FulfilOrder;
public interface IFulfilOrderTrain : IServiceTrain<FulfilOrderInput, FulfilOrderOutput>;Shopfront.Core/Trains/FulfilOrder/Junctions/PackOrderJunction.cs:
using Shopfront.Warehouse;
using Trax.Effect.Services.EffectJunction;
namespace Shopfront.Trains.FulfilOrder.Junctions;
internal class PackOrderJunction(IWarehouse warehouse, ILogger<PackOrderJunction> logger)
: EffectJunction<FulfilOrderInput, FulfilOrderOutput>
{
public override async Task<FulfilOrderOutput> Run(FulfilOrderInput input)
{
logger.LogInformation("Packing order {OrderId}", input.OrderId);
var tracking = await warehouse.Pack(input.OrderId, input.Items, CancellationToken);
logger.LogInformation("Packed order {OrderId} as {Tracking}", input.OrderId, tracking);
return new FulfilOrderOutput(input.OrderId, tracking);
}
}Shopfront.Core/Trains/FulfilOrder/FulfilOrderTrain.cs:
using Shopfront.Trains.FulfilOrder.Junctions;
using Trax.Core.Functional;
using Trax.Effect.Models.Metadata;
using Trax.Effect.Services.ServiceTrain;
namespace Shopfront.Trains.FulfilOrder;
public class FulfilOrderTrain : ServiceTrain<FulfilOrderInput, FulfilOrderOutput>, IFulfilOrderTrain
{
protected override Task<Either<Exception, FulfilOrderOutput>> Junctions() =>
Chain<PackOrderJunction>().Resolve();
// One order is fulfilled one queued run at a time, however often it is queued.
protected override string? QueueSubjectKey(Metadata metadata) =>
$"order:{TrainInput.OrderId}";
}QueueSubjectKey names what the queued work is about. Two queued runs with the same key never run
at once: the second waits until the first is done. An order queued twice by mistake is therefore
never packed by two workers side by side.
SDK Reference
AddTraxWorker | ITrainExecutionService | Queued work | Job submission | Structuring your application | Host tracking