Skip to content

Pattern 15: Enrollment Process

Purpose

Use this pattern when the adapter must capture a new credential or template for a person.

The overall process should stay the same regardless of what is being enrolled:

  • card
  • fingerprint
  • face
  • another opaque credential blob

What changes between adapters is mainly:

  • which PQ command starts the process
  • what device action or event completes it
  • what payload is sent to IEnrollmentService.Complete(...)

Core Rule

Treat enrollment as a short-lived process owned by the framework:

  1. start session on command
  2. set reader status to enrollment in progress
  3. wait for device capture or scan
  4. transform captured data to PQ enrollment payload
  5. complete via IEnrollmentService
  6. let framework publish completion event and clear status

Do not build custom enrollment state machines in transport code unless the device protocol truly requires it.

Keep enrollment in a small handler, similar to command handlers in existing adapters.

public static async Task<DeviceCommandResult> EnrollX(
    Thing thing,
    Access.Enroll.Card command,
    Protocol protocol,
    IEnrollmentService enrollment,
    CancellationToken ct)
{
    enrollment.Begin(thing);

    try
    {
        var deviceEvent = await protocol.WaitForEvent(
            MatchesTargetThing,
            TimeSpan.FromSeconds(60),
            ct: ct);

        var data = new CardEnrollmentData
        {
            EnrollmentId = command.EnrollmentId,
            PersonId = command.PersonId,
            CardCode = ExtractCardCode(deviceEvent),
            Technology = "vendor.card",
            BitLength = ExtractBitCount(deviceEvent),   // set when the reader/SDK reports a bit count
        };

        return await enrollment.Complete(data, ct);
    }
    catch (TimeoutException)
    {
        await thing.SetStatus("enrollment", null, CancellationToken.None);
        return new DeviceCommandResult(CommandResult.Timeout);
    }
    catch (OperationCanceledException)
    {
        await thing.SetStatus("enrollment", null, CancellationToken.None);
        return new DeviceCommandResult(CommandResult.Timeout);
    }
}

For biometrics, the same process applies, but the completion payload is EnrollmentData instead of CardEnrollmentData.

Framework Contract

IEnrollmentService owns the framework side of the lifecycle.

  • Begin(thing)
  • marks the thing as enrolling
  • Complete(CardEnrollmentData)
  • completes card enrollment and publishes success event with card_code (and card_bits when BitLength is set)
  • Complete(EnrollmentData)
  • completes biometric enrollment and uploads template/blob to server

The adapter should focus on capture and transformation, not on persistence.

Card Enrollment Variant

Use this when the device produces a card identifier but PQ does not yet know the credential.

Typical sources:

  • explicit scan API call
  • intercepted unknown-card event
  • reader transaction event

Typical output:

  • CardEnrollmentData.CardCode
  • CardEnrollmentData.Technology
  • CardEnrollmentData.BitLength — set it when the reader/SDK reports the wire bit count (see the Card Wire Bit-Length concept and RULE-053); leave it null only when no bit count is available

Examples in current adapters:

  • reader-transaction adapter: waits for a reader card transaction and completes with raw card hex
  • denial-driven adapter: waits for an unknown-card denial on the target door reader and completes with raw serial hex
  • scan-API adapter: calls a scan API and completes with card bytes converted to hex

Biometric Enrollment Variant

Use this when the device returns an opaque template or blob.

Typical output:

  • EnrollmentData.TemplateData
  • EnrollmentData.Technology
  • optional quality score and metadata

The framework stores the blob. The adapter should not interpret template internals unless the protocol requires it.

Target Resolution

Enrollment must be scoped to the correct thing.

Common strategies:

  • wait for event already addressed to the target reader
  • match protocol address or ACR number
  • match door address derived from reader topology
  • match device ID from scan response

Prefer a small predicate over global mutable state.

Where To Put The Logic

Preferred layout:

  • Capabilities/Enrollment/Card/CardEnrollment.cs
  • Capabilities/Enrollment/Face/FaceEnrollment.cs
  • similar feature-local handlers for other credential types

Keep transport/protocol generic. Enrollment-specific orchestration belongs close to the capability, not buried in shared connection code.

Error Handling

Expected outcomes:

  • success: enrollment.Complete(...)
  • timeout: return CommandResult.Timeout
  • cancellation: usually same external result as timeout unless protocol needs something else
  • no usable captured data: return CommandResult.Failure

If cleanup must happen after timeout/cancellation, do not reuse an already-cancelled token for clearing enrollment status.

Technology Strings

Use stable vendor-specific technology strings.

Examples:

  • acme.card
  • beta.card
  • acme.csn
  • vendor.fingerprint.v2

Technology should identify the credential family or template format, not UI wording.

Verification Checklist

  1. command is exposed only on things that can actually complete enrollment
  2. handler calls Begin(...) before waiting for capture
  3. completion payload contains the intended raw identifier or template
  4. timeout path clears enrollment state
  5. project builds and generated command wiring matches expected thing type

Relationship To Pattern 8

Pattern 8 remains the biometric-specific variant. This document is the general enrollment process pattern that should be used first, then specialized for card or biometric capture.