ComposeUI

Module Loader events

ADR ID
adr-015
Status
Accepted
Scope
global
Deciders
github group
Date
6/11/2024

Context

Module Loader is responsible for starting and stopping module instances. It is also responsible for emitting lifetime events of module instances, such as Starting, Started, Stopping, Stopped.

The current architecture is built on top of IObservable<T>:

public interface IModuleLoader
{
    IObservable<LifetimeEvent> LifetimeEvents { get; }

    // other members omitted for brevity
}

Current usages

Raising events

The Module Loader is responsible for raising lifetime events. This is currently achieved by using a Subject. Subjects implement both IObservable<T> and IObserver<T>, and when a lifetime event needs to be emitted we call OnNext() on the subject.

Consuming events

Module Loader lifetime events are consumed by ComposeUI infrastructure code, but can also be consumed by external applications.

Current infrastructure usages utilize System.Reactive extensions and its LINQ operators. For example:

internal sealed class ModuleService : IHostedService
{
    // ...
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _disposables.Add(
            _moduleLoader.LifetimeEvents
                .OfType<LifetimeEvent.Started>()
                .Where(e => e.Instance.Manifest.ModuleType == ModuleType.Web)
                .Subscribe(OnWebModuleStarted));

This usage filters lifetime events by type and by content to subscribe only to the Started events of Web module instances.

Another usage uses the ObserveOn() extension method which ensures that the observer’s OnNext() method is invoked via the specified scheduler, in this case using the DispatcherSynchronizationContext from WPF.

In our FDC3 Desktop Agent implementation we use System.Reactive.Async to convert the IObservable<T> events to IAsyncObservable<T> so that the OnNext() calls are properly awaited:

        var observable = _moduleLoader.LifetimeEvents.ToAsyncObservable();
        var subscription = await observable.SubscribeAsync(async lifetimeEvent =>
        {
            // code containing await

Alternatives considered

Events

The most obvious alternative to IObservable<T> is the built-in event feature in C# / .NET. Raising lifetime events would be very similar to the current code, however subscribing to these events would be limited to the += operator. Since we’re already relying on the LINQ operators provided by Rx.NET using built-in events instead would be inferior.

The current implementation of the FDC3 Desktop Agent relies on awaiting in the OnNextAsync() handler. Implementing the same behavior by using built-in events would be challenging (if possible at all).

IAsyncObservable<T>

IAsyncObservable<T> is part of System.Reactive.Async package, for which the latest available version as of today is 6.0.0-alpha.18, which is a pre-release version.

We are already using this package internally but we don’t want to enforce pre-release dependencies on our customers by changing the public interface to use IAsyncObservable<T>.

Decision

We continue using IObservable<T> for Module Loader lifetime events.

Consequences

We will revisit this ADR once there will be a stable version of System.Reactive.Async package available.