Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

DSoft.MessageBus is a cross-platform EventBus/messaging library (like `NSNotificationCenter` on iOS, `otto` on Android) that decouples application components. Shipped as two NuGet packages: `DSoft.MessageBus` (main) and `DSoft.MessageBus.Core` (contracts/core types).

## Build & Test

Solution file is `MessageBus.slnx` (new XML solution format — requires recent .NET 10 SDK / VS).

```bash
dotnet restore MessageBus.slnx
dotnet build MessageBus.slnx --configuration Release
dotnet test UnitTest/UnitTest.csproj # tests target net9.0, MSTest
dotnet test UnitTest/UnitTest.csproj --filter "FullyQualifiedName~MessageBusTest.MethodName" # single test
```

Building the full `DSoft.Messaging` project requires platform workloads (`dotnet workload restore`) because it multi-targets iOS/Android/macOS/tvos/MacCatalyst/Windows. To iterate quickly without workloads, build/test against a single TFM via the UnitTest project (which references the main project but is consumed as `net9.0`). Note `Directory.Build.props` sets `GeneratePackageOnBuild=true` and signs assemblies with `DSoft.snk`.

## Architecture

Three source projects plus a WPF sample:

- **DSoft.MessageBus.Core** (`netstandard2.0`) — platform-agnostic contracts and data types: `IMessageBusService`, the abstract `MessageBusEvent` + concrete `CoreMessageBusEvent`, `MessageBusEventHandler`/`TypedMessageBusEventHandler`, `LogEvent`, `ILogListener`, and the handler/listener collections. Namespace is `DSoft.MessageBus` (not `.Core`).
- **DSoft.Messaging** (multi-target, PackageId `DSoft.MessageBus`) — the runtime: `MessageBusService` (internal impl of `IMessageBusService`), the static `MessageBus` facade, DI registration, and `ThreadControl`.
- **UnitTest** — MSTest. `BaseTest` builds a DI `ServiceProvider` via `RegisterMessageBus()` in `[AssemblyInitialize]`; resolve `IMessageBusService` from `BaseTest.Provider`.

### Two ways to consume the bus
1. **Static facade** — `MessageBus.Post/Subscribe/...` wraps a lazily-created singleton `MessageBusService`.
2. **DI** — call `services.RegisterMessageBus()` (extension in `Microsoft.Extensions.DependencyInjection` namespace) to register `IMessageBusService` as a singleton, then inject it.

Both routes hit the same `MessageBusService` logic.

### Event dispatch model
- **String-id events**: subscribe/post by `eventId` string. Matching is **case-insensitive** (`HandlersForEvent` lowercases both sides). `CoreMessageBusEvent` auto-generates a GUID id if none supplied.
- **Typed events**: subclass `MessageBusEvent`, override `EventId`, subscribe with `Subscribe<T>()`. These register as `TypedMessageBusEventHandler` and dispatch by `Type`. In `PostInternal`, a non-`CoreMessageBusEvent` is delivered to both type handlers **and** any handlers registered for its `EventId`.
- `RunPostOnSeperateTask` (default false) offloads `Post`/`Log` to `Task.Run`; `PostAsync` variants always run on a background task.

### Platform-specific compilation
`DSoft.Messaging` uses **filename-suffix conventions** instead of one file per TFM — the `.csproj` includes files by suffix via `EnableDefaultCompileItems=false`:
- `*.shared.cs` — all targets
- `*.netstandard.cs` — netstandard/net8/net9/net10/net472
- `*.wpf.cs` (windows7.0), `*.winui.cs` (windows10), `*.android.cs`, `*.ios.cs` (ios+maccatalyst), `*.mac.cs` (macos), `*.tvos.cs`

