Table of Contents

Library Lifecycle

Every CodeLogic library and plugin implements the same 4-phase lifecycle. The framework calls these phases in a deterministic order across all registered libraries, ensuring dependencies are always fully ready before their consumers start.


The 4 Phases

Phase Method Called during Purpose
1 — Configure OnConfigureAsync CodeLogic.ConfigureAsync() Register config and localization models
2 — Initialize OnInitializeAsync CodeLogic.StartAsync(), first pass Read config, open connections, set up services
3 — Start OnStartAsync CodeLogic.StartAsync(), second pass Start background tasks, begin accepting work
4 — Stop OnStopAsync CodeLogic.StopAsync() Graceful shutdown (called in reverse start order)

Phase 1 — Configure

OnConfigureAsync is called for all libraries before any config file is loaded. Its only job is to register which config and localization models the library needs.

public Task OnConfigureAsync(LibraryContext context)
{
    // Register config models
    context.Configuration.Register<MyConfig>();
    context.Configuration.Register<AdvancedConfig>();

    // Register localization models
    context.Localization.Register<MyStrings>();

    return Task.CompletedTask;
}

Critical rule: Do NOT read config values in this phase. Config files are generated/loaded after all OnConfigureAsync calls complete.


Phase 2 — Initialize

OnInitializeAsync is called after all libraries have configured and all config files are loaded. Dependencies are initialized before their dependants.

private MyConfig _config = null!;
private ILogger _logger = null!;
private DbConnection _db = null!;

public async Task OnInitializeAsync(LibraryContext context)
{
    _logger = context.Logger;
    _config = context.Configuration.Get<MyConfig>();

    // Validate config
    var validation = _config.Validate();
    if (!validation.IsValid)
        throw new InvalidOperationException($"Config invalid: {string.Join(", ", validation.Errors)}");

    // Open connection
    _db = new DbConnection(_config.ConnectionString);
    await _db.OpenAsync();

    _logger.LogInformation("Initialized with connection pool size {Size}", _config.PoolSize);
}

Phase 3 — Start

OnStartAsync is called after all libraries have initialized. This is where you start background tasks and begin accepting work.

private CancellationTokenSource _cts = new();
private Task _backgroundTask = Task.CompletedTask;

public Task OnStartAsync(LibraryContext context)
{
    _backgroundTask = RunBackgroundAsync(_cts.Token);
    context.Logger.LogInformation("Library started");
    return Task.CompletedTask;
}

private async Task RunBackgroundAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await DoWorkAsync();
        await Task.Delay(TimeSpan.FromSeconds(30), ct);
    }
}

Phase 4 — Stop

OnStopAsync is called in reverse start order (last started, first stopped). Stop background tasks, flush pending work, and close connections.

public async Task OnStopAsync()
{
    // Signal background task to stop
    _cts.Cancel();

    // Wait for it to finish
    await _backgroundTask.ConfigureAwait(false);

    // Close database connection
    await _db.CloseAsync();
    _db.Dispose();
}

LibraryContext

The same LibraryContext instance is passed to all four phases. Store it in a field if you need it during OnStopAsync:

public sealed class LibraryContext
{
    public string LibraryId          { get; }
    public string LibraryDirectory   { get; }   // {FrameworkRoot}/Libraries/{Id}/
    public string ConfigDirectory    { get; }
    public string LocalizationDirectory { get; }
    public string LogsDirectory      { get; }
    public string DataDirectory      { get; }

    public ILogger Logger                        { get; }
    public IConfigurationManager Configuration   { get; }
    public ILocalizationManager Localization     { get; }
    public IEventBus Events                      { get; }
}

LibraryManifest

The manifest describes the library's identity and requirements:

public LibraryManifest Manifest => new()
{
    Id          = "MyApp.Database",          // determines directory name
    Name        = "Database Library",        // shown in console output
    Version     = "1.0.0",
    Description = "Manages database access",
    Author      = "Media2A",
    Dependencies = [
        new LibraryDependency("MyApp.Config", "1.0.0")
    ],
    Tags = ["database", "infrastructure"]
};

Dependency Resolution

Dependencies are resolved by ID and minimum version. The framework initializes libraries in dependency order — if library B depends on library A, A's OnInitializeAsync and OnStartAsync will complete before B's.


Ordering Summary

Given libraries A, B (depends on A), and C (depends on B):

ConfigureAsync():
  A.OnConfigureAsync()
  B.OnConfigureAsync()
  C.OnConfigureAsync()

StartAsync() - Initialize pass (dependency order):
  A.OnInitializeAsync()
  B.OnInitializeAsync()
  C.OnInitializeAsync()

StartAsync() - Start pass:
  A.OnStartAsync()
  B.OnStartAsync()
  C.OnStartAsync()

StopAsync() - reverse start order:
  C.OnStopAsync()
  B.OnStopAsync()
  A.OnStopAsync()

IDisposable

ILibrary extends IDisposable. The framework calls Dispose() after OnStopAsync. Use it for any final synchronous cleanup:

public void Dispose()
{
    _cts.Dispose();
    _db?.Dispose();
}