11.4 PlaceOrder queues it
The last junction of PlaceOrder queues fulfilment for the order it just confirmed. It queues
through ITrainExecutionService, the same path a GraphQL caller's queued work takes, so the queued
train's own rules apply to it: its authorization, its queue hooks and its subject key.
Shopfront.Core/Trains/PlaceOrder/Junctions/QueueFulfilmentJunction.cs:
using System.Text.Json;
using Shopfront.Trains.FulfilOrder;
using Shopfront.Trains.PlaceOrder.Models;
using Trax.Core.Functional;
using Trax.Effect.Services.EffectJunction;
using Trax.Mediator.Services.TrainExecution;
namespace Shopfront.Trains.PlaceOrder.Junctions;
internal class QueueFulfilmentJunction(
ITrainExecutionService trains,
ILogger<QueueFulfilmentJunction> logger
) : EffectJunction<(PlaceOrderOutput Order, PricedCart Cart), Unit>
{
public override async Task<Unit> Run((PlaceOrderOutput Order, PricedCart Cart) input)
{
var queued = await trains.QueueAsync(
typeof(IFulfilOrderTrain).FullName!,
JsonSerializer.Serialize(
new FulfilOrderInput
{
OrderId = input.Order.OrderId,
Items = input.Cart.Lines.Sum(l => l.Quantity),
}
),
ct: CancellationToken
);
logger.LogInformation(
"Queued fulfilment of {OrderId} as work queue entry {Entry}",
input.Order.OrderId,
queued.WorkQueueId
);
return Unit.Default;
}
}It comes last, after the order is stored, because a queued entry is written at once and is not
undone if the run fails later. It returns Unit, so the train's output is still the
PlaceOrderOutput the confirm made.
Shopfront.Core/Trains/PlaceOrder/PlaceOrderTrain.cs:
using Shopfront.Auth;
using Shopfront.Fraud;
using Shopfront.Trains.PlaceOrder.Junctions;
using Trax.Core.Functional;
using Trax.Effect.Attributes;
using Trax.Effect.Services.ServiceTrain;
namespace Shopfront.Trains.PlaceOrder;
[TraxMutation(GraphQLOperation.Run)]
[TraxAuthorize(Roles = Roles.Customer)]
[TraxRunProgress]
public class PlaceOrderTrain : ServiceTrain<PlaceOrderInput, PlaceOrderOutput>, IPlaceOrderTrain
{
protected override Task<Either<Exception, PlaceOrderOutput>> Junctions() =>
Chain<IdentifyCustomerJunction>()
.Chain<PriceCartJunction>()
.Chain<AssessRiskJunction>()
.Gate<OrderRisk, LooksFraudulent>(gate =>
gate.Yes(hold => hold.Chain<RefuseOrderJunction>())
.No(take => take)
.Shadow<ICandidateFraudDecider>()
.WaitForShadows(TimeSpan.FromSeconds(1))
)
.Chain<ReserveStockJunction>()
.Chain<TakePaymentJunction>()
.Chain<ConfirmOrderJunction>()
.Chain<QueueFulfilmentJunction>()
.Resolve();
}The warehouse joins the shop's services.
Shopfront.Core/Extensions/ShopfrontServiceCollectionExtensions.cs:
using Microsoft.EntityFrameworkCore;
using Shopfront.Auth;
using Shopfront.Data;
using Shopfront.Failures;
using Shopfront.Fraud;
using Shopfront.Inventory;
using Shopfront.Payments;
using Shopfront.Staff;
using Shopfront.Suppliers;
using Shopfront.Warehouse;
using Trax.Core.Decisions;
using Trax.Effect.Services.FailureClassifier;
namespace Shopfront.Extensions;
public static class ShopfrontServiceCollectionExtensions
{
// Everything Shopfront's trains need, apart from Trax itself. Every process that runs the
// trains calls it, because Trax checks every train's chain when a process starts.
public static IServiceCollection AddShopfront(
this IServiceCollection services,
string connectionString
) =>
services
// One context per scope, built for that scope's caller: a pool would share it.
.AddDbContextFactory<ShopfrontDbContext>(
options => options.UseNpgsql(connectionString),
ServiceLifetime.Scoped
)
.AddScoped<IShopfrontDbContext>(sp =>
sp.GetRequiredService<IDbContextFactory<ShopfrontDbContext>>().CreateDbContext()
)
.AddScoped<IShopfrontCaller, TraxShopfrontCaller>()
.AddScoped<Catalogue>()
.AddSingleton<IPaymentProvider, FakePaymentProvider>()
.AddSingleton<IFailureClassifier, ShopfrontFailureClassifier>()
.AddSingleton<IDecider>(FraudRules.Live())
.AddSingleton<ICandidateFraudDecider, StricterFraudRules>()
.AddSingleton<IStaffNotifier, LoggingStaffNotifier>()
.AddSingleton<FakeSupplier>()
.AddSingleton<ISupplier>(sp => sp.GetRequiredService<FakeSupplier>())
.AddSingleton<IWarehouse, FakeWarehouse>();
}A fulfilment is not a manifest, so the failure hook from chapter 10 would not hear about it. Its rule becomes what it meant: staff hear about every failure but an order's, which is the customer's to see.
Shopfront.Core/Staff/StaffFailureHook.cs:
using Shopfront.Trains.PlaceOrder;
using Trax.Effect.Models.Metadata;
using Trax.Effect.Services.TrainLifecycleHook;
namespace Shopfront.Staff;
// Tells staff about every failed run of the work nobody waits for. A failed order is the customer's.
public class StaffFailureHook(IStaffNotifier staff) : ITrainLifecycleHook
{
public Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) =>
metadata.Name == typeof(IPlaceOrderTrain).FullName
? Task.CompletedTask
: staff.Notify($"{metadata.Name} failed: {exception.Message}", ct);
}SDK Reference
AddTraxWorker | ITrainExecutionService | Queued work | Job submission | Structuring your application | Host tracking
Next: 11.5 The hub