From f087fef9c98edef112892ca32ec221263aa586dd Mon Sep 17 00:00:00 2001 From: Dirk Peters Date: Mon, 20 Dec 2021 14:01:07 +0100 Subject: [PATCH] cleanup for first release - move component instantiation into factory methods to prevent problems with static interface methods - mark agent and message bus implementations as internal - use common task result handling - update usage in readme --- FoundationalBits.Spec/Messaging/Specs.cs | 14 ++++---- FoundationalBits.sln.DotSettings | 2 ++ FoundationalBits/Agent.cs | 18 +++++++++++ FoundationalBits/Agents/StatefulAgent.cs | 25 ++++++--------- FoundationalBits/Agents/StatelessAgent.cs | 22 ++++--------- FoundationalBits/Agents/Tasks.cs | 24 ++++++++++++++ FoundationalBits/FoundationalBits.csproj | 4 +-- FoundationalBits/IAgent.cs | 10 ------ FoundationalBits/MessageBus.cs | 9 ++++++ .../Messaging/AgentBasedMessageBus.cs | 7 ++-- FoundationalBits/Messaging/ImmutableList.cs | 32 ++++--------------- README.md | 4 +-- 12 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 FoundationalBits.sln.DotSettings create mode 100644 FoundationalBits/Agent.cs create mode 100644 FoundationalBits/Agents/Tasks.cs create mode 100644 FoundationalBits/MessageBus.cs diff --git a/FoundationalBits.Spec/Messaging/Specs.cs b/FoundationalBits.Spec/Messaging/Specs.cs index 404bb1b..707ca9a 100644 --- a/FoundationalBits.Spec/Messaging/Specs.cs +++ b/FoundationalBits.Spec/Messaging/Specs.cs @@ -14,7 +14,7 @@ public static class messages_are_dispatched [Test] public static async Task to_an_interested_subscriber() { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); var monitor = new HandleMonitor(); cut.Subscribe(monitor); await cut.Publish(new object()); @@ -27,7 +27,7 @@ public static class a_subscriber_error [TestCaseSource(nameof(ErrorSubscribers))] public static void is_propagated_to_sender(object subscriber) { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); cut.Subscribe(subscriber); Assert.CatchAsync(() => cut.Publish(new object())) @@ -38,7 +38,7 @@ public static void is_propagated_to_sender(object subscriber) public static void does_not_prevent_dispatch_to_other_subscribers( object subscriber) { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); cut.Subscribe(subscriber); var monitor = new HandleMonitor(); cut.Subscribe(monitor); @@ -64,7 +64,7 @@ public static void are_collected_and_propagated_to_sender() new ThrowingHandler(), new AsyncThrowingHandler() }; - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); errorSubscribers.ForEach(s => cut.Subscribe(s)); Assert.CatchAsync(() => cut.Publish(new object())) .Should().Match(e => HasMultipleDispatchErrors(e)); @@ -81,7 +81,7 @@ public static class messages_are_not_dispatched [Test] public static async Task to_garbage_collected_subscribers() { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); var monitor = new HandleMonitor(); await cut.SubscribeWeak(new TestHandler(monitor)).WaitForCollection(); await cut.Publish(new object()); @@ -91,7 +91,7 @@ public static async Task to_garbage_collected_subscribers() [Test] public static async Task to_unsubscribed_subscribers() { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); var monitor = new HandleMonitor(); cut.SubscribeUnsubscribe(new TestHandler(monitor)); await cut.Publish(new object()); @@ -101,7 +101,7 @@ public static async Task to_unsubscribed_subscribers() [Test] public static async Task to_uninterested_subscribers() { - var cut = new AgentBasedMessageBus(); + var cut = MessageBus.Create(); var monitor = new HandleMonitor(); var subscriber = new TestHandler(monitor); cut.Subscribe(subscriber); diff --git a/FoundationalBits.sln.DotSettings b/FoundationalBits.sln.DotSettings new file mode 100644 index 0000000..12b6333 --- /dev/null +++ b/FoundationalBits.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/FoundationalBits/Agent.cs b/FoundationalBits/Agent.cs new file mode 100644 index 0000000..457899e --- /dev/null +++ b/FoundationalBits/Agent.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using bridgefield.FoundationalBits.Agents; + +namespace bridgefield.FoundationalBits +{ + public static class Agent + { + public static IAgent Start( + TState initialState, + Func> update) + => new StatefulAgent(initialState, update); + + public static IAgent Start( + Func> update) + => new StatelessAgent(update); + } +} \ No newline at end of file diff --git a/FoundationalBits/Agents/StatefulAgent.cs b/FoundationalBits/Agents/StatefulAgent.cs index 2961a2c..ba59650 100644 --- a/FoundationalBits/Agents/StatefulAgent.cs +++ b/FoundationalBits/Agents/StatefulAgent.cs @@ -4,35 +4,30 @@ namespace bridgefield.FoundationalBits.Agents { - public sealed class StatefulAgent : IAgent + internal sealed class StatefulAgent : IAgent { - private readonly ActionBlock<(TCommand command, TaskCompletionSource task)> actions; + private readonly ActionBlock<(TCommand command, TaskCompletionSource task)> actionBlock; public StatefulAgent(TState initialState, Func> processor) { var state = initialState; - actions = new ActionBlock<(TCommand command, TaskCompletionSource task)>( + actionBlock = new( data => processor(state, data.command) - .ContinueWith(task => - { - if (task.IsFaulted) + .HandleResult( + r => { - data.task.SetException(task.Exception); - } - else - { - state = task.Result.newState; - data.task.SetResult(task.Result.reply); - } - })); + state = r.newState; + data.task.SetResult(r.reply); + }, + data.task.SetException)); } public Task Tell(TCommand command) { var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - actions.Post((command, completionSource)); + actionBlock.Post((command, completionSource)); return completionSource.Task; } } diff --git a/FoundationalBits/Agents/StatelessAgent.cs b/FoundationalBits/Agents/StatelessAgent.cs index 761ee94..fd7af0a 100644 --- a/FoundationalBits/Agents/StatelessAgent.cs +++ b/FoundationalBits/Agents/StatelessAgent.cs @@ -4,30 +4,22 @@ namespace bridgefield.FoundationalBits.Agents { - public sealed class StatelessAgent : IAgent + internal sealed class StatelessAgent : IAgent { - private readonly ActionBlock<(TCommand command, TaskCompletionSource task)> actions; + private readonly ActionBlock<(TCommand command, TaskCompletionSource task)> actionBlock; public StatelessAgent(Func> processor) => - actions = new ActionBlock<(TCommand command, TaskCompletionSource task)>( + actionBlock = new( data => processor(data.command) - .ContinueWith(task => - { - if (task.IsFaulted) - { - data.task.SetException(task.Exception); - } - else - { - data.task.SetResult(task.Result); - } - }) + .HandleResult( + data.task.SetResult, + data.task.SetException) ); public Task Tell(TCommand command) { var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - actions.Post((command, completionSource)); + actionBlock.Post((command, completionSource)); return completionSource.Task; } } diff --git a/FoundationalBits/Agents/Tasks.cs b/FoundationalBits/Agents/Tasks.cs new file mode 100644 index 0000000..a49ffe8 --- /dev/null +++ b/FoundationalBits/Agents/Tasks.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; + +namespace bridgefield.FoundationalBits.Agents +{ + internal static class Tasks + { + public static Task HandleResult( + this Task task, + Action onSuccess, + Action onError) => + task.ContinueWith(t => + { + if (t.IsFaulted) + { + onError(task.Exception); + } + else + { + onSuccess(task.Result); + } + }); + } +} \ No newline at end of file diff --git a/FoundationalBits/FoundationalBits.csproj b/FoundationalBits/FoundationalBits.csproj index 163ebf8..fd98bc2 100644 --- a/FoundationalBits/FoundationalBits.csproj +++ b/FoundationalBits/FoundationalBits.csproj @@ -13,7 +13,7 @@ - - + + diff --git a/FoundationalBits/IAgent.cs b/FoundationalBits/IAgent.cs index 1a5c994..e97c8b2 100644 --- a/FoundationalBits/IAgent.cs +++ b/FoundationalBits/IAgent.cs @@ -1,6 +1,4 @@ -using System; using System.Threading.Tasks; -using bridgefield.FoundationalBits.Agents; namespace bridgefield.FoundationalBits { @@ -8,13 +6,5 @@ namespace bridgefield.FoundationalBits public interface IAgent { Task Tell(TCommand command); - - static IAgent Start( - TState initialState, - Func> update) - => new StatefulAgent(initialState, update); - - static IAgent Start(Func> update) - => new StatelessAgent(update); } } \ No newline at end of file diff --git a/FoundationalBits/MessageBus.cs b/FoundationalBits/MessageBus.cs new file mode 100644 index 0000000..b5b12b2 --- /dev/null +++ b/FoundationalBits/MessageBus.cs @@ -0,0 +1,9 @@ +using bridgefield.FoundationalBits.Messaging; + +namespace bridgefield.FoundationalBits +{ + public static class MessageBus + { + public static IMessageBus Create() => new AgentBasedMessageBus(); + } +} \ No newline at end of file diff --git a/FoundationalBits/Messaging/AgentBasedMessageBus.cs b/FoundationalBits/Messaging/AgentBasedMessageBus.cs index 1683f63..a97ec74 100644 --- a/FoundationalBits/Messaging/AgentBasedMessageBus.cs +++ b/FoundationalBits/Messaging/AgentBasedMessageBus.cs @@ -5,16 +5,17 @@ namespace bridgefield.FoundationalBits.Messaging { - public sealed class AgentBasedMessageBus : IMessageBus + internal sealed class AgentBasedMessageBus : IMessageBus { private sealed record State(ImmutableList Subscriptions); private readonly IAgent> agent; public AgentBasedMessageBus() => - agent = IAgent>.Start( + agent = Agent.Start>( new State(ImmutableList.Create()), - (state, command) => command.Execute(state)); + (state, command) => command.Execute(state) + ); public void Subscribe(object subscriber) => Subscribe(subscriber, SubscriptionLifecycle.GarbageCollected); diff --git a/FoundationalBits/Messaging/ImmutableList.cs b/FoundationalBits/Messaging/ImmutableList.cs index c006ffb..e32805e 100644 --- a/FoundationalBits/Messaging/ImmutableList.cs +++ b/FoundationalBits/Messaging/ImmutableList.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; @@ -17,23 +16,12 @@ namespace bridgefield.FoundationalBits.Messaging public ImmutableList Remove(T item) => bucket.Remove(item).ToList(); public static ImmutableList Create() => new EmptyBucket().ToList(); - public TResult Match( - Func empty, - Func, TResult> headAndTail) => - bucket.Match( - empty, - (h, t) => headAndTail(h, t.ToList())); - private interface IBucket { IEnumerable AsEnumerable(); ImmutableList ToList(); IBucket Add(T item) => new TailedBucket(item, this); IBucket Remove(T item); - - TResult Match( - Func empty, - Func headAndTail); } private sealed record EmptyBucket : IBucket @@ -45,15 +33,12 @@ public IEnumerable AsEnumerable() public ImmutableList ToList() => new(this); public IBucket Remove(T item) => this; - - public TResult Match(Func empty, Func headAndTail) => - empty(); } private sealed record TailedBucket(T Head, IBucket Tail) : IBucket { public IEnumerable AsEnumerable() => - new TailedBucketEnumerable(this); + new ValueEnumerable(this); public ImmutableList ToList() => new(this); @@ -61,30 +46,27 @@ public IBucket Remove(T item) => Equals(item, Head) ? Tail.Remove(item) : new TailedBucket(Head, Tail.Remove(item)); - - public TResult Match( - Func empty, - Func headAndTail) => headAndTail(Head, Tail); } - private sealed class TailedBucketEnumerable : IEnumerable + private sealed class ValueEnumerable : IEnumerable { private readonly TailedBucket bucket; - public TailedBucketEnumerable(TailedBucket bucket) => this.bucket = bucket; + public ValueEnumerable(TailedBucket bucket) => this.bucket = bucket; - public IEnumerator GetEnumerator() => new TailedBucketEnumerator(bucket); + public IEnumerator GetEnumerator() => new ValueEnumerator(bucket); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - private sealed class TailedBucketEnumerator : IEnumerator + private sealed class ValueEnumerator : IEnumerator { private TailedBucket currentBucket; private bool started; private bool hasMore = true; - public TailedBucketEnumerator(TailedBucket currentBucket) => this.currentBucket = currentBucket; + public ValueEnumerator(TailedBucket currentBucket) => + this.currentBucket = currentBucket; public bool MoveNext() { diff --git a/README.md b/README.md index 58f3edc..c360fea 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Useful bits for foundational groundwork in C# applications. ... - var messageBus = new AgentBasedMessageBus() + var messageBus = MessageBus.Create() messageBus.Subscribe( new Receiver(), SubscriptionLifecycle.ExplicitUnsubscribe); @@ -35,7 +35,7 @@ Useful bits for foundational groundwork in C# applications. Decrement, Current, } - var counter = IAgent.Start( + var counter = Agent.Start( 0, (current,command) => command switch{ CounterCommand.Increment => (current+1, current+1),