Tutorial

5.4 Register Shopfront in one place

The tests have copied Program.cs's registrations since chapter 2, and the list keeps growing. From here on both call one method, so a test runs with what the application runs with.

Extensions/ShopfrontServiceCollectionExtensions.cs:

using Microsoft.EntityFrameworkCore;
using Shopfront.Data;
using Shopfront.Failures;
using Shopfront.Fraud;
using Shopfront.Inventory;
using Shopfront.Payments;
using Trax.Core.Decisions;
using Trax.Effect.Data.Services.DomainContext;
using Trax.Effect.Services.FailureClassifier;
 
namespace Shopfront.Extensions;
 
public static class ShopfrontServiceCollectionExtensions
{
    // Everything Shopfront's trains need, apart from Trax itself.
    public static IServiceCollection AddShopfront(
        this IServiceCollection services,
        string connectionString
    ) =>
        services
            .AddDomainDataContext<IShopfrontDbContext, ShopfrontDbContext>(options =>
                options.UseNpgsql(connectionString)
            )
            .AddScoped<Catalogue>()
            .AddSingleton<IPaymentProvider, FakePaymentProvider>()
            .AddSingleton<IFailureClassifier, ShopfrontFailureClassifier>()
            .AddSingleton<IDecider>(FraudRules.Live())
            .AddSingleton<ICandidateFraudDecider, StricterFraudRules>();
}

One IDecider serves every question in every train. The shadow is registered under the type the gate names.

Program.cs:

using Shopfront.Data;
using Shopfront.Extensions;
using Shopfront.Trains.PlaceOrder;
using Trax.Core.Exceptions;
using Trax.Effect.Data.Extensions;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Data.Services.DomainContext;
using Trax.Effect.Extensions;
using Trax.Effect.JunctionProvider.Logging.Extensions;
using Trax.Effect.Provider.Parameter.Extensions;
using Trax.Mediator.Extensions;
 
var builder = WebApplication.CreateBuilder(args);
 
var connectionString =
    builder.Configuration.GetConnectionString("Shopfront")
    ?? throw new InvalidOperationException("ConnectionStrings:Shopfront is not set.");
 
builder.Services.AddShopfront(connectionString);
 
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects =>
            effects
                .UsePostgres(connectionString)
                .SaveTrainParameters()
                .AddJunctionLogger(serializeJunctionData: true)
                .AddDecisionRecording()
        )
        .AddMediator(typeof(Program).Assembly)
);
 
var app = builder.Build();
 
await app.Services.EnsureSchemaCreatedAsync<ShopfrontDbContext>();
 
app.MapPost(
    "/orders",
    async (PlaceOrderInput input, IPlaceOrderTrain train, CancellationToken cancellationToken) =>
    {
        try
        {
            return Results.Ok(await train.Run(input, cancellationToken));
        }
        catch (TrainException ex)
        {
            return Results.UnprocessableEntity(ex.Message);
        }
    }
);
 
await app.RunAsync();

AddDecisionRecording writes every question a run asks to trax.decision, with the answer, the decider that gave it, each shadow's answer and whether it agreed, and the track taken. It writes as each decision is made, before the train acts on it, so a run that fails afterwards still shows what it decided. It needs the data provider before it.

Each answer is stored with a hash of the value it was about, not the value. Without a key the hash is a plain SHA-256; a deployment that wants it keyed sets one, as Keying the state hash describes.

SDK Reference

Gate | Decide | AddDecisionRecording | Write a decider | Try a decider in shadow | Test a train's decisions


Next: 5.5 Run it