Skip to content

Custom Processors — Authoring Guide

TL;DR — A processor inspects an inbound request and produces the response payload. Derive BaseTransactionProcessor<TConfiguration>, override Process, and reference it from a mocker Stubs[].Processor entry.

When to use

  • The built-in processor catalog (see Available Processors) does not cover your transformation.
  • You need to mutate request bodies (redact, enrich, project) before the response is computed.
  • You need to read multiple data sources to assemble a response.

YAML configuration

A processor binding lives inside a mocker stub:

Stubs:
  - Name: SanitisedEcho
    Processor: PiiRedactingEcho
    ProcessorConfiguration:
      RedactedFields: [email, phone, ssn]
      Replacement: "***"
      StatusCode: 200

Servers:
  - Http:
      Port: 8080
      Endpoints:
        - Path: /users
          Actions:
            - Name: UpsertUser
              Method: Post
              TransactionStubName: SanitisedEcho

C# (CAC) usage

Derive BaseTransactionProcessor<TConfiguration> from QaaS.Framework.SDK.Hooks.Processor. Override Process(IImmutableList<DataSource>, Data<object>), return a new Data<object> whose Body is the bytes you want to send back.

using System.Collections.Immutable;
using QaaS.Framework.SDK.DataSourceObjects;
using QaaS.Framework.SDK.Hooks.Processor;
using QaaS.Framework.SDK.Session.DataObjects;

public sealed record MyConfig;

public sealed class MyProcessor : BaseTransactionProcessor<MyConfig>
{
    public override Data<object> Process(
        IImmutableList<DataSource> dataSourceList,
        Data<object> requestData)
    {
        // produce response bytes
        return new Data<object> { Body = System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}") };
    }
}

Minimal example

public record EchoConfig { public int StatusCode { get; set; } = 200; }

public sealed class Echo : BaseTransactionProcessor<EchoConfig>
{
    public override Data<object> Process(
        IImmutableList<DataSource> _,
        Data<object> requestData) => new() { Body = requestData.Body };
}

Realistic example

A processor that walks the request JSON, redacts configured property names (case-insensitive), and returns the sanitised body. The same configuration drives every endpoint that references the stub.

using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Json.Nodes;
using QaaS.Framework.SDK.DataSourceObjects;
using QaaS.Framework.SDK.Hooks.Processor;
using QaaS.Framework.SDK.Session.DataObjects;
using QaaS.Framework.SDK.Session.MetaDataObjects;

namespace MyMock.Processors;

public record PiiRedactingEchoConfig
{
    [Description("Case-insensitive JSON property names to redact wherever they appear.")]
    [Required, MinLength(1)]
    public List<string> RedactedFields { get; set; } = new();

    [Description("Replacement token written to the response."), DefaultValue("***")]
    public string Replacement { get; set; } = "***";

    [Description("HTTP status to return."), DefaultValue(200)]
    [Range(100, 599)]
    public int StatusCode { get; set; } = 200;
}

public class PiiRedactingEcho : BaseTransactionProcessor<PiiRedactingEchoConfig>
{
    public override Data<object> Process(
        IImmutableList<DataSource> dataSourceList,
        Data<object> requestData)
    {
        var raw = requestData.Body as byte[] ?? Array.Empty<byte>();
        var json = raw.Length == 0 ? new JsonObject() : JsonNode.Parse(raw) ?? new JsonObject();

        var fields = Configuration.RedactedFields
            .Select(f => f.ToLowerInvariant())
            .ToHashSet();

        Redact(json, fields, Configuration.Replacement);

        var sanitised = Encoding.UTF8.GetBytes(json.ToJsonString());

        return new Data<object>
        {
            Body = sanitised,
            MetaData = new MetaData
            {
                Http = new Http
                {
                    StatusCode = Configuration.StatusCode,
                    ResponseHeaders = new Dictionary<string, string>
                    {
                        ["Content-Type"] = "application/json",
                        ["X-Redacted-Field-Count"] = fields.Count.ToString(),
                    },
                },
            },
        };
    }

    private static void Redact(JsonNode? node, HashSet<string> fields, string replacement)
    {
        switch (node)
        {
            case JsonObject obj:
                foreach (var key in obj.Select(kv => kv.Key).ToList())
                {
                    if (fields.Contains(key.ToLowerInvariant()))
                        obj[key] = replacement;
                    else
                        Redact(obj[key], fields, replacement);
                }
                break;
            case JsonArray arr:
                for (var i = 0; i < arr.Count; i++)
                    Redact(arr[i], fields, replacement);
                break;
        }
    }
}

Mocker YAML:

Stubs:
  - Name: SanitisedEcho
    Processor: PiiRedactingEcho
    ProcessorConfiguration:
      RedactedFields: [email, phone, ssn, nationalId]
      Replacement: "***"
      StatusCode: 200

Servers:
  - Http:
      Port: 8080
      IsLocalhost: false
      Endpoints:
        - Path: /users
          Actions:
            - Name: UpsertUser
              Method: Post
              TransactionStubName: SanitisedEcho

After the round-trip a request body of

{ "id": 42, "email": "alice@example.com", "phone": "+1-555-0142", "profile": { "ssn": "123-45-6789", "nickname": "ali" } }

is rewritten to

{ "id": 42, "email": "***", "phone": "***", "profile": { "ssn": "***", "nickname": "ali" } }

Registration and discovery

Custom processors are discovered by short type name. The mocker scans referenced assemblies for types deriving from BaseProcessor<>. To wire one in:

  1. Place the class in any namespace inside an assembly the mocker host loads (your mocker project, or a referenced library).
  2. Reference the assembly from the project that hosts the mocker YAML — a project reference or a NuGet package both work.
  3. In YAML, set Processor: to the simple type name (e.g. PiiRedactingEcho), not the fully-qualified name.
Stubs:
  - Name: SanitisedEcho
    Processor: PiiRedactingEcho      # simple type name

Two processors with the same simple name across assemblies will collide; rename one. The mocker only discovers types whose assembly is already loaded in its AppDomain — a transitive dependency that nothing references will not be visible. Data annotations on TConfiguration ([Required], [Range]) are validated before Process runs.

After adding or renaming a custom processor, regenerate the mocker schema so editors pick up the new enum value. See Schema extensions for the regeneration command and the bin/ cache flush.

Edge cases

  • Non-JSON payloads raise on JsonNode.Parse. Wrap in try/catch and return the body unchanged if you need a tolerant variant.
  • Field matching is case-insensitive on the property name; recursion handles nested paths automatically.
  • Body is byte[], not string. Encode with UTF-8 explicitly.
  • The processor instance is shared across in-flight requests. Do not hold per-request state in fields.
  • Custom response headers belong in MetaData.Http.ResponseHeaders; the mocker emits them verbatim.

See also