Custom Processors — Authoring Guide¶
TL;DR — A processor inspects an inbound request and produces the response payload. Derive
BaseTransactionProcessor<TConfiguration>, overrideProcess, and reference it from a mockerStubs[].Processorentry.
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:
- Place the class in any namespace inside an assembly the mocker host loads (your mocker project, or a referenced library).
- Reference the assembly from the project that hosts the mocker YAML — a project reference or a NuGet package both work.
- 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 intry/catchand return the body unchanged if you need a tolerant variant. - Field matching is case-insensitive on the property name; recursion handles nested paths automatically.
Bodyisbyte[], notstring. 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.