From 8b21f39b1f8cda94615ae5c314bbd3a707fc6151 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 13 Dec 2020 15:14:23 +0100 Subject: [PATCH 01/31] Introduces TypeRecipient and factory method. Closes #13. --- .../Program.cs | 6 +- .../Recipients/DelegateRecipient.cs | 2 +- .../Recipients/InstanceRecipient.cs | 83 +-------- .../Recipients/RecipientsCollection.cs | 22 +-- .../Recipients/TypeRecipient.cs | 101 +++++++++++ .../Responses/AggregatedResponseFactory.cs | 2 +- src/NScatterGather/Run/RecipientRunner.cs | 22 ++- tests/NScatterGather.Tests/AggregatorTests.cs | 67 ++----- .../Inspection/MethodAnalyzerTests.cs | 53 ++---- .../Inspection/MethodInspectionTests.cs | 12 +- .../Inspection/TypeInspectorTests.cs | 15 +- .../Invocations/CompletedInvocationTests.cs | 27 +++ .../Invocations/FaultedInvocation.cs | 27 +++ .../Invocations/IncompleteInvocationTests.cs | 20 ++ .../Recipients/DelegateRecipientTests.cs | 26 --- .../Recipients/InstanceRecipientTests.cs | 171 +----------------- .../Recipients/RecipientsCollectionTests.cs | 90 +++------ .../Recipients/TypeRecipientTests.cs | 139 ++++++++++++++ .../AggregatedResponseExtensionsTests.cs | 8 +- .../Responses/AggregatedResponseTests.cs | 10 +- .../Run/RecipientRunnerTest.cs | 100 +++++++++- .../_TestTypes/AlmostCollidingType.cs | 9 + .../_TestTypes/SomeAsyncComputingType.cs | 9 + .../_TestTypes/SomeAsyncType.cs | 9 + .../_TestTypes/SomeCollidingType.cs | 9 + .../_TestTypes/SomeComputingType.cs | 7 + .../_TestTypes/SomeDifferentType.cs | 7 + .../_TestTypes/SomeEchoType.cs | 9 + .../_TestTypes/SomeFaultingType.cs | 9 + .../_TestTypes/SomeNeverEndingType.cs | 16 ++ .../_TestTypes/SomeOtherType.cs | 9 + .../_TestTypes/SomePossiblyAsyncType.cs | 9 + .../_TestTypes/SomeType.cs | 7 + .../SomeTypeWithComplexArguments.cs | 9 + .../_TestTypes/SomeTypeWithConstructor.cs | 7 + .../_TestTypes/SomeTypeWithManyMethods.cs | 23 +++ 36 files changed, 676 insertions(+), 475 deletions(-) create mode 100644 src/NScatterGather/Recipients/TypeRecipient.cs create mode 100644 tests/NScatterGather.Tests/Invocations/CompletedInvocationTests.cs create mode 100644 tests/NScatterGather.Tests/Invocations/FaultedInvocation.cs create mode 100644 tests/NScatterGather.Tests/Invocations/IncompleteInvocationTests.cs create mode 100644 tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/AlmostCollidingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeAsyncComputingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeAsyncType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeCollidingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeComputingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeDifferentType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeEchoType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeFaultingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeNeverEndingType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeOtherType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomePossiblyAsyncType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeType.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeTypeWithComplexArguments.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeTypeWithConstructor.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeTypeWithManyMethods.cs diff --git a/samples/NScatterGather.Samples.CompetingTasks/Program.cs b/samples/NScatterGather.Samples.CompetingTasks/Program.cs index b6a0e43..c7c1463 100644 --- a/samples/NScatterGather.Samples.CompetingTasks/Program.cs +++ b/samples/NScatterGather.Samples.CompetingTasks/Program.cs @@ -112,9 +112,9 @@ static bool IsProductOutOfStock(Evaluation evaluation) static RecipientsCollection CollectRecipients() { var collection = new RecipientsCollection(); - collection.Add("Alibaba"); - collection.Add("Amazon"); - collection.Add("Walmart"); + collection.Add(name: "Alibaba"); + collection.Add(name: "Amazon"); + collection.Add(name: "Walmart"); return collection; } diff --git a/src/NScatterGather/Recipients/DelegateRecipient.cs b/src/NScatterGather/Recipients/DelegateRecipient.cs index ca88744..1877207 100644 --- a/src/NScatterGather/Recipients/DelegateRecipient.cs +++ b/src/NScatterGather/Recipients/DelegateRecipient.cs @@ -34,7 +34,7 @@ public static DelegateRecipient Create( name); } - internal DelegateRecipient( + protected DelegateRecipient( Func @delegate, Type inType, Type outType, diff --git a/src/NScatterGather/Recipients/InstanceRecipient.cs b/src/NScatterGather/Recipients/InstanceRecipient.cs index a8c521b..e21199a 100644 --- a/src/NScatterGather/Recipients/InstanceRecipient.cs +++ b/src/NScatterGather/Recipients/InstanceRecipient.cs @@ -1,89 +1,22 @@ using System; -using NScatterGather.Inspection; namespace NScatterGather.Recipients { - internal class InstanceRecipient : Recipient + internal class InstanceRecipient : TypeRecipient { - public Type Type => _type; - - private readonly object _instance; - private readonly Type _type; - private readonly TypeInspector _inspector; - - public InstanceRecipient( + public static InstanceRecipient Create( object instance, - string? name = null, - TypeInspector? inspector = null) : base(name) - { - _instance = instance ?? throw new ArgumentNullException(nameof(instance)); - _type = _instance.GetType(); - _inspector = inspector ?? new TypeInspector(_type); - } - - public InstanceRecipient( - Type type, - string? name = null, - TypeInspector? inspector = null) : base(name) - { - _type = type ?? throw new ArgumentNullException(nameof(type)); - _inspector = inspector ?? new TypeInspector(_type); - - try - { - _instance = Activator.CreateInstance(_type)!; - } - catch (MissingMethodException mMEx) - { - throw new InvalidOperationException($"Could not create a new instance of type '{_type.Name}'.", mMEx); - } - } - - protected internal override string GetRecipientName() => - _type.Name; - - public override bool CanAccept(Type requestType) + string? name = null) { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); + if (instance is null) + throw new ArgumentNullException(nameof(instance)); - var accepts = _inspector.HasMethodAccepting(requestType); - return accepts; + return new InstanceRecipient(instance, name); } - public override bool CanReplyWith(Type requestType, Type responseType) + protected InstanceRecipient(object instance, string? name = null) + : base(instance.GetType(), factory: () => instance, name) { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); - - if (responseType is null) - throw new ArgumentNullException(nameof(responseType)); - - var repliesWith = _inspector.HasMethodReturning(requestType, responseType); - return repliesWith; - } - - protected internal override object? Invoke(object request) - { - if (!_inspector.TryGetMethodAccepting(request.GetType(), out var method)) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting requests " + - $"of type '{request.GetType().Name}'."); - - var response = method.Invoke(_instance, new object?[] { request }); - return response; - } - - protected internal override object? Invoke(object request) - { - if (!_inspector.TryGetMethodReturning(request.GetType(), typeof(TResponse), out var method)) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting " + - $"requests of type '{request.GetType().Name}' and " + - $"returning '{typeof(TResponse).Name}'."); - - var response = method.Invoke(_instance, new object?[] { request }); - return response; } } } diff --git a/src/NScatterGather/Recipients/RecipientsCollection.cs b/src/NScatterGather/Recipients/RecipientsCollection.cs index 9ccb759..f2cd32d 100644 --- a/src/NScatterGather/Recipients/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/RecipientsCollection.cs @@ -16,7 +16,7 @@ public class RecipientsCollection private readonly TypeInspectorRegistry _registry; public IReadOnlyList RecipientTypes => _recipients - .OfType() + .OfType() .Select(x => x.Type) .ToArray(); @@ -34,16 +34,11 @@ internal RecipientsCollection(TypeInspectorRegistry registry) _registry = registry; } - public void Add(string? name = null) => - Add(typeof(T), name); - - public void Add(Type recipientType, string? name = null) + public void Add( + Func? factory = null, + string? name = null) { - if (recipientType is null) - throw new ArgumentNullException(nameof(recipientType)); - - var inspector = _registry.Register(recipientType); - _recipients.Add(new InstanceRecipient(recipientType, name, inspector)); + _recipients.Add(TypeRecipient.Create(factory, name)); } public void Add(object instance, string? name = null) @@ -51,8 +46,7 @@ public void Add(object instance, string? name = null) if (instance is null) throw new ArgumentNullException(nameof(instance)); - var inspector = _registry.Register(instance.GetType()); - _recipients.Add(new InstanceRecipient(instance, name, inspector)); + _recipients.Add(InstanceRecipient.Create(instance, name)); } public void Add(Func @delegate, string? name = null) @@ -66,8 +60,8 @@ public void Add(Func @delegate, string internal void Add(Recipient recipient) { - if (recipient is InstanceRecipient ir) - _ = _registry.Register(ir.Type); + if (recipient is TypeRecipient tr) + _ = _registry.Register(tr.Type); _recipients.Add(recipient); } diff --git a/src/NScatterGather/Recipients/TypeRecipient.cs b/src/NScatterGather/Recipients/TypeRecipient.cs new file mode 100644 index 0000000..b5a0936 --- /dev/null +++ b/src/NScatterGather/Recipients/TypeRecipient.cs @@ -0,0 +1,101 @@ +using System; +using System.Reflection; +using NScatterGather.Inspection; + +namespace NScatterGather.Recipients +{ + internal class TypeRecipient : Recipient + { + public Type Type => _type; + + private readonly Type _type; + private readonly Func _factory; + private readonly TypeInspector _inspector; + + public static TypeRecipient Create( + Func? customFactory = null, + string? name = null) + { + Func factory = customFactory is not null + ? () => customFactory() + : HasADefaultConstructor() + ? () => Activator.CreateInstance(typeof(TRecipient))! + : throw new ArgumentException($"Type '{typeof(TRecipient).Name}' is missing a public, parameterless constructor."); + + return new TypeRecipient(typeof(TRecipient), factory, name); + + // Local functions. + + static bool HasADefaultConstructor() + { + var defaultContructor = typeof(T).GetConstructor(Type.EmptyTypes); + return defaultContructor is not null; + } + } + + protected TypeRecipient( + Type type, + Func factory, + string? name = null) : base(name) + { + _type = type; + _factory = factory; + _inspector = new TypeInspector(type); + } + + protected internal override string GetRecipientName() => + _type.Name; + + public override bool CanAccept(Type requestType) + { + if (requestType is null) + throw new ArgumentNullException(nameof(requestType)); + + var accepts = _inspector.HasMethodAccepting(requestType); + return accepts; + } + + public override bool CanReplyWith(Type requestType, Type responseType) + { + if (requestType is null) + throw new ArgumentNullException(nameof(requestType)); + + if (responseType is null) + throw new ArgumentNullException(nameof(responseType)); + + var repliesWith = _inspector.HasMethodReturning(requestType, responseType); + return repliesWith; + } + + protected internal override object? Invoke(object request) + { + if (!_inspector.TryGetMethodAccepting(request.GetType(), out var method)) + throw new InvalidOperationException( + $"Type '{GetRecipientName()}' doesn't support accepting requests " + + $"of type '{request.GetType().Name}'."); + + var recipientInstance = GetRecipientInstance(); + var response = method.Invoke(recipientInstance, new object?[] { request }); + return response; + } + + protected internal override object? Invoke(object request) + { + if (!_inspector.TryGetMethodReturning(request.GetType(), typeof(TResponse), out var method)) + throw new InvalidOperationException( + $"Type '{GetRecipientName()}' doesn't support accepting " + + $"requests of type '{request.GetType().Name}' and " + + $"returning '{typeof(TResponse).Name}'."); + + var recipientInstance = GetRecipientInstance(); + var response = method.Invoke(recipientInstance, new object?[] { request }); + return response; + } + + protected virtual object GetRecipientInstance() + { + var instance = _factory(); + return instance; + } + } +} diff --git a/src/NScatterGather/Responses/AggregatedResponseFactory.cs b/src/NScatterGather/Responses/AggregatedResponseFactory.cs index fe698b1..a189474 100644 --- a/src/NScatterGather/Responses/AggregatedResponseFactory.cs +++ b/src/NScatterGather/Responses/AggregatedResponseFactory.cs @@ -16,7 +16,7 @@ public static AggregatedResponse CreateFrom( foreach (var invocation in invocations) { - var recipientType = invocation.Recipient is InstanceRecipient ir ? ir.Type : null; + var recipientType = invocation.Recipient is TypeRecipient ir ? ir.Type : null; if (invocation.CompletedSuccessfully) { diff --git a/src/NScatterGather/Run/RecipientRunner.cs b/src/NScatterGather/Run/RecipientRunner.cs index 3341ef7..30dc483 100644 --- a/src/NScatterGather/Run/RecipientRunner.cs +++ b/src/NScatterGather/Run/RecipientRunner.cs @@ -67,19 +67,27 @@ private void InspectAndExtract(Task task) else if (task.IsFaulted) { Faulted = true; - Exception = ExtractException(task.Exception); + + /* + * task.Exception would be null only due to a race condition while accessing + * the task.Faulted property and the set of the exception. + * Since the task is already complete, it's ensured that the exception is not null. + * + * From the source code: + * A "benevolent" race condition makes it possible to return null when IsFaulted is + * true (i.e., if IsFaulted is set just after the check to IsFaulted above). + */ + Exception = ExtractException(task.Exception!); } } - private Exception? ExtractException(Exception? exception) + private Exception? ExtractException(Exception exception) { - if (exception is null) return null; - - if (exception is AggregateException aEx) - return ExtractException(aEx.InnerException) ?? aEx; + if (exception is AggregateException aEx && aEx.InnerExceptions.Count == 1) + return aEx.InnerException is null ? aEx : ExtractException(aEx.InnerException); if (exception is TargetInvocationException tIEx) - return ExtractException(tIEx.InnerException) ?? tIEx; + return tIEx.InnerException is null ? tIEx : ExtractException(tIEx.InnerException); return exception; } diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index c9cac67..db9ea32 100644 --- a/tests/NScatterGather.Tests/AggregatorTests.cs +++ b/tests/NScatterGather.Tests/AggregatorTests.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -8,45 +7,6 @@ namespace NScatterGather { public class AggregatorTests { - class SomeType - { - public int Do(int n) => n * 2; - } - - class SomeAsyncType - { - public Task Do(int n) => Task.FromResult(n.ToString()); - } - - class SomePossiblyAsyncType - { - public ValueTask Do(int n) => new ValueTask(n.ToString()); - } - - class SomeCollidingType - { - public string Do(int n) => n.ToString(); - - public string DoDifferently(int n) => $"{n}"; - } - - class SomeFaultingType - { - public string Fail(int n) => throw new Exception("A failure."); - } - - class SomeNeverEndingType - { - private static readonly SemaphoreSlim _semaphore = - new SemaphoreSlim(initialCount: 0); - - public string TryDo(int n) - { - _semaphore.Wait(); - return n.ToString(); - } - } - private readonly Aggregator _aggregator; public AggregatorTests() @@ -65,7 +25,7 @@ public AggregatorTests() [Fact(Timeout = 5_000)] public async Task Sends_request_and_aggregates_responses() { - var result = await _aggregator.Send(42, timeout: TimeSpan.FromSeconds(1)); + var result = await _aggregator.Send(42, timeout: TimeSpan.FromSeconds(2)); Assert.NotNull(result); Assert.Equal(3, result.Completed.Count); @@ -83,10 +43,11 @@ public async Task Sends_request_and_aggregates_responses() [Fact(Timeout = 5_000)] public async Task Receives_expected_response_types() { - var result = await _aggregator.Send(42, timeout: TimeSpan.FromSeconds(1)); + var result = await _aggregator.Send(42, timeout: TimeSpan.FromSeconds(2)); Assert.NotNull(result); - Assert.Equal(2, result.Completed.Count); + Assert.Equal(3, result.Completed.Count); + Assert.Contains(typeof(SomeType), result.Completed.Select(x => x.RecipientType)); Assert.Contains(typeof(SomeAsyncType), result.Completed.Select(x => x.RecipientType)); Assert.Contains(typeof(SomePossiblyAsyncType), result.Completed.Select(x => x.RecipientType)); @@ -101,26 +62,26 @@ public async Task Receives_expected_response_types() public async Task Responses_expose_the_recipient_name_and_type() { var collection = new RecipientsCollection(); - collection.Add((int n) => n.ToString(), "Some delegate"); - collection.Add(new SomeFaultingType(), "Some faulting type"); - collection.Add("Some never ending type"); + collection.Add((int n) => n.ToString(), name: "Some delegate"); + collection.Add(new SomeFaultingType(), name: "Some faulting type"); + collection.Add(name: "Some never ending type"); var localAggregator = new Aggregator(collection); - var result = await localAggregator.Send(42, timeout: TimeSpan.FromSeconds(1)); + var result = await localAggregator.Send(42, timeout: TimeSpan.FromSeconds(2)); Assert.NotNull(result); Assert.Equal(1, result.Completed.Count); - Assert.Equal("Some delegate", result.Completed.First().RecipientName); - Assert.Null(result.Completed.First().RecipientType); + Assert.Equal("Some delegate", result.Completed[0].RecipientName); + Assert.Null(result.Completed[0].RecipientType); Assert.Equal(1, result.Faulted.Count); - Assert.Equal("Some faulting type", result.Faulted.First().RecipientName); - Assert.Equal(typeof(SomeFaultingType), result.Faulted.First().RecipientType); + Assert.Equal("Some faulting type", result.Faulted[0].RecipientName); + Assert.Equal(typeof(SomeFaultingType), result.Faulted[0].RecipientType); Assert.Equal(1, result.Incomplete.Count); - Assert.Equal("Some never ending type", result.Incomplete.First().RecipientName); - Assert.Equal(typeof(SomeNeverEndingType), result.Incomplete.First().RecipientType); + Assert.Equal("Some never ending type", result.Incomplete[0].RecipientName); + Assert.Equal(typeof(SomeNeverEndingType), result.Incomplete[0].RecipientType); } } } diff --git a/tests/NScatterGather.Tests/Inspection/MethodAnalyzerTests.cs b/tests/NScatterGather.Tests/Inspection/MethodAnalyzerTests.cs index ae1a63e..2a565b8 100644 --- a/tests/NScatterGather.Tests/Inspection/MethodAnalyzerTests.cs +++ b/tests/NScatterGather.Tests/Inspection/MethodAnalyzerTests.cs @@ -5,25 +5,6 @@ namespace NScatterGather.Inspection { public class MethodAnalyzerTests { - class SomeType - { - public void DoVoid() { } - - public void AcceptIntVoid(int n) { } - - public string EchoString(string s) => s; - - public Task DoTask() => Task.CompletedTask; - - public ValueTask DoValueTask() => new ValueTask(); - - public ValueTask DoAndReturnValueTask() => new ValueTask(42); - - public Task ReturnTask(int n) => Task.FromResult(n); - - public string Multi(int n, string s) => "Don't Panic"; - } - public enum Check { Request, @@ -41,31 +22,31 @@ public enum Check public MethodAnalyzerTests() { - var t = typeof(SomeType); + var t = typeof(SomeTypeWithManyMethods); _doVoidInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.DoVoid))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.DoVoid))!); _acceptIntVoidInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.AcceptIntVoid))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.AcceptIntVoid))!); _echoStringInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.EchoString))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.EchoString))!); _doTaskInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.DoTask))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.DoTask))!); _doValueTaskInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.DoValueTask))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.DoValueTask))!); _doAndReturnValueTaskInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.DoAndReturnValueTask))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.DoAndReturnValueTask))!); _returnTaskInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.ReturnTask))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.ReturnTask))!); _multiInspection = new MethodInspection( - t, t.GetMethod(nameof(SomeType.Multi))!); + t, t.GetMethod(nameof(SomeTypeWithManyMethods.Multi))!); } [Fact] @@ -77,7 +58,7 @@ public void Method_without_parameters_matches() Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.DoVoid)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.DoVoid)), match); } [Fact] @@ -89,7 +70,7 @@ public void Method_returning_void_matches() Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.DoVoid)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.DoVoid)), match); } [Theory] @@ -105,7 +86,7 @@ public void Method_with_parameters_returning_void_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.AcceptIntVoid)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.AcceptIntVoid)), match); } [Theory] @@ -121,7 +102,7 @@ public void Method_with_parameters_returning_result_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.EchoString)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.EchoString)), match); } [Theory] @@ -137,7 +118,7 @@ public void Method_returning_task_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.DoTask)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.DoTask)), match); } [Theory] @@ -153,7 +134,7 @@ public void Method_returning_valuetask_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.DoValueTask)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.DoValueTask)), match); } [Theory] @@ -169,7 +150,7 @@ public void Method_returning_valuetask_with_result_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.DoAndReturnValueTask)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.DoAndReturnValueTask)), match); } [Fact] @@ -203,7 +184,7 @@ public void Method_returning_generic_task_matches(Check check) Assert.True(isMatch); Assert.NotNull(match); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.ReturnTask)), match); + Assert.Equal(typeof(SomeTypeWithManyMethods).GetMethod(nameof(SomeTypeWithManyMethods.ReturnTask)), match); } [Fact] diff --git a/tests/NScatterGather.Tests/Inspection/MethodInspectionTests.cs b/tests/NScatterGather.Tests/Inspection/MethodInspectionTests.cs index 9d2b720..3e83872 100644 --- a/tests/NScatterGather.Tests/Inspection/MethodInspectionTests.cs +++ b/tests/NScatterGather.Tests/Inspection/MethodInspectionTests.cs @@ -6,21 +6,13 @@ namespace NScatterGather.Inspection { public class MethodInspectionTests { - class SomeType - { - public int AMethod( - Guid guid, - string s, - IDisposable d) => 42; - } - private readonly Type _inspectedType; private readonly MethodInfo _inspectedMethod; public MethodInspectionTests() { - _inspectedType = typeof(SomeType); - _inspectedMethod = _inspectedType.GetMethod(nameof(SomeType.AMethod))!; + _inspectedType = typeof(SomeTypeWithComplexArguments); + _inspectedMethod = _inspectedType.GetMethod(nameof(SomeTypeWithComplexArguments.AMethod))!; } [Fact] diff --git a/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs b/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs index 512c294..2850dbe 100644 --- a/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs +++ b/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs @@ -4,37 +4,30 @@ namespace NScatterGather.Inspection { public class TypeInspectorTests { - class SomeType - { - public int Do(int n) => n * 2; - - public string Echo(long x) => x.ToString(); - } - [Fact] public void Method_accepting_request_is_found() { - var inspector = new TypeInspector(typeof(SomeType)); + var inspector = new TypeInspector(typeof(SomeOtherType)); Assert.True(inspector.HasMethodAccepting(typeof(int))); Assert.True(inspector.HasMethodAccepting(typeof(long))); bool found = inspector.TryGetMethodAccepting(typeof(int), out var method); Assert.True(found); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.Do)), method); + Assert.Equal(typeof(SomeOtherType).GetMethod(nameof(SomeOtherType.Do)), method); } [Fact] public void Method_returning_response_is_found() { - var inspector = new TypeInspector(typeof(SomeType)); + var inspector = new TypeInspector(typeof(SomeOtherType)); Assert.True(inspector.HasMethodReturning(typeof(int), typeof(int))); Assert.True(inspector.HasMethodReturning(typeof(long), typeof(string))); bool found = inspector.TryGetMethodReturning(typeof(int), typeof(int), out var method); Assert.True(found); - Assert.Equal(typeof(SomeType).GetMethod(nameof(SomeType.Do)), method); + Assert.Equal(typeof(SomeOtherType).GetMethod(nameof(SomeOtherType.Do)), method); } } } diff --git a/tests/NScatterGather.Tests/Invocations/CompletedInvocationTests.cs b/tests/NScatterGather.Tests/Invocations/CompletedInvocationTests.cs new file mode 100644 index 0000000..e7411de --- /dev/null +++ b/tests/NScatterGather.Tests/Invocations/CompletedInvocationTests.cs @@ -0,0 +1,27 @@ +using System; +using Xunit; + +namespace NScatterGather.Invocations +{ + public class CompletedInvocationTests + { + [Fact] + public void Can_be_deconstructed() + { + var expectedName = "foo"; + var expectedType = typeof(int); + var expectedResult = 42; + var expectedDuration = TimeSpan.FromSeconds(1); + + var invocation = new CompletedInvocation( + expectedName, expectedType, expectedResult, expectedDuration); + + var (name, type, result, duration) = invocation; + + Assert.Equal(expectedName, name); + Assert.Equal(expectedType, type); + Assert.Equal(expectedResult, result); + Assert.Equal(expectedDuration, duration); + } + } +} diff --git a/tests/NScatterGather.Tests/Invocations/FaultedInvocation.cs b/tests/NScatterGather.Tests/Invocations/FaultedInvocation.cs new file mode 100644 index 0000000..2cad899 --- /dev/null +++ b/tests/NScatterGather.Tests/Invocations/FaultedInvocation.cs @@ -0,0 +1,27 @@ +using System; +using Xunit; + +namespace NScatterGather.Invocations +{ + public class FaultedInvocationTests + { + [Fact] + public void Can_be_deconstructed() + { + var expectedName = "foo"; + var expectedType = typeof(int); + var expectedException = new Exception(); + var expectedDuration = TimeSpan.FromSeconds(1); + + var invocation = new FaultedInvocation( + expectedName, expectedType, expectedException, expectedDuration); + + var (name, type, exception, duration) = invocation; + + Assert.Equal(expectedName, name); + Assert.Equal(expectedType, type); + Assert.Equal(expectedException, exception); + Assert.Equal(expectedDuration, duration); + } + } +} diff --git a/tests/NScatterGather.Tests/Invocations/IncompleteInvocationTests.cs b/tests/NScatterGather.Tests/Invocations/IncompleteInvocationTests.cs new file mode 100644 index 0000000..24d5181 --- /dev/null +++ b/tests/NScatterGather.Tests/Invocations/IncompleteInvocationTests.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace NScatterGather.Invocations +{ + public class IncompleteInvocationTests + { + [Fact] + public void Can_be_deconstructed() + { + var expectedName = "foo"; + var expectedType = typeof(int); + + var invocation = new IncompleteInvocation(expectedName, expectedType); + var (name, type) = invocation; + + Assert.Equal(expectedName, name); + Assert.Equal(expectedType, type); + } + } +} diff --git a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs index a44cbed..47a52b4 100644 --- a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs @@ -1,36 +1,10 @@ using System; -using System.Threading.Tasks; using Xunit; namespace NScatterGather.Recipients { public class DelegateRecipientTests { - class SomeType - { - public string EchoAsString(int n) => n.ToString(); - } - - class SomeTypeWithConstructor - { - public SomeTypeWithConstructor(int n) { } - } - - class SomeAsyncType - { - public Task EchoAsString(int n) => Task.FromResult(n.ToString()); - } - - class SomeComputingType - { - public void Do(int n) { } - } - - class SomeAsyncComputingType - { - public Task Do(int n) => Task.CompletedTask; - } - [Fact] public void Error_if_instance_is_null() { diff --git a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs index 739beba..1fb2572 100644 --- a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs @@ -1,185 +1,20 @@ using System; -using System.Threading.Tasks; using Xunit; namespace NScatterGather.Recipients { public class InstanceRecipientTests { - class SomeType - { - public string EchoAsString(int n) => n.ToString(); - } - - class SomeTypeWithConstructor - { - public SomeTypeWithConstructor(int n) { } - } - - class SomeAsyncType - { - public Task EchoAsString(int n) => Task.FromResult(n.ToString()); - } - - class SomeComputingType - { - public void Do(int n) { } - } - - class SomeAsyncComputingType - { - public Task Do(int n) => Task.CompletedTask; - } - - [Fact] - public void Error_if_instance_is_null() - { - Assert.Throws(() => new InstanceRecipient((null as object)!)); - } - - [Fact] - public void Error_if_type_is_null() - { - Assert.Throws(() => new InstanceRecipient((null as Type)!)); - } - - [Fact] - public void Error_if_request_type_is_null() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - Assert.Throws(() => recipient.CanAccept(null!)); - } - - [Fact] - public void Recipient_accepts_input_parameter_type() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - bool canAccept = recipient.CanAccept(typeof(int)); - Assert.True(canAccept); - } - - [Fact] - public void Error_if_no_empty_constructor() - { - Assert.Throws(() => new InstanceRecipient(typeof(SomeTypeWithConstructor))); - } - - [Fact] - public void Recipient_can_be_created_from_type() - { - _ = new InstanceRecipient(typeof(SomeType)); - } - [Fact] public void Recipient_can_be_created_from_instance() { - _ = new InstanceRecipient(new SomeType()); - } - - [Fact] - public void Recipient_type_is_visible() - { - var type = typeof(SomeType); - var recipient = new InstanceRecipient(type); - Assert.Same(type, recipient.Type); - } - - [Fact] - public void Recipient_replies_with_response_type() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - bool canAccept = recipient.CanReplyWith(typeof(int), typeof(string)); - Assert.True(canAccept); - } - - [Fact] - public void Error_if_request_or_response_types_are_null() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - Assert.Throws(() => recipient.CanReplyWith(typeof(int), null!)); - Assert.Throws(() => recipient.CanReplyWith(null!, typeof(string))); - } - - [Fact] - public void Error_if_request_type_not_supported() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - Assert.ThrowsAsync(() => recipient.Accept(Guid.NewGuid())); + _ = InstanceRecipient.Create(new SomeType()); } [Fact] - public async Task Recipient_accepts_request() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - var input = 42; - var response = await recipient.Accept(input); - Assert.Equal(input.ToString(), response); - } - - [Fact] - public async Task Recipient_accepts_request_and_replies_with_task() - { - var recipient = new InstanceRecipient(typeof(SomeAsyncType)); - var input = 42; - var response = await recipient.Accept(input); - Assert.Equal(input.ToString(), response); - } - - // aaa - - [Fact] - public void Error_if_response_type_not_supported() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); - } - - [Fact] - public async Task Recipient_replies_with_response() - { - var recipient = new InstanceRecipient(typeof(SomeType)); - var input = 42; - var response = await recipient.ReplyWith(input); - Assert.Equal(input.ToString(), response); - } - - [Fact] - public async Task Recipient_can_reply_with_task() - { - var recipient = new InstanceRecipient(typeof(SomeAsyncType)); - var input = 42; - var response = await recipient.ReplyWith(input); - Assert.Equal(input.ToString(), response); - } - - [Fact] - public void Recipient_must_return_something() - { - var recipient = new InstanceRecipient(typeof(SomeComputingType)); - bool accepts = recipient.CanAccept(typeof(int)); - Assert.False(accepts); - } - - [Fact] - public void Error_if_void_returning() - { - var recipient = new InstanceRecipient(typeof(SomeComputingType)); - Assert.ThrowsAsync(() => recipient.Accept(42)); - } - - [Fact] - public void Recipient_must_return_something_async() - { - var recipient = new InstanceRecipient(typeof(SomeAsyncComputingType)); - bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); - Assert.False(accepts); - } - - [Fact] - public void Error_if_returning_task_without_result() + public void Error_if_instance_is_null() { - var recipient = new InstanceRecipient(typeof(SomeAsyncComputingType)); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + Assert.Throws(() => InstanceRecipient.Create((null as object)!)); } } } diff --git a/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs index 4ba2daa..f2c12b5 100644 --- a/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs +++ b/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs @@ -1,41 +1,11 @@ using System; using System.Linq; -using System.Threading.Tasks; using Xunit; namespace NScatterGather.Recipients { public class RecipientsCollectionTests { - class SomeType - { - public string Echo(int n) => n.ToString(); - } - - class SomeOtherType - { - public Task Echo(int n) => Task.FromResult(n.ToString()); - } - - class SomeDifferentType - { - public int SomethingElse(string s) => s.Length; - } - - class CollidingType - { - public string Do(int n) => n.ToString(); - - public string DoDifferently(int n) => $"n"; - } - - class AlmostCollidingType - { - public string Do(int n) => n.ToString(); - - public long DoDifferently(int n) => (long)n; - } - private readonly RecipientsCollection _collection; public RecipientsCollectionTests() @@ -95,31 +65,31 @@ public void Recipients_accepting_request_can_be_found() _collection.Add(); var one = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(one); Assert.Equal(typeof(SomeType), one.First().Type); - _collection.Add(); + _collection.Add(); var two = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Equal(2, two.Count); Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - Assert.Contains(typeof(SomeOtherType), two.Select(x => x.Type)); + Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); _collection.Add(); var stillTwo = _collection.ListRecipientsAccepting(typeof(int)); Assert.Equal(2, stillTwo.Count); var differentOne = _collection.ListRecipientsAccepting(typeof(string)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(differentOne); @@ -135,31 +105,31 @@ public void Recipients_returning_response_can_be_found() _collection.Add(); var one = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(one); Assert.Equal(typeof(SomeType), one.First().Type); - _collection.Add(); + _collection.Add(); var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Equal(2, two.Count); Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - Assert.Contains(typeof(SomeOtherType), two.Select(x => x.Type)); + Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); _collection.Add(); var stillTwo = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); Assert.Equal(2, stillTwo.Count); var differentOne = _collection.ListRecipientsReplyingWith(typeof(string), typeof(int)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(differentOne); @@ -170,11 +140,11 @@ public void Recipients_returning_response_can_be_found() public void Recipients_with_request_collisions_are_ignored() { _collection.Add(); - _collection.Add(); + _collection.Add(); var onlyNonCollidingType = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(onlyNonCollidingType); @@ -185,11 +155,11 @@ public void Recipients_with_request_collisions_are_ignored() public void Recipients_with_request_and_response_collisions_are_ignored() { _collection.Add(); - _collection.Add(); + _collection.Add(); var onlyNonCollidingType = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is InstanceRecipient) - .Cast() + .Where(x => x is TypeRecipient) + .Cast() .ToList(); Assert.Single(onlyNonCollidingType); @@ -210,7 +180,7 @@ public void Collisions_can_be_resolved_via_return_type() public void Can_be_cloned() { _collection.Add(); - _collection.Add(); + _collection.Add(); Assert.NotEmpty(_collection.RecipientTypes); @@ -223,20 +193,20 @@ public void Can_be_cloned() [Fact] public void Recipients_can_have_a_name() { - _collection.Add("Some type"); - _collection.Add(new SomeOtherType(), "Some other type"); + _collection.Add(name: "Some type"); + _collection.Add(new SomeEchoType(), "Some other type"); _collection.Add((int n) => n.ToString(), "Delegate recipient"); Assert.Equal(3, _collection.Recipients.Count); foreach (var recipient in _collection.Recipients) { - if (recipient is InstanceRecipient ir) + if (recipient is TypeRecipient tr) { - if (ir.Type == typeof(SomeType)) - Assert.Equal("Some type", ir.Name); - else if (ir.Type == typeof(SomeOtherType)) - Assert.Equal("Some other type", ir.Name); + if (tr.Type == typeof(SomeType)) + Assert.Equal("Some type", tr.Name); + else if (tr.Type == typeof(SomeEchoType)) + Assert.Equal("Some other type", tr.Name); else throw new Xunit.Sdk.XunitException(); } diff --git a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs new file mode 100644 index 0000000..832e8f2 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace NScatterGather.Recipients +{ + public class TypeRecipientTests + { + [Fact] + public void Recipient_can_be_created_from_type() + { + _ = TypeRecipient.Create(); + } + + [Fact] + public void Error_if_no_empty_constructor() + { + Assert.Throws(() => TypeRecipient.Create()); + } + + [Fact] + public async Task Recipient_accepts_request() + { + var recipient = TypeRecipient.Create(); + var input = 42; + var response = await recipient.Accept(input); + Assert.Equal(input.ToString(), response); + } + + [Fact] + public async Task Recipient_accepts_request_and_replies_with_task() + { + var recipient = TypeRecipient.Create(); + var input = 42; + var response = await recipient.Accept(input); + Assert.Equal(input.ToString(), response); + } + + [Fact] + public void Error_if_request_type_is_null() + { + var recipient = TypeRecipient.Create(); + Assert.Throws(() => recipient.CanAccept(null!)); + } + + [Fact] + public void Recipient_accepts_input_parameter_type() + { + var recipient = TypeRecipient.Create(); + bool canAccept = recipient.CanAccept(typeof(int)); + Assert.True(canAccept); + } + + [Fact] + public void Recipient_type_is_visible() + { + var recipient = TypeRecipient.Create(); + Assert.Same(typeof(SomeType), recipient.Type); + } + + [Fact] + public void Recipient_replies_with_response_type() + { + var recipient = TypeRecipient.Create(); + bool canAccept = recipient.CanReplyWith(typeof(int), typeof(string)); + Assert.True(canAccept); + } + + [Fact] + public void Error_if_request_or_response_types_are_null() + { + var recipient = TypeRecipient.Create(); + Assert.Throws(() => recipient.CanReplyWith(typeof(int), null!)); + Assert.Throws(() => recipient.CanReplyWith(null!, typeof(string))); + } + + [Fact] + public void Error_if_request_type_not_supported() + { + var recipient = TypeRecipient.Create(); + Assert.ThrowsAsync(() => recipient.Accept(Guid.NewGuid())); + } + + [Fact] + public void Error_if_response_type_not_supported() + { + var recipient = TypeRecipient.Create(); + Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + } + + [Fact] + public async Task Recipient_replies_with_response() + { + var recipient = TypeRecipient.Create(); + var input = 42; + var response = await recipient.ReplyWith(input); + Assert.Equal(input.ToString(), response); + } + + [Fact] + public async Task Recipient_can_reply_with_task() + { + var recipient = TypeRecipient.Create(); + var input = 42; + var response = await recipient.ReplyWith(input); + Assert.Equal(input.ToString(), response); + } + + [Fact] + public void Recipient_must_return_something() + { + var recipient = TypeRecipient.Create(); + bool accepts = recipient.CanAccept(typeof(int)); + Assert.False(accepts); + } + + [Fact] + public void Error_if_void_returning() + { + var recipient = TypeRecipient.Create(); + Assert.ThrowsAsync(() => recipient.Accept(42)); + } + + [Fact] + public void Recipient_must_return_something_async() + { + var recipient = TypeRecipient.Create(); + bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); + Assert.False(accepts); + } + + [Fact] + public void Error_if_returning_task_without_result() + { + var recipient = TypeRecipient.Create(); + Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + } + } +} diff --git a/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs b/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs index 79306a1..f684195 100644 --- a/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs +++ b/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs @@ -13,13 +13,13 @@ public class AggregatedResponseExtensionsTests public AggregatedResponseExtensionsTests() { - var runner = new RecipientRunner(new InstanceRecipient(typeof(object))); + var runner = new RecipientRunner(InstanceRecipient.Create(42)); runner.Run(_ => Task.FromResult(42)).Wait(); - var runnerFaulted = new RecipientRunner(new InstanceRecipient(typeof(bool))); + var runnerFaulted = new RecipientRunner(InstanceRecipient.Create(42f)); runnerFaulted.Run(_ => Task.FromException(new Exception())).Wait(); - var runnerIncomplete = new RecipientRunner(new InstanceRecipient(typeof(long))); + var runnerIncomplete = new RecipientRunner(InstanceRecipient.Create(42L)); runnerIncomplete.Run(_ => GetInfiniteTask()); _runners = new[] { runner, runnerFaulted, runnerIncomplete }; @@ -50,7 +50,7 @@ public void Can_be_projected_onto_results_dictionary() var results = response.AsResultsDictionary(); Assert.NotNull(results); Assert.Single(results.Keys); - Assert.Equal(typeof(object), results.Keys.First()); + Assert.Equal(typeof(int), results.Keys.First()); Assert.Single(results.Values); Assert.Equal(42, results.Values.First()); } diff --git a/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs b/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs index 0ffe27b..60805e8 100644 --- a/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs +++ b/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs @@ -15,13 +15,13 @@ public AggregatedResponseTests() { _ex = new Exception("Test ex."); - var runner = new RecipientRunner(new InstanceRecipient(typeof(object))); + var runner = new RecipientRunner(InstanceRecipient.Create(42)); runner.Run(_ => Task.FromResult(42)).Wait(); - var runnerFaulted = new RecipientRunner(new InstanceRecipient(typeof(bool))); + var runnerFaulted = new RecipientRunner(InstanceRecipient.Create(42f)); runnerFaulted.Run(_ => Task.FromException(_ex)).Wait(); - var runnerIncomplete = new RecipientRunner(new InstanceRecipient(typeof(long))); + var runnerIncomplete = new RecipientRunner(InstanceRecipient.Create(42L)); runnerIncomplete.Run(_ => GetInfiniteTask()); _runners = new[] { runner, runnerFaulted, runnerIncomplete }; @@ -50,10 +50,10 @@ public void Invocations_are_grouped_correctly() { var response = AggregatedResponseFactory.CreateFrom(_runners); - Assert.Equal(typeof(object), response.Completed[0].RecipientType); + Assert.Equal(typeof(int), response.Completed[0].RecipientType); Assert.Equal(42, response.Completed[0].Result); - Assert.Equal(typeof(bool), response.Faulted[0].RecipientType); + Assert.Equal(typeof(float), response.Faulted[0].RecipientType); Assert.Equal(_ex, response.Faulted[0].Exception); Assert.Equal(typeof(long), response.Incomplete[0].RecipientType); diff --git a/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs b/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs index be01b70..2e22918 100644 --- a/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs +++ b/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs @@ -11,7 +11,7 @@ public class RecipientRunnerTest public RecipientRunnerTest() { - _recipient = new InstanceRecipient(typeof(object)); + _recipient = TypeRecipient.Create(); } [Fact] @@ -88,15 +88,113 @@ await runner.Run(async _ => throw ex; }); + Assert.False(runner.CompletedSuccessfully); + Assert.Equal(default, runner.Result); + Assert.True(runner.Faulted); + + Assert.NotEqual(default, runner.StartedAt); + Assert.NotEqual(default, runner.FinishedAt); + Assert.True(runner.FinishedAt >= runner.StartedAt); + + Assert.Equal(ex, runner.Exception); + } + + [Fact] + public async Task Exception_is_extracted() + { + var runner = new RecipientRunner(_recipient); + + var ex = new Exception(); + + await runner.Run(_ => + { + Fail().Wait(); + return Task.FromResult(42); + }); + Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); Assert.True(runner.Faulted); + + Assert.NotEqual(default, runner.StartedAt); + Assert.NotEqual(default, runner.FinishedAt); + Assert.True(runner.FinishedAt >= runner.StartedAt); + + Assert.NotNull(runner.Exception); Assert.Equal(ex, runner.Exception); + // Local functions. + + async Task Fail() + { + await Task.Delay(10); + throw ex; + } + } + + [Fact] + public async Task Aggregated_exceptions_are_decomposed() + { + var runner = new RecipientRunner(_recipient); + + var ex = new Exception(); + + await runner.Run(_ => + { + Task.WhenAll(Fail(), Fail(), Fail()).Wait(); + return Task.FromResult(42); + }); + + Assert.False(runner.CompletedSuccessfully); + Assert.Equal(default, runner.Result); + + Assert.True(runner.Faulted); + + Assert.NotEqual(default, runner.StartedAt); + Assert.NotEqual(default, runner.FinishedAt); + Assert.True(runner.FinishedAt >= runner.StartedAt); + + Assert.NotNull(runner.Exception); + Assert.IsType(runner.Exception); + + var aggEx = (AggregateException)runner.Exception!; + + Assert.Equal(3, aggEx.InnerExceptions.Count); + + foreach (var exception in aggEx.InnerExceptions) + Assert.Equal(ex, exception); + + // Local functions. + + async Task Fail() + { + await Task.Delay(10); + throw ex; + } + } + + [Fact] + public async Task Reflection_exception_are_decomposed() + { + var runner = new RecipientRunner(_recipient); + + var ex = new Exception(); + + await runner.Run(_ => + { + throw new System.Reflection.TargetInvocationException(ex); + }); + + Assert.False(runner.CompletedSuccessfully); + Assert.Equal(default, runner.Result); + Assert.True(runner.Faulted); + Assert.NotEqual(default, runner.StartedAt); Assert.NotEqual(default, runner.FinishedAt); Assert.True(runner.FinishedAt >= runner.StartedAt); + + Assert.Equal(ex, runner.Exception); } } } diff --git a/tests/NScatterGather.Tests/_TestTypes/AlmostCollidingType.cs b/tests/NScatterGather.Tests/_TestTypes/AlmostCollidingType.cs new file mode 100644 index 0000000..313ab9b --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/AlmostCollidingType.cs @@ -0,0 +1,9 @@ +namespace NScatterGather +{ + public class AlmostCollidingType + { + public string Do(int n) => n.ToString(); + + public long DoDifferently(int n) => (long)n; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeAsyncComputingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeAsyncComputingType.cs new file mode 100644 index 0000000..5b270da --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeAsyncComputingType.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomeAsyncComputingType + { + public Task Do(int n) => Task.CompletedTask; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeAsyncType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeAsyncType.cs new file mode 100644 index 0000000..c9c5651 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeAsyncType.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomeAsyncType + { + public Task EchoAsString(int n) => Task.FromResult(n.ToString()); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeCollidingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeCollidingType.cs new file mode 100644 index 0000000..0116b2f --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeCollidingType.cs @@ -0,0 +1,9 @@ +namespace NScatterGather +{ + public class SomeCollidingType + { + public string Do(int n) => n.ToString(); + + public string DoDifferently(int n) => $"{n}"; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeComputingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeComputingType.cs new file mode 100644 index 0000000..a677712 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeComputingType.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeComputingType + { + public void Do(int n) { } + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeDifferentType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeDifferentType.cs new file mode 100644 index 0000000..b504bd3 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeDifferentType.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeDifferentType + { + public int SomethingElse(string s) => s.Length; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeEchoType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeEchoType.cs new file mode 100644 index 0000000..9aa7e57 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeEchoType.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomeEchoType + { + public Task Echo(int n) => Task.FromResult(n.ToString()); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeFaultingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeFaultingType.cs new file mode 100644 index 0000000..de52cc6 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeFaultingType.cs @@ -0,0 +1,9 @@ +using System; + +namespace NScatterGather +{ + public class SomeFaultingType + { + public string Fail(int n) => throw new Exception("A failure."); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeNeverEndingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeNeverEndingType.cs new file mode 100644 index 0000000..fc8e300 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeNeverEndingType.cs @@ -0,0 +1,16 @@ +using System.Threading; + +namespace NScatterGather +{ + public class SomeNeverEndingType + { + private static readonly SemaphoreSlim _semaphore = + new SemaphoreSlim(initialCount: 0); + + public string TryDo(int n) + { + _semaphore.Wait(); + return n.ToString(); + } + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeOtherType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeOtherType.cs new file mode 100644 index 0000000..53e6e79 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeOtherType.cs @@ -0,0 +1,9 @@ +namespace NScatterGather +{ + public class SomeOtherType + { + public int Do(int n) => n * 2; + + public string Echo(long x) => x.ToString(); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomePossiblyAsyncType.cs b/tests/NScatterGather.Tests/_TestTypes/SomePossiblyAsyncType.cs new file mode 100644 index 0000000..f51c58f --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomePossiblyAsyncType.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomePossiblyAsyncType + { + public ValueTask Do(int n) => new ValueTask(n.ToString()); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeType.cs new file mode 100644 index 0000000..aab8e6f --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeType.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeType + { + public string EchoAsString(int n) => n.ToString(); + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithComplexArguments.cs b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithComplexArguments.cs new file mode 100644 index 0000000..c0b9000 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithComplexArguments.cs @@ -0,0 +1,9 @@ +using System; + +namespace NScatterGather +{ + public class SomeTypeWithComplexArguments + { + public int AMethod(Guid guid, string s, IDisposable d) => 42; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithConstructor.cs b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithConstructor.cs new file mode 100644 index 0000000..7dedea0 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithConstructor.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeTypeWithConstructor + { + public SomeTypeWithConstructor(int n) { } + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithManyMethods.cs b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithManyMethods.cs new file mode 100644 index 0000000..55f726c --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeTypeWithManyMethods.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomeTypeWithManyMethods + { + public void DoVoid() { } + + public void AcceptIntVoid(int n) { } + + public string EchoString(string s) => s; + + public Task DoTask() => Task.CompletedTask; + + public ValueTask DoValueTask() => new ValueTask(); + + public ValueTask DoAndReturnValueTask() => new ValueTask(42); + + public Task ReturnTask(int n) => Task.FromResult(n); + + public string Multi(int n, string s) => "Don't Panic"; + } +} From c034f390202882abe08c5acaafee6d72124463de Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 16 Dec 2020 22:19:04 +0100 Subject: [PATCH 02/31] Introduces Lifetime enum. --- src/NScatterGather/Recipients/Lifetime.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/NScatterGather/Recipients/Lifetime.cs diff --git a/src/NScatterGather/Recipients/Lifetime.cs b/src/NScatterGather/Recipients/Lifetime.cs new file mode 100644 index 0000000..e9d7aa2 --- /dev/null +++ b/src/NScatterGather/Recipients/Lifetime.cs @@ -0,0 +1,20 @@ +namespace NScatterGather +{ + public enum Lifetime + { + /// + /// A new instance is created each time. + /// + Transient, + + /// + /// Only one instance is created for each Aggregator instance. + /// + Scoped, + + /// + /// Only one instance is created for each registration. + /// + Singleton, + } +} From c482e611ec4849149e0b624f44c28fe7eecbf77e Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Thu, 17 Dec 2020 21:31:20 +0100 Subject: [PATCH 03/31] Introduces Factories. --- .../Recipients/Factories/IRecipientFactory.cs | 7 ++++++ .../Recipients/Factories/RecipientFactory.cs | 16 ++++++++++++ .../Factories/SingletonRecipientFactory.cs | 25 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 src/NScatterGather/Recipients/Factories/IRecipientFactory.cs create mode 100644 src/NScatterGather/Recipients/Factories/RecipientFactory.cs create mode 100644 src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs diff --git a/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs new file mode 100644 index 0000000..fb6e02f --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs @@ -0,0 +1,7 @@ +namespace NScatterGather.Recipients.Factories +{ + internal interface IRecipientFactory + { + object Get(); + } +} diff --git a/src/NScatterGather/Recipients/Factories/RecipientFactory.cs b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs new file mode 100644 index 0000000..2dfb8b4 --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs @@ -0,0 +1,16 @@ +using System; + +namespace NScatterGather.Recipients.Factories +{ + internal class RecipientFactory : IRecipientFactory + { + private readonly Func _factory; + + public RecipientFactory(Func factory) + { + _factory = factory; + } + + public object Get() => _factory(); + } +} diff --git a/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs b/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs new file mode 100644 index 0000000..d250c67 --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs @@ -0,0 +1,25 @@ +using System; + +namespace NScatterGather.Recipients.Factories +{ + internal class SingletonRecipientFactory : IRecipientFactory + { + private readonly Lazy _lazyInstance; + + public SingletonRecipientFactory(object instance) + { +#if NETSTANDARD2_0 + _lazyInstance = new Lazy(() => instance); +#else + _lazyInstance = new Lazy(instance); +#endif + } + + public SingletonRecipientFactory(IRecipientFactory anotherFactory) + { + _lazyInstance = new Lazy(anotherFactory.Get); + } + + public object Get() => _lazyInstance.Value; + } +} From 581ec065a851a4f85b99cfc36244bd0883b6007d Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Fri, 18 Dec 2020 18:11:43 +0100 Subject: [PATCH 04/31] Introduces descriptors. --- .../DelegateRecipientDescriptor.cs | 44 +++++++++++++++++++ .../Descriptors/IRecipientDescriptor.cs | 11 +++++ .../Descriptors/TypeRecipientDescriptor.cs | 32 ++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/NScatterGather/Recipients/Descriptors/DelegateRecipientDescriptor.cs create mode 100644 src/NScatterGather/Recipients/Descriptors/IRecipientDescriptor.cs create mode 100644 src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs diff --git a/src/NScatterGather/Recipients/Descriptors/DelegateRecipientDescriptor.cs b/src/NScatterGather/Recipients/Descriptors/DelegateRecipientDescriptor.cs new file mode 100644 index 0000000..651e85e --- /dev/null +++ b/src/NScatterGather/Recipients/Descriptors/DelegateRecipientDescriptor.cs @@ -0,0 +1,44 @@ +using System; + +namespace NScatterGather.Recipients.Descriptors +{ + internal class DelegateRecipientDescriptor : IRecipientDescriptor + { + public Type RequestType { get; } + + public Type ResponseType { get; } + + public DelegateRecipientDescriptor(Type requestType, Type responseType) + { + RequestType = requestType; + ResponseType = responseType; + } + + public bool CanAccept(Type requestType) + { + var requestTypeMatches = TypesMatch(RequestType, requestType); + return requestTypeMatches; + } + + public bool CanReplyWith(Type requestType, Type responseType) + { + var requestAndResponseMatch = + TypesMatch(RequestType, requestType) && + TypesMatch(ResponseType, responseType); + + return requestAndResponseMatch; + } + + private bool TypesMatch(Type target, Type actual) + { + if (target == actual) + return true; + + var nonNullableType = Nullable.GetUnderlyingType(target); + if (nonNullableType is not null && nonNullableType == actual) + return true; + + return false; + } + } +} diff --git a/src/NScatterGather/Recipients/Descriptors/IRecipientDescriptor.cs b/src/NScatterGather/Recipients/Descriptors/IRecipientDescriptor.cs new file mode 100644 index 0000000..580e372 --- /dev/null +++ b/src/NScatterGather/Recipients/Descriptors/IRecipientDescriptor.cs @@ -0,0 +1,11 @@ +using System; + +namespace NScatterGather.Recipients.Descriptors +{ + internal interface IRecipientDescriptor + { + bool CanAccept(Type requestType); + + bool CanReplyWith(Type requestType, Type responseType); + } +} diff --git a/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs b/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs new file mode 100644 index 0000000..9c4a332 --- /dev/null +++ b/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs @@ -0,0 +1,32 @@ +using System; +using NScatterGather.Inspection; + +namespace NScatterGather.Recipients.Descriptors +{ + internal class TypeRecipientDescriptor : IRecipientDescriptor + { + private readonly TypeInspector _inspector; + + public TypeRecipientDescriptor(Type type) + : this(new TypeInspector(type)) + { + } + + public TypeRecipientDescriptor(TypeInspector inspector) + { + _inspector = inspector; + } + + public bool CanAccept(Type requestType) + { + var accepts = _inspector.HasMethodAccepting(requestType); + return accepts; + } + + public bool CanReplyWith(Type requestType, Type responseType) + { + var repliesWith = _inspector.HasMethodReturning(requestType, responseType); + return repliesWith; + } + } +} From 089a21aadef91a26dd24d69eef7029c3ad25b10a Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Fri, 18 Dec 2020 23:20:18 +0100 Subject: [PATCH 05/31] Accept a TypeInspector from outside. --- .../Recipients/Descriptors/TypeRecipientDescriptor.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs b/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs index 9c4a332..a36149f 100644 --- a/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs +++ b/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs @@ -7,11 +7,6 @@ internal class TypeRecipientDescriptor : IRecipientDescriptor { private readonly TypeInspector _inspector; - public TypeRecipientDescriptor(Type type) - : this(new TypeInspector(type)) - { - } - public TypeRecipientDescriptor(TypeInspector inspector) { _inspector = inspector; From adcdab84f0b6d5d043fe72e9125b4edb966389dd Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sat, 19 Dec 2020 18:40:14 +0100 Subject: [PATCH 06/31] Introduces invokers. --- .../Invokers/DelegateRecipientInvoker.cs | 40 ++++++++++ .../Recipients/Invokers/IRecipientInvoker.cs | 9 +++ .../Invokers/InstanceRecipientInvoker.cs | 47 ++++++++++++ .../Recipients/Invokers/PreparedInvocation.cs | 76 +++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs create mode 100644 src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs create mode 100644 src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs create mode 100644 src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs diff --git a/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs new file mode 100644 index 0000000..cc75f4f --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs @@ -0,0 +1,40 @@ +using System; +using NScatterGather.Recipients.Descriptors; + +namespace NScatterGather.Recipients.Invokers +{ + internal class DelegateRecipientInvoker : IRecipientInvoker + { + private readonly DelegateRecipientDescriptor _descriptor; + private readonly Func _delegate; + + public DelegateRecipientInvoker( + DelegateRecipientDescriptor descriptor, + Func @delegate) + { + _descriptor = descriptor; + _delegate = @delegate; + } + + public PreparedInvocation PrepareInvocation(object request) + { + if (!_descriptor.CanAccept(request.GetType())) + throw new InvalidOperationException( + $"Delegate '{_delegate}' doesn't support accepting requests " + + $"of type '{request.GetType().Name}'."); + + return new PreparedInvocation(() => _delegate(request!)); + } + + public PreparedInvocation PrepareInvocation(object request) + { + if (!_descriptor.CanReplyWith(request.GetType(), typeof(TResult))) + throw new InvalidOperationException( + $"Type '{_delegate}' doesn't support accepting " + + $"requests of type '{request.GetType().Name}' and " + + $"returning '{typeof(TResult).Name}'."); + + return new PreparedInvocation(() => (TResult)_delegate(request)!); + } + } +} diff --git a/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs new file mode 100644 index 0000000..d1fd164 --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs @@ -0,0 +1,9 @@ +namespace NScatterGather.Recipients.Invokers +{ + internal interface IRecipientInvoker + { + PreparedInvocation PrepareInvocation(object request); + + PreparedInvocation PrepareInvocation(object request); + } +} diff --git a/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs new file mode 100644 index 0000000..d8bdcd0 --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs @@ -0,0 +1,47 @@ +using System; +using NScatterGather.Inspection; +using NScatterGather.Recipients.Factories; + +namespace NScatterGather.Recipients.Invokers +{ + internal class InstanceRecipientInvoker : IRecipientInvoker + { + private readonly TypeInspector _inspector; + private readonly IRecipientFactory _factory; + + public InstanceRecipientInvoker( + TypeInspector inspector, + IRecipientFactory factory) + { + _inspector = inspector; + _factory = factory; + } + + public PreparedInvocation PrepareInvocation(object request) + { + if (!_inspector.TryGetMethodAccepting(request.GetType(), out var method)) + throw new InvalidOperationException( + $"Type '{_inspector.Type.Name}' doesn't support accepting requests " + + $"of type '{request.GetType().Name}'."); + + var recipientInstance = _factory.Get(); + + return new PreparedInvocation(invocation: () => + method.Invoke(recipientInstance, new object?[] { request })); + } + + public PreparedInvocation PrepareInvocation(object request) + { + if (!_inspector.TryGetMethodReturning(request.GetType(), typeof(TResult), out var method)) + throw new InvalidOperationException( + $"Type '{_inspector.Type.Name}' doesn't support accepting " + + $"requests of type '{request.GetType().Name}' and " + + $"returning '{typeof(TResult).Name}'."); + + var recipientInstance = _factory.Get(); + + return new PreparedInvocation(invocation: () => + (TResult)method.Invoke(recipientInstance, new object?[] { request })!); + } + } +} diff --git a/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs b/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs new file mode 100644 index 0000000..4151f4b --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs @@ -0,0 +1,76 @@ +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using IsAwaitable.Analysis; + +namespace NScatterGather.Recipients.Invokers +{ + internal class PreparedInvocation + { + private readonly Func _invocation; + + public PreparedInvocation(Func invocation) + { + _invocation = invocation; + } + + public async Task Execute() + { + try + { + var invocationResult = _invocation(); + var completedResult = await UnwrapAsyncResult(invocationResult).ConfigureAwait(false); + return (TResult)completedResult!; // The response type will match TResponse, even on structs. + } + catch (TargetInvocationException tIEx) when (tIEx.InnerException is not null) + { + ExceptionDispatchInfo.Capture(tIEx.InnerException).Throw(); + return default!; // Unreachable + } + } + + private static async Task UnwrapAsyncResult(object? response) + { + if (response is null) + return response; + + if (!response.IsAwaitableWithResult()) + return response!; + + // Fun fact: when the invoked method returns (asynchronously) + // a non-public type (e.g. anonymous, internal...), the + // 'dynamic await' approach will fail with the following exception: + // `RuntimeBinderException: Cannot implicitly convert type 'void' to 'object'`. + // + // This happens because the dynamic binding treats them as the closest public + // inherited type it knows about. + // https://stackoverflow.com/questions/31778977/why-cant-i-access-properties-of-an-anonymous-type-returned-from-a-function-via/31779069#31779069 + // + // If the method returned a `Task`, the result of the dynamic binding + // will be a `Task`, i.e. the closest public inherited type. + // + // Since `Task` is awaitable, but has no result, the runtime will try to: + // ``` + // dynamic awaiter = ((dynamic)response).GetAwaiter(); + // var result = awaiter.GetResult(); + // ``` + // but `GetResult()` returns `void`! And that's why the exception. + // + // Solution: treat it like a Task, then extract the result via method invocation. + + await (dynamic)response; + + // At this point the task is completed. + + var description = Awaitable.Describe(response); + + if (description is null) + throw new Exception("Couldn't extract async response."); + + var awaiter = description.GetAwaiterMethod.Invoke(response, null); + var awaitedResult = description.AwaiterDescriptor.GetResultMethod.Invoke(awaiter, null); + return awaitedResult; + } + } +} From fec9680d0cf8ecfe50cfd5c6a86e1b96894f1f0f Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sat, 19 Dec 2020 20:12:30 +0100 Subject: [PATCH 07/31] Uses untyped invocations: task-like objects could be returned. --- .../Recipients/Invokers/InstanceRecipientInvoker.cs | 2 +- src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs index d8bdcd0..5fd8b31 100644 --- a/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs +++ b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs @@ -41,7 +41,7 @@ public PreparedInvocation PrepareInvocation(object request) var recipientInstance = _factory.Get(); return new PreparedInvocation(invocation: () => - (TResult)method.Invoke(recipientInstance, new object?[] { request })!); + method.Invoke(recipientInstance, new object?[] { request })!); } } } diff --git a/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs b/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs index 4151f4b..44031ab 100644 --- a/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs +++ b/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs @@ -8,9 +8,9 @@ namespace NScatterGather.Recipients.Invokers { internal class PreparedInvocation { - private readonly Func _invocation; + private readonly Func _invocation; - public PreparedInvocation(Func invocation) + public PreparedInvocation(Func invocation) { _invocation = invocation; } From 59a29524ddbf070513a25505311acb7474543887 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 20 Dec 2020 12:34:31 +0100 Subject: [PATCH 08/31] Introduces IRecipientScope. --- .../Collection/RecipientsCollection.cs | 102 +++++++++++++ .../Collection/Scope/IRecipientsScope.cs | 12 ++ .../Collection/Scope/RecipientsScope.cs | 76 ++++++++++ .../Recipients/RecipientsCollection.cs | 137 ------------------ 4 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 src/NScatterGather/Recipients/Collection/RecipientsCollection.cs create mode 100644 src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs create mode 100644 src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs delete mode 100644 src/NScatterGather/Recipients/RecipientsCollection.cs diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs new file mode 100644 index 0000000..7e148c4 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NScatterGather.Inspection; +using NScatterGather.Recipients; +using NScatterGather.Recipients.Collection.Scope; + +namespace NScatterGather +{ + public delegate void ConflictHandler(ConflictException ex); + + public delegate void ErrorHandler(Exception ex); + + public class RecipientsCollection + { + public event ConflictHandler? OnConflict; + + public event ErrorHandler? OnError; + + private readonly List _singletonRecipients = new(); + private readonly List _scopedRecipients = new(); + private readonly List _transientRecipients = new(); + private readonly TypeInspectorRegistry _registry = new TypeInspectorRegistry(); + + public void Add( + string? name = null, + Lifetime lifetime = Lifetime.Transient) + { + if (!HasADefaultConstructor()) + throw new ArgumentException($"Type '{typeof(TRecipient).Name}' is missing a public, parameterless constructor."); + + Add(() => ((TRecipient)Activator.CreateInstance(typeof(TRecipient)))!, name, lifetime); + + // Local functions. + + static bool HasADefaultConstructor() + { + var defaultContructor = typeof(T).GetConstructor(Type.EmptyTypes); + return defaultContructor is not null; + } + } + + public void Add( + Func factoryMethod, + string? name = null, + Lifetime lifetime = Lifetime.Transient) + { + if (factoryMethod is null) + throw new ArgumentNullException(nameof(factoryMethod)); + + var typeRecipient = TypeRecipient.Create(_registry, factoryMethod, name, lifetime); + Add(typeRecipient); + } + + public void Add( + object instance, + string? name = null) + { + if (instance is null) + throw new ArgumentNullException(nameof(instance)); + + var instanceRecipient = InstanceRecipient.Create(_registry, instance, name); + Add(instanceRecipient); + } + + public void Add( + Func @delegate, + string? name = null) + { + if (@delegate is null) + throw new ArgumentNullException(nameof(@delegate)); + + var delegateRecipient = DelegateRecipient.Create(@delegate, name); + Add(delegateRecipient); + } + + private void Add(Recipient recipient) + { + if (recipient.Lifetime == Lifetime.Singleton) + _singletonRecipients.Add(recipient); + else if (recipient.Lifetime == Lifetime.Scoped) + _scopedRecipients.Add(recipient); + else + _transientRecipients.Add(recipient); + } + + internal IRecipientsScope CreateScope() + { + var scope = new RecipientsScope(); + scope.AddRange(_singletonRecipients); + scope.AddRange(_transientRecipients); + + var clonedScoped = _scopedRecipients.Select(r => r.Clone()); + scope.AddRange(clonedScoped); + + scope.OnConflict += OnConflict; + scope.OnError += OnError; + + return scope; + } + } +} diff --git a/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs new file mode 100644 index 0000000..b72a9e8 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace NScatterGather.Recipients.Collection.Scope +{ + internal interface IRecipientsScope + { + IReadOnlyList ListRecipientsAccepting(Type requestType); + + IReadOnlyList ListRecipientsReplyingWith(Type requestType, Type responseType); + } +} diff --git a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs new file mode 100644 index 0000000..7502eb4 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NScatterGather.Recipients.Collection.Scope +{ + internal class RecipientsScope : IRecipientsScope + { + public event ConflictHandler? OnConflict; + + public event ErrorHandler? OnError; + + private readonly List _recipients = new(); + + internal void AddRange(IEnumerable recipients) => + _recipients.AddRange(recipients); + + public IReadOnlyList ListRecipientsAccepting(Type requestType) + { + var validRecipients = _recipients + .Where(RecipientCanAccept) + .ToArray(); + + return validRecipients; + + // Local functions. + + bool RecipientCanAccept(Recipient recipient) + { + try + { + return recipient.CanAccept(requestType); + } + catch (ConflictException ex) + { + OnConflict?.Invoke(ex); + return false; + } + catch (Exception ex) + { + OnError?.Invoke(ex); + return false; + } + } + } + + public IReadOnlyList ListRecipientsReplyingWith(Type requestType, Type responseType) + { + var validRecipients = _recipients + .Where(RecipientCanReplyWith) + .ToArray(); + + return validRecipients; + + // Local functions. + + bool RecipientCanReplyWith(Recipient recipient) + { + try + { + return recipient.CanReplyWith(requestType, responseType); + } + catch (ConflictException ex) + { + OnConflict?.Invoke(ex); + return false; + } + catch (Exception ex) + { + OnError?.Invoke(ex); + return false; + } + } + } + } +} diff --git a/src/NScatterGather/Recipients/RecipientsCollection.cs b/src/NScatterGather/Recipients/RecipientsCollection.cs deleted file mode 100644 index f2cd32d..0000000 --- a/src/NScatterGather/Recipients/RecipientsCollection.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NScatterGather.Inspection; -using NScatterGather.Recipients; - -namespace NScatterGather -{ - public delegate void ConflictHandler(ConflictException ex); - - public delegate void ErrorHandler(Exception ex); - - public class RecipientsCollection - { - private readonly List _recipients = new List(); - private readonly TypeInspectorRegistry _registry; - - public IReadOnlyList RecipientTypes => _recipients - .OfType() - .Select(x => x.Type) - .ToArray(); - - internal IReadOnlyList Recipients => _recipients.ToArray(); - - public event ConflictHandler? OnConflict; - - public event ErrorHandler? OnError; - - public RecipientsCollection() - : this(new TypeInspectorRegistry()) { } - - internal RecipientsCollection(TypeInspectorRegistry registry) - { - _registry = registry; - } - - public void Add( - Func? factory = null, - string? name = null) - { - _recipients.Add(TypeRecipient.Create(factory, name)); - } - - public void Add(object instance, string? name = null) - { - if (instance is null) - throw new ArgumentNullException(nameof(instance)); - - _recipients.Add(InstanceRecipient.Create(instance, name)); - } - - public void Add(Func @delegate, string? name = null) - { - if (@delegate is null) - throw new ArgumentNullException(nameof(@delegate)); - - var recipient = DelegateRecipient.Create(@delegate, name); - _recipients.Add(recipient); - } - - internal void Add(Recipient recipient) - { - if (recipient is TypeRecipient tr) - _ = _registry.Register(tr.Type); - - _recipients.Add(recipient); - } - - internal IReadOnlyList ListRecipientsAccepting(Type requestType) - { - var validRecipients = _recipients - .Where(RecipientCanAccept) - .ToArray(); - - return validRecipients; - - // Local functions. - - bool RecipientCanAccept(Recipient recipient) - { - try - { - return recipient.CanAccept(requestType); - } - catch (ConflictException ex) - { - OnConflict?.Invoke(ex); - return false; - } - catch (Exception ex) - { - OnError?.Invoke(ex); - return false; - } - } - } - - internal IReadOnlyList ListRecipientsReplyingWith(Type requestType, Type responseType) - { - var validRecipients = _recipients - .Where(RecipientCanReplyWith) - .ToArray(); - - return validRecipients; - - // Local functions. - - bool RecipientCanReplyWith(Recipient recipient) - { - try - { - return recipient.CanReplyWith(requestType, responseType); - } - catch (ConflictException ex) - { - OnConflict?.Invoke(ex); - return false; - } - catch (Exception ex) - { - OnError?.Invoke(ex); - return false; - } - } - } - - public RecipientsCollection Clone() - { - var clone = new RecipientsCollection(_registry); - - foreach (var recipient in _recipients) - clone.Add(recipient); - - return clone; - } - } -} From 81172fae07da011698fd8402906cb95c1303f63c Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 20 Dec 2020 16:43:38 +0100 Subject: [PATCH 09/31] Renames into CollisionException. --- .../{ConflictException.cs => CollisionException.cs} | 4 ++-- .../Recipients/Collection/RecipientsCollection.cs | 6 +++--- .../Recipients/Collection/Scope/IRecipientsScope.cs | 2 ++ .../Recipients/Collection/Scope/RecipientsScope.cs | 12 +++++++----- 4 files changed, 14 insertions(+), 10 deletions(-) rename src/NScatterGather/Inspection/{ConflictException.cs => CollisionException.cs} (93%) diff --git a/src/NScatterGather/Inspection/ConflictException.cs b/src/NScatterGather/Inspection/CollisionException.cs similarity index 93% rename from src/NScatterGather/Inspection/ConflictException.cs rename to src/NScatterGather/Inspection/CollisionException.cs index e5e2756..2f0a7f3 100644 --- a/src/NScatterGather/Inspection/ConflictException.cs +++ b/src/NScatterGather/Inspection/CollisionException.cs @@ -2,7 +2,7 @@ namespace NScatterGather { - public class ConflictException : Exception + public class CollisionException : Exception { public Type RecipientType { get; } @@ -10,7 +10,7 @@ public class ConflictException : Exception public Type? ResponseType { get; } - internal ConflictException( + internal CollisionException( Type recipientType, Type requestType, Type? responseType = null) diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs index 7e148c4..1a33869 100644 --- a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -7,13 +7,13 @@ namespace NScatterGather { - public delegate void ConflictHandler(ConflictException ex); + public delegate void CollisionHandler(CollisionException ex); public delegate void ErrorHandler(Exception ex); public class RecipientsCollection { - public event ConflictHandler? OnConflict; + public event CollisionHandler? OnCollision; public event ErrorHandler? OnError; @@ -93,7 +93,7 @@ internal IRecipientsScope CreateScope() var clonedScoped = _scopedRecipients.Select(r => r.Clone()); scope.AddRange(clonedScoped); - scope.OnConflict += OnConflict; + scope.OnCollision += OnCollision; scope.OnError += OnError; return scope; diff --git a/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs index b72a9e8..1db2c64 100644 --- a/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs +++ b/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs @@ -5,6 +5,8 @@ namespace NScatterGather.Recipients.Collection.Scope { internal interface IRecipientsScope { + int RecipientsCount { get; } + IReadOnlyList ListRecipientsAccepting(Type requestType); IReadOnlyList ListRecipientsReplyingWith(Type requestType, Type responseType); diff --git a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs index 7502eb4..b9cd797 100644 --- a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs +++ b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs @@ -6,7 +6,9 @@ namespace NScatterGather.Recipients.Collection.Scope { internal class RecipientsScope : IRecipientsScope { - public event ConflictHandler? OnConflict; + public int RecipientsCount => _recipients.Count; + + public event CollisionHandler? OnCollision; public event ErrorHandler? OnError; @@ -31,9 +33,9 @@ bool RecipientCanAccept(Recipient recipient) { return recipient.CanAccept(requestType); } - catch (ConflictException ex) + catch (CollisionException ex) { - OnConflict?.Invoke(ex); + OnCollision?.Invoke(ex); return false; } catch (Exception ex) @@ -60,9 +62,9 @@ bool RecipientCanReplyWith(Recipient recipient) { return recipient.CanReplyWith(requestType, responseType); } - catch (ConflictException ex) + catch (CollisionException ex) { - OnConflict?.Invoke(ex); + OnCollision?.Invoke(ex); return false; } catch (Exception ex) From a24fcfbe5cabf9cfaee1d35a014016a79d59da94 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Mon, 21 Dec 2020 15:16:03 +0100 Subject: [PATCH 10/31] Self-contained RecipientRun. --- .../Recipients/DelegateRecipient.cs | 90 +++++--------- .../Recipients/InstanceRecipient.cs | 43 ++++++- src/NScatterGather/Recipients/Recipient.cs | 109 +++++------------ .../Run/RecipientRun.cs} | 23 ++-- .../Recipients/TypeRecipient.cs | 114 ++++++------------ .../Responses/AggregatedResponseFactory.cs | 7 +- 6 files changed, 148 insertions(+), 238 deletions(-) rename src/NScatterGather/{Run/RecipientRunner.cs => Recipients/Run/RecipientRun.cs} (83%) diff --git a/src/NScatterGather/Recipients/DelegateRecipient.cs b/src/NScatterGather/Recipients/DelegateRecipient.cs index 1877207..729d9b4 100644 --- a/src/NScatterGather/Recipients/DelegateRecipient.cs +++ b/src/NScatterGather/Recipients/DelegateRecipient.cs @@ -1,90 +1,54 @@ using System; +using NScatterGather.Recipients.Descriptors; +using NScatterGather.Recipients.Invokers; namespace NScatterGather.Recipients { internal class DelegateRecipient : Recipient { - private readonly Func _delegate; + public Type RequestType { get; } - internal Type In => _inType; + public Type ResponseType { get; } - internal Type Out => _outType; - - private readonly Type _inType; - private readonly Type _outType; + private readonly DelegateRecipientDescriptor _typedDescriptor; + private readonly DelegateRecipientInvoker _typedInvoker; public static DelegateRecipient Create( Func @delegate, - string? name = null) + string? name) { if (@delegate is null) - throw new ArgumentNullException(nameof(@delegate)); + throw new ArgumentNullException(nameof(@delegate)); - object? delegateInvoker(object @in) + object? delegateInvoker(object request) { - var request = (TRequest)@in; - TResponse response = @delegate(request); + var typedRequest = (TRequest)request; + TResponse response = @delegate(typedRequest); return response; } - return new DelegateRecipient( - delegateInvoker, - inType: typeof(TRequest), - outType: typeof(TResponse), - name); - } - - protected DelegateRecipient( - Func @delegate, - Type inType, - Type outType, - string? name = null) : base(name) - { - _delegate = @delegate; - _inType = inType; - _outType = outType; - } - - protected internal override string GetRecipientName() => - _delegate.ToString() ?? "delegate"; - - public override bool CanAccept(Type requestType) => - Match(_inType, requestType); + var descriptor = new DelegateRecipientDescriptor(typeof(TRequest), typeof(TResponse)); + var invoker = new DelegateRecipientInvoker(descriptor, delegateInvoker); - public override bool CanReplyWith(Type requestType, Type responseType) => - Match(_inType, requestType) && Match(_outType, responseType); - - private bool Match(Type target, Type actual) - { - if (target == actual) - return true; - - var nonNullableType = Nullable.GetUnderlyingType(target); - if (nonNullableType is not null && nonNullableType == actual) - return true; - - return false; + return new DelegateRecipient(descriptor, invoker, name); } - protected internal override object? Invoke(object request) + protected DelegateRecipient( + DelegateRecipientDescriptor descriptor, + DelegateRecipientInvoker invoker, + string? name) : base(descriptor, invoker, name, Lifetime.Singleton) { - if (!CanAccept(request.GetType())) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting requests " + - $"of type '{request.GetType().Name}'."); + _typedDescriptor = descriptor; + _typedInvoker = invoker; - return _delegate(request!); + RequestType = descriptor.RequestType; + ResponseType = descriptor.ResponseType; } - protected internal override object? Invoke(object request) - { - if (!CanReplyWith(request.GetType(), typeof(TResponse))) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting " + - $"requests of type '{request.GetType().Name}' and " + - $"returning '{typeof(TResponse).Name}'."); - - return _delegate(request!); - } +#if NETSTANDARD2_0 || NETSTANDARD2_1 + public override Recipient Clone() => new DelegateRecipient(_typedDescriptor, _typedInvoker, Name); +#else + public override DelegateRecipient Clone() => new(_typedDescriptor, _typedInvoker, Name); +#endif } } diff --git a/src/NScatterGather/Recipients/InstanceRecipient.cs b/src/NScatterGather/Recipients/InstanceRecipient.cs index e21199a..4013189 100644 --- a/src/NScatterGather/Recipients/InstanceRecipient.cs +++ b/src/NScatterGather/Recipients/InstanceRecipient.cs @@ -1,22 +1,57 @@ using System; +using NScatterGather.Inspection; +using NScatterGather.Recipients.Descriptors; +using NScatterGather.Recipients.Factories; +using NScatterGather.Recipients.Invokers; namespace NScatterGather.Recipients { internal class InstanceRecipient : TypeRecipient { + private readonly object _instance; + private readonly TypeRecipientDescriptor _typedDescriptor; + private readonly InstanceRecipientInvoker _typedInvoker; + public static InstanceRecipient Create( + TypeInspectorRegistry registry, object instance, - string? name = null) + string? name) { + if (registry is null) + throw new ArgumentNullException(nameof(registry)); + if (instance is null) throw new ArgumentNullException(nameof(instance)); - return new InstanceRecipient(instance, name); + var inspector = registry.For(instance.GetType()); + var descriptor = new TypeRecipientDescriptor(inspector); + + var invoker = new InstanceRecipientInvoker( + inspector, + new SingletonRecipientFactory(instance)); + + return new InstanceRecipient( + instance, + descriptor, + invoker, + name); } - protected InstanceRecipient(object instance, string? name = null) - : base(instance.GetType(), factory: () => instance, name) + protected InstanceRecipient( + object instance, + TypeRecipientDescriptor descriptor, + InstanceRecipientInvoker invoker, + string? name) : base(instance.GetType(), descriptor, invoker, name, Lifetime.Singleton) { + _instance = instance; + _typedDescriptor = descriptor; + _typedInvoker = invoker; } + +#if NETSTANDARD2_0 || NETSTANDARD2_1 + public override Recipient Clone() => new InstanceRecipient(_instance, _typedDescriptor, _typedInvoker, Name); +#else + public override InstanceRecipient Clone() => new(_instance, _typedDescriptor, _typedInvoker, Name); +#endif } } diff --git a/src/NScatterGather/Recipients/Recipient.cs b/src/NScatterGather/Recipients/Recipient.cs index 25e224f..9729dc6 100644 --- a/src/NScatterGather/Recipients/Recipient.cs +++ b/src/NScatterGather/Recipients/Recipient.cs @@ -1,8 +1,7 @@ using System; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Threading.Tasks; -using IsAwaitable.Analysis; +using NScatterGather.Recipients.Descriptors; +using NScatterGather.Recipients.Invokers; +using NScatterGather.Recipients.Run; namespace NScatterGather.Recipients { @@ -10,92 +9,44 @@ internal abstract class Recipient { public string? Name { get; } - public Recipient(string? name = null) - { - Name = name; - } + public Lifetime Lifetime { get; } - protected internal abstract string GetRecipientName(); + protected readonly IRecipientDescriptor _descriptor; + protected readonly IRecipientInvoker _invoker; - public abstract bool CanAccept(Type requestType); + public Recipient( + IRecipientDescriptor descriptor, + IRecipientInvoker invoker, + string? name, + Lifetime lifetime) + { + _descriptor = descriptor; + _invoker = invoker; - public abstract bool CanReplyWith(Type requestType, Type responseType); + Name = name; + Lifetime = lifetime; + } - protected internal abstract object? Invoke(object request); + public bool CanAccept(Type requestType) => + _descriptor.CanAccept(requestType); - protected internal abstract object? Invoke(object request); + public bool CanReplyWith(Type requestType, Type responseType) => + _descriptor.CanReplyWith(requestType, responseType); - public async Task Accept(object request) + public RecipientRun Accept(object request) { - try - { - var invocationResult = Invoke(request); - var completedResult = await UnwrapAsyncResult(invocationResult).ConfigureAwait(false); - return completedResult; - } - catch (TargetInvocationException tIEx) when (tIEx.InnerException is not null) - { - ExceptionDispatchInfo.Capture(tIEx.InnerException).Throw(); - return default; // Unreachable - } + var preparedInvocation = _invoker.PrepareInvocation(request); + var run = new RecipientRun(this, preparedInvocation); + return run; } - public async Task ReplyWith(object request) + public RecipientRun ReplyWith(object request) { - try - { - var invocationResult = Invoke(request); - var completedResult = await UnwrapAsyncResult(invocationResult).ConfigureAwait(false); - return (TResponse)completedResult!; // The response type will match TResponse, even on structs. - } - catch (TargetInvocationException tIEx) when (tIEx.InnerException is not null) - { - ExceptionDispatchInfo.Capture(tIEx.InnerException).Throw(); - return default!; // Unreachable - } + var preparedInvocation = _invoker.PrepareInvocation(request); + var run = new RecipientRun(this, preparedInvocation); + return run; } - private async Task UnwrapAsyncResult(object? response) - { - if (response is null) - return response; - - if (!response.IsAwaitableWithResult()) - return response!; - - // Fun fact: when the invoked method returns (asynchronously) - // a non-public type (e.g. anonymous, internal...), the - // 'dynamic await' approach will fail with the following exception: - // `RuntimeBinderException: Cannot implicitly convert type 'void' to 'object'`. - // - // This happens because the dynamic binding treats them as the closest public - // inherited type it knows about. - // https://stackoverflow.com/questions/31778977/why-cant-i-access-properties-of-an-anonymous-type-returned-from-a-function-via/31779069#31779069 - // - // If the method returned a `Task`, the result of the dynamic binding - // will be a `Task`, i.e. the closest public inherited type. - // - // Since `Task` is awaitable, but has no result, the runtime will try to: - // ``` - // dynamic awaiter = ((dynamic)response).GetAwaiter(); - // var result = awaiter.GetResult(); - // ``` - // but `GetResult()` returns `void`! And that's why the exception. - // - // Solution: treat it like a Task, then extract the result via method invocation. - - await (dynamic)response; - - // At this point the task is completed. - - var description = Awaitable.Describe(response); - - if (description is null) - throw new Exception("Couldn't extract async response."); - - var awaiter = description.GetAwaiterMethod.Invoke(response, null); - var awaitedResult = description.AwaiterDescriptor.GetResultMethod.Invoke(awaiter, null); - return awaitedResult; - } + public abstract Recipient Clone(); } } diff --git a/src/NScatterGather/Run/RecipientRunner.cs b/src/NScatterGather/Recipients/Run/RecipientRun.cs similarity index 83% rename from src/NScatterGather/Run/RecipientRunner.cs rename to src/NScatterGather/Recipients/Run/RecipientRun.cs index 30dc483..6203e5b 100644 --- a/src/NScatterGather/Run/RecipientRunner.cs +++ b/src/NScatterGather/Recipients/Run/RecipientRun.cs @@ -2,12 +2,12 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading.Tasks; -using NScatterGather.Recipients; +using NScatterGather.Recipients.Invokers; using static System.Threading.Tasks.TaskContinuationOptions; -namespace NScatterGather.Run +namespace NScatterGather.Recipients.Run { - internal class RecipientRunner + internal class RecipientRun { public Recipient Recipient { get; } @@ -24,23 +24,22 @@ internal class RecipientRunner public DateTime? FinishedAt { get; private set; } - public RecipientRunner(Recipient recipient) + private readonly PreparedInvocation _preparedInvocation; + + public RecipientRun(Recipient recipient, PreparedInvocation preparedInvocation) { - Recipient = recipient ?? throw new ArgumentNullException(nameof(recipient)); - Result = default; + Recipient = recipient; + _preparedInvocation = preparedInvocation; } - public Task Run(Func> invocation) + public Task Start() { - if (invocation is null) - throw new ArgumentNullException(nameof(invocation)); - if (StartedAt.HasValue) throw new InvalidOperationException("Run already executed."); - StartedAt = DateTime.UtcNow; + var runnerTask = Task.Run(async () => await _preparedInvocation.Execute()); - var runnerTask = Task.Run(async () => await invocation(Recipient)); + StartedAt = DateTime.UtcNow; var tcs = new TaskCompletionSource(); diff --git a/src/NScatterGather/Recipients/TypeRecipient.cs b/src/NScatterGather/Recipients/TypeRecipient.cs index b5a0936..8d37635 100644 --- a/src/NScatterGather/Recipients/TypeRecipient.cs +++ b/src/NScatterGather/Recipients/TypeRecipient.cs @@ -1,101 +1,63 @@ using System; -using System.Reflection; using NScatterGather.Inspection; +using NScatterGather.Recipients.Descriptors; +using NScatterGather.Recipients.Factories; +using NScatterGather.Recipients.Invokers; namespace NScatterGather.Recipients { internal class TypeRecipient : Recipient { - public Type Type => _type; + private readonly TypeRecipientDescriptor _typedDescriptor; - private readonly Type _type; - private readonly Func _factory; - private readonly TypeInspector _inspector; + public Type Type { get; } public static TypeRecipient Create( - Func? customFactory = null, - string? name = null) + TypeInspectorRegistry registry, + Func factoryMethod, + string? name, + Lifetime lifetime) { - Func factory = customFactory is not null - ? () => customFactory() - : HasADefaultConstructor() - ? () => Activator.CreateInstance(typeof(TRecipient))! - : throw new ArgumentException($"Type '{typeof(TRecipient).Name}' is missing a public, parameterless constructor."); + if (registry is null) + throw new ArgumentNullException(nameof(registry)); - return new TypeRecipient(typeof(TRecipient), factory, name); + if (factoryMethod is null) + throw new ArgumentNullException(nameof(factoryMethod)); - // Local functions. + if (!lifetime.IsValid()) + throw new ArgumentException($"Invalid {nameof(lifetime)} value: {lifetime}"); - static bool HasADefaultConstructor() - { - var defaultContructor = typeof(T).GetConstructor(Type.EmptyTypes); - return defaultContructor is not null; - } - } - - protected TypeRecipient( - Type type, - Func factory, - string? name = null) : base(name) - { - _type = type; - _factory = factory; - _inspector = new TypeInspector(type); - } - - protected internal override string GetRecipientName() => - _type.Name; - - public override bool CanAccept(Type requestType) - { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); - - var accepts = _inspector.HasMethodAccepting(requestType); - return accepts; - } - - public override bool CanReplyWith(Type requestType, Type responseType) - { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); + var inspector = registry.For(); - if (responseType is null) - throw new ArgumentNullException(nameof(responseType)); - - var repliesWith = _inspector.HasMethodReturning(requestType, responseType); - return repliesWith; - } + IRecipientFactory factory = new RecipientFactory(() => factoryMethod()!); - protected internal override object? Invoke(object request) - { - if (!_inspector.TryGetMethodAccepting(request.GetType(), out var method)) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting requests " + - $"of type '{request.GetType().Name}'."); + if (lifetime != Lifetime.Transient) + factory = new SingletonRecipientFactory(factory); - var recipientInstance = GetRecipientInstance(); - var response = method.Invoke(recipientInstance, new object?[] { request }); - return response; + return new TypeRecipient( + typeof(TRecipient), + new TypeRecipientDescriptor(inspector), + new InstanceRecipientInvoker(inspector, factory), + name, + lifetime); } - protected internal override object? Invoke(object request) + protected TypeRecipient( + Type type, + TypeRecipientDescriptor descriptor, + IRecipientInvoker invoker, + string? name, + Lifetime lifetime) : base(descriptor, invoker, name, lifetime) { - if (!_inspector.TryGetMethodReturning(request.GetType(), typeof(TResponse), out var method)) - throw new InvalidOperationException( - $"Type '{GetRecipientName()}' doesn't support accepting " + - $"requests of type '{request.GetType().Name}' and " + - $"returning '{typeof(TResponse).Name}'."); + _typedDescriptor = descriptor; - var recipientInstance = GetRecipientInstance(); - var response = method.Invoke(recipientInstance, new object?[] { request }); - return response; + Type = type; } - protected virtual object GetRecipientInstance() - { - var instance = _factory(); - return instance; - } +#if NETSTANDARD2_0 || NETSTANDARD2_1 + public override Recipient Clone() => new TypeRecipient(Type, _typedDescriptor, _invoker, Name, Lifetime); +#else + public override TypeRecipient Clone() => new(Type, _typedDescriptor, _invoker, Name, Lifetime); +#endif } } diff --git a/src/NScatterGather/Responses/AggregatedResponseFactory.cs b/src/NScatterGather/Responses/AggregatedResponseFactory.cs index a189474..0d30101 100644 --- a/src/NScatterGather/Responses/AggregatedResponseFactory.cs +++ b/src/NScatterGather/Responses/AggregatedResponseFactory.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using NScatterGather.Recipients; -using NScatterGather.Run; +using NScatterGather.Recipients.Run; namespace NScatterGather.Responses { internal class AggregatedResponseFactory { public static AggregatedResponse CreateFrom( - IEnumerable> invocations) + IEnumerable> invocations) { var completed = new List>(); var faulted = new List(); @@ -45,8 +45,7 @@ public static AggregatedResponse CreateFrom( return new AggregatedResponse(completed, faulted, incomplete); } - private static TimeSpan GetDuration( - RecipientRunner invocation) + private static TimeSpan GetDuration(RecipientRun invocation) { if (invocation.StartedAt.HasValue && invocation.FinishedAt.HasValue) return invocation.FinishedAt.Value - invocation.StartedAt.Value; From e984798646c92d5a0914852e017fa8486673b8cb Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Tue, 22 Dec 2020 19:46:24 +0100 Subject: [PATCH 11/31] Simplifies TypeInspectorRegistry. --- .../Inspection/TypeInspector.cs | 8 +++++--- .../Inspection/TypeInspectorRegistry.cs | 19 +++---------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/NScatterGather/Inspection/TypeInspector.cs b/src/NScatterGather/Inspection/TypeInspector.cs index c7d7f0a..d42804d 100644 --- a/src/NScatterGather/Inspection/TypeInspector.cs +++ b/src/NScatterGather/Inspection/TypeInspector.cs @@ -11,6 +11,8 @@ internal class TypeInspector private static readonly BindingFlags DefaultFlags = BindingFlags.Public | BindingFlags.Instance; private static readonly MethodAnalyzer _methodAnalyzer = new MethodAnalyzer(); + public Type Type => _type; + private readonly MethodMatchEvaluationCache _evaluationsCache = new MethodMatchEvaluationCache(); private readonly IReadOnlyList _methodInspections; @@ -18,7 +20,7 @@ internal class TypeInspector public TypeInspector(Type type) { - _type = type; + _type = type ?? throw new ArgumentNullException(nameof(type)); _methodInspections = InspectMethods(type); // Local functions. @@ -68,7 +70,7 @@ public bool TryGetMethodAccepting( _evaluationsCache.TryAdd(requestType, new MethodMatchEvaluation(false, method)); if (matches.Count > 1) - throw new ConflictException(_type, requestType); + throw new CollisionException(_type, requestType); return false; } @@ -126,7 +128,7 @@ public bool TryGetMethodReturning( _evaluationsCache.TryAdd(requestType, responseType, new MethodMatchEvaluation(false, method)); if (matches.Count > 1) - throw new ConflictException(_type, requestType, responseType); + throw new CollisionException(_type, requestType, responseType); return false; } diff --git a/src/NScatterGather/Inspection/TypeInspectorRegistry.cs b/src/NScatterGather/Inspection/TypeInspectorRegistry.cs index e27c058..67c0a8b 100644 --- a/src/NScatterGather/Inspection/TypeInspectorRegistry.cs +++ b/src/NScatterGather/Inspection/TypeInspectorRegistry.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; namespace NScatterGather.Inspection { @@ -9,10 +8,9 @@ internal class TypeInspectorRegistry private readonly ConcurrentDictionary _registry = new ConcurrentDictionary(); - public TypeInspector Register() => - Register(typeof(T)); + public TypeInspector For() => For(typeof(T)); - public TypeInspector Register(Type type) + public TypeInspector For(Type type) { if (_registry.TryGetValue(type, out var cached)) return cached; @@ -22,17 +20,6 @@ public TypeInspector Register(Type type) return inspector; } - public TypeInspector Of() => - Of(typeof(T)); - - public TypeInspector Of(Type t) - { - _ = _registry.TryGetValue(t, out var inspector); - return inspector ?? - throw new KeyNotFoundException($"No {nameof(TypeInspector)} for type '{t.Name}' was registered."); - } - - internal void Clear() => - _registry.Clear(); + internal void Clear() => _registry.Clear(); } } From 3beb1c31eec07e1b6b568bafd8d00b03bda46349 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 23 Dec 2020 14:43:04 +0100 Subject: [PATCH 12/31] Updates Aggregator using scopes and runners. --- src/NScatterGather/Aggregator.cs | 27 ++++++++++++++----------- src/NScatterGather/Internals/EnumBcl.cs | 18 +++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/NScatterGather/Internals/EnumBcl.cs diff --git a/src/NScatterGather/Aggregator.cs b/src/NScatterGather/Aggregator.cs index 17f9ea3..b3f5064 100644 --- a/src/NScatterGather/Aggregator.cs +++ b/src/NScatterGather/Aggregator.cs @@ -4,17 +4,20 @@ using System.Threading; using System.Threading.Tasks; using NScatterGather.Recipients; +using NScatterGather.Recipients.Collection.Scope; +using NScatterGather.Recipients.Run; using NScatterGather.Responses; -using NScatterGather.Run; namespace NScatterGather { public class Aggregator { - private readonly RecipientsCollection _recipients; + private readonly IRecipientsScope _scope; - public Aggregator(RecipientsCollection recipients) => - _recipients = recipients; + public Aggregator(RecipientsCollection collection) + { + _scope = collection.CreateScope(); + } public async Task> Send( object request, @@ -31,7 +34,7 @@ public Aggregator(RecipientsCollection recipients) => if (request is null) throw new ArgumentNullException(nameof(request)); - var recipients = _recipients.ListRecipientsAccepting(request.GetType()); + var recipients = _scope.ListRecipientsAccepting(request.GetType()); var invocations = await Invoke(recipients, request, cancellationToken) .ConfigureAwait(false); @@ -39,15 +42,15 @@ public Aggregator(RecipientsCollection recipients) => return AggregatedResponseFactory.CreateFrom(invocations); } - private async Task>> Invoke( + private async Task>> Invoke( IReadOnlyList recipients, object request, CancellationToken cancellationToken) { - var runners = recipients.Select(r => new RecipientRunner(r)).ToArray(); + var runners = recipients.Select(recipient => recipient.Accept(request)).ToArray(); var tasks = runners - .Select(runner => runner.Run(x => x.Accept(request))) + .Select(runner => runner.Start()) .ToArray(); var allTasksCompleted = Task.WhenAll(tasks); @@ -76,7 +79,7 @@ public async Task> Send( if (request is null) throw new ArgumentNullException(nameof(request)); - var recipients = _recipients.ListRecipientsReplyingWith(request.GetType(), typeof(TResponse)); + var recipients = _scope.ListRecipientsReplyingWith(request.GetType(), typeof(TResponse)); var runners = await Invoke(recipients, request, cancellationToken) .ConfigureAwait(false); @@ -84,15 +87,15 @@ public async Task> Send( return AggregatedResponseFactory.CreateFrom(runners); } - private async Task>> Invoke( + private async Task>> Invoke( IReadOnlyList recipients, object request, CancellationToken cancellationToken) { - var runners = recipients.Select(r => new RecipientRunner(r)).ToArray(); + var runners = recipients.Select(recipient => recipient.ReplyWith(request)).ToArray(); var tasks = runners - .Select(runner => runner.Run(x => x.ReplyWith(request))) + .Select(runner => runner.Start()) .ToArray(); var allTasksCompleted = Task.WhenAll(tasks); diff --git a/src/NScatterGather/Internals/EnumBcl.cs b/src/NScatterGather/Internals/EnumBcl.cs new file mode 100644 index 0000000..b28d7f9 --- /dev/null +++ b/src/NScatterGather/Internals/EnumBcl.cs @@ -0,0 +1,18 @@ +using System; + +namespace NScatterGather +{ + internal static class EnumBcl + { + public static bool IsValid(this TEnum value) where TEnum : struct, Enum + { +#if NETSTANDARD2_0 || NETSTANDARD2_1 + return Enum.IsDefined(typeof(TEnum), value); +#else + return Enum.IsDefined(value); +#endif + } + + } + +} From 338152b1fc43dbf2c96052b3e2517741008735db Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Thu, 24 Dec 2020 12:23:28 +0100 Subject: [PATCH 13/31] Updates tests with scopes and new runners. --- tests/NScatterGather.Tests/AggregatorTests.cs | 7 + .../Inspection/TypeInspectorRegistryTests.cs | 28 +- .../Inspection/TypeInspectorTests.cs | 16 +- .../Collection/RecipientsCollectionTests.cs | 280 ++++++++++++++++++ .../Recipients/DelegateRecipientTests.cs | 69 +++-- .../Factories/RecipientFactoryTests.cs | 29 ++ .../SingletonRecipientFactoryTests.cs | 41 +++ .../Recipients/InstanceRecipientTests.cs | 47 ++- .../Recipients/RecipientsCollectionTests.cs | 224 -------------- .../Run/RecipientRunTests.cs} | 112 ++----- .../Recipients/TypeRecipientTests.cs | 240 ++++++++++----- .../AggregatedResponseExtensionsTests.cs | 37 ++- .../Responses/AggregatedResponseTests.cs | 45 ++- .../_TestTypes/SomeComplexFaultingType.cs | 28 ++ 14 files changed, 737 insertions(+), 466 deletions(-) create mode 100644 tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs create mode 100644 tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs create mode 100644 tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs delete mode 100644 tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs rename tests/NScatterGather.Tests/{Run/RecipientRunnerTest.cs => Recipients/Run/RecipientRunTests.cs} (57%) create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeComplexFaultingType.cs diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index db9ea32..e9f8d0e 100644 --- a/tests/NScatterGather.Tests/AggregatorTests.cs +++ b/tests/NScatterGather.Tests/AggregatorTests.cs @@ -83,5 +83,12 @@ public async Task Responses_expose_the_recipient_name_and_type() Assert.Equal("Some never ending type", result.Incomplete[0].RecipientName); Assert.Equal(typeof(SomeNeverEndingType), result.Incomplete[0].RecipientType); } + + [Fact] + public void Error_if_request_is_null() + { + Assert.ThrowsAsync(() => _aggregator.Send((null as object)!)); + Assert.ThrowsAsync(() => _aggregator.Send((null as object)!)); + } } } diff --git a/tests/NScatterGather.Tests/Inspection/TypeInspectorRegistryTests.cs b/tests/NScatterGather.Tests/Inspection/TypeInspectorRegistryTests.cs index 7faf930..60f75e8 100644 --- a/tests/NScatterGather.Tests/Inspection/TypeInspectorRegistryTests.cs +++ b/tests/NScatterGather.Tests/Inspection/TypeInspectorRegistryTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Xunit; namespace NScatterGather.Inspection @@ -17,29 +16,28 @@ public TypeInspectorRegistryTests() [Fact] public void Can_register_generic_type() { - _registry.Register(); + _registry.For(); } [Fact] public void Can_register_type() { - _registry.Register(typeof(object)); + var inspector = _registry.For(typeof(object)); + Assert.NotNull(inspector); } [Fact] - public void Registered_type_inspector_can_be_found() + public void Inspector_are_cached() { - _registry.Register(); - - Assert.NotNull(_registry.Of()); - Assert.NotNull(_registry.Of(typeof(object))); - } - - [Fact] - public void Error_if_type_was_never_registered() - { - Assert.Throws(() => _registry.Of()); - Assert.Throws(() => _registry.Of(typeof(object))); + var inspector1 = _registry.For(typeof(object)); + var inspector2 = _registry.For(typeof(object)); + var inspector3 = _registry.For(typeof(int)); + var inspector4 = _registry.For(typeof(int)); + + Assert.Same(inspector1, inspector2); + Assert.NotSame(inspector2, inspector3); + Assert.NotEqual(inspector2, inspector3); + Assert.Same(inspector3, inspector4); } public void Dispose() diff --git a/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs b/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs index 2850dbe..8604d4d 100644 --- a/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs +++ b/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs @@ -1,9 +1,16 @@ -using Xunit; +using System; +using Xunit; namespace NScatterGather.Inspection { public class TypeInspectorTests { + [Fact] + public void Error_if_type_is_null() + { + Assert.Throws(() => new TypeInspector((null as Type)!)); + } + [Fact] public void Method_accepting_request_is_found() { @@ -29,5 +36,12 @@ public void Method_returning_response_is_found() Assert.True(found); Assert.Equal(typeof(SomeOtherType).GetMethod(nameof(SomeOtherType.Do)), method); } + + [Fact] + public void Error_if_request_type_is_null() + { + var inspector = new TypeInspector(typeof(SomeType)); + Assert.Throws(() => inspector.HasMethodAccepting((null as Type)!)); + } } } diff --git a/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs new file mode 100644 index 0000000..482e4b7 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Linq; +using Xunit; + +namespace NScatterGather.Recipients +{ + public class RecipientsCollectionTests + { + private readonly RecipientsCollection _collection; + + public RecipientsCollectionTests() + { + _collection = new RecipientsCollection(); + } + + [Fact] + public void Can_add_generic_type() + { + _collection.Add(); + } + + [Fact] + public void Can_add_generic_type_with_name() + { + _collection.Add(name: "My name is"); + } + + [Fact] + public void Can_add_generic_type_with_lifetime() + { + _collection.Add(lifetime: Lifetime.Transient); + } + + [Fact] + public void Can_add_generic_type_with_factory_method() + { + _collection.Add(() => new SomeType()); + } + + [Fact] + public void Error_if_factory_method_is_null() + { + Assert.Throws(() => + _collection.Add((null as Func)!)); + } + + [Fact] + public void Error_if_lifetime_is_not_valid() + { + Assert.Throws(() => + _collection.Add(lifetime: (Lifetime)42)); + } + + [Fact] + public void Can_add_instance() + { + _collection.Add(new SomeType()); + } + + [Fact] + public void Error_if_instance_is_null() + { + Assert.Throws(() => + _collection.Add((null as object)!)); + } + + [Fact] + public void Can_add_delegate() + { + _collection.Add((int n) => n.ToString()); + } + + [Fact] + public void Error_if_delegate_is_null() + { + Func func = null!; + Assert.Throws(() => _collection.Add(func)); + } + + [Fact] + public void Error_if_type_is_null() + { + Assert.Throws(() => _collection.Add((null as Type)!)); + } + + [Fact] + public void Can_add_recipient_instance() + { + _collection.Add(new SomeType()); + } + + [Fact] + public void Fail_if_recipient_instance_is_null() + { + Assert.Throws(() => _collection.Add((null as object)!)); + } + + [Fact] + public void Can_create_scope() + { + var initialScope = _collection.CreateScope(); + + _collection.Add(); + _collection.Add(); + _collection.Add(); + + var nonEmptyScope = _collection.CreateScope(); + + Assert.NotNull(initialScope); + Assert.Equal(0, initialScope.RecipientsCount); + Assert.NotNull(nonEmptyScope); + Assert.Equal(3, nonEmptyScope.RecipientsCount); + } + + //[Fact] + //public void Recipients_accepting_request_can_be_found() + //{ + // var empty = _collection.ListRecipientsAccepting(typeof(int)); + // Assert.Empty(empty); + + // _collection.Add(); + + // var one = _collection.ListRecipientsAccepting(typeof(int)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(one); + // Assert.Equal(typeof(SomeType), one.First().Type); + + // _collection.Add(); + + // var two = _collection.ListRecipientsAccepting(typeof(int)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Equal(2, two.Count); + // Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); + // Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); + + // _collection.Add(); + // var stillTwo = _collection.ListRecipientsAccepting(typeof(int)); + // Assert.Equal(2, stillTwo.Count); + + // var differentOne = _collection.ListRecipientsAccepting(typeof(string)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(differentOne); + // Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); + //} + + //[Fact] + //public void Recipients_returning_response_can_be_found() + //{ + // var empty = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); + // Assert.Empty(empty); + + // _collection.Add(); + + // var one = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(one); + // Assert.Equal(typeof(SomeType), one.First().Type); + + // _collection.Add(); + + // var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Equal(2, two.Count); + // Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); + // Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); + + // _collection.Add(); + // var stillTwo = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); + // Assert.Equal(2, stillTwo.Count); + + // var differentOne = _collection.ListRecipientsReplyingWith(typeof(string), typeof(int)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(differentOne); + // Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); + //} + + //[Fact] + //public void Recipients_with_request_collisions_are_ignored() + //{ + // _collection.Add(); + // _collection.Add(); + + // var onlyNonCollidingType = _collection.ListRecipientsAccepting(typeof(int)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(onlyNonCollidingType); + // Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); + //} + + //[Fact] + //public void Recipients_with_request_and_response_collisions_are_ignored() + //{ + // _collection.Add(); + // _collection.Add(); + + // var onlyNonCollidingType = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) + // .Where(x => x is TypeRecipient) + // .Cast() + // .ToList(); + + // Assert.Single(onlyNonCollidingType); + // Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); + //} + + //[Fact] + //public void Collisions_can_be_resolved_via_return_type() + //{ + // _collection.Add(); + // _collection.Add(); + + // var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); + // Assert.Equal(2, two.Count); + //} + + //[Fact] + //public void Can_be_cloned() + //{ + // _collection.Add(); + // _collection.Add(); + + // Assert.NotEmpty(_collection.RecipientTypes); + + // var clone = _collection.Clone(); + + // foreach (var type in _collection.RecipientTypes) + // Assert.Contains(type, clone.RecipientTypes); + //} + + //[Fact] + //public void Recipients_can_have_a_name() + //{ + // _collection.Add(name: "Some type"); + // _collection.Add(new SomeEchoType(), "Some other type"); + // _collection.Add((int n) => n.ToString(), "Delegate recipient"); + + // Assert.Equal(3, _collection.Recipients.Count); + + // foreach (var recipient in _collection.Recipients) + // { + // if (recipient is TypeRecipient tr) + // { + // if (tr.Type == typeof(SomeType)) + // Assert.Equal("Some type", tr.Name); + // else if (tr.Type == typeof(SomeEchoType)) + // Assert.Equal("Some other type", tr.Name); + // else + // throw new Xunit.Sdk.XunitException(); + // } + // else if (recipient is DelegateRecipient dr) + // { + // Assert.Equal("Delegate recipient", dr.Name); + // } + // else + // { + // throw new Xunit.Sdk.XunitException(); + // } + // } + //} + } +} diff --git a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs index 47a52b4..f929f3f 100644 --- a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Xunit; namespace NScatterGather.Recipients @@ -8,32 +9,33 @@ public class DelegateRecipientTests [Fact] public void Error_if_instance_is_null() { - Assert.Throws(() => DelegateRecipient.Create(null!)); + Assert.Throws(() => + DelegateRecipient.Create(null!, name: null)); } [Fact] public void Types_are_parsed_correctly() { static string? func(int? n) => n?.ToString(); - var recipient = DelegateRecipient.Create(func); - Assert.Equal(typeof(int?), recipient.In); - Assert.Equal(typeof(string), recipient.Out); + var recipient = DelegateRecipient.Create(func, name: null); + Assert.Equal(typeof(int?), recipient.RequestType); + Assert.Equal(typeof(string), recipient.ResponseType); } [Fact] public void Recipient_has_a_name() { static string? func(int? n) => n?.ToString(); - var recipient = DelegateRecipient.Create(func); - Assert.NotNull(recipient.GetRecipientName()); - Assert.NotEmpty(recipient.GetRecipientName()); + var recipient = DelegateRecipient.Create(func, name: "My name is"); + Assert.NotNull(recipient.Name); + Assert.NotEmpty(recipient.Name); } [Fact] public void Recipient_can_accept_request_type() { static string? func(int? n) => n?.ToString(); - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); Assert.True(recipient.CanAccept(typeof(int))); Assert.True(recipient.CanAccept(typeof(int?))); @@ -45,7 +47,7 @@ public void Recipient_can_accept_request_type() public void Recipient_can_reply_with_response_type() { static int? func(int? n) => n; - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); Assert.True(recipient.CanReplyWith(typeof(int), typeof(int))); Assert.True(recipient.CanReplyWith(typeof(int?), typeof(int))); @@ -59,22 +61,22 @@ public void Recipient_can_reply_with_response_type() public void Error_if_it_does_not_accept_the_request_type() { static string? func(int? n) => n?.ToString(); - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); - Assert.Throws(() => recipient.Invoke(DateTime.UtcNow)); + Assert.Throws(() => recipient.Accept(DateTime.UtcNow)); } [Fact] public void Error_if_it_does_not_reply_with_the_response_type() { static string? func(int? n) => n?.ToString(); - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); - Assert.Throws(() => recipient.Invoke(42)); + Assert.Throws(() => recipient.ReplyWith(42)); } [Fact] - public void Invokes_delegate_with_matching_request_type() + public async Task Invokes_delegate_with_matching_request_type() { bool invoked = false; @@ -84,17 +86,20 @@ public void Invokes_delegate_with_matching_request_type() return n?.ToString(); } - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); var input = 42; - var result = recipient.Invoke(input); + var runner = recipient.Accept(input); + await runner.Start(); + + var result = runner.Result; Assert.True(invoked); Assert.Equal(input.ToString(), result); } [Fact] - public void Invokes_delegate_with_matching_response_type() + public async Task Invokes_delegate_with_matching_response_type() { bool invoked = false; @@ -104,13 +109,39 @@ public void Invokes_delegate_with_matching_response_type() return n?.ToString(); } - var recipient = DelegateRecipient.Create(func); + var recipient = DelegateRecipient.Create(func, name: null); var input = 42; - var result = recipient.Invoke(input); + var runner = recipient.ReplyWith(input); + await runner.Start(); + + var result = runner.Result; Assert.True(invoked); Assert.Equal(input.ToString(), result); } + + [Fact] + public void Can_be_cloned() + { + static string? func(int? n) => n?.ToString(); + var recipient = DelegateRecipient.Create(func, name: "My name is"); + var clone = recipient.Clone(); + + Assert.NotNull(clone); + Assert.IsType(clone); + Assert.Equal(recipient.Name, clone.Name); + Assert.Equal(recipient.Lifetime, clone.Lifetime); + Assert.Equal(recipient.RequestType, (clone as DelegateRecipient).RequestType); + Assert.Equal(recipient.ResponseType, (clone as DelegateRecipient).ResponseType); + } + + [Fact] + public void Has_expected_lifetime() + { + static string? func(int? n) => n?.ToString(); + var recipient = DelegateRecipient.Create(func, name: null); + Assert.Equal(Lifetime.Singleton, recipient.Lifetime); + } } } diff --git a/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs b/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs new file mode 100644 index 0000000..a05f166 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs @@ -0,0 +1,29 @@ +using Xunit; + +namespace NScatterGather.Recipients.Factories +{ + public class RecipientFactoryTests + { + [Fact] + public void Factory_method_is_invoked() + { + var expectedInstance = new object(); + int count = 0; + + object factoryMethod() + { + count++; + return expectedInstance; + }; + + var factory = new RecipientFactory(factoryMethod); + + _ = factory.Get(); + _ = factory.Get(); + var instance = factory.Get(); + + Assert.Equal(expectedInstance, instance); + Assert.Equal(3, count); + } + } +} diff --git a/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs b/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs new file mode 100644 index 0000000..8c20673 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs @@ -0,0 +1,41 @@ +using Xunit; + +namespace NScatterGather.Recipients.Factories +{ + public class SingletonRecipientFactoryTests + { + [Fact] + public void Factory_method_is_invoked_once() + { + var expectedInstance = new object(); + int count = 0; + + object factoryMethod() + { + count++; + return expectedInstance; + }; + + var factory = new RecipientFactory(factoryMethod); + var singletonFactory = new SingletonRecipientFactory(factory); + + _ = singletonFactory.Get(); + _ = singletonFactory.Get(); + var instance = singletonFactory.Get(); + + Assert.Equal(expectedInstance, instance); + Assert.Equal(1, count); + } + + [Fact] + public void Factory_can_accept_instance() + { + var expectedInstance = new object(); + + var singletonFactory = new SingletonRecipientFactory(expectedInstance); + var instance = singletonFactory.Get(); + + Assert.Equal(expectedInstance, instance); + } + } +} diff --git a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs index 1fb2572..7cd46e1 100644 --- a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs @@ -1,4 +1,5 @@ using System; +using NScatterGather.Inspection; using Xunit; namespace NScatterGather.Recipients @@ -8,13 +9,55 @@ public class InstanceRecipientTests [Fact] public void Recipient_can_be_created_from_instance() { - _ = InstanceRecipient.Create(new SomeType()); + var registry = new TypeInspectorRegistry(); + _ = InstanceRecipient.Create(registry, new SomeType(), name: null); + } + + [Fact] + public void Error_if_registry_is_null() + { + Assert.Throws(() => + InstanceRecipient.Create((null as TypeInspectorRegistry)!, new SomeType(), name: null)); } [Fact] public void Error_if_instance_is_null() { - Assert.Throws(() => InstanceRecipient.Create((null as object)!)); + var registry = new TypeInspectorRegistry(); + + Assert.Throws(() => + InstanceRecipient.Create(registry, (null as object)!, name: null)); + } + + [Fact] + public void Recipient_has_a_name() + { + var registry = new TypeInspectorRegistry(); + var recipient = InstanceRecipient.Create(registry, new SomeType(), name: "My name is"); + Assert.NotNull(recipient.Name); + Assert.NotEmpty(recipient.Name); + } + + [Fact] + public void Can_be_cloned() + { + var registry = new TypeInspectorRegistry(); + var recipient = InstanceRecipient.Create(registry, new SomeType(), name: "My name is"); + var clone = recipient.Clone(); + + Assert.NotNull(clone); + Assert.IsType(clone); + Assert.Equal(recipient.Name, clone.Name); + Assert.Equal(recipient.Lifetime, clone.Lifetime); + Assert.Equal(recipient.Type, (clone as InstanceRecipient).Type); + } + + [Fact] + public void Has_expected_lifetime() + { + var registry = new TypeInspectorRegistry(); + var recipient = InstanceRecipient.Create(registry, new SomeType(), name: null); + Assert.Equal(Lifetime.Singleton, recipient.Lifetime); } } } diff --git a/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs deleted file mode 100644 index f2c12b5..0000000 --- a/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Linq; -using Xunit; - -namespace NScatterGather.Recipients -{ - public class RecipientsCollectionTests - { - private readonly RecipientsCollection _collection; - - public RecipientsCollectionTests() - { - _collection = new RecipientsCollection(); - } - - [Fact] - public void Can_add_generic_type() - { - _collection.Add(); - } - - [Fact] - public void Can_add_type() - { - _collection.Add(typeof(SomeType)); - } - - [Fact] - public void Can_add_delegate() - { - _collection.Add((int n) => n.ToString()); - } - - [Fact] - public void Error_if_delegate_is_null() - { - Func func = null!; - Assert.Throws(() => _collection.Add(func)); - } - - [Fact] - public void Error_if_type_is_null() - { - Assert.Throws(() => _collection.Add((null as Type)!)); - } - - [Fact] - public void Can_add_recipient_instance() - { - _collection.Add(new SomeType()); - } - - [Fact] - public void Fail_if_recipient_instance_is_null() - { - Assert.Throws(() => _collection.Add((null as object)!)); - } - - [Fact] - public void Recipients_accepting_request_can_be_found() - { - var empty = _collection.ListRecipientsAccepting(typeof(int)); - Assert.Empty(empty); - - _collection.Add(); - - var one = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(one); - Assert.Equal(typeof(SomeType), one.First().Type); - - _collection.Add(); - - var two = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Equal(2, two.Count); - Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); - - _collection.Add(); - var stillTwo = _collection.ListRecipientsAccepting(typeof(int)); - Assert.Equal(2, stillTwo.Count); - - var differentOne = _collection.ListRecipientsAccepting(typeof(string)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(differentOne); - Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); - } - - [Fact] - public void Recipients_returning_response_can_be_found() - { - var empty = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - Assert.Empty(empty); - - _collection.Add(); - - var one = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(one); - Assert.Equal(typeof(SomeType), one.First().Type); - - _collection.Add(); - - var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Equal(2, two.Count); - Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); - - _collection.Add(); - var stillTwo = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - Assert.Equal(2, stillTwo.Count); - - var differentOne = _collection.ListRecipientsReplyingWith(typeof(string), typeof(int)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(differentOne); - Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); - } - - [Fact] - public void Recipients_with_request_collisions_are_ignored() - { - _collection.Add(); - _collection.Add(); - - var onlyNonCollidingType = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(onlyNonCollidingType); - Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); - } - - [Fact] - public void Recipients_with_request_and_response_collisions_are_ignored() - { - _collection.Add(); - _collection.Add(); - - var onlyNonCollidingType = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - .Where(x => x is TypeRecipient) - .Cast() - .ToList(); - - Assert.Single(onlyNonCollidingType); - Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); - } - - [Fact] - public void Collisions_can_be_resolved_via_return_type() - { - _collection.Add(); - _collection.Add(); - - var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - Assert.Equal(2, two.Count); - } - - [Fact] - public void Can_be_cloned() - { - _collection.Add(); - _collection.Add(); - - Assert.NotEmpty(_collection.RecipientTypes); - - var clone = _collection.Clone(); - - foreach (var type in _collection.RecipientTypes) - Assert.Contains(type, clone.RecipientTypes); - } - - [Fact] - public void Recipients_can_have_a_name() - { - _collection.Add(name: "Some type"); - _collection.Add(new SomeEchoType(), "Some other type"); - _collection.Add((int n) => n.ToString(), "Delegate recipient"); - - Assert.Equal(3, _collection.Recipients.Count); - - foreach (var recipient in _collection.Recipients) - { - if (recipient is TypeRecipient tr) - { - if (tr.Type == typeof(SomeType)) - Assert.Equal("Some type", tr.Name); - else if (tr.Type == typeof(SomeEchoType)) - Assert.Equal("Some other type", tr.Name); - else - throw new Xunit.Sdk.XunitException(); - } - else if (recipient is DelegateRecipient dr) - { - Assert.Equal("Delegate recipient", dr.Name); - } - else - { - throw new Xunit.Sdk.XunitException(); - } - } - } - } -} diff --git a/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs b/tests/NScatterGather.Tests/Recipients/Run/RecipientRunTests.cs similarity index 57% rename from tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs rename to tests/NScatterGather.Tests/Recipients/Run/RecipientRunTests.cs index 2e22918..b69899c 100644 --- a/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs +++ b/tests/NScatterGather.Tests/Recipients/Run/RecipientRunTests.cs @@ -1,30 +1,36 @@ using System; using System.Threading.Tasks; +using NScatterGather.Inspection; using NScatterGather.Recipients; using Xunit; namespace NScatterGather.Run { - public class RecipientRunnerTest + public class RecipientRunTests { private readonly Recipient _recipient; + private readonly Recipient _faultingRecipient; + private readonly Recipient _anotherFaultingRecipient; - public RecipientRunnerTest() + public RecipientRunTests() { - _recipient = TypeRecipient.Create(); + var registry = new TypeInspectorRegistry(); + _recipient = InstanceRecipient.Create(registry, new SomeType(), name: null); + _faultingRecipient = InstanceRecipient.Create(registry, new SomeFaultingType(), name: null); + _anotherFaultingRecipient = InstanceRecipient.Create(registry, new SomeComplexFaultingType(), name: null); } [Fact] public void Can_be_created() { - var runner = new RecipientRunner(_recipient); + var runner = _recipient.Accept(42); Assert.Same(_recipient, runner.Recipient); } [Fact] public void Initially_has_default_parameters() { - var runner = new RecipientRunner(_recipient); + var runner = _recipient.Accept(42); Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); Assert.False(runner.Faulted); @@ -36,36 +42,19 @@ public void Initially_has_default_parameters() [Fact] public async Task Error_if_started_multiple_times() { - var runner = new RecipientRunner(_recipient); - - await runner.Run(_ => Task.FromResult(42)); - - await Assert.ThrowsAsync(() => runner.Run(_ => Task.FromResult(42))); - } - - [Fact] - public void Error_if_recipient_is_null() - { - Assert.Throws(() => new RecipientRunner((null as Recipient)!)); - } - - [Fact] - public void Error_if_invocation_is_null() - { - var runner = new RecipientRunner(_recipient); - - Assert.ThrowsAsync(() => runner.Run(null!)); + var runner = _recipient.Accept(42); + await runner.Start(); + await Assert.ThrowsAsync(() => runner.Start()); } [Fact] public async Task Runner_completes() { - var runner = new RecipientRunner(_recipient); - - await runner.Run(_ => Task.FromResult(42)); + var runner = _recipient.Accept(42); + await runner.Start(); Assert.True(runner.CompletedSuccessfully); - Assert.Equal(42, runner.Result); + Assert.Equal("42", runner.Result); Assert.False(runner.Faulted); Assert.Null(runner.Exception); @@ -78,15 +67,8 @@ public async Task Runner_completes() [Fact] public async Task Runner_fails() { - var runner = new RecipientRunner(_recipient); - - var ex = new Exception(); - - await runner.Run(async _ => - { - await Task.Yield(); - throw ex; - }); + var runner = _faultingRecipient.Accept(42); + await runner.Start(); Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); @@ -96,21 +78,14 @@ await runner.Run(async _ => Assert.NotEqual(default, runner.FinishedAt); Assert.True(runner.FinishedAt >= runner.StartedAt); - Assert.Equal(ex, runner.Exception); + Assert.NotNull(runner.Exception); } [Fact] public async Task Exception_is_extracted() { - var runner = new RecipientRunner(_recipient); - - var ex = new Exception(); - - await runner.Run(_ => - { - Fail().Wait(); - return Task.FromResult(42); - }); + var runner = _faultingRecipient.Accept(42); + await runner.Start(); Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); @@ -122,29 +97,14 @@ await runner.Run(_ => Assert.True(runner.FinishedAt >= runner.StartedAt); Assert.NotNull(runner.Exception); - Assert.Equal(ex, runner.Exception); - - // Local functions. - - async Task Fail() - { - await Task.Delay(10); - throw ex; - } + Assert.Equal("A failure.", runner.Exception!.Message); } [Fact] public async Task Aggregated_exceptions_are_decomposed() { - var runner = new RecipientRunner(_recipient); - - var ex = new Exception(); - - await runner.Run(_ => - { - Task.WhenAll(Fail(), Fail(), Fail()).Wait(); - return Task.FromResult(42); - }); + var runner = _anotherFaultingRecipient.Accept(42); + await runner.Start(); Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); @@ -163,28 +123,17 @@ await runner.Run(_ => Assert.Equal(3, aggEx.InnerExceptions.Count); foreach (var exception in aggEx.InnerExceptions) - Assert.Equal(ex, exception); - - // Local functions. - - async Task Fail() { - await Task.Delay(10); - throw ex; + Assert.NotNull(exception); + Assert.Equal("A failure.", exception!.Message); } } [Fact] public async Task Reflection_exception_are_decomposed() { - var runner = new RecipientRunner(_recipient); - - var ex = new Exception(); - - await runner.Run(_ => - { - throw new System.Reflection.TargetInvocationException(ex); - }); + var runner = _anotherFaultingRecipient.Accept(42L); + await runner.Start(); Assert.False(runner.CompletedSuccessfully); Assert.Equal(default, runner.Result); @@ -194,7 +143,8 @@ await runner.Run(_ => Assert.NotEqual(default, runner.FinishedAt); Assert.True(runner.FinishedAt >= runner.StartedAt); - Assert.Equal(ex, runner.Exception); + Assert.NotNull(runner.Exception); + Assert.Equal("An invocation failure.", runner.Exception!.Message); } } } diff --git a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs index 832e8f2..86097f3 100644 --- a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using NScatterGather.Inspection; using Xunit; namespace NScatterGather.Recipients @@ -9,131 +10,214 @@ public class TypeRecipientTests [Fact] public void Recipient_can_be_created_from_type() { - _ = TypeRecipient.Create(); + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + () => new SomeType(), + name: null, + lifetime: Lifetime.Transient); } [Fact] - public void Error_if_no_empty_constructor() + public void Error_if_registry_is_null() { - Assert.Throws(() => TypeRecipient.Create()); + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + (null as Func)!, + name: null, + lifetime: Lifetime.Transient); + }); } [Fact] - public async Task Recipient_accepts_request() + public void Error_if_no_factory_method() { - var recipient = TypeRecipient.Create(); - var input = 42; - var response = await recipient.Accept(input); - Assert.Equal(input.ToString(), response); + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + (null as Func)!, + name: null, + lifetime: Lifetime.Transient); + }); } [Fact] - public async Task Recipient_accepts_request_and_replies_with_task() + public void Error_if_invalid_lifetime() { - var recipient = TypeRecipient.Create(); - var input = 42; - var response = await recipient.Accept(input); - Assert.Equal(input.ToString(), response); + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + () => new SomeType(), + name: null, + lifetime: (Lifetime)42); + }); } [Fact] - public void Error_if_request_type_is_null() + public void Recipient_has_a_name() { - var recipient = TypeRecipient.Create(); - Assert.Throws(() => recipient.CanAccept(null!)); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: "My name is", Lifetime.Transient); + Assert.NotNull(recipient.Name); + Assert.NotEmpty(recipient.Name); } [Fact] - public void Recipient_accepts_input_parameter_type() + public async Task Recipient_accepts_request() { - var recipient = TypeRecipient.Create(); - bool canAccept = recipient.CanAccept(typeof(int)); - Assert.True(canAccept); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + var input = 42; + var runner = recipient.Accept(input); + await runner.Start(); + Assert.Equal(input.ToString(), runner.Result); } [Fact] - public void Recipient_type_is_visible() + public async Task Recipient_accepts_request_and_replies_with_task() { - var recipient = TypeRecipient.Create(); - Assert.Same(typeof(SomeType), recipient.Type); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeAsyncType(), name: null, Lifetime.Transient); + var input = 42; + var runner = recipient.Accept(input); + await runner.Start(); + Assert.Equal(input.ToString(), runner.Result); } [Fact] - public void Recipient_replies_with_response_type() + public void Recipient_accepts_input_parameter_type() { - var recipient = TypeRecipient.Create(); - bool canAccept = recipient.CanReplyWith(typeof(int), typeof(string)); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + bool canAccept = recipient.CanAccept(typeof(int)); Assert.True(canAccept); } [Fact] - public void Error_if_request_or_response_types_are_null() - { - var recipient = TypeRecipient.Create(); - Assert.Throws(() => recipient.CanReplyWith(typeof(int), null!)); - Assert.Throws(() => recipient.CanReplyWith(null!, typeof(string))); - } - - [Fact] - public void Error_if_request_type_not_supported() - { - var recipient = TypeRecipient.Create(); - Assert.ThrowsAsync(() => recipient.Accept(Guid.NewGuid())); - } - - [Fact] - public void Error_if_response_type_not_supported() - { - var recipient = TypeRecipient.Create(); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); - } - - [Fact] - public async Task Recipient_replies_with_response() - { - var recipient = TypeRecipient.Create(); - var input = 42; - var response = await recipient.ReplyWith(input); - Assert.Equal(input.ToString(), response); - } - - [Fact] - public async Task Recipient_can_reply_with_task() + public void Recipient_type_is_visible() { - var recipient = TypeRecipient.Create(); - var input = 42; - var response = await recipient.ReplyWith(input); - Assert.Equal(input.ToString(), response); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + Assert.Same(typeof(SomeType), recipient.Type); } [Fact] - public void Recipient_must_return_something() + public void Recipient_replies_with_response_type() { - var recipient = TypeRecipient.Create(); - bool accepts = recipient.CanAccept(typeof(int)); - Assert.False(accepts); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + bool canAccept = recipient.CanReplyWith(typeof(int), typeof(string)); + Assert.True(canAccept); } [Fact] - public void Error_if_void_returning() + public void Recipient_replies_with_async_response_type() { - var recipient = TypeRecipient.Create(); - Assert.ThrowsAsync(() => recipient.Accept(42)); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeAsyncType(), name: null, Lifetime.Transient); + bool canAccept = recipient.CanReplyWith(typeof(int), typeof(string)); + Assert.True(canAccept); } [Fact] - public void Recipient_must_return_something_async() + public void Error_if_request_or_response_types_are_null() { - var recipient = TypeRecipient.Create(); - bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); - Assert.False(accepts); + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + Assert.Throws(() => recipient.CanReplyWith(typeof(int), null!)); + Assert.Throws(() => recipient.CanReplyWith(null!, typeof(string))); } - [Fact] - public void Error_if_returning_task_without_result() - { - var recipient = TypeRecipient.Create(); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + //[Fact] + //public void Error_if_request_type_not_supported() + //{ + // var recipient = TypeRecipient.Create(); + // Assert.ThrowsAsync(() => recipient.Accept(Guid.NewGuid())); + //} + + //[Fact] + //public void Error_if_response_type_not_supported() + //{ + // var recipient = TypeRecipient.Create(); + // Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + //} + + //[Fact] + //public async Task Recipient_replies_with_response() + //{ + // var recipient = TypeRecipient.Create(); + // var input = 42; + // var response = await recipient.ReplyWith(input); + // Assert.Equal(input.ToString(), response); + //} + + //[Fact] + //public async Task Recipient_can_reply_with_task() + //{ + // var recipient = TypeRecipient.Create(); + // var input = 42; + // var response = await recipient.ReplyWith(input); + // Assert.Equal(input.ToString(), response); + //} + + //[Fact] + //public void Recipient_must_return_something() + //{ + // var recipient = TypeRecipient.Create(); + // bool accepts = recipient.CanAccept(typeof(int)); + // Assert.False(accepts); + //} + + //[Fact] + //public void Error_if_void_returning() + //{ + // var recipient = TypeRecipient.Create(); + // Assert.ThrowsAsync(() => recipient.Accept(42)); + //} + + //[Fact] + //public void Recipient_must_return_something_async() + //{ + // var recipient = TypeRecipient.Create(); + // bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); + // Assert.False(accepts); + //} + + //[Fact] + //public void Error_if_returning_task_without_result() + //{ + // var recipient = TypeRecipient.Create(); + // Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + //} + + [Fact] + public void Can_be_cloned() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + var clone = recipient.Clone(); + + Assert.NotNull(clone); + Assert.IsType(clone); + Assert.Equal(recipient.Name, clone.Name); + Assert.Equal(recipient.Lifetime, clone.Lifetime); + Assert.Equal(recipient.Type, (clone as TypeRecipient).Type); + } + + [Fact] + public void Has_expected_lifetime() + { + var registry = new TypeInspectorRegistry(); + var transientRecipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + var scopedRecipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Scoped); + var singletonRecipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Singleton); + + Assert.Equal(Lifetime.Transient, transientRecipient.Lifetime); + Assert.Equal(Lifetime.Scoped, scopedRecipient.Lifetime); + Assert.Equal(Lifetime.Singleton, singletonRecipient.Lifetime); } } } diff --git a/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs b/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs index f684195..0bd6a18 100644 --- a/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs +++ b/tests/NScatterGather.Tests/Responses/AggregatedResponseExtensionsTests.cs @@ -1,36 +1,33 @@ using System; using System.Linq; -using System.Threading.Tasks; +using NScatterGather.Inspection; using NScatterGather.Recipients; -using NScatterGather.Run; +using NScatterGather.Recipients.Run; using Xunit; namespace NScatterGather.Responses { public class AggregatedResponseExtensionsTests { - private readonly RecipientRunner[] _runners; + private readonly RecipientRun[] _runners; public AggregatedResponseExtensionsTests() { - var runner = new RecipientRunner(InstanceRecipient.Create(42)); - runner.Run(_ => Task.FromResult(42)).Wait(); + var registry = new TypeInspectorRegistry(); - var runnerFaulted = new RecipientRunner(InstanceRecipient.Create(42f)); - runnerFaulted.Run(_ => Task.FromException(new Exception())).Wait(); + var someRecipient = InstanceRecipient.Create(registry, new SomeType(), name: null); + var someRun = someRecipient.Accept(42); + someRun.Start().Wait(); - var runnerIncomplete = new RecipientRunner(InstanceRecipient.Create(42L)); - runnerIncomplete.Run(_ => GetInfiniteTask()); + var someFaultingRecipient = InstanceRecipient.Create(registry, new SomeFaultingType(), name: null); + var someFaultingRun = someFaultingRecipient.Accept(42); + someFaultingRun.Start().Wait(); - _runners = new[] { runner, runnerFaulted, runnerIncomplete }; + var someNeverEndingRecipient = InstanceRecipient.Create(registry, new SomeNeverEndingType(), name: null); + var someNeverEndingRun = someNeverEndingRecipient.Accept(42); + someNeverEndingRun.Start(); - // Local functions. - - static Task GetInfiniteTask() - { - var source = new TaskCompletionSource(); - return source.Task; - } + _runners = new[] { someRun, someFaultingRun, someNeverEndingRun }; } [Fact] @@ -50,9 +47,9 @@ public void Can_be_projected_onto_results_dictionary() var results = response.AsResultsDictionary(); Assert.NotNull(results); Assert.Single(results.Keys); - Assert.Equal(typeof(int), results.Keys.First()); + Assert.Equal(typeof(SomeType), results.Keys.First()); Assert.Single(results.Values); - Assert.Equal(42, results.Values.First()); + Assert.Equal("42", results.Values.First()); } [Fact] @@ -61,7 +58,7 @@ public void Can_be_projected_onto_results_list() var response = AggregatedResponseFactory.CreateFrom(_runners); var results = response.AsResultsList(); Assert.NotNull(results); - Assert.Single(results, 42); + Assert.Single(results, "42"); } } } diff --git a/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs b/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs index 60805e8..d75f643 100644 --- a/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs +++ b/tests/NScatterGather.Tests/Responses/AggregatedResponseTests.cs @@ -1,38 +1,31 @@ -using System; -using System.Threading.Tasks; +using NScatterGather.Inspection; using NScatterGather.Recipients; -using NScatterGather.Run; +using NScatterGather.Recipients.Run; using Xunit; namespace NScatterGather.Responses { public class AggregatedResponseTests { - private readonly RecipientRunner[] _runners; - private readonly Exception _ex; + private readonly RecipientRun[] _runners; public AggregatedResponseTests() { - _ex = new Exception("Test ex."); + var registry = new TypeInspectorRegistry(); - var runner = new RecipientRunner(InstanceRecipient.Create(42)); - runner.Run(_ => Task.FromResult(42)).Wait(); + var someRecipient = InstanceRecipient.Create(registry, new SomeType(), name: null); + var someRun = someRecipient.Accept(42); + someRun.Start().Wait(); - var runnerFaulted = new RecipientRunner(InstanceRecipient.Create(42f)); - runnerFaulted.Run(_ => Task.FromException(_ex)).Wait(); + var someFaultingRecipient = InstanceRecipient.Create(registry, new SomeFaultingType(), name: null); + var someFaultingRun = someFaultingRecipient.Accept(42); + someFaultingRun.Start().Wait(); - var runnerIncomplete = new RecipientRunner(InstanceRecipient.Create(42L)); - runnerIncomplete.Run(_ => GetInfiniteTask()); + var someNeverEndingRecipient = InstanceRecipient.Create(registry, new SomeNeverEndingType(), name: null); + var someNeverEndingRun = someNeverEndingRecipient.Accept(42); + someNeverEndingRun.Start(); - _runners = new[] { runner, runnerFaulted, runnerIncomplete }; - - // Local functions. - - static Task GetInfiniteTask() - { - var source = new TaskCompletionSource(); - return source.Task; - } + _runners = new[] { someRun, someFaultingRun, someNeverEndingRun }; } [Fact] @@ -50,13 +43,13 @@ public void Invocations_are_grouped_correctly() { var response = AggregatedResponseFactory.CreateFrom(_runners); - Assert.Equal(typeof(int), response.Completed[0].RecipientType); - Assert.Equal(42, response.Completed[0].Result); + Assert.Equal(typeof(SomeType), response.Completed[0].RecipientType); + Assert.Equal("42", response.Completed[0].Result); - Assert.Equal(typeof(float), response.Faulted[0].RecipientType); - Assert.Equal(_ex, response.Faulted[0].Exception); + Assert.Equal(typeof(SomeFaultingType), response.Faulted[0].RecipientType); + Assert.Equal("A failure.", response.Faulted[0].Exception?.Message); - Assert.Equal(typeof(long), response.Incomplete[0].RecipientType); + Assert.Equal(typeof(SomeNeverEndingType), response.Incomplete[0].RecipientType); } [Fact] diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeComplexFaultingType.cs b/tests/NScatterGather.Tests/_TestTypes/SomeComplexFaultingType.cs new file mode 100644 index 0000000..a9fa872 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeComplexFaultingType.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace NScatterGather +{ + public class SomeComplexFaultingType + { + public Task Fail(int n) + { + Task.WhenAll(Fail(), Fail(), Fail()).Wait(); + return Task.FromResult(n); + + // Local functions. + + static async Task Fail() + { + await Task.Delay(10); + throw new Exception("A failure."); + } + } + + public Task FailInvocation(long n) + { + throw new TargetInvocationException(new Exception("An invocation failure.")); + } + } +} From 273ee1114c90ba847b7e3dfc947e7f29dabb529f Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Fri, 25 Dec 2020 15:41:20 +0100 Subject: [PATCH 14/31] Updates to v0.4.0. Merry Xmas. --- src/NScatterGather/NScatterGather.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NScatterGather/NScatterGather.csproj b/src/NScatterGather/NScatterGather.csproj index d6d3345..779c713 100644 --- a/src/NScatterGather/NScatterGather.csproj +++ b/src/NScatterGather/NScatterGather.csproj @@ -4,7 +4,7 @@ net5.0;netstandard2.1;netstandard2.0 latest enable - 0.3.0 + 0.4.0 true false From bb438c2c7144d11d98fb5e33c42c008e72c66ab9 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sat, 26 Dec 2020 13:20:33 +0100 Subject: [PATCH 15/31] Updates Spectre.Console. --- .../NScatterGather.Samples.CompetingTasks.csproj | 2 +- samples/NScatterGather.Samples/NScatterGather.Samples.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/NScatterGather.Samples.CompetingTasks/NScatterGather.Samples.CompetingTasks.csproj b/samples/NScatterGather.Samples.CompetingTasks/NScatterGather.Samples.CompetingTasks.csproj index bba25ed..913ed40 100644 --- a/samples/NScatterGather.Samples.CompetingTasks/NScatterGather.Samples.CompetingTasks.csproj +++ b/samples/NScatterGather.Samples.CompetingTasks/NScatterGather.Samples.CompetingTasks.csproj @@ -13,7 +13,7 @@ - + diff --git a/samples/NScatterGather.Samples/NScatterGather.Samples.csproj b/samples/NScatterGather.Samples/NScatterGather.Samples.csproj index bba25ed..913ed40 100644 --- a/samples/NScatterGather.Samples/NScatterGather.Samples.csproj +++ b/samples/NScatterGather.Samples/NScatterGather.Samples.csproj @@ -13,7 +13,7 @@ - + From caf44cfdd8ae7b0e102a65f830bc8ca3d2ac8747 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sat, 26 Dec 2020 13:21:37 +0100 Subject: [PATCH 16/31] Fixes naming and index of results in samples. --- .../NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs b/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs index 8efc052..30211f4 100644 --- a/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs +++ b/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs @@ -32,9 +32,9 @@ public async Task Run() // or ValueTask get invoked. var response2 = await aggregator.Send(42); - var guidResults = response2.AsResultsList(); - Console.WriteLine($"{guidResults[0]} ({guidResults[0].GetType().Name})"); - Console.WriteLine($"{guidResults[0]} ({guidResults[1].GetType().Name})"); + var longResults = response2.AsResultsList(); + Console.WriteLine($"{longResults[0]} ({longResults[0].GetType().Name})"); + Console.WriteLine($"{longResults[1]} ({longResults[1].GetType().Name})"); var response3 = await aggregator.Send(42); var stringResults = response3.AsResultsList(); From b5c4177a692b14954101448664f1c46ec2db7f4c Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 27 Dec 2020 11:32:38 +0100 Subject: [PATCH 17/31] Moves handlers to their own file. --- src/NScatterGather/Recipients/Collection/Handlers.cs | 8 ++++++++ .../Recipients/Collection/RecipientsCollection.cs | 9 +++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/NScatterGather/Recipients/Collection/Handlers.cs diff --git a/src/NScatterGather/Recipients/Collection/Handlers.cs b/src/NScatterGather/Recipients/Collection/Handlers.cs new file mode 100644 index 0000000..b1c4ae4 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/Handlers.cs @@ -0,0 +1,8 @@ +using System; + +namespace NScatterGather +{ + public delegate void CollisionHandler(CollisionException ex); + + public delegate void ErrorHandler(Exception ex); +} diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs index 1a33869..2a4b498 100644 --- a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -7,16 +7,17 @@ namespace NScatterGather { - public delegate void CollisionHandler(CollisionException ex); - - public delegate void ErrorHandler(Exception ex); - public class RecipientsCollection { public event CollisionHandler? OnCollision; public event ErrorHandler? OnError; + public int RecipientsCount => + _singletonRecipients.Count + + _scopedRecipients.Count + + _transientRecipients.Count; + private readonly List _singletonRecipients = new(); private readonly List _scopedRecipients = new(); private readonly List _transientRecipients = new(); From db8fb3b4fc1c48985ccad1b98edb90f286aad4f1 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 27 Dec 2020 12:04:09 +0100 Subject: [PATCH 18/31] Introduces Clone on recipient factories. --- .../Recipients/Factories/IRecipientFactory.cs | 2 ++ .../Recipients/Factories/RecipientFactory.cs | 3 +++ .../Recipients/Factories/SingletonRecipientFactory.cs | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs index fb6e02f..2081119 100644 --- a/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs +++ b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs @@ -3,5 +3,7 @@ internal interface IRecipientFactory { object Get(); + + IRecipientFactory Clone(); } } diff --git a/src/NScatterGather/Recipients/Factories/RecipientFactory.cs b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs index 2dfb8b4..5c42085 100644 --- a/src/NScatterGather/Recipients/Factories/RecipientFactory.cs +++ b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs @@ -12,5 +12,8 @@ public RecipientFactory(Func factory) } public object Get() => _factory(); + + public IRecipientFactory Clone() => + new RecipientFactory(_factory); } } diff --git a/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs b/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs index d250c67..c61dae6 100644 --- a/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs +++ b/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs @@ -4,6 +4,7 @@ namespace NScatterGather.Recipients.Factories { internal class SingletonRecipientFactory : IRecipientFactory { + private readonly IRecipientFactory? _anotherFactory; private readonly Lazy _lazyInstance; public SingletonRecipientFactory(object instance) @@ -17,9 +18,15 @@ public SingletonRecipientFactory(object instance) public SingletonRecipientFactory(IRecipientFactory anotherFactory) { + _anotherFactory = anotherFactory; _lazyInstance = new Lazy(anotherFactory.Get); } public object Get() => _lazyInstance.Value; + + public IRecipientFactory Clone() => + _anotherFactory is null + ? new SingletonRecipientFactory(_lazyInstance.Value) + : new SingletonRecipientFactory(_anotherFactory.Clone()); } } From 9e4e9c546097588ac08eb582add7c815aae510b8 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 27 Dec 2020 12:04:39 +0100 Subject: [PATCH 19/31] Introduces Clone on recipient invokers. --- .../Recipients/Invokers/DelegateRecipientInvoker.cs | 3 +++ src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs | 2 ++ .../Recipients/Invokers/InstanceRecipientInvoker.cs | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs index cc75f4f..4c64313 100644 --- a/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs +++ b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs @@ -36,5 +36,8 @@ public PreparedInvocation PrepareInvocation(object request) return new PreparedInvocation(() => (TResult)_delegate(request)!); } + + public IRecipientInvoker Clone() => + new DelegateRecipientInvoker(_descriptor, _delegate); } } diff --git a/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs index d1fd164..cc4ea45 100644 --- a/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs +++ b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs @@ -5,5 +5,7 @@ internal interface IRecipientInvoker PreparedInvocation PrepareInvocation(object request); PreparedInvocation PrepareInvocation(object request); + + IRecipientInvoker Clone(); } } diff --git a/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs index 5fd8b31..d428a3b 100644 --- a/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs +++ b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs @@ -43,5 +43,11 @@ public PreparedInvocation PrepareInvocation(object request) return new PreparedInvocation(invocation: () => method.Invoke(recipientInstance, new object?[] { request })!); } + + public IRecipientInvoker Clone() + { + var factory = _factory.Clone(); + return new InstanceRecipientInvoker(_inspector, factory); + } } } From 9e34a785925926cc88a7f798562a840d81b60862 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Sun, 27 Dec 2020 14:31:53 +0100 Subject: [PATCH 20/31] Accepts abstract arguments for descriptor and invoker. --- .../Recipients/DelegateRecipient.cs | 23 ++++++++----------- .../Recipients/InstanceRecipient.cs | 16 ++++++------- .../Recipients/TypeRecipient.cs | 14 +++++------ 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/NScatterGather/Recipients/DelegateRecipient.cs b/src/NScatterGather/Recipients/DelegateRecipient.cs index 729d9b4..c5c3cb2 100644 --- a/src/NScatterGather/Recipients/DelegateRecipient.cs +++ b/src/NScatterGather/Recipients/DelegateRecipient.cs @@ -10,9 +10,6 @@ internal class DelegateRecipient : Recipient public Type ResponseType { get; } - private readonly DelegateRecipientDescriptor _typedDescriptor; - private readonly DelegateRecipientInvoker _typedInvoker; - public static DelegateRecipient Create( Func @delegate, string? name) @@ -30,25 +27,25 @@ public static DelegateRecipient Create( var descriptor = new DelegateRecipientDescriptor(typeof(TRequest), typeof(TResponse)); var invoker = new DelegateRecipientInvoker(descriptor, delegateInvoker); - return new DelegateRecipient(descriptor, invoker, name); + return new DelegateRecipient(descriptor.RequestType, descriptor.ResponseType, descriptor, invoker, name); } protected DelegateRecipient( - DelegateRecipientDescriptor descriptor, - DelegateRecipientInvoker invoker, + Type requestType, + Type responseType, + IRecipientDescriptor descriptor, + IRecipientInvoker invoker, string? name) : base(descriptor, invoker, name, Lifetime.Singleton) { - _typedDescriptor = descriptor; - _typedInvoker = invoker; - - RequestType = descriptor.RequestType; - ResponseType = descriptor.ResponseType; + RequestType = requestType; + ResponseType = responseType; } #if NETSTANDARD2_0 || NETSTANDARD2_1 - public override Recipient Clone() => new DelegateRecipient(_typedDescriptor, _typedInvoker, Name); + public override Recipient Clone() => #else - public override DelegateRecipient Clone() => new(_typedDescriptor, _typedInvoker, Name); + public override DelegateRecipient Clone() => #endif + new DelegateRecipient(RequestType, ResponseType, _descriptor, _invoker, Name); } } diff --git a/src/NScatterGather/Recipients/InstanceRecipient.cs b/src/NScatterGather/Recipients/InstanceRecipient.cs index 4013189..f57b202 100644 --- a/src/NScatterGather/Recipients/InstanceRecipient.cs +++ b/src/NScatterGather/Recipients/InstanceRecipient.cs @@ -9,8 +9,6 @@ namespace NScatterGather.Recipients internal class InstanceRecipient : TypeRecipient { private readonly object _instance; - private readonly TypeRecipientDescriptor _typedDescriptor; - private readonly InstanceRecipientInvoker _typedInvoker; public static InstanceRecipient Create( TypeInspectorRegistry registry, @@ -39,19 +37,21 @@ public static InstanceRecipient Create( protected InstanceRecipient( object instance, - TypeRecipientDescriptor descriptor, - InstanceRecipientInvoker invoker, + IRecipientDescriptor descriptor, + IRecipientInvoker invoker, string? name) : base(instance.GetType(), descriptor, invoker, name, Lifetime.Singleton) { _instance = instance; - _typedDescriptor = descriptor; - _typedInvoker = invoker; } #if NETSTANDARD2_0 || NETSTANDARD2_1 - public override Recipient Clone() => new InstanceRecipient(_instance, _typedDescriptor, _typedInvoker, Name); + public override Recipient Clone() #else - public override InstanceRecipient Clone() => new(_instance, _typedDescriptor, _typedInvoker, Name); + public override InstanceRecipient Clone() #endif + { + var invoker = _invoker.Clone(); + return new InstanceRecipient(_instance, _descriptor, invoker, Name); + } } } diff --git a/src/NScatterGather/Recipients/TypeRecipient.cs b/src/NScatterGather/Recipients/TypeRecipient.cs index 8d37635..1729462 100644 --- a/src/NScatterGather/Recipients/TypeRecipient.cs +++ b/src/NScatterGather/Recipients/TypeRecipient.cs @@ -8,8 +8,6 @@ namespace NScatterGather.Recipients { internal class TypeRecipient : Recipient { - private readonly TypeRecipientDescriptor _typedDescriptor; - public Type Type { get; } public static TypeRecipient Create( @@ -44,20 +42,22 @@ public static TypeRecipient Create( protected TypeRecipient( Type type, - TypeRecipientDescriptor descriptor, + IRecipientDescriptor descriptor, IRecipientInvoker invoker, string? name, Lifetime lifetime) : base(descriptor, invoker, name, lifetime) { - _typedDescriptor = descriptor; - Type = type; } #if NETSTANDARD2_0 || NETSTANDARD2_1 - public override Recipient Clone() => new TypeRecipient(Type, _typedDescriptor, _invoker, Name, Lifetime); + public override Recipient Clone() #else - public override TypeRecipient Clone() => new(Type, _typedDescriptor, _invoker, Name, Lifetime); + public override TypeRecipient Clone() #endif + { + var invoker = _invoker.Clone(); + return new TypeRecipient(Type, _descriptor, invoker, Name, Lifetime); + } } } From 68f57c42466bca08e13da1e771718551fe1341a7 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Mon, 28 Dec 2020 11:46:19 +0100 Subject: [PATCH 21/31] Simplifies extraction of Duration. --- src/NScatterGather/Recipients/Run/RecipientRun.cs | 12 +++++++----- .../Responses/AggregatedResponseFactory.cs | 12 ++---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/NScatterGather/Recipients/Run/RecipientRun.cs b/src/NScatterGather/Recipients/Run/RecipientRun.cs index 6203e5b..1e69762 100644 --- a/src/NScatterGather/Recipients/Run/RecipientRun.cs +++ b/src/NScatterGather/Recipients/Run/RecipientRun.cs @@ -20,9 +20,11 @@ internal class RecipientRun public Exception? Exception { get; set; } - public DateTime? StartedAt { get; private set; } + public DateTime StartedAt { get; private set; } - public DateTime? FinishedAt { get; private set; } + public DateTime FinishedAt { get; private set; } + + public TimeSpan Duration => FinishedAt - StartedAt; private readonly PreparedInvocation _preparedInvocation; @@ -34,13 +36,13 @@ public RecipientRun(Recipient recipient, PreparedInvocation preparedInv public Task Start() { - if (StartedAt.HasValue) + if (StartedAt != default) throw new InvalidOperationException("Run already executed."); - var runnerTask = Task.Run(async () => await _preparedInvocation.Execute()); - StartedAt = DateTime.UtcNow; + var runnerTask = Task.Run(async () => await _preparedInvocation.Execute()); + var tcs = new TaskCompletionSource(); runnerTask.ContinueWith(completedTask => diff --git a/src/NScatterGather/Responses/AggregatedResponseFactory.cs b/src/NScatterGather/Responses/AggregatedResponseFactory.cs index 0d30101..076bdaf 100644 --- a/src/NScatterGather/Responses/AggregatedResponseFactory.cs +++ b/src/NScatterGather/Responses/AggregatedResponseFactory.cs @@ -24,7 +24,7 @@ public static AggregatedResponse CreateFrom( invocation.Recipient.Name, recipientType, invocation.Result, - GetDuration(invocation)); + invocation.Duration); completed.Add(completedInvocation); } @@ -34,7 +34,7 @@ public static AggregatedResponse CreateFrom( invocation.Recipient.Name, recipientType, invocation.Exception, - GetDuration(invocation)); + invocation.Duration); faulted.Add(faultedInvocation); } @@ -44,13 +44,5 @@ public static AggregatedResponse CreateFrom( return new AggregatedResponse(completed, faulted, incomplete); } - - private static TimeSpan GetDuration(RecipientRun invocation) - { - if (invocation.StartedAt.HasValue && invocation.FinishedAt.HasValue) - return invocation.FinishedAt.Value - invocation.StartedAt.Value; - - return default; - } } } From c60dacec928ba0a6dc24420c50f80e8a6aa6659f Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Mon, 28 Dec 2020 12:12:30 +0100 Subject: [PATCH 22/31] Small rework on samples. --- .../Samples/2.FilterOnResponse.cs | 11 +++++++++-- .../Samples/3.InvokeAsyncMethods.cs | 2 +- samples/NScatterGather.Samples/Samples/5.Timeout.cs | 13 ++++++------- .../Samples/6.DelegateRecipients.cs | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/samples/NScatterGather.Samples/Samples/2.FilterOnResponse.cs b/samples/NScatterGather.Samples/Samples/2.FilterOnResponse.cs index e947441..3f999fc 100644 --- a/samples/NScatterGather.Samples/Samples/2.FilterOnResponse.cs +++ b/samples/NScatterGather.Samples/Samples/2.FilterOnResponse.cs @@ -11,6 +11,7 @@ public async Task Run() var collection = new RecipientsCollection(); collection.Add(); collection.Add(); + collection.Add(); var aggregator = new Aggregator(collection); @@ -18,10 +19,11 @@ public async Task Run() // parameter of the methods: AggregatedResponse all = await aggregator.Send(42); - var allResults = all.AsResultsList(); // 42L, "42" + var allResults = all.AsResultsList(); // "42", 42L, 42 Console.WriteLine($"" + $"{allResults[0]} ({allResults[0]?.GetType().Name}), " + - $"{allResults[1]} ({allResults[1]?.GetType().Name})"); + $"{allResults[1]} ({allResults[1]?.GetType().Name}), " + + $"{allResults[2]} ({allResults[2]?.GetType().Name})"); // Instead the Send method checks // also the return type of the methods, allowing to filter @@ -41,5 +43,10 @@ class Bar { public long Longify(int n) => n * 1L; } + + class Baz + { + public int Echo(int n) => n; + } } } diff --git a/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs b/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs index 30211f4..4fc8792 100644 --- a/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs +++ b/samples/NScatterGather.Samples/Samples/3.InvokeAsyncMethods.cs @@ -20,7 +20,7 @@ public async Task Run() // before aggregating the response. var response1 = await aggregator.Send(42); - var results = response1.AsResultsList(); // "42", 42L, 84L + var results = response1.AsResultsList(); // 84L, 42L, "42" Console.WriteLine($"" + $"{results[0]} ({results[0]?.GetType().Name}), " + $"{results[1]} ({results[1]?.GetType().Name}), " + diff --git a/samples/NScatterGather.Samples/Samples/5.Timeout.cs b/samples/NScatterGather.Samples/Samples/5.Timeout.cs index f02aabd..89e8c43 100644 --- a/samples/NScatterGather.Samples/Samples/5.Timeout.cs +++ b/samples/NScatterGather.Samples/Samples/5.Timeout.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using System.Threading.Tasks; namespace NScatterGather.Samples.Samples @@ -15,20 +14,20 @@ public async Task Run() var aggregator = new Aggregator(collection); - // The consumer can limit the duration of the aggregation - // by providing a CancellationToken to the aggregator: + // The consumer can limit the duration of the aggregation by + // providing a timeout (or CancellationToken) to the aggregator: // this will ensure that the response will be ready in // the given amount of time and will "discard" incomplete // (and also never-ending) invocations. - using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); - var response = await aggregator.Send(42, cts.Token); + var response = await aggregator.Send(42, TimeSpan.FromMilliseconds(50)); // The recipients that didn't complete in time will be // listed in the Incomplete property of the result: Console.WriteLine($"Completed {response.Completed.Count}"); Console.WriteLine( $"Incomplete {response.Incomplete.Count}: " + - $"{response.Incomplete[0].RecipientType?.Name}"); + $"{response.Incomplete[0].RecipientType?.Name}, " + + $"{response.Incomplete[1].RecipientType?.Name}"); } class Foo @@ -40,7 +39,7 @@ class Bar { public async Task Long(int n) { - await Task.Delay(100); + await Task.Delay(1000); return n; } } diff --git a/samples/NScatterGather.Samples/Samples/6.DelegateRecipients.cs b/samples/NScatterGather.Samples/Samples/6.DelegateRecipients.cs index 060bcf0..c0e25c5 100644 --- a/samples/NScatterGather.Samples/Samples/6.DelegateRecipients.cs +++ b/samples/NScatterGather.Samples/Samples/6.DelegateRecipients.cs @@ -17,7 +17,7 @@ public async Task Run() var aggregator = new Aggregator(collection); var responseOfInt = await aggregator.Send(42); - var resultsOfInt = responseOfInt.AsResultsList(); // 84, "42" + var resultsOfInt = responseOfInt.AsResultsList(); // "42", 84 Console.WriteLine($"{resultsOfInt[0]}, {resultsOfInt[1]}"); var onlyStrings = await aggregator.Send(42); From 95b63b38e7e6ed45f04726b91c7e4459d1f7bce8 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Tue, 29 Dec 2020 11:22:51 +0100 Subject: [PATCH 23/31] Introduces Clone tests on factories. --- .../Factories/RecipientFactoryTests.cs | 25 +++++++++++++++ .../SingletonRecipientFactoryTests.cs | 32 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs b/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs index a05f166..0625ec6 100644 --- a/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs +++ b/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs @@ -25,5 +25,30 @@ object factoryMethod() Assert.Equal(expectedInstance, instance); Assert.Equal(3, count); } + + [Fact] + public void Can_be_cloned() + { + int count = 0; + + object factoryMethod() + { + count++; + return new object(); + }; + + var factory = new RecipientFactory(factoryMethod); + var clone = factory.Clone(); + + Assert.IsType(clone); + + _ = factory.Get(); + _ = clone.Get(); + Assert.Equal(2, count); + + _ = factory.Get(); + _ = clone.Get(); + Assert.Equal(4, count); + } } } diff --git a/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs b/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs index 8c20673..f3eed38 100644 --- a/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs +++ b/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs @@ -37,5 +37,37 @@ public void Factory_can_accept_instance() Assert.Equal(expectedInstance, instance); } + + [Fact] + public void Can_be_cloned() + { + int count = 0; + + object factoryMethod() + { + count++; + return new object(); + }; + + var singletonFactory = new SingletonRecipientFactory(new RecipientFactory(factoryMethod)); + var singletonClone = singletonFactory.Clone(); + + Assert.IsType(singletonClone); + + _ = singletonFactory.Get(); + _ = singletonClone.Get(); + Assert.Equal(2, count); + + _ = singletonFactory.Get(); + _ = singletonClone.Get(); + Assert.Equal(2, count); + + var instance = new object(); + var singletonFactoryWithInstance = new SingletonRecipientFactory(instance); + var singletonCloneWithInstance = singletonFactoryWithInstance.Clone(); + + Assert.Same(instance, singletonFactoryWithInstance.Get()); + Assert.Same(instance, singletonCloneWithInstance.Get()); + } } } From 71db7dcc466ebde487810969ab664f7e2ff64fe8 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Tue, 29 Dec 2020 11:23:14 +0100 Subject: [PATCH 24/31] Introduces Lifetime test. --- tests/NScatterGather.Tests/AggregatorTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index e9f8d0e..b034e84 100644 --- a/tests/NScatterGather.Tests/AggregatorTests.cs +++ b/tests/NScatterGather.Tests/AggregatorTests.cs @@ -90,5 +90,40 @@ public void Error_if_request_is_null() Assert.ThrowsAsync(() => _aggregator.Send((null as object)!)); Assert.ThrowsAsync(() => _aggregator.Send((null as object)!)); } + + [Fact] + public async Task Recipients_comply_with_lifetime() + { + var transients = 0; + var scoped = 0; + var singletons = 0; + + var collection = new RecipientsCollection(); + + collection.Add(() => { transients++; return new SomeType(); }, lifetime: Lifetime.Transient); + collection.Add(() => { scoped++; return new SomeOtherType(); }, lifetime: Lifetime.Scoped); + collection.Add(() => { singletons++; return new SomeAsyncType(); }, lifetime: Lifetime.Singleton); + + var aggregator = new Aggregator(collection); + var anotherAggregator = new Aggregator(collection); + + await aggregator.Send(42); + + Assert.Equal(1, transients); + Assert.Equal(1, scoped); + Assert.Equal(1, singletons); + + await anotherAggregator.Send(42); + + Assert.Equal(2, transients); + Assert.Equal(2, scoped); + Assert.Equal(1, singletons); + + await Task.WhenAll(aggregator.Send(42), anotherAggregator.Send(42)); + + Assert.Equal(4, transients); + Assert.Equal(2, scoped); + Assert.Equal(1, singletons); + } } } From 72c90c70e720b3240c2c84b3a242f1653f935396 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Tue, 29 Dec 2020 11:23:51 +0100 Subject: [PATCH 25/31] Introduces Scope tests. --- .../Collection/RecipientsCollectionTests.cs | 182 ++---------------- .../Collection/Scope/RecipientsScopeTests.cs | 172 +++++++++++++++++ .../_Utils/TestExtensions.cs | 23 +++ 3 files changed, 213 insertions(+), 164 deletions(-) create mode 100644 tests/NScatterGather.Tests/Recipients/Collection/Scope/RecipientsScopeTests.cs create mode 100644 tests/NScatterGather.Tests/_Utils/TestExtensions.cs diff --git a/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs index 482e4b7..24eb22a 100644 --- a/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs +++ b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs @@ -1,8 +1,7 @@ using System; -using System.Linq; using Xunit; -namespace NScatterGather.Recipients +namespace NScatterGather.Recipients.Collection { public class RecipientsCollectionTests { @@ -29,6 +28,8 @@ public void Can_add_generic_type_with_name() public void Can_add_generic_type_with_lifetime() { _collection.Add(lifetime: Lifetime.Transient); + _collection.Add(lifetime: Lifetime.Scoped); + _collection.Add(lifetime: Lifetime.Singleton); } [Fact] @@ -70,6 +71,12 @@ public void Can_add_delegate() _collection.Add((int n) => n.ToString()); } + [Fact] + public void Error_if_no_parameterless_constructor() + { + Assert.Throws(() => _collection.Add()); + } + [Fact] public void Error_if_delegate_is_null() { @@ -112,169 +119,16 @@ public void Can_create_scope() Assert.Equal(3, nonEmptyScope.RecipientsCount); } - //[Fact] - //public void Recipients_accepting_request_can_be_found() - //{ - // var empty = _collection.ListRecipientsAccepting(typeof(int)); - // Assert.Empty(empty); - - // _collection.Add(); - - // var one = _collection.ListRecipientsAccepting(typeof(int)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(one); - // Assert.Equal(typeof(SomeType), one.First().Type); - - // _collection.Add(); - - // var two = _collection.ListRecipientsAccepting(typeof(int)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Equal(2, two.Count); - // Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - // Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); - - // _collection.Add(); - // var stillTwo = _collection.ListRecipientsAccepting(typeof(int)); - // Assert.Equal(2, stillTwo.Count); - - // var differentOne = _collection.ListRecipientsAccepting(typeof(string)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(differentOne); - // Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); - //} - - //[Fact] - //public void Recipients_returning_response_can_be_found() - //{ - // var empty = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - // Assert.Empty(empty); - - // _collection.Add(); - - // var one = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(one); - // Assert.Equal(typeof(SomeType), one.First().Type); - - // _collection.Add(); - - // var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Equal(2, two.Count); - // Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); - // Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); - - // _collection.Add(); - // var stillTwo = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - // Assert.Equal(2, stillTwo.Count); - - // var differentOne = _collection.ListRecipientsReplyingWith(typeof(string), typeof(int)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(differentOne); - // Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); - //} - - //[Fact] - //public void Recipients_with_request_collisions_are_ignored() - //{ - // _collection.Add(); - // _collection.Add(); - - // var onlyNonCollidingType = _collection.ListRecipientsAccepting(typeof(int)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(onlyNonCollidingType); - // Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); - //} - - //[Fact] - //public void Recipients_with_request_and_response_collisions_are_ignored() - //{ - // _collection.Add(); - // _collection.Add(); - - // var onlyNonCollidingType = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)) - // .Where(x => x is TypeRecipient) - // .Cast() - // .ToList(); - - // Assert.Single(onlyNonCollidingType); - // Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); - //} - - //[Fact] - //public void Collisions_can_be_resolved_via_return_type() - //{ - // _collection.Add(); - // _collection.Add(); - - // var two = _collection.ListRecipientsReplyingWith(typeof(int), typeof(string)); - // Assert.Equal(2, two.Count); - //} - - //[Fact] - //public void Can_be_cloned() - //{ - // _collection.Add(); - // _collection.Add(); - - // Assert.NotEmpty(_collection.RecipientTypes); - - // var clone = _collection.Clone(); - - // foreach (var type in _collection.RecipientTypes) - // Assert.Contains(type, clone.RecipientTypes); - //} - - //[Fact] - //public void Recipients_can_have_a_name() - //{ - // _collection.Add(name: "Some type"); - // _collection.Add(new SomeEchoType(), "Some other type"); - // _collection.Add((int n) => n.ToString(), "Delegate recipient"); + [Fact] + public void Recipients_count_is_visible() + { + Assert.Equal(0, _collection.RecipientsCount); - // Assert.Equal(3, _collection.Recipients.Count); + _collection.Add(); + _collection.Add(new SomeAsyncType()); + _collection.Add((int n) => n); - // foreach (var recipient in _collection.Recipients) - // { - // if (recipient is TypeRecipient tr) - // { - // if (tr.Type == typeof(SomeType)) - // Assert.Equal("Some type", tr.Name); - // else if (tr.Type == typeof(SomeEchoType)) - // Assert.Equal("Some other type", tr.Name); - // else - // throw new Xunit.Sdk.XunitException(); - // } - // else if (recipient is DelegateRecipient dr) - // { - // Assert.Equal("Delegate recipient", dr.Name); - // } - // else - // { - // throw new Xunit.Sdk.XunitException(); - // } - // } - //} + Assert.Equal(3, _collection.RecipientsCount); + } } } diff --git a/tests/NScatterGather.Tests/Recipients/Collection/Scope/RecipientsScopeTests.cs b/tests/NScatterGather.Tests/Recipients/Collection/Scope/RecipientsScopeTests.cs new file mode 100644 index 0000000..88c3cad --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Collection/Scope/RecipientsScopeTests.cs @@ -0,0 +1,172 @@ +using System.Linq; +using NScatterGather.Inspection; +using Xunit; + +namespace NScatterGather.Recipients.Collection.Scope +{ + public class RecipientsScopeTests + { + [Fact] + public void Recipients_can_be_added() + { + var scope = new RecipientsScope(); + + Assert.Equal(0, scope.RecipientsCount); + + scope.AddRange(new[] { DelegateRecipient.Create((int n) => n, name: null) }); + + Assert.Equal(1, scope.RecipientsCount); + } + + [Fact] + public void Recipients_accepting_request_can_be_found() + { + var scope = new RecipientsScope(); + + var empty = scope.ListRecipientsAccepting(typeof(int)); + Assert.Empty(empty); + + scope.AddTypeRecipient(); + + var one = scope.ListRecipientsAccepting(typeof(int)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(one); + Assert.Equal(typeof(SomeType), one.First().Type); + + scope.AddTypeRecipient(); + + var two = scope.ListRecipientsAccepting(typeof(int)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Equal(2, two.Count); + Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); + Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); + + scope.AddTypeRecipient(); + + var stillTwo = scope.ListRecipientsAccepting(typeof(int)); + Assert.Equal(2, stillTwo.Count); + + var differentOne = scope.ListRecipientsAccepting(typeof(string)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(differentOne); + Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); + } + + [Fact] + public void Recipients_returning_response_can_be_found() + { + var scope = new RecipientsScope(); + + var empty = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)); + Assert.Empty(empty); + + scope.AddTypeRecipient(); + + var one = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(one); + Assert.Equal(typeof(SomeType), one.First().Type); + + scope.AddTypeRecipient(); + + var two = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Equal(2, two.Count); + Assert.Contains(typeof(SomeType), two.Select(x => x.Type)); + Assert.Contains(typeof(SomeEchoType), two.Select(x => x.Type)); + + scope.AddTypeRecipient(); + + var stillTwo = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)); + Assert.Equal(2, stillTwo.Count); + + var differentOne = scope.ListRecipientsReplyingWith(typeof(string), typeof(int)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(differentOne); + Assert.Equal(typeof(SomeDifferentType), differentOne.First().Type); + } + + [Fact] + public void Recipients_with_request_collisions_are_ignored() + { + CollisionException? collisionException = null; + + var scope = new RecipientsScope(); + scope.OnCollision += (e) => collisionException = e; + + scope.AddTypeRecipient(); + scope.AddTypeRecipient(); + + var onlyNonCollidingType = scope.ListRecipientsAccepting(typeof(int)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(onlyNonCollidingType); + Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); + + Assert.NotNull(collisionException); + Assert.NotNull(collisionException!.Message); + Assert.Equal(typeof(SomeCollidingType), collisionException!.RecipientType); + Assert.Equal(typeof(int), collisionException!.RequestType); + Assert.Null(collisionException!.ResponseType); + } + + [Fact] + public void Recipients_with_request_and_response_collisions_are_ignored() + { + CollisionException? collisionException = null; + + var scope = new RecipientsScope(); + scope.OnCollision += (e) => collisionException = e; + + scope.AddTypeRecipient(); + scope.AddTypeRecipient(); + + var onlyNonCollidingType = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)) + .Where(x => x is TypeRecipient) + .Cast() + .ToList(); + + Assert.Single(onlyNonCollidingType); + Assert.Equal(typeof(SomeType), onlyNonCollidingType.First().Type); + + Assert.NotNull(collisionException); + Assert.NotNull(collisionException!.Message); + Assert.Equal(typeof(SomeCollidingType), collisionException!.RecipientType); + Assert.Equal(typeof(int), collisionException!.RequestType); + Assert.Equal(typeof(string), collisionException!.ResponseType); + } + + [Fact] + public void Collisions_can_be_resolved_via_return_type() + { + var scope = new RecipientsScope(); + scope.OnCollision += _ => Assert.False(true, "No collisions should be detected"); + + scope.AddTypeRecipient(); + scope.AddTypeRecipient(); + + var two = scope.ListRecipientsReplyingWith(typeof(int), typeof(string)); + Assert.Equal(2, two.Count); + } + } +} diff --git a/tests/NScatterGather.Tests/_Utils/TestExtensions.cs b/tests/NScatterGather.Tests/_Utils/TestExtensions.cs new file mode 100644 index 0000000..3ad0154 --- /dev/null +++ b/tests/NScatterGather.Tests/_Utils/TestExtensions.cs @@ -0,0 +1,23 @@ +using NScatterGather.Inspection; +using NScatterGather.Recipients; +using NScatterGather.Recipients.Collection.Scope; + +namespace NScatterGather +{ + internal static class TestExtensions + { + // Helper method for adding a new TypeRecipient to a RecipientsScope. + public static void AddTypeRecipient(this RecipientsScope scope) + where TRecipients : new() + { + scope.AddRange(new[] + { + TypeRecipient.Create( + new TypeInspectorRegistry(), + () => new TRecipients(), + name: null, + Lifetime.Transient) + }); + } + } +} From e89cd02c0153684d2f2a5d2ac043d5f0ed5d68ef Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Tue, 29 Dec 2020 11:24:18 +0100 Subject: [PATCH 26/31] Enhances tests. --- .../MethodMatchEvaluationCacheTests.cs | 16 +- .../Recipients/TypeRecipientTests.cs | 144 ++++++++++-------- 2 files changed, 94 insertions(+), 66 deletions(-) diff --git a/tests/NScatterGather.Tests/Inspection/MethodMatchEvaluationCacheTests.cs b/tests/NScatterGather.Tests/Inspection/MethodMatchEvaluationCacheTests.cs index c46ecb0..0036799 100644 --- a/tests/NScatterGather.Tests/Inspection/MethodMatchEvaluationCacheTests.cs +++ b/tests/NScatterGather.Tests/Inspection/MethodMatchEvaluationCacheTests.cs @@ -13,21 +13,31 @@ class SomeResponse { } public void Error_if_request_type_is_null() { var cache = new MethodMatchEvaluationCache(); - Assert.Throws(() => cache.TryAdd(null!, new MethodMatchEvaluation(false, null))); + + Assert.Throws(() => cache.TryAdd( + null!, new MethodMatchEvaluation(false, null))); + + Assert.Throws(() => cache.TryAdd( + null!, typeof(SomeResponse), new MethodMatchEvaluation(false, null))); } [Fact] public void Error_if_response_type_is_null() { var cache = new MethodMatchEvaluationCache(); - Assert.Throws(() => cache.TryAdd(typeof(SomeRequest), null!, new MethodMatchEvaluation(false, null))); + + Assert.Throws(() => cache.TryAdd( + typeof(SomeRequest), null!, new MethodMatchEvaluation(false, null))); } [Fact] - public void Error_if_inspection_is_null() + public void Error_if_evaluation_is_null() { var cache = new MethodMatchEvaluationCache(); Assert.Throws(() => cache.TryAdd(null!)); + + Assert.Throws(() => cache.TryAdd( + typeof(SomeRequest), typeof(SomeResponse), (null as MethodMatchEvaluation)!)); } [Fact] diff --git a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs index 86097f3..6f6885e 100644 --- a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs @@ -23,8 +23,8 @@ public void Error_if_registry_is_null() Assert.Throws(() => { _ = TypeRecipient.Create( - registry: new TypeInspectorRegistry(), - (null as Func)!, + registry: (null as TypeInspectorRegistry)!, + () => new SomeType(), name: null, lifetime: Lifetime.Transient); }); @@ -131,67 +131,85 @@ public void Error_if_request_or_response_types_are_null() Assert.Throws(() => recipient.CanReplyWith(null!, typeof(string))); } - //[Fact] - //public void Error_if_request_type_not_supported() - //{ - // var recipient = TypeRecipient.Create(); - // Assert.ThrowsAsync(() => recipient.Accept(Guid.NewGuid())); - //} - - //[Fact] - //public void Error_if_response_type_not_supported() - //{ - // var recipient = TypeRecipient.Create(); - // Assert.ThrowsAsync(() => recipient.ReplyWith(42)); - //} - - //[Fact] - //public async Task Recipient_replies_with_response() - //{ - // var recipient = TypeRecipient.Create(); - // var input = 42; - // var response = await recipient.ReplyWith(input); - // Assert.Equal(input.ToString(), response); - //} - - //[Fact] - //public async Task Recipient_can_reply_with_task() - //{ - // var recipient = TypeRecipient.Create(); - // var input = 42; - // var response = await recipient.ReplyWith(input); - // Assert.Equal(input.ToString(), response); - //} - - //[Fact] - //public void Recipient_must_return_something() - //{ - // var recipient = TypeRecipient.Create(); - // bool accepts = recipient.CanAccept(typeof(int)); - // Assert.False(accepts); - //} - - //[Fact] - //public void Error_if_void_returning() - //{ - // var recipient = TypeRecipient.Create(); - // Assert.ThrowsAsync(() => recipient.Accept(42)); - //} - - //[Fact] - //public void Recipient_must_return_something_async() - //{ - // var recipient = TypeRecipient.Create(); - // bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); - // Assert.False(accepts); - //} - - //[Fact] - //public void Error_if_returning_task_without_result() - //{ - // var recipient = TypeRecipient.Create(); - // Assert.ThrowsAsync(() => recipient.ReplyWith(42)); - //} + [Fact] + public void Error_if_request_type_not_supported() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + + Assert.Throws(() => recipient.Accept(Guid.NewGuid())); + } + + [Fact] + public void Error_if_response_type_not_supported() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + + Assert.Throws(() => recipient.ReplyWith(42)); + } + + [Fact] + public async Task Recipient_replies_with_response() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeType(), name: null, Lifetime.Transient); + + var input = 42; + var runner = recipient.ReplyWith(input); + await runner.Start(); + Assert.Equal(input.ToString(), runner.Result); + } + + [Fact] + public async Task Recipient_can_reply_with_task() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeAsyncType(), name: null, Lifetime.Transient); + + var input = 42; + var runner = recipient.ReplyWith(input); + await runner.Start(); + Assert.Equal(input.ToString(), runner.Result); + } + + [Fact] + public void Recipient_must_return_something() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeComputingType(), name: null, Lifetime.Transient); + + bool accepts = recipient.CanAccept(typeof(int)); + Assert.False(accepts); + } + + [Fact] + public void Error_if_void_returning() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeComputingType(), name: null, Lifetime.Transient); + + Assert.Throws(() => recipient.Accept(42)); + } + + [Fact] + public void Recipient_must_return_something_async() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeAsyncComputingType(), name: null, Lifetime.Transient); + + bool accepts = recipient.CanReplyWith(typeof(int), typeof(Task)); + Assert.False(accepts); + } + + [Fact] + public void Error_if_returning_task_without_result() + { + var registry = new TypeInspectorRegistry(); + var recipient = TypeRecipient.Create(registry, () => new SomeAsyncComputingType(), name: null, Lifetime.Transient); + + Assert.Throws(() => recipient.ReplyWith(42)); + } [Fact] public void Can_be_cloned() From 7f5f6a1f2401167cf91f7daf4bd5eca5e96d2032 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 30 Dec 2020 10:51:20 +0100 Subject: [PATCH 27/31] Removes OnError. The library should be exception free during the inspection, except for CollisionException. --- .../Recipients/Collection/CollisionHandler.cs | 4 ++++ src/NScatterGather/Recipients/Collection/Handlers.cs | 8 -------- .../Recipients/Collection/RecipientsCollection.cs | 3 --- .../Recipients/Collection/Scope/RecipientsScope.cs | 12 ------------ 4 files changed, 4 insertions(+), 23 deletions(-) create mode 100644 src/NScatterGather/Recipients/Collection/CollisionHandler.cs delete mode 100644 src/NScatterGather/Recipients/Collection/Handlers.cs diff --git a/src/NScatterGather/Recipients/Collection/CollisionHandler.cs b/src/NScatterGather/Recipients/Collection/CollisionHandler.cs new file mode 100644 index 0000000..a4c24de --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/CollisionHandler.cs @@ -0,0 +1,4 @@ +namespace NScatterGather +{ + public delegate void CollisionHandler(CollisionException ex); +} diff --git a/src/NScatterGather/Recipients/Collection/Handlers.cs b/src/NScatterGather/Recipients/Collection/Handlers.cs deleted file mode 100644 index b1c4ae4..0000000 --- a/src/NScatterGather/Recipients/Collection/Handlers.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace NScatterGather -{ - public delegate void CollisionHandler(CollisionException ex); - - public delegate void ErrorHandler(Exception ex); -} diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs index 2a4b498..916a409 100644 --- a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -11,8 +11,6 @@ public class RecipientsCollection { public event CollisionHandler? OnCollision; - public event ErrorHandler? OnError; - public int RecipientsCount => _singletonRecipients.Count + _scopedRecipients.Count + @@ -95,7 +93,6 @@ internal IRecipientsScope CreateScope() scope.AddRange(clonedScoped); scope.OnCollision += OnCollision; - scope.OnError += OnError; return scope; } diff --git a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs index b9cd797..ff3aeea 100644 --- a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs +++ b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs @@ -10,8 +10,6 @@ internal class RecipientsScope : IRecipientsScope public event CollisionHandler? OnCollision; - public event ErrorHandler? OnError; - private readonly List _recipients = new(); internal void AddRange(IEnumerable recipients) => @@ -38,11 +36,6 @@ bool RecipientCanAccept(Recipient recipient) OnCollision?.Invoke(ex); return false; } - catch (Exception ex) - { - OnError?.Invoke(ex); - return false; - } } } @@ -67,11 +60,6 @@ bool RecipientCanReplyWith(Recipient recipient) OnCollision?.Invoke(ex); return false; } - catch (Exception ex) - { - OnError?.Invoke(ex); - return false; - } } } } From 2c2dfdb28d49e7875e124c3b062add55a108358f Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 30 Dec 2020 11:00:57 +0100 Subject: [PATCH 28/31] Iterates recipients using registration order. --- .../Collection/RecipientsCollection.cs | 33 +++++++++---------- .../Collection/Scope/RecipientsScope.cs | 4 +-- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs index 916a409..e5d71ec 100644 --- a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -11,14 +11,9 @@ public class RecipientsCollection { public event CollisionHandler? OnCollision; - public int RecipientsCount => - _singletonRecipients.Count + - _scopedRecipients.Count + - _transientRecipients.Count; - - private readonly List _singletonRecipients = new(); - private readonly List _scopedRecipients = new(); - private readonly List _transientRecipients = new(); + public int RecipientsCount => _recipients.Count; + + private readonly List _recipients = new(); private readonly TypeInspectorRegistry _registry = new TypeInspectorRegistry(); public void Add( @@ -48,6 +43,7 @@ public void Add( throw new ArgumentNullException(nameof(factoryMethod)); var typeRecipient = TypeRecipient.Create(_registry, factoryMethod, name, lifetime); + Add(typeRecipient); } @@ -59,6 +55,7 @@ public void Add( throw new ArgumentNullException(nameof(instance)); var instanceRecipient = InstanceRecipient.Create(_registry, instance, name); + Add(instanceRecipient); } @@ -70,27 +67,27 @@ public void Add( throw new ArgumentNullException(nameof(@delegate)); var delegateRecipient = DelegateRecipient.Create(@delegate, name); + Add(delegateRecipient); } private void Add(Recipient recipient) { - if (recipient.Lifetime == Lifetime.Singleton) - _singletonRecipients.Add(recipient); - else if (recipient.Lifetime == Lifetime.Scoped) - _scopedRecipients.Add(recipient); - else - _transientRecipients.Add(recipient); + _recipients.Add(recipient); } internal IRecipientsScope CreateScope() { var scope = new RecipientsScope(); - scope.AddRange(_singletonRecipients); - scope.AddRange(_transientRecipients); - var clonedScoped = _scopedRecipients.Select(r => r.Clone()); - scope.AddRange(clonedScoped); + var scopedRecipients = _recipients.Select(recipient => + { + return recipient.Lifetime == Lifetime.Scoped + ? recipient.Clone() + : recipient; + }); + + scope.AddRange(scopedRecipients); scope.OnCollision += OnCollision; diff --git a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs index ff3aeea..ef483d5 100644 --- a/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs +++ b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs @@ -6,10 +6,10 @@ namespace NScatterGather.Recipients.Collection.Scope { internal class RecipientsScope : IRecipientsScope { - public int RecipientsCount => _recipients.Count; - public event CollisionHandler? OnCollision; + public int RecipientsCount => _recipients.Count; + private readonly List _recipients = new(); internal void AddRange(IEnumerable recipients) => From 1f7ca2f7b39dfe066ceab09021bab66909132c08 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 30 Dec 2020 11:32:19 +0100 Subject: [PATCH 29/31] Adds simpler overloads for registering recipients. --- .../Collection/RecipientsCollection.cs | 17 +++++++++++++---- tests/NScatterGather.Tests/AggregatorTests.cs | 6 +++--- .../Collection/RecipientsCollectionTests.cs | 8 ++++---- .../Recipients/DelegateRecipientTests.cs | 4 ++-- .../Recipients/InstanceRecipientTests.cs | 2 +- .../Recipients/TypeRecipientTests.cs | 2 +- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs index e5d71ec..6c8e55a 100644 --- a/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -16,9 +16,18 @@ public class RecipientsCollection private readonly List _recipients = new(); private readonly TypeInspectorRegistry _registry = new TypeInspectorRegistry(); + public void Add(string? name = null) => + Add(name: name, lifetime: Lifetime.Transient); + + public void Add(Lifetime lifetime) => + Add(name: null, lifetime: lifetime); + + public void Add(Func factoryMethod) => + Add(factoryMethod, name: null, lifetime: Lifetime.Transient); + public void Add( - string? name = null, - Lifetime lifetime = Lifetime.Transient) + string? name, + Lifetime lifetime) { if (!HasADefaultConstructor()) throw new ArgumentException($"Type '{typeof(TRecipient).Name}' is missing a public, parameterless constructor."); @@ -36,8 +45,8 @@ static bool HasADefaultConstructor() public void Add( Func factoryMethod, - string? name = null, - Lifetime lifetime = Lifetime.Transient) + string? name, + Lifetime lifetime) { if (factoryMethod is null) throw new ArgumentNullException(nameof(factoryMethod)); diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index b034e84..b84c46e 100644 --- a/tests/NScatterGather.Tests/AggregatorTests.cs +++ b/tests/NScatterGather.Tests/AggregatorTests.cs @@ -100,9 +100,9 @@ public async Task Recipients_comply_with_lifetime() var collection = new RecipientsCollection(); - collection.Add(() => { transients++; return new SomeType(); }, lifetime: Lifetime.Transient); - collection.Add(() => { scoped++; return new SomeOtherType(); }, lifetime: Lifetime.Scoped); - collection.Add(() => { singletons++; return new SomeAsyncType(); }, lifetime: Lifetime.Singleton); + collection.Add(() => { transients++; return new SomeType(); }, name: null, lifetime: Lifetime.Transient); + collection.Add(() => { scoped++; return new SomeOtherType(); }, name: null, lifetime: Lifetime.Scoped); + collection.Add(() => { singletons++; return new SomeAsyncType(); }, name: null, lifetime: Lifetime.Singleton); var aggregator = new Aggregator(collection); var anotherAggregator = new Aggregator(collection); diff --git a/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs index 24eb22a..e1a6f09 100644 --- a/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs +++ b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs @@ -27,9 +27,9 @@ public void Can_add_generic_type_with_name() [Fact] public void Can_add_generic_type_with_lifetime() { - _collection.Add(lifetime: Lifetime.Transient); - _collection.Add(lifetime: Lifetime.Scoped); - _collection.Add(lifetime: Lifetime.Singleton); + _collection.Add(Lifetime.Transient); + _collection.Add(Lifetime.Scoped); + _collection.Add(Lifetime.Singleton); } [Fact] @@ -49,7 +49,7 @@ public void Error_if_factory_method_is_null() public void Error_if_lifetime_is_not_valid() { Assert.Throws(() => - _collection.Add(lifetime: (Lifetime)42)); + _collection.Add((Lifetime)42)); } [Fact] diff --git a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs index f929f3f..7d7871c 100644 --- a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs @@ -132,8 +132,8 @@ public void Can_be_cloned() Assert.IsType(clone); Assert.Equal(recipient.Name, clone.Name); Assert.Equal(recipient.Lifetime, clone.Lifetime); - Assert.Equal(recipient.RequestType, (clone as DelegateRecipient).RequestType); - Assert.Equal(recipient.ResponseType, (clone as DelegateRecipient).ResponseType); + Assert.Equal(recipient.RequestType, (clone as DelegateRecipient)!.RequestType); + Assert.Equal(recipient.ResponseType, (clone as DelegateRecipient)!.ResponseType); } [Fact] diff --git a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs index 7cd46e1..7832ff7 100644 --- a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs @@ -49,7 +49,7 @@ public void Can_be_cloned() Assert.IsType(clone); Assert.Equal(recipient.Name, clone.Name); Assert.Equal(recipient.Lifetime, clone.Lifetime); - Assert.Equal(recipient.Type, (clone as InstanceRecipient).Type); + Assert.Equal(recipient.Type, (clone as InstanceRecipient)!.Type); } [Fact] diff --git a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs index 6f6885e..af14e8a 100644 --- a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs @@ -222,7 +222,7 @@ public void Can_be_cloned() Assert.IsType(clone); Assert.Equal(recipient.Name, clone.Name); Assert.Equal(recipient.Lifetime, clone.Lifetime); - Assert.Equal(recipient.Type, (clone as TypeRecipient).Type); + Assert.Equal(recipient.Type, (clone as TypeRecipient)!.Type); } [Fact] From 8320c18211039725a7586aef854e318b6fddd7b5 Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Wed, 30 Dec 2020 11:41:14 +0100 Subject: [PATCH 30/31] Adds tests for null-returning and nullable-returning recipients. --- tests/NScatterGather.Tests/AggregatorTests.cs | 38 +++++++++++++++++++ .../_TestTypes/SomeTypeReturningNull.cs | 7 ++++ .../_TestTypes/SomeTypeReturningNullable.cs | 7 ++++ 3 files changed, 52 insertions(+) create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNull.cs create mode 100644 tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNullable.cs diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index b84c46e..2627e52 100644 --- a/tests/NScatterGather.Tests/AggregatorTests.cs +++ b/tests/NScatterGather.Tests/AggregatorTests.cs @@ -125,5 +125,43 @@ public async Task Recipients_comply_with_lifetime() Assert.Equal(2, scoped); Assert.Equal(1, singletons); } + + [Fact] + public async Task Recipients_can_return_null() + { + var collection = new RecipientsCollection(); + collection.Add(); + + var aggregator = new Aggregator(collection); + + var response = await aggregator.Send(42); + + Assert.NotNull(response); + Assert.Single(response.Completed); + Assert.Empty(response.Faulted); + + var completed = response.Completed.First(); + Assert.Equal(typeof(SomeTypeReturningNull), completed.RecipientType); + Assert.Null(completed.Result); + } + + [Fact] + public async Task Recipients_can_return_nullable() + { + var collection = new RecipientsCollection(); + collection.Add(); + + var aggregator = new Aggregator(collection); + + var response = await aggregator.Send(42); + + Assert.NotNull(response); + Assert.Single(response.Completed); + Assert.Empty(response.Faulted); + + var completed = response.Completed.First(); + Assert.Equal(typeof(SomeTypeReturningNullable), completed.RecipientType); + Assert.Null(completed.Result); + } } } diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNull.cs b/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNull.cs new file mode 100644 index 0000000..033df76 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNull.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeTypeReturningNull + { + public string? Null(int n) => null; + } +} diff --git a/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNullable.cs b/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNullable.cs new file mode 100644 index 0000000..2643835 --- /dev/null +++ b/tests/NScatterGather.Tests/_TestTypes/SomeTypeReturningNullable.cs @@ -0,0 +1,7 @@ +namespace NScatterGather +{ + public class SomeTypeReturningNullable + { + public int? Null(int n) => null; + } +} From b7152b043fb01ed8f2434e47e6db0756176d33bb Mon Sep 17 00:00:00 2001 From: Tommaso Bertoni Date: Thu, 31 Dec 2020 11:32:39 +0100 Subject: [PATCH 31/31] Resolves StyleCop rule. --- src/NScatterGather/Internals/EnumBcl.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/NScatterGather/Internals/EnumBcl.cs b/src/NScatterGather/Internals/EnumBcl.cs index b28d7f9..1196542 100644 --- a/src/NScatterGather/Internals/EnumBcl.cs +++ b/src/NScatterGather/Internals/EnumBcl.cs @@ -12,7 +12,5 @@ public static bool IsValid(this TEnum value) where TEnum : struct, Enum return Enum.IsDefined(value); #endif } - } - }