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.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/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 @@ - + 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 8efc052..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}), " + @@ -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(); 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); 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 class Aggregator 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 class Aggregator 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 class Aggregator 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 class Aggregator 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/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/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 @@ static IReadOnlyList InspectMethods(Type type) _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 @@ private IReadOnlyList ListMatchingMethods(Type requestType) _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(); } } diff --git a/src/NScatterGather/Internals/EnumBcl.cs b/src/NScatterGather/Internals/EnumBcl.cs new file mode 100644 index 0000000..1196542 --- /dev/null +++ b/src/NScatterGather/Internals/EnumBcl.cs @@ -0,0 +1,16 @@ +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 + } + } +} 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 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/RecipientsCollection.cs b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs new file mode 100644 index 0000000..6c8e55a --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/RecipientsCollection.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NScatterGather.Inspection; +using NScatterGather.Recipients; +using NScatterGather.Recipients.Collection.Scope; + +namespace NScatterGather +{ + public class RecipientsCollection + { + public event CollisionHandler? OnCollision; + + public int RecipientsCount => _recipients.Count; + + 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, + Lifetime lifetime) + { + 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, + Lifetime lifetime) + { + 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) + { + _recipients.Add(recipient); + } + + internal IRecipientsScope CreateScope() + { + var scope = new RecipientsScope(); + + var scopedRecipients = _recipients.Select(recipient => + { + return recipient.Lifetime == Lifetime.Scoped + ? recipient.Clone() + : recipient; + }); + + scope.AddRange(scopedRecipients); + + scope.OnCollision += OnCollision; + + 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..1db2c64 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/Scope/IRecipientsScope.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +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 new file mode 100644 index 0000000..ef483d5 --- /dev/null +++ b/src/NScatterGather/Recipients/Collection/Scope/RecipientsScope.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NScatterGather.Recipients.Collection.Scope +{ + internal class RecipientsScope : IRecipientsScope + { + public event CollisionHandler? OnCollision; + + public int RecipientsCount => _recipients.Count; + + 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 (CollisionException ex) + { + OnCollision?.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 (CollisionException ex) + { + OnCollision?.Invoke(ex); + return false; + } + } + } + } +} diff --git a/src/NScatterGather/Recipients/DelegateRecipient.cs b/src/NScatterGather/Recipients/DelegateRecipient.cs index ca88744..c5c3cb2 100644 --- a/src/NScatterGather/Recipients/DelegateRecipient.cs +++ b/src/NScatterGather/Recipients/DelegateRecipient.cs @@ -1,90 +1,51 @@ 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; - - internal Type Out => _outType; - - private readonly Type _inType; - private readonly Type _outType; + public Type ResponseType { get; } 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); - } - - internal 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.RequestType, descriptor.ResponseType, descriptor, invoker, name); } - protected internal override object? Invoke(object request) + protected DelegateRecipient( + Type requestType, + Type responseType, + IRecipientDescriptor descriptor, + IRecipientInvoker 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}'."); - - return _delegate(request!); + RequestType = requestType; + ResponseType = 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() => +#else + public override DelegateRecipient Clone() => +#endif + new DelegateRecipient(RequestType, ResponseType, _descriptor, _invoker, Name); } } 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..a36149f --- /dev/null +++ b/src/NScatterGather/Recipients/Descriptors/TypeRecipientDescriptor.cs @@ -0,0 +1,27 @@ +using System; +using NScatterGather.Inspection; + +namespace NScatterGather.Recipients.Descriptors +{ + internal class TypeRecipientDescriptor : IRecipientDescriptor + { + private readonly TypeInspector _inspector; + + 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; + } + } +} diff --git a/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs new file mode 100644 index 0000000..2081119 --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/IRecipientFactory.cs @@ -0,0 +1,9 @@ +namespace NScatterGather.Recipients.Factories +{ + internal interface IRecipientFactory + { + object Get(); + + IRecipientFactory Clone(); + } +} diff --git a/src/NScatterGather/Recipients/Factories/RecipientFactory.cs b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs new file mode 100644 index 0000000..5c42085 --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/RecipientFactory.cs @@ -0,0 +1,19 @@ +using System; + +namespace NScatterGather.Recipients.Factories +{ + internal class RecipientFactory : IRecipientFactory + { + private readonly Func _factory; + + public RecipientFactory(Func factory) + { + _factory = 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 new file mode 100644 index 0000000..c61dae6 --- /dev/null +++ b/src/NScatterGather/Recipients/Factories/SingletonRecipientFactory.cs @@ -0,0 +1,32 @@ +using System; + +namespace NScatterGather.Recipients.Factories +{ + internal class SingletonRecipientFactory : IRecipientFactory + { + private readonly IRecipientFactory? _anotherFactory; + 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) + { + _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()); + } +} diff --git a/src/NScatterGather/Recipients/InstanceRecipient.cs b/src/NScatterGather/Recipients/InstanceRecipient.cs index a8c521b..f57b202 100644 --- a/src/NScatterGather/Recipients/InstanceRecipient.cs +++ b/src/NScatterGather/Recipients/InstanceRecipient.cs @@ -1,89 +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 : 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( + TypeInspectorRegistry registry, 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) { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); + if (registry is null) + throw new ArgumentNullException(nameof(registry)); - var accepts = _inspector.HasMethodAccepting(requestType); - return accepts; - } + if (instance is null) + throw new ArgumentNullException(nameof(instance)); - public override bool CanReplyWith(Type requestType, Type responseType) - { - if (requestType is null) - throw new ArgumentNullException(nameof(requestType)); + var inspector = registry.For(instance.GetType()); + var descriptor = new TypeRecipientDescriptor(inspector); - if (responseType is null) - throw new ArgumentNullException(nameof(responseType)); + var invoker = new InstanceRecipientInvoker( + inspector, + new SingletonRecipientFactory(instance)); - var repliesWith = _inspector.HasMethodReturning(requestType, responseType); - return repliesWith; + return new InstanceRecipient( + instance, + descriptor, + invoker, + name); } - protected internal override object? Invoke(object request) + protected InstanceRecipient( + object instance, + IRecipientDescriptor descriptor, + IRecipientInvoker invoker, + string? name) : base(instance.GetType(), descriptor, invoker, name, Lifetime.Singleton) { - 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; + _instance = instance; } - protected internal override object? Invoke(object request) +#if NETSTANDARD2_0 || NETSTANDARD2_1 + public override Recipient Clone() +#else + public override InstanceRecipient Clone() +#endif { - 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; + var invoker = _invoker.Clone(); + return new InstanceRecipient(_instance, _descriptor, invoker, Name); } } } diff --git a/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs new file mode 100644 index 0000000..4c64313 --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/DelegateRecipientInvoker.cs @@ -0,0 +1,43 @@ +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)!); + } + + public IRecipientInvoker Clone() => + new DelegateRecipientInvoker(_descriptor, _delegate); + } +} diff --git a/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs new file mode 100644 index 0000000..cc4ea45 --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/IRecipientInvoker.cs @@ -0,0 +1,11 @@ +namespace NScatterGather.Recipients.Invokers +{ + 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 new file mode 100644 index 0000000..d428a3b --- /dev/null +++ b/src/NScatterGather/Recipients/Invokers/InstanceRecipientInvoker.cs @@ -0,0 +1,53 @@ +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: () => + method.Invoke(recipientInstance, new object?[] { request })!); + } + + public IRecipientInvoker Clone() + { + var factory = _factory.Clone(); + return new InstanceRecipientInvoker(_inspector, factory); + } + } +} diff --git a/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs b/src/NScatterGather/Recipients/Invokers/PreparedInvocation.cs new file mode 100644 index 0000000..44031ab --- /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; + } + } +} 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, + } +} 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/Recipients/RecipientsCollection.cs b/src/NScatterGather/Recipients/RecipientsCollection.cs deleted file mode 100644 index 9ccb759..0000000 --- a/src/NScatterGather/Recipients/RecipientsCollection.cs +++ /dev/null @@ -1,143 +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(string? name = null) => - Add(typeof(T), name); - - public void Add(Type recipientType, string? name = null) - { - if (recipientType is null) - throw new ArgumentNullException(nameof(recipientType)); - - var inspector = _registry.Register(recipientType); - _recipients.Add(new InstanceRecipient(recipientType, name, inspector)); - } - - 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)); - } - - 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 InstanceRecipient ir) - _ = _registry.Register(ir.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; - } - } -} diff --git a/src/NScatterGather/Recipients/Run/RecipientRun.cs b/src/NScatterGather/Recipients/Run/RecipientRun.cs new file mode 100644 index 0000000..1e69762 --- /dev/null +++ b/src/NScatterGather/Recipients/Run/RecipientRun.cs @@ -0,0 +1,96 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; +using NScatterGather.Recipients.Invokers; +using static System.Threading.Tasks.TaskContinuationOptions; + +namespace NScatterGather.Recipients.Run +{ + internal class RecipientRun + { + public Recipient Recipient { get; } + + public bool CompletedSuccessfully { get; private set; } + + [MaybeNull, AllowNull] + public TResult Result { get; private set; } + + public bool Faulted { get; private set; } + + public Exception? Exception { get; set; } + + public DateTime StartedAt { get; private set; } + + public DateTime FinishedAt { get; private set; } + + public TimeSpan Duration => FinishedAt - StartedAt; + + private readonly PreparedInvocation _preparedInvocation; + + public RecipientRun(Recipient recipient, PreparedInvocation preparedInvocation) + { + Recipient = recipient; + _preparedInvocation = preparedInvocation; + } + + public Task Start() + { + if (StartedAt != default) + throw new InvalidOperationException("Run already executed."); + + StartedAt = DateTime.UtcNow; + + var runnerTask = Task.Run(async () => await _preparedInvocation.Execute()); + + var tcs = new TaskCompletionSource(); + + runnerTask.ContinueWith(completedTask => + { + InspectAndExtract(completedTask); + tcs.SetResult(!completedTask.IsFaulted); + }, ExecuteSynchronously | NotOnCanceled); + + // This task won't throw if the invocation failed with an exception. + return tcs.Task; + } + + private void InspectAndExtract(Task task) + { + if (task.IsCompleted) + FinishedAt = DateTime.UtcNow; + + if (task.IsCompletedSuccessfully()) + { + CompletedSuccessfully = true; + Result = task.Result; + } + else if (task.IsFaulted) + { + Faulted = true; + + /* + * 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) + { + if (exception is AggregateException aEx && aEx.InnerExceptions.Count == 1) + return aEx.InnerException is null ? aEx : ExtractException(aEx.InnerException); + + if (exception is TargetInvocationException tIEx) + return tIEx.InnerException is null ? tIEx : ExtractException(tIEx.InnerException); + + return exception; + } + } +} diff --git a/src/NScatterGather/Recipients/TypeRecipient.cs b/src/NScatterGather/Recipients/TypeRecipient.cs new file mode 100644 index 0000000..1729462 --- /dev/null +++ b/src/NScatterGather/Recipients/TypeRecipient.cs @@ -0,0 +1,63 @@ +using System; +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 { get; } + + public static TypeRecipient Create( + TypeInspectorRegistry registry, + Func factoryMethod, + string? name, + Lifetime lifetime) + { + if (registry is null) + throw new ArgumentNullException(nameof(registry)); + + if (factoryMethod is null) + throw new ArgumentNullException(nameof(factoryMethod)); + + if (!lifetime.IsValid()) + throw new ArgumentException($"Invalid {nameof(lifetime)} value: {lifetime}"); + + var inspector = registry.For(); + + IRecipientFactory factory = new RecipientFactory(() => factoryMethod()!); + + if (lifetime != Lifetime.Transient) + factory = new SingletonRecipientFactory(factory); + + return new TypeRecipient( + typeof(TRecipient), + new TypeRecipientDescriptor(inspector), + new InstanceRecipientInvoker(inspector, factory), + name, + lifetime); + } + + protected TypeRecipient( + Type type, + IRecipientDescriptor descriptor, + IRecipientInvoker invoker, + string? name, + Lifetime lifetime) : base(descriptor, invoker, name, lifetime) + { + Type = type; + } + +#if NETSTANDARD2_0 || NETSTANDARD2_1 + public override Recipient Clone() +#else + public override TypeRecipient Clone() +#endif + { + var invoker = _invoker.Clone(); + return new TypeRecipient(Type, _descriptor, invoker, Name, Lifetime); + } + } +} diff --git a/src/NScatterGather/Responses/AggregatedResponseFactory.cs b/src/NScatterGather/Responses/AggregatedResponseFactory.cs index fe698b1..076bdaf 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(); @@ -16,7 +16,7 @@ internal class AggregatedResponseFactory 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) { @@ -24,7 +24,7 @@ internal class AggregatedResponseFactory invocation.Recipient.Name, recipientType, invocation.Result, - GetDuration(invocation)); + invocation.Duration); completed.Add(completedInvocation); } @@ -34,7 +34,7 @@ internal class AggregatedResponseFactory invocation.Recipient.Name, recipientType, invocation.Exception, - GetDuration(invocation)); + invocation.Duration); faulted.Add(faultedInvocation); } @@ -44,14 +44,5 @@ internal class AggregatedResponseFactory return new AggregatedResponse(completed, faulted, incomplete); } - - private static TimeSpan GetDuration( - RecipientRunner invocation) - { - if (invocation.StartedAt.HasValue && invocation.FinishedAt.HasValue) - return invocation.FinishedAt.Value - invocation.StartedAt.Value; - - return default; - } } } diff --git a/src/NScatterGather/Run/RecipientRunner.cs b/src/NScatterGather/Run/RecipientRunner.cs deleted file mode 100644 index 3341ef7..0000000 --- a/src/NScatterGather/Run/RecipientRunner.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Threading.Tasks; -using NScatterGather.Recipients; -using static System.Threading.Tasks.TaskContinuationOptions; - -namespace NScatterGather.Run -{ - internal class RecipientRunner - { - public Recipient Recipient { get; } - - public bool CompletedSuccessfully { get; private set; } - - [MaybeNull, AllowNull] - public TResult Result { get; private set; } - - public bool Faulted { get; private set; } - - public Exception? Exception { get; set; } - - public DateTime? StartedAt { get; private set; } - - public DateTime? FinishedAt { get; private set; } - - public RecipientRunner(Recipient recipient) - { - Recipient = recipient ?? throw new ArgumentNullException(nameof(recipient)); - Result = default; - } - - public Task Run(Func> invocation) - { - 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 invocation(Recipient)); - - var tcs = new TaskCompletionSource(); - - runnerTask.ContinueWith(completedTask => - { - InspectAndExtract(completedTask); - tcs.SetResult(!completedTask.IsFaulted); - }, ExecuteSynchronously | NotOnCanceled); - - // This task won't throw if the invocation failed with an exception. - return tcs.Task; - } - - private void InspectAndExtract(Task task) - { - if (task.IsCompleted) - FinishedAt = DateTime.UtcNow; - - if (task.IsCompletedSuccessfully()) - { - CompletedSuccessfully = true; - Result = task.Result; - } - else if (task.IsFaulted) - { - Faulted = true; - Exception = ExtractException(task.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 TargetInvocationException tIEx) - return ExtractException(tIEx.InnerException) ?? tIEx; - - return exception; - } - } -} diff --git a/tests/NScatterGather.Tests/AggregatorTests.cs b/tests/NScatterGather.Tests/AggregatorTests.cs index c9cac67..2627e52 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,106 @@ 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); + } + + [Fact] + 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(); }, 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); + + 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); + } + + [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/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/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/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 512c294..8604d4d 100644 --- a/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs +++ b/tests/NScatterGather.Tests/Inspection/TypeInspectorTests.cs @@ -1,40 +1,47 @@ -using Xunit; +using System; +using Xunit; namespace NScatterGather.Inspection { public class TypeInspectorTests { - class SomeType + [Fact] + public void Error_if_type_is_null() { - public int Do(int n) => n * 2; - - public string Echo(long x) => x.ToString(); + Assert.Throws(() => new TypeInspector((null as Type)!)); } [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); + } + + [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/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/Collection/RecipientsCollectionTests.cs b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs new file mode 100644 index 0000000..e1a6f09 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Collection/RecipientsCollectionTests.cs @@ -0,0 +1,134 @@ +using System; +using Xunit; + +namespace NScatterGather.Recipients.Collection +{ + 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.Transient); + _collection.Add(Lifetime.Scoped); + _collection.Add(Lifetime.Singleton); + } + + [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)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_no_parameterless_constructor() + { + Assert.Throws(() => _collection.Add()); + } + + [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_count_is_visible() + { + Assert.Equal(0, _collection.RecipientsCount); + + _collection.Add(); + _collection.Add(new SomeAsyncType()); + _collection.Add((int n) => n); + + 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/Recipients/DelegateRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs index a44cbed..7d7871c 100644 --- a/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/DelegateRecipientTests.cs @@ -6,60 +6,36 @@ 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() { - 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?))); @@ -71,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))); @@ -85,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; @@ -110,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; @@ -130,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..0625ec6 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Factories/RecipientFactoryTests.cs @@ -0,0 +1,54 @@ +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); + } + + [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 new file mode 100644 index 0000000..f3eed38 --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Factories/SingletonRecipientFactoryTests.cs @@ -0,0 +1,73 @@ +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); + } + + [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()); + } + } +} diff --git a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs index 739beba..7832ff7 100644 --- a/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs +++ b/tests/NScatterGather.Tests/Recipients/InstanceRecipientTests.cs @@ -1,185 +1,63 @@ using System; -using System.Threading.Tasks; +using NScatterGather.Inspection; 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())); - } - - [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); + var registry = new TypeInspectorRegistry(); + _ = InstanceRecipient.Create(registry, new SomeType(), name: null); } - // aaa - [Fact] - public void Error_if_response_type_not_supported() + public void Error_if_registry_is_null() { - var recipient = new InstanceRecipient(typeof(SomeType)); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + Assert.Throws(() => + InstanceRecipient.Create((null as TypeInspectorRegistry)!, new SomeType(), name: null)); } [Fact] - public async Task Recipient_replies_with_response() + public void Error_if_instance_is_null() { - var recipient = new InstanceRecipient(typeof(SomeType)); - var input = 42; - var response = await recipient.ReplyWith(input); - Assert.Equal(input.ToString(), response); - } + var registry = new TypeInspectorRegistry(); - [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); + Assert.Throws(() => + InstanceRecipient.Create(registry, (null as object)!, name: null)); } [Fact] - public void Recipient_must_return_something() + public void Recipient_has_a_name() { - var recipient = new InstanceRecipient(typeof(SomeComputingType)); - bool accepts = recipient.CanAccept(typeof(int)); - Assert.False(accepts); + 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 Error_if_void_returning() + public void Can_be_cloned() { - var recipient = new InstanceRecipient(typeof(SomeComputingType)); - Assert.ThrowsAsync(() => recipient.Accept(42)); - } + var registry = new TypeInspectorRegistry(); + var recipient = InstanceRecipient.Create(registry, new SomeType(), name: "My name is"); + var clone = recipient.Clone(); - [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); + 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 Error_if_returning_task_without_result() + public void Has_expected_lifetime() { - var recipient = new InstanceRecipient(typeof(SomeAsyncComputingType)); - Assert.ThrowsAsync(() => recipient.ReplyWith(42)); + 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 4ba2daa..0000000 --- a/tests/NScatterGather.Tests/Recipients/RecipientsCollectionTests.cs +++ /dev/null @@ -1,254 +0,0 @@ -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() - { - _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 InstanceRecipient) - .Cast() - .ToList(); - - Assert.Single(one); - Assert.Equal(typeof(SomeType), one.First().Type); - - _collection.Add(); - - var two = _collection.ListRecipientsAccepting(typeof(int)) - .Where(x => x is InstanceRecipient) - .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)); - - _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() - .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 InstanceRecipient) - .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 InstanceRecipient) - .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)); - - _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() - .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 InstanceRecipient) - .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 InstanceRecipient) - .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("Some type"); - _collection.Add(new SomeOtherType(), "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 (ir.Type == typeof(SomeType)) - Assert.Equal("Some type", ir.Name); - else if (ir.Type == typeof(SomeOtherType)) - Assert.Equal("Some other type", ir.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/Run/RecipientRunTests.cs b/tests/NScatterGather.Tests/Recipients/Run/RecipientRunTests.cs new file mode 100644 index 0000000..b69899c --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/Run/RecipientRunTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Threading.Tasks; +using NScatterGather.Inspection; +using NScatterGather.Recipients; +using Xunit; + +namespace NScatterGather.Run +{ + public class RecipientRunTests + { + private readonly Recipient _recipient; + private readonly Recipient _faultingRecipient; + private readonly Recipient _anotherFaultingRecipient; + + public RecipientRunTests() + { + 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 = _recipient.Accept(42); + Assert.Same(_recipient, runner.Recipient); + } + + [Fact] + public void Initially_has_default_parameters() + { + var runner = _recipient.Accept(42); + Assert.False(runner.CompletedSuccessfully); + Assert.Equal(default, runner.Result); + Assert.False(runner.Faulted); + Assert.Null(runner.Exception); + Assert.Equal(default, runner.StartedAt); + Assert.Equal(default, runner.FinishedAt); + } + + [Fact] + public async Task Error_if_started_multiple_times() + { + var runner = _recipient.Accept(42); + await runner.Start(); + await Assert.ThrowsAsync(() => runner.Start()); + } + + [Fact] + public async Task Runner_completes() + { + var runner = _recipient.Accept(42); + await runner.Start(); + + Assert.True(runner.CompletedSuccessfully); + Assert.Equal("42", runner.Result); + + Assert.False(runner.Faulted); + Assert.Null(runner.Exception); + + Assert.NotEqual(default, runner.StartedAt); + Assert.NotEqual(default, runner.FinishedAt); + Assert.True(runner.FinishedAt >= runner.StartedAt); + } + + [Fact] + public async Task Runner_fails() + { + var runner = _faultingRecipient.Accept(42); + await runner.Start(); + + 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); + } + + [Fact] + public async Task Exception_is_extracted() + { + var runner = _faultingRecipient.Accept(42); + await runner.Start(); + + 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("A failure.", runner.Exception!.Message); + } + + [Fact] + public async Task Aggregated_exceptions_are_decomposed() + { + var runner = _anotherFaultingRecipient.Accept(42); + await runner.Start(); + + 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.NotNull(exception); + Assert.Equal("A failure.", exception!.Message); + } + } + + [Fact] + public async Task Reflection_exception_are_decomposed() + { + var runner = _anotherFaultingRecipient.Accept(42L); + await runner.Start(); + + 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("An invocation failure.", runner.Exception!.Message); + } + } +} diff --git a/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs new file mode 100644 index 0000000..af14e8a --- /dev/null +++ b/tests/NScatterGather.Tests/Recipients/TypeRecipientTests.cs @@ -0,0 +1,241 @@ +using System; +using System.Threading.Tasks; +using NScatterGather.Inspection; +using Xunit; + +namespace NScatterGather.Recipients +{ + public class TypeRecipientTests + { + [Fact] + public void Recipient_can_be_created_from_type() + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + () => new SomeType(), + name: null, + lifetime: Lifetime.Transient); + } + + [Fact] + public void Error_if_registry_is_null() + { + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: (null as TypeInspectorRegistry)!, + () => new SomeType(), + name: null, + lifetime: Lifetime.Transient); + }); + } + + [Fact] + public void Error_if_no_factory_method() + { + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + (null as Func)!, + name: null, + lifetime: Lifetime.Transient); + }); + } + + [Fact] + public void Error_if_invalid_lifetime() + { + Assert.Throws(() => + { + _ = TypeRecipient.Create( + registry: new TypeInspectorRegistry(), + () => new SomeType(), + name: null, + lifetime: (Lifetime)42); + }); + } + + [Fact] + public void Recipient_has_a_name() + { + 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 async Task Recipient_accepts_request() + { + 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 async Task Recipient_accepts_request_and_replies_with_task() + { + 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_accepts_input_parameter_type() + { + 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 Recipient_type_is_visible() + { + 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_replies_with_response_type() + { + 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 Recipient_replies_with_async_response_type() + { + 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 Error_if_request_or_response_types_are_null() + { + 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_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() + { + 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 79306a1..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(new InstanceRecipient(typeof(object))); - runner.Run(_ => Task.FromResult(42)).Wait(); + var registry = new TypeInspectorRegistry(); - var runnerFaulted = new RecipientRunner(new InstanceRecipient(typeof(bool))); - 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(new InstanceRecipient(typeof(long))); - 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(object), 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 0ffe27b..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(new InstanceRecipient(typeof(object))); - 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(new InstanceRecipient(typeof(bool))); - 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(new InstanceRecipient(typeof(long))); - 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(object), 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(bool), 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/Run/RecipientRunnerTest.cs b/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs deleted file mode 100644 index be01b70..0000000 --- a/tests/NScatterGather.Tests/Run/RecipientRunnerTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Threading.Tasks; -using NScatterGather.Recipients; -using Xunit; - -namespace NScatterGather.Run -{ - public class RecipientRunnerTest - { - private readonly Recipient _recipient; - - public RecipientRunnerTest() - { - _recipient = new InstanceRecipient(typeof(object)); - } - - [Fact] - public void Can_be_created() - { - var runner = new RecipientRunner(_recipient); - Assert.Same(_recipient, runner.Recipient); - } - - [Fact] - public void Initially_has_default_parameters() - { - var runner = new RecipientRunner(_recipient); - Assert.False(runner.CompletedSuccessfully); - Assert.Equal(default, runner.Result); - Assert.False(runner.Faulted); - Assert.Null(runner.Exception); - Assert.Equal(default, runner.StartedAt); - Assert.Equal(default, runner.FinishedAt); - } - - [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!)); - } - - [Fact] - public async Task Runner_completes() - { - var runner = new RecipientRunner(_recipient); - - await runner.Run(_ => Task.FromResult(42)); - - Assert.True(runner.CompletedSuccessfully); - Assert.Equal(42, runner.Result); - - Assert.False(runner.Faulted); - Assert.Null(runner.Exception); - - Assert.NotEqual(default, runner.StartedAt); - Assert.NotEqual(default, runner.FinishedAt); - Assert.True(runner.FinishedAt >= runner.StartedAt); - } - - [Fact] - public async Task Runner_fails() - { - var runner = new RecipientRunner(_recipient); - - var ex = new Exception(); - - await runner.Run(async _ => - { - await Task.Yield(); - throw ex; - }); - - Assert.False(runner.CompletedSuccessfully); - Assert.Equal(default, runner.Result); - - Assert.True(runner.Faulted); - Assert.Equal(ex, runner.Exception); - - Assert.NotEqual(default, runner.StartedAt); - Assert.NotEqual(default, runner.FinishedAt); - Assert.True(runner.FinishedAt >= runner.StartedAt); - } - } -} 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/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.")); + } + } +} 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/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; + } +} 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"; + } +} 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) + }); + } + } +}