Module Lifetime
The FlowModule base class provides three lifecycle methods: Initialize, Start, and Stop. Override these to manage resources and connections.
Lifecycle Sequence
- Constructor - Module instance is created
- Initialize - Called once per module to set up resources
- Start - Called after all modules are initialized, signals the flow is ready
- MessageReceived - Called repeatedly as messages flow through the module
- Stop - Called when the flow is stopping, used for cleanup
┌─────────────┐
│ Constructor │
└──────┬──────┘
│
▼
┌─────────────┐
│ Initialize │ ◄─── Setup resources, validate settings
└──────┬──────┘
│
▼
┌─────────────┐
│ Start │ ◄─── Start background tasks, open connections
└──────┬──────┘
│
▼
┌─────────────┐
│ Running │ ◄─── MessageReceived called for each message
│ (Active) │
└──────┬──────┘
│
▼
┌─────────────┐
│ Stop │ ◄─── Cleanup, close connections, dispose resources
└─────────────┘
Initialize
Initialize is called once when the flow starts. Use it to validate settings and create resources.
Use for: Validate settings, create connection pools, initialize caches, check prerequisites
Return: await base.Initialize() on success, or an IError to prevent flow startup
public override async Task<IError?> Initialize()
{
if (string.IsNullOrEmpty(this.Settings.ConnectionString))
return Error.Create("Connection string is required");
try
{
this.connectionPool = new ConnectionPool(this.Settings.ConnectionString);
}
catch (Exception ex)
{
return Error.Create($"Failed to initialize: {ex.Message}");
}
return await base.Initialize();
}
Warning
Don't start background tasks here. Use Start instead.
Start
Start is called after all modules are initialized. Use it to begin active operations.
Use for: Start timers, open connections, begin listening for events, activate input sources
Return: await base.Start() on success, or an IError to stop the flow
public override async Task<IError?> Start()
{
this.cancellationSource = new CancellationTokenSource();
this.timer = new Timer(_ => PollDataSource(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(this.Settings.Interval));
try
{
this.mqttClient.Connect();
this.mqttClient.OnMessageReceived += HandleIncomingMessage;
}
catch (Exception ex)
{
return Error.Create($"Failed to connect: {ex.Message}");
}
return await base.Start();
}
Stop
Stop is called when the flow stops. Use it to cleanup and release resources.
Use for: Stop timers, close connections, dispose resources, cancel operations
public override void Stop()
{
this.cancellationSource?.Cancel();
this.timer?.Dispose();
this.timer = null;
try
{
this.mqttClient?.Disconnect();
this.mqttClient?.Dispose();
}
catch (Exception ex)
{
this.LogWarning(ex, "Error disconnecting");
}
this.cancellationSource?.Dispose();
this.cancellationSource = null;
}
Important
Always implement Stop if you override Start or Initialize. Handle exceptions gracefully to avoid blocking other modules' cleanup.
Best Practices
Constructor vs Initialize
- ❌ Don't create resources in constructor (settings not available)
- ✅ Do create resources in Initialize (settings available, can validate)
Resource Management
- Initialize: Create resources (HttpClient, connection pools)
- Start: Activate resources (start timers, open connections)
- Stop: Cleanup in reverse order (cancel, dispose, null)
Error Handling
- Return IError from Initialize/Start to prevent flow startup
- Log and swallow exceptions in Stop to allow other modules to cleanup
Examples
Input Module (Timer-based)
public class MyInputModule : InputModule<MySettings>
{
public override async Task<IError?> Start()
{
this.timer = new Timer(_ => GenerateData(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(this.Settings.Interval));
return await base.Start();
}
private void GenerateData()
{
var message = new FlowMessage();
message.Set("value", GetSensorReading());
this.Next(message);
}
public override void Stop()
{
this.timer?.Dispose();
this.timer = null;
}
}
Output Module (HTTP)
public class MyOutputModule : OutputModule<MySettings>
{
public override async Task<IError?> Initialize()
{
this.httpClient = new HttpClient { BaseAddress = new Uri(this.Settings.ApiEndpoint) };
return await base.Initialize();
}
protected override void MessageReceived(IFlowMessage message)
{
try
{
var response = this.httpClient.PostAsync("/data",
new StringContent(message.ToJSON(), Encoding.UTF8, "application/json"))
.GetAwaiter().GetResult();
message.SetSuccess(response.IsSuccessStatusCode);
}
catch (Exception ex)
{
message.SetError(ex.Message);
}
this.Next(message);
}
public override void Stop()
{
this.httpClient?.Dispose();
this.httpClient = null;
}
}
Troubleshooting
| Issue | Solution |
|---|---|
| Module doesn't start | Check Initialize/Start return IError, review logs, verify settings |
| Resources not cleaned up | Implement Stop, dispose all IDisposable, cancel tokens |
| Flow startup slow | Move long operations to Start, use async/await, lazy initialization |