EventCoalTrain 2.1.0

dotnet add package EventCoalTrain --version 2.1.0
                    
NuGet\Install-Package EventCoalTrain -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EventCoalTrain" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EventCoalTrain" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="EventCoalTrain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EventCoalTrain --version 2.1.0
                    
#r "nuget: EventCoalTrain, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EventCoalTrain@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EventCoalTrain&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=EventCoalTrain&version=2.1.0
                    
Install as a Cake Tool

EventCoalTrain

Simple, typed event bus for .NET (Unity, Godot, and general C# apps).

Supported TFMs: netstandard2.1, net8.0.

Features

  • Strongly-typed event keys (EventKey<T>)
  • Packets with payloads and Notifications without payloads
  • Thread-safe subscriptions and publishing
  • Snapshot iteration during publish (handlers can add/remove safely)
  • Empty-list cleanup to avoid leaks
  • Optional error callback for subscriber exceptions
  • Static EventBus wrapper and an IEventBus interface with IDisposable subscriptions
  • Global preprocessor symbol for consumers: EVENTCOALTRAIN

Best practice: define record payloads and cache keys and packet descriptors

Model your domain events with record (or record struct) payloads for clarity and immutability, then create a typed EventKey based on that record. Cache and reuse:

  • the EventKey<TPayload>, or EventKey<Unit> for no-payload events.
  • an optional Packet<TPayload> descriptor constructed with the key only (no payload)
  • Notification instances for no-payload events

Warning:

  • Don’t allocate Notification or Packet<T> inline at Publish/Subscribe call sites (e.g., new Notification(...) or new Packet<T>(key, payload) in the argument list). Instead, cache the Notification or a Packet<T> descriptor (key-only) and publish with a fresh payload per call via Publish(descriptor, payload) or Publish(key, payload).
  • This avoids unnecessary allocations, keeps event identity stable, and prevents stale state.
using EventCoalTrain.EventStructure;
using EventCoalTrain.EventSource;

// 1) Define the payload as a record/record struct
public readonly record struct ScoreIncreased(int Amount, string Source);

public static class Events
{
    // 2) Define and cache the event key typed on the payload record
    public static readonly EventKey<ScoreIncreased> ScoreIncreasedKey = EventKey<ScoreIncreased>.Of("ScoreIncreased");

    // 3) Optionally cache a packet descriptor (key only)
    public static readonly Packet<ScoreIncreased> ScoreIncreased = new Packet<ScoreIncreased>(ScoreIncreasedKey);

    // Notification-style event (no payload). Cache both key and notification instance.
    public static readonly EventKey<Unit> ButtonClickedKey = EventKey<Unit>.Of("ButtonClicked");
    public static readonly Notification ButtonClicked = new Notification(ButtonClickedKey);
}
  • Subscribe using the cached keys/notifications via EventBus.Instance.
  • Publish using either the cached packet descriptor or the key directly with a fresh payload instance each time.

Quick start

using EventCoalTrain.EventStructure;
using EventCoalTrain.EventSource;
using EventCoalTrain.EventHandling;

// Subscribe (preferred instance API returning IDisposable)
var subScore = EventBus.Instance.Subscribe(Events.ScoreIncreasedKey, e => Console.WriteLine($"+{e.Amount} ({e.Source})"));
var subClick = EventBus.Instance.Subscribe(Events.ButtonClicked, () => Console.WriteLine("clicked"));

// Publish with a cached packet descriptor + fresh payload
EventBus.Publish(Events.ScoreIncreased, new ScoreIncreased(10, "LevelComplete"));

// Or publish with key + payload
EventBus.Publish(Events.ScoreIncreasedKey, new ScoreIncreased(5, "Combo"));

// Notification publish reuses cached notification
EventBus.Publish(Events.ButtonClicked);

// Unsubscribe via disposables
subScore.Dispose();
subClick.Dispose();

Notes:

  • Cache and reuse EventKey<T> and optional Packet<T> descriptors; supply a new payload per publish. This is allocation-light (especially with record structs) and thread-safe.
  • Prefer EventBus.Instance for subscriptions to get IDisposable handles.
  • The static wrapper methods exist for convenience/back-compat; the instance API is recommended for lifecycle management.

API overview

Event keys, packet descriptors, and payload records

public readonly record struct DamageTaken(int Amount, string Cause);

var DamageTakenKey = EventKey<DamageTaken>.Of("DamageTaken");
var DamageTakenPacket = new Packet<DamageTaken>(DamageTakenKey); // descriptor
var TickedKey = EventKey<Unit>.Of("Ticked");
var Ticked = new Notification(TickedKey);
  • Equality and GetHashCode of EventKey include both Name and payload type to avoid cross-type collisions.
  • Names must still be unique globally (across all payload types).

Subscribing (instance, preferred)

var sub1 = EventBus.Instance.Subscribe(DamageTakenKey, d => Console.WriteLine($"damage {d.Amount} from {d.Cause}"));
var sub2 = EventBus.Instance.Subscribe(Ticked, () => Console.WriteLine("ticked"));

// Unsubscribe
sub1.Dispose();
sub2.Dispose();

Publishing

// Packets: Using cached descriptor + fresh payload
EventBus.Publish(DamageTakenPacket, new DamageTaken(5, "Trap"));

// Notifications: Using the cached notification.
EventBus.Publish(Ticked);

Bulk operations

EventBus.UnsubscribeAll(DamageTakenKey);
bool any = EventBus.HasSubscribers(DamageTakenKey);
int count = EventBus.Count(DamageTakenKey);
EventBus.Clear(); // remove all subscriptions

Error handling

When a subscriber throws during publish, publishing continues for other subscribers. You can observe errors via a callback:

EventBus.OnPublishError += (ex, key, del) =>
{
    Console.Error.WriteLine($"Subscriber threw for {key.Name}: {ex}");
};

Thread-safety and iteration

  • The internal subscribers map is guarded by a lock for all mutations and lookups.
  • Publish iterates over a snapshot to avoid issues if handlers subscribe/unsubscribe during delivery.
  • After unsubscription, empty handler lists are removed to keep memory usage small.

NuGet preprocessor symbol

When you add the EventCoalTrain NuGet package, the compile symbol EVENTCOALTRAIN is automatically defined in your consuming project via a buildTransitive MSBuild props.

Example:

#if EVENTCOALTRAIN
Console.WriteLine("EventCoalTrain is referenced.");
#endif

Works for direct and transitive references. No project changes required.

Unity without NuGet

If you drop the DLL directly into Unity (not via NuGet), Unity won’t import MSBuild props. Define the symbol manually in Player Settings > Scripting Define Symbols: EVENTCOALTRAIN.

Opting out

Override DefineConstants in your project, or add a props after package imports to remove the symbol if needed.

What’s inside the package

The package ships buildTransitive/EventCoalTrain.props with:

<Project>
  <PropertyGroup>
    <DefineConstants>$(DefineConstants);EVENTCOALTRAIN</DefineConstants>
  </PropertyGroup>
</Project>

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 61 8/23/2025
2.0.2 61 8/23/2025
2.0.1 125 8/20/2025
2.0.0 123 8/20/2025
1.0.0 146 6/29/2025