Table of Contents

Module Lifetime

The FlowModule base class provides three lifecycle methods: Initialize, Start, and Stop. Override these to manage resources and connections.

Lifecycle Sequence

  1. Constructor - Module instance is created
  2. Initialize - Called once per module to set up resources
  3. Start - Called after all modules are initialized, signals the flow is ready
  4. MessageReceived - Called repeatedly as messages flow through the module
  5. 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

Next Steps

>> Module Settings