`ThreadControl` is the main example: `ThreadControl.shared.cs` defines the public API and calls `partial` members (`PlatformIsMainThread`, `PlatformBeginInvokeOnMainThread`) implemented per platform. When adding platform-specific code, add a new `*.<platform>.cs` partial rather than `#if`. (ThreadControl's `MainThread` logic is adapted from Xamarin.Essentials.)

## Conventions
- Keep `IMessageBusService`, the static `MessageBus` facade, and `MessageBusService` in sync — all three expose the same Post/Subscribe/Unsubscribe/Log surface.
- New public APIs need XML doc comments (`GenerateDocumentationFile=true`; many nullable/doc warnings are suppressed in `Directory.Build.props`).
- Version/release notes live in the `.csproj` PropertyGroups (`Version`, `AssemblyVersion`, `PackageReleaseNotes`); bump there for releases.
30 changes: 20 additions & 10 deletions DSoft.MessageBus.Core/Collections/LogListernersCollection.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,35 @@ namespace DSoft.MessageBus
{
public class LogListernersCollection : Collection<ILogListener>
{
private readonly object _syncRoot = new object();

/// <summary>
/// Lock object guarding access to this collection.
/// </summary>
public object SyncRoot => _syncRoot;

public void Register(ILogListener instance)
{
if (this.Contains(instance))
return;

if (instance.Channels == null || instance.Channels.Count() == 0)
throw new Exception($"Cannot register {instance.GetType().FullName} as an ILogListener as it has no channels to listen too");

this.Add(instance);
lock (_syncRoot)
{
if (this.Contains(instance))
return;

this.Add(instance);
}
}

public IEnumerable<ILogListener> FindAll(string channelName)
{
var results = from item in this.Items
where item.Channels.Contains(channelName, StringComparer.OrdinalIgnoreCase)
select item;


return results;
lock (_syncRoot)
{
return this.Items
.Where(item => item.Channels.Contains(channelName, StringComparer.OrdinalIgnoreCase))
.ToArray();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.Collections.Generic;

Expand All @@ -10,6 +9,22 @@ namespace DSoft.MessageBus
/// </summary>
public class MessageBusEventHandlerCollection : Collection<MessageBusEventHandler>
{
#region Fields

private readonly object _syncRoot = new object ();

#endregion

#region Properties

/// <summary>
/// Lock object guarding access to this collection. Callers performing
/// compound (check-then-act) operations should lock on this.
/// </summary>
public object SyncRoot => _syncRoot;

#endregion

#region Methods

/// <summary>
Expand All @@ -19,14 +34,23 @@ public class MessageBusEventHandlerCollection : Collection<MessageBusEventHandle
/// <returns></returns>
public MessageBusEventHandler[] HandlersForEvent (String EventId)
{
var results = from item in this.Items
where !String.IsNullOrWhiteSpace (item.EventId)
where item.EventId.ToLower ().Equals (EventId.ToLower ())
where item.EventAction != null
select item;

var array = results.ToArray ();
return array;
List<MessageBusEventHandler> results = null;

lock (_syncRoot)
{
foreach (var item in this.Items)
{
if (item.EventAction == null || String.IsNullOrWhiteSpace (item.EventId))
continue;

if (!String.Equals (item.EventId, EventId, StringComparison.OrdinalIgnoreCase))
continue;

(results ??= new List<MessageBusEventHandler> ()).Add (item);
}
}

return results == null ? Array.Empty<MessageBusEventHandler> () : results.ToArray ();
}

/// <summary>
Expand All @@ -36,22 +60,23 @@ where item.EventId.ToLower ().Equals (EventId.ToLower ())
/// <param name="EventType">Event type.</param>
public MessageBusEventHandler[] HandlersForEvent (Type EventType)
{
var results = from item in this.Items
where item is TypedMessageBusEventHandler
where item.EventAction != null
select item;

var list = new List<MessageBusEventHandler> ();
List<MessageBusEventHandler> results = null;

foreach (TypedMessageBusEventHandler item in results.ToArray())
lock (_syncRoot)
{
if (item.EventType != null && item.EventType.Equals (EventType))
foreach (var item in this.Items)
{
list.Add (item);
if (item.EventAction == null || !(item is TypedMessageBusEventHandler typed))
continue;

if (typed.EventType == null || !typed.EventType.Equals (EventType))
continue;

(results ??= new List<MessageBusEventHandler> ()).Add (typed);
}
}

return list.ToArray ();
return results == null ? Array.Empty<MessageBusEventHandler> () : results.ToArray ();
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions DSoft.Messaging/DSoft.MessageBus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@
</ItemGroup>

<ItemGroup Condition=" $(TargetFramework.Contains('windows10')) ">
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240227000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />

<Compile Include="**\*.winui.cs" />
<Compile Include="**\*.winui.*.cs" />
Expand Down Expand Up @@ -105,7 +105,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
</ItemGroup>

<ItemGroup>
Expand Down
48 changes: 33 additions & 15 deletions DSoft.Messaging/MessageBusService.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,14 @@ public Task PostAsync(string eventId, object Sender, params object[] Data)
/// <param name="action">Handler action</param>
public void Unsubscribe(string eventId, Action<object, MessageBusEvent> action)
{
foreach (var item in FindHandlersForEvent(eventId))
lock (EventHandlers.SyncRoot)
{
if (item.EventAction.Equals(action))
foreach (var item in FindHandlersForEvent(eventId))
{
EventHandlers.Remove(item);
if (item.EventAction.Equals(action))
{
EventHandlers.Remove(item);
}
}
}
}
Expand All @@ -254,9 +257,12 @@ public void Unsubscribe(string eventId, Action<object, MessageBusEvent> action)
/// <param name="EventHandler">The event handler instance</param>
public void Unsubscribe(MessageBusEventHandler EventHandler)
{
if (EventHandlers.Contains(EventHandler))
lock (EventHandlers.SyncRoot)
{
EventHandlers.Remove(EventHandler);
if (EventHandlers.Contains(EventHandler))
{
EventHandlers.Remove(EventHandler);
}
}
}

Expand All @@ -268,13 +274,16 @@ public void Unsubscribe(MessageBusEventHandler EventHandler)
/// <param name="Action">The action to remove</param>
public void Unsubscribe<T>(Action<object, MessageBusEvent> Action) where T : MessageBusEvent
{
var results = new List<MessageBusEventHandler>(EventHandlers.HandlersForEvent<T>());

foreach (var item in results)
lock (EventHandlers.SyncRoot)
{
if (item.EventAction == Action)
var results = new List<MessageBusEventHandler>(EventHandlers.HandlersForEvent<T>());

foreach (var item in results)
{
EventHandlers.Remove(item);
if (item.EventAction == Action)
{
EventHandlers.Remove(item);
}
}
}
}
Expand All @@ -285,9 +294,12 @@ public void Unsubscribe<T>(Action<object, MessageBusEvent> Action) where T : Mes
/// <param name="eventId">Event identifier</param>
public void Unsubscribe(string eventId)
{
foreach (var item in FindHandlersForEvent(eventId))
lock (EventHandlers.SyncRoot)
{
EventHandlers.Remove(item);
foreach (var item in FindHandlersForEvent(eventId))
{
EventHandlers.Remove(item);
}
}
}
#endregion
Expand Down Expand Up @@ -340,9 +352,12 @@ public void Subscribe(MessageBusEventHandler EventHandler)
if (EventHandler == null)
return;

if (!EventHandlers.Contains(EventHandler))
lock (EventHandlers.SyncRoot)
{
EventHandlers.Add(EventHandler);
if (!EventHandlers.Contains(EventHandler))
{
EventHandlers.Add(EventHandler);
}
}
}
/// <summary>
Expand Down Expand Up @@ -435,7 +450,10 @@ public void StopListening(ILogListener instance)
if (LogListeners == null)
return;

LogListeners.Remove(instance);
lock (LogListeners.SyncRoot)
{
LogListeners.Remove(instance);
}
}
#endregion

Expand Down
Loading