Plugin Architecture — Bring Your Own Handler
The DataStateMachine engine is fully plugin-based. Any team can ship a handler assembly that is discovered automatically at startup. You control the backing store, the key strategy, the update model — everything.
The Bring Your Own Design
The platform ships two handler implementations — built-in (SQL) and sales-leads (CRM demo). But the engine itself has zero compile-time dependency on either. The handler is resolved at runtime by a string key declared in the node's configuration block.
This means a node that was built and deployed six months ago can be pointed at a brand-new handler — one that writes to Redis, a blockchain ledger, or a third-party compliance API — without changing a single line of the node's code. The node never knows which handler it's talking to.
The Plugin Interface
// Ship this in your plugin assembly
public interface IDataStateMachinePlugin
{
void Register(IServiceCollection services, IConfiguration config);
}
// Example implementation
public class MyCustomDataStateMachinePlugin : IDataStateMachinePlugin
{
public void Register(IServiceCollection services, IConfiguration config)
{
// Register your handler as Singleton (handlers are shared across requests)
services.AddSingleton<IDataStateMachineHandler, MyCustomHandler>();
// Register any scoped dependencies your handler needs
services.AddScoped<IMyCustomRepository, MyCustomRepository>();
}
}
Handler Discovery — AppDomain Scan
At application startup, DataStateMachinePluginLoader scans every assembly in the AppDomain for types that implement IDataStateMachinePlugin. It instantiates each one and calls Register(services, config). No manual wiring in Program.cs required — drop the DLL in the output folder and it is picked up.
Application Startup
│
▼
DataStateMachinePluginLoader.LoadPlugins(services, config)
│
├── AppDomain.CurrentDomain.GetAssemblies()
│ filter: types implementing IDataStateMachinePlugin
│
├── BuiltInDataStateMachinePlugin.Register(...)
│ → registers BuiltInDataStateMachineHandler (Singleton)
│ → registers IProcessStateLogRepository (Scoped)
│
├── SalesLeadsDataStateMachinePlugin.Register(...)
│ → registers SalesLeadsDataStateMachineHandler (Singleton)
│ → registers ISalesLeadRepository (Scoped)
│
└── YourCustomPlugin.Register(...)
→ registers whatever your handler needs
Implementing a Handler
public class MyCustomHandler : IDataStateMachineHandler
{
public string HandlerKey => "my-custom"; // matches "handler": "my-custom" in node config
public Task<bool> ExistsAsync(StateHandlerContext ctx, CancellationToken ct)
{
// ctx.ItemKey, ctx.ItemEntityKey, ctx.SubItemKey, ctx.SubItemEntityKey
// ctx.TenantID, ctx.EntityID, ctx.StateLogScopeID
// Query YOUR store using these keys
}
public Task<bool> TryAcquireLockAsync(StateHandlerContext ctx, CancellationToken ct) { ... }
public Task SetAsync(StateHandlerContext ctx, IdempotencyStatus status, string? data, CancellationToken ct) { ... }
public Task UpsertAsync(StateHandlerContext ctx, string? data, CancellationToken ct) { ... }
public Task ReleaseLockAsFailedAsync(StateHandlerContext ctx, string? errorDetail, CancellationToken ct) { ... }
public Task<StateRecord?> GetAsync(StateHandlerContext ctx, CancellationToken ct) { ... }
public Task<IReadOnlyList<StateRecord>> QueryAsync(StateHandlerContext ctx, StateQuery query, CancellationToken ct) { ... }
}
Handler vs Plugin vs Factory — Responsibilities
| Component | Lifetime | Responsibility |
|---|---|---|
IDataStateMachinePlugin |
Called once at startup | Registers handler and its dependencies into DI |
IDataStateMachineHandler |
Singleton | Executes all state operations against your backing store |
DataStateMachineFactory |
Scoped (per request) | Resolves handler by key; builds StateHandlerContext from node config + execution context |
ContextualDataStateMachine |
Per node execution | Wraps handler + base context; builds per-call ctx using C# with |
IDataStateMachine |
Per node execution | The interface the node developer calls — never sees the handler directly |
Flexibility Matrix
| Backing Store | Example Use Case | Custom Handler? |
|---|---|---|
| SQL Server (built-in) | Standard workflow idempotency | No — built-in handler |
| CRM Table | Lead/contact state tracking | Yes — SalesLeads demo as template |
| Redis | High-throughput ephemeral locks | Yes — implement IDataStateMachineHandler |
| REST API | Third-party compliance record | Yes |
| Blockchain / DID Ledger | Immutable state anchoring | Yes |
| Custom SQL table | Domain-specific entity state | Yes |
3-Project Structure Per Handler
Every handler follows a 3-project pattern — Domain, Infrastructure, Service — matching the rest of the BizFirstAI architecture. This ensures clean separation and testability.
BizFirst.Ai.DataStateMachine.{Name}.Domain
Entities\ ← EF Core entity mapping to your table
Repositories\ ← IYourRepository interface
Services\ ← IYourService interface
BizFirst.Ai.DataStateMachine.{Name}.Infrastructure
Data\ ← DbContext — your EF Core tables
Repositories\ ← EF implementation
DependencyInjection.cs
BizFirst.Ai.DataStateMachine.{Name}.Service
Services\ ← Business logic, Guard validations
Handlers\ ← IDataStateMachineHandler implementation ← KEY FILE
{Name}DataStateMachinePlugin.cs ← IDataStateMachinePlugin
DependencyInjection.cs