diff --git a/Liquid.All.sln b/Liquid.All.sln index d3f886b..f4b3681 100644 --- a/Liquid.All.sln +++ b/Liquid.All.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.1000 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29215.179 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Base", "src\Liquid.Base\Liquid.Base.csproj", "{C53B53BE-BA7D-4C74-B49F-23894B6AA5A6}" EndProject @@ -49,7 +49,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Domain.Tests", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Tests", "test\Liquid.Tests\Liquid.Tests.csproj", "{6BA7DC4B-0F66-415A-AE74-5DEA5D3943D5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.OnAzure.Tests", "test\Liquid.OnAzure.Tests\Liquid.OnAzure.Tests.csproj", "{9DEA4109-7542-40D5-9CCF-A673C9787E6D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Activation.Tests", "test\Liquid.Activation.Tests\Liquid.Activation.Tests.csproj", "{E5FCB486-0C76-4714-8AC3-3E468FC1DB61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.OnAzure.Tests", "test\Liquid.OnAzure.Tests\Liquid.OnAzure.Tests.csproj", "{CE80BBAE-350E-444B-A4FD-D9B58C5214E2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liquid.Runtime.Tests", "test\Liquid.Runtime.Tests\Liquid.Runtime.Tests.csproj", "{83C12AC3-1AE7-4297-B414-347790EF32A3}" EndProject @@ -115,10 +117,14 @@ Global {6BA7DC4B-0F66-415A-AE74-5DEA5D3943D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {6BA7DC4B-0F66-415A-AE74-5DEA5D3943D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BA7DC4B-0F66-415A-AE74-5DEA5D3943D5}.Release|Any CPU.Build.0 = Release|Any CPU - {9DEA4109-7542-40D5-9CCF-A673C9787E6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9DEA4109-7542-40D5-9CCF-A673C9787E6D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9DEA4109-7542-40D5-9CCF-A673C9787E6D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9DEA4109-7542-40D5-9CCF-A673C9787E6D}.Release|Any CPU.Build.0 = Release|Any CPU + {E5FCB486-0C76-4714-8AC3-3E468FC1DB61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5FCB486-0C76-4714-8AC3-3E468FC1DB61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5FCB486-0C76-4714-8AC3-3E468FC1DB61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5FCB486-0C76-4714-8AC3-3E468FC1DB61}.Release|Any CPU.Build.0 = Release|Any CPU + {CE80BBAE-350E-444B-A4FD-D9B58C5214E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE80BBAE-350E-444B-A4FD-D9B58C5214E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE80BBAE-350E-444B-A4FD-D9B58C5214E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE80BBAE-350E-444B-A4FD-D9B58C5214E2}.Release|Any CPU.Build.0 = Release|Any CPU {83C12AC3-1AE7-4297-B414-347790EF32A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {83C12AC3-1AE7-4297-B414-347790EF32A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {83C12AC3-1AE7-4297-B414-347790EF32A3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -142,7 +148,8 @@ Global {5279F425-0A2C-4889-968D-FFEB25975453} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} {90CF0830-CD3C-4DD7-8C90-584772885BE0} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} {6BA7DC4B-0F66-415A-AE74-5DEA5D3943D5} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} - {9DEA4109-7542-40D5-9CCF-A673C9787E6D} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} + {E5FCB486-0C76-4714-8AC3-3E468FC1DB61} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} + {CE80BBAE-350E-444B-A4FD-D9B58C5214E2} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} {83C12AC3-1AE7-4297-B414-347790EF32A3} = {6A0B6B3D-15FE-4C0B-97A1-7897E31C0C4E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Liquid.Activation/Liquid.Activation.csproj b/src/Liquid.Activation/Liquid.Activation.csproj index 4f87fcb..5ba3930 100644 --- a/src/Liquid.Activation/Liquid.Activation.csproj +++ b/src/Liquid.Activation/Liquid.Activation.csproj @@ -15,8 +15,15 @@ full true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Liquid.Activation/Worker/LightWorker.cs b/src/Liquid.Activation/Worker/LightWorker.cs index d3b445a..ded6bae 100644 --- a/src/Liquid.Activation/Worker/LightWorker.cs +++ b/src/Liquid.Activation/Worker/LightWorker.cs @@ -25,7 +25,7 @@ public abstract class LightWorker : LightBackgroundTask, ILightWorker protected readonly static Dictionary _queues = new Dictionary(); protected readonly static Dictionary _topics = new Dictionary(); private readonly List _inputValidationErrors = new List(); - protected ILightTelemetry Telemetry { get; } = Workbench.Instance.Telemetry != null ? (ILightTelemetry)Workbench.Instance.Telemetry.CloneService() : null; + protected ILightTelemetry Telemetry => Workbench.Instance.Telemetry; protected ILightCache Cache => Workbench.Instance.Cache; //Instance of CriticHandler to inject on the others classes private readonly CriticHandler _criticHandler = new CriticHandler(); diff --git a/src/Liquid.Base/Interfaces/Telemetry/ILightTelemetry.cs b/src/Liquid.Base/Interfaces/Telemetry/ILightTelemetry.cs index ba59721..73bf4c4 100644 --- a/src/Liquid.Base/Interfaces/Telemetry/ILightTelemetry.cs +++ b/src/Liquid.Base/Interfaces/Telemetry/ILightTelemetry.cs @@ -1,7 +1,13 @@ -using Liquid.Base.Interfaces; +// Copyright (c) Avanade Inc. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; namespace Liquid.Interfaces { + /// + /// Service that enables observability for implementers and Liquid itself. + /// public interface ILightTelemetry : IWorkbenchService { void TrackTrace(params object[] trace); @@ -10,7 +16,13 @@ public interface ILightTelemetry : IWorkbenchService void ComputeMetric(string metricLabel, double value); void BeginMetricComputation(string metricLabel); void EndMetricComputation(string metricLabel); - void EnqueueContext(string parentID, object value = null, string operationID = ""); + void EnqueueContext(string parentID, object value = null, string operationID = ""); void DequeueContext(); + + /// + /// Captures an exception in the telemetry provider. + /// + /// The exception to be captured. + void TrackException(Exception exception); } } diff --git a/src/Liquid.Base/Workbench.cs b/src/Liquid.Base/Workbench.cs index aa789ab..1adc38a 100644 --- a/src/Liquid.Base/Workbench.cs +++ b/src/Liquid.Base/Workbench.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using Liquid.Base.Interfaces.Polly; using Liquid.Interfaces; @@ -14,6 +15,10 @@ namespace Liquid /// Provides a global way to configure a Liquid application. /// [Obsolete("Please use the correct spelled class, Liquid.Base.Workbench")] + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1402:File may only contain a single type", + Justification = "Obsolete class will be removed.")] public static class WorkBench { public static ILightRepository Repository => Workbench.Instance.Repository; diff --git a/src/Liquid.OnAWS/MessageBuses/AwsSqsSns.cs b/src/Liquid.OnAWS/MessageBuses/AwsSqsSns.cs index 8ce6ced..bebc48b 100644 --- a/src/Liquid.OnAWS/MessageBuses/AwsSqsSns.cs +++ b/src/Liquid.OnAWS/MessageBuses/AwsSqsSns.cs @@ -99,7 +99,7 @@ public void ProcessQueue() catch (Exception exRegister) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exRegister); + Workbench.Instance.Telemetry.TrackException(exRegister); } } } @@ -107,7 +107,7 @@ public void ProcessQueue() catch (Exception exception) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exception); + Workbench.Instance.Telemetry.TrackException(exception); } } /// @@ -158,7 +158,7 @@ public void ProcessSubscription() catch (Exception exRegister) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exRegister); + Workbench.Instance.Telemetry.TrackException(exRegister); } } } @@ -166,7 +166,7 @@ public void ProcessSubscription() catch (Exception exception) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exception); + Workbench.Instance.Telemetry.TrackException(exception); } } diff --git a/src/Liquid.OnAzure/Databases/CosmosDB.cs b/src/Liquid.OnAzure/Databases/CosmosDB.cs index ce59de8..e192cfb 100644 --- a/src/Liquid.OnAzure/Databases/CosmosDB.cs +++ b/src/Liquid.OnAzure/Databases/CosmosDB.cs @@ -321,7 +321,7 @@ public override async Task> AddOrUpdateAsync(List listModel } catch (Exception exRegister) { - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exRegister); + Workbench.Instance.Telemetry.TrackException(exRegister); errorEntities.Add(model); } } diff --git a/src/Liquid.OnAzure/Hubs/EventHub.cs b/src/Liquid.OnAzure/Hubs/EventHub.cs index 3560d51..101b156 100644 --- a/src/Liquid.OnAzure/Hubs/EventHub.cs +++ b/src/Liquid.OnAzure/Hubs/EventHub.cs @@ -58,7 +58,7 @@ private string GetConnection(KeyValuePair item) public Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exceptionReceivedEventArgs.Exception); + Workbench.Instance.Telemetry.TrackException(exceptionReceivedEventArgs.Exception); return Task.CompletedTask; } @@ -104,7 +104,7 @@ private void ProcessHub() Exception moreInfo = new Exception($"Exception reading topic={topic.Value.TopicName} with subscription={topic.Value.Subscription} from event hub. See inner exception for details. Message={exception.Message}", exception); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); } } diff --git a/src/Liquid.OnAzure/Liquid.OnAzure.csproj b/src/Liquid.OnAzure/Liquid.OnAzure.csproj index ce0f505..d01e6b5 100644 --- a/src/Liquid.OnAzure/Liquid.OnAzure.csproj +++ b/src/Liquid.OnAzure/Liquid.OnAzure.csproj @@ -15,6 +15,9 @@ full true + + + @@ -26,6 +29,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Liquid.OnAzure/MessageBuses/ServiceBus.cs b/src/Liquid.OnAzure/MessageBuses/ServiceBus.cs index ef293e3..f58d6fa 100644 --- a/src/Liquid.OnAzure/MessageBuses/ServiceBus.cs +++ b/src/Liquid.OnAzure/MessageBuses/ServiceBus.cs @@ -1,23 +1,125 @@ -using Liquid.Base.Interfaces; +// Copyright (c) Avanade Inc. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Liquid.Activation; using Liquid.Domain; using Liquid.Domain.Base; -using Liquid.Activation; using Liquid.Runtime.Configuration.Base; using Liquid.Runtime.Telemetry; using Microsoft.Azure.ServiceBus; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Liquid.OnAzure { /// - /// Implementation of the communication component between queues and topics of the Azure, this class is specific to azure + /// Defines an object capable of creating instances of . + /// + public interface IQueueClientFactory + { + /// + /// Creates a new instance of . + /// + /// The connection string for the client. + /// The name of the queue. + /// The receive mode that the client will connect to the queue. + /// A new instance of . + IQueueClient CreateClient(string connectionString, string queueName, ReceiveMode receiveMode); + } + + /// + /// Defines an object capable of creating instances of . + /// + public interface ISubscriptionClientFactory + { + /// + /// Creates a new instance of . + /// + /// The connection string for the client. + /// The name of the topic to connect to. + /// Identifies the subscription to this topic. + /// The receive mode that the client will connect to the queue. + /// A new instance of . + ISubscriptionClient CreateClient(string connectionString, string topicName, string subscriptionName, ReceiveMode receiveMode); + } + + /// + /// Configuration source for . + /// + // TODO: should remove this class once we move to .NET configuration system + public interface IServiceBusConfigurationProvider + { + /// + /// Gets the configuration for a . + /// + /// + /// The current configuration for a service bus. + /// + ServiceBusConfiguration GetConfiguration(); + + /// + /// Gets the configuration for a . + /// + /// + /// Identifies which connection should be retrieved from the file. + /// + /// + /// The current configuration for a service bus. + /// + ServiceBusConfiguration GetConfiguration(string connectionName); + } + + /// + /// Implementation of the communication component between queues and topics of the Azure, this class is specific to azure. /// public class ServiceBus : LightWorker, IWorkbenchService { + /// + /// Factory used to create a . + /// + private readonly IQueueClientFactory _queueClientFactory = new DefaultQueueClientFactory(); + + /// + /// Factory used to create a . + /// + private readonly ISubscriptionClientFactory _subscriptionClientFactory = new DefaultSubscriptionClientFactory(); + + /// + /// Service that retrives a . + /// + private readonly IServiceBusConfigurationProvider _configurationProvider = new DefaultServiceBusConfigurationProvider(); + + /// + /// Initializes a new instance of the class. + /// + public ServiceBus() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Dependency. Used to obtain new instances of a . + /// + /// + /// Dependency. Used to obtain new instances of a . + /// + /// + /// Dependency. Used to retrieve a configuration for this class. + /// + public ServiceBus( + IQueueClientFactory queueClientFactory, + ISubscriptionClientFactory subscriptionClientFactory, + IServiceBusConfigurationProvider configurationProvider) + { + _queueClientFactory = queueClientFactory ?? throw new ArgumentNullException(nameof(queueClientFactory)); + _subscriptionClientFactory = subscriptionClientFactory ?? throw new ArgumentNullException(nameof(subscriptionClientFactory)); + _configurationProvider = configurationProvider ?? throw new ArgumentNullException(nameof(configurationProvider)); + } + /// /// Implementation of the start process queue and process topic. It must be called parent before start processes. /// @@ -36,22 +138,22 @@ public override void Initialize() /// StringConnection of the ServiceBus private string GetConnection(KeyValuePair item) { - MethodInfo method = item.Key; - string connectionKey = GetKeyConnection(method); - ServiceBusConfiguration config = null; + var method = item.Key; + var connectionKey = GetKeyConnection(method); + + ServiceBusConfiguration config; if (string.IsNullOrEmpty(connectionKey)) // Load specific settings if provided { - config = LightConfigurator.Config($"{nameof(ServiceBus)}"); + config = _configurationProvider.GetConfiguration();//LightConfigurator.Config($"{nameof(ServiceBus)}"); } else { - config = LightConfigurator.Config($"{nameof(ServiceBus)}_{connectionKey}"); + config = _configurationProvider.GetConfiguration(connectionKey);//LightConfigurator.Config($"{nameof(ServiceBus)}_{connectionKey}"); } return config.ConnectionString; } - /// /// If an error occurs in the processing, this method going to called /// @@ -60,7 +162,7 @@ private string GetConnection(KeyValuePair item) public Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exceptionReceivedEventArgs.Exception); + Workbench.Instance.Telemetry.TrackException(exceptionReceivedEventArgs.Exception); return Task.CompletedTask; } @@ -80,7 +182,7 @@ public void ProcessQueue() int takeQuantity = queue.Value.TakeQuantity; //Register Trace on the telemetry - QueueClient queueReceiver = new QueueClient(GetConnection(queue), queueName, receiveMode); + var queueReceiver = _queueClientFactory.CreateClient(GetConnection(queue), queueName, receiveMode); //Register the method to process receive message //The RegisterMessageHandler is validate for all register exist on the queue, without need loop for items @@ -96,7 +198,7 @@ public void ProcessQueue() { Exception moreInfo = new Exception($"Exception reading message from queue {queueName}. See inner exception for details. Message={exRegister.Message}", exRegister); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); //If there is a error , set DeadLetter on register if (queueReceiver.ReceiveMode == ReceiveMode.PeekLock) @@ -114,7 +216,7 @@ await queueReceiver.DeadLetterAsync(message.SystemProperties.LockToken, { Exception moreInfo = new Exception($"Error setting up queue consumption from service bus. See inner exception for details. Message={exception.Message}", exception); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); } } @@ -132,14 +234,17 @@ private void ProcessSubscription() string topicName = topic.Value.TopicName; string subscriptName = topic.Value.Subscription; ReceiveMode receiveMode = ReceiveMode.PeekLock; + if (topic.Value.DeleteAfterRead) { receiveMode = ReceiveMode.ReceiveAndDelete; } + int takeQuantity = topic.Value.TakeQuantity; //Register Trace on the telemetry - SubscriptionClient subscriptionClient = new SubscriptionClient(GetConnection(topic), topicName, subscriptName, receiveMode, null); + var subscriptionClient = _subscriptionClientFactory.CreateClient( + GetConnection(topic), topicName, subscriptName, receiveMode); //Register the method to process receive message //The RegisterMessageHandler is validate for all register exist on the queue, without need loop for items @@ -155,7 +260,7 @@ private void ProcessSubscription() { Exception moreInfo = new Exception($"Exception reading message from topic {topicName} and subscriptName {subscriptName}. See inner exception for details. Message={exRegister.Message}", exRegister); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); var exceptionDetails = $"{exRegister.Message}"; @@ -200,7 +305,7 @@ await subscriptionClient.DeadLetterAsync(message.SystemProperties.LockToken, { Exception moreInfo = new Exception($"Error setting up subscription consumption from service bus. See inner exception for details. Message={exception.Message}", exception); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); } } @@ -208,5 +313,49 @@ protected override Task ProcessAsync() { throw new NotImplementedException(); } + + /// + /// Default implementation for , + /// creates instances of . + /// + private class DefaultQueueClientFactory : IQueueClientFactory + { + /// + public IQueueClient CreateClient(string connectionString, string queueName, ReceiveMode receiveMode) + { + return new QueueClient(connectionString, queueName, receiveMode); + } + } + + /// + /// Default implementation for , + /// creates instances of . + /// + private class DefaultSubscriptionClientFactory : ISubscriptionClientFactory + { + /// + public ISubscriptionClient CreateClient(string connectionString, string topicName, string subscriptionName, ReceiveMode mode) + { + return new SubscriptionClient(connectionString, topicName, subscriptionName, mode, null); + } + } + + /// + /// Retrieves configuration using . + /// + private class DefaultServiceBusConfigurationProvider : IServiceBusConfigurationProvider + { + /// + public ServiceBusConfiguration GetConfiguration() + { + return LightConfigurator.Config($"{nameof(ServiceBus)}"); + } + + /// + public ServiceBusConfiguration GetConfiguration(string connectionKey) + { + return LightConfigurator.Config($"{nameof(ServiceBus)}_{connectionKey}"); + } + } } } diff --git a/src/Liquid.OnGoogle/MessageBuses/PubSub.cs b/src/Liquid.OnGoogle/MessageBuses/PubSub.cs index 397a821..4afa9f1 100644 --- a/src/Liquid.OnGoogle/MessageBuses/PubSub.cs +++ b/src/Liquid.OnGoogle/MessageBuses/PubSub.cs @@ -96,7 +96,7 @@ public void ProcessSubscription() catch (Exception exRegister) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exRegister); + Workbench.Instance.Telemetry.TrackException(exRegister); return await Task.FromResult(SubscriberClient.Reply.Nack); } @@ -108,7 +108,7 @@ public void ProcessSubscription() catch (Exception exception) { //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exception); + Workbench.Instance.Telemetry.TrackException(exception); } } diff --git a/src/Liquid.OnPre/MessageBuses/MicrosoftMessageQueuing.cs b/src/Liquid.OnPre/MessageBuses/MicrosoftMessageQueuing.cs index f254440..61638d0 100644 --- a/src/Liquid.OnPre/MessageBuses/MicrosoftMessageQueuing.cs +++ b/src/Liquid.OnPre/MessageBuses/MicrosoftMessageQueuing.cs @@ -73,7 +73,7 @@ public void ProcessQueue() { Exception moreInfo = new Exception($"Error setting up queue consumption from service bus. See inner exception for details. Message={exception.Message}", exception); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); } } @@ -100,7 +100,7 @@ private void ProcessSubscription() { Exception moreInfo = new Exception($"Error setting up subscription consumption from service bus. See inner exception for details. Message={exception.Message}", exception); //Use the class instead of interface because tracking exceptions directly is not supposed to be done outside AMAW (i.e. by the business code) - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(moreInfo); + Workbench.Instance.Telemetry.TrackException(moreInfo); } } diff --git a/src/Liquid.OnPre/Model/LightModel.cs b/src/Liquid.OnPre/Model/LightModel.cs new file mode 100644 index 0000000..2c81abe --- /dev/null +++ b/src/Liquid.OnPre/Model/LightModel.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using FluentValidation; +using Liquid.Domain; +using Liquid.Interfaces; +using Liquid.Runtime; +using Newtonsoft.Json; + +namespace Liquid.Repository +{ + public abstract class LightModel : ILightModel where T : LightModel, ILightModel, new() + { + public virtual string id { get; set; } + + private List _inputErrors; + + protected LightModel() + { + ValidateInstances(); + } + + [JsonIgnore] + public List InputErrors + { + get { return _inputErrors; } + set { _inputErrors = value; } + } + + /// + /// The method receive the error code to add on errors list of validation. + /// + /// The code error + protected void AddModelValidationErrorCode(string error) + { + _inputErrors.Add(error); + } + + + /// + /// The properties used to return the InputValidator. + /// + [JsonIgnore] + public Validator Validator { get; } = new Validator(); + + /// + /// The method used to input validation of ViewModel. + /// + /// Must be implemented in each derived class. + public abstract void Validate(); + + /// + /// The method used to input validation of ViewModel on FluentValidation. + /// + /// + /// + /// + protected IRuleBuilderInitial RuleFor(Expression> expression) + { + return Validator.RuleFor(expression); + } + + /// + /// Method used for mapping between Model to ViewModel + /// + /// + public void MapFrom(ILightViewModel data) + { + this.DynamicHelperData(data); + } + + /// + /// Method used to map data from Model to ViewModel. + /// + /// + private void DynamicHelperData(dynamic data) + { + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in data.GetType().GetFields()) + { + dynamic value = fieldInfo.GetValue(data); + if (value != null) + { + FieldInfo filed = this.GetFieldByNameAndType(this, fieldInfo.Name, fieldInfo.FieldType.Name); + if (filed != null) + filed.SetValue(this, value); + } + } + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (PropertyInfo propertyInfo in data.GetType().GetProperties()) + { + dynamic value = propertyInfo.GetValue(data); + if (value != null) + { + PropertyInfo field = GetPropertyByNameAndType(this, propertyInfo.Name, propertyInfo.PropertyType.Name); + if (field != null) + field.SetValue(this, value); + } + } + } + + /// + /// From an object it verifies the parameter informed, it has the same name and data type. + /// + /// + /// + /// + /// + private FieldInfo GetFieldByNameAndType(dynamic data, String name, String type) + { + FieldInfo retorno = null; + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in data.GetType().GetFields()) + { + if (fieldInfo.Name.Equals(name) && fieldInfo.FieldType.Name.Equals(type)) + { + retorno = fieldInfo; + break; + } + } + return retorno; + } + /// + /// From an object it verifies the parameter informed, it has the same name and data type. + /// + /// + /// + /// + /// + private PropertyInfo GetPropertyByNameAndType(dynamic data, String name, String type) + { + PropertyInfo retorno = null; + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (PropertyInfo propertyInfo in data.GetType().GetProperties()) + { + if (propertyInfo.Name.Equals(name) && propertyInfo.PropertyType.Name.Equals(type)) + { + retorno = propertyInfo; + break; + } + } + return retorno; + } + /// + /// Method used to create new ViewModel object from a Model. + /// + /// + /// + public LightViewModel MapTo() where U : LightViewModel, ILightViewModel, new() + { + U viewModel = new U(); + + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in this.GetType().GetFields()) + { + dynamic value = fieldInfo.GetValue(this); + if (value != null) + { + FieldInfo field = this.GetFieldByNameAndType(viewModel, fieldInfo.Name, fieldInfo.FieldType.Name); + if (field != null) + field.SetValue(viewModel, value); + } + } + + foreach (PropertyInfo fieldInfo in this.GetType().GetProperties()) + { + dynamic value = fieldInfo.GetValue(this); + if (value != null) + { + PropertyInfo field = this.GetPropertyByNameAndType(viewModel, fieldInfo.Name, fieldInfo.PropertyType.Name); + if (field != null) + field.SetValue(viewModel, value); + } + } + return viewModel; + } + + private void ValidateInstances() + { + if (!typeof(T).IsInstanceOfType(this)) + { + throw new TypeInitializationException(this.GetType().FullName, null); + } + } + } +} diff --git a/src/Liquid.OnPre/Model/LightValueObject.cs b/src/Liquid.OnPre/Model/LightValueObject.cs new file mode 100644 index 0000000..6b67252 --- /dev/null +++ b/src/Liquid.OnPre/Model/LightValueObject.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using FluentValidation; +using Liquid.Domain; +using Liquid.Interfaces; +using Liquid.Runtime; +using Newtonsoft.Json; + +namespace Liquid.Repository +{ + public abstract class LightValueObject : ILightValueObject where T : LightValueObject, ILightValueObject, new() + { + private List _inputErrors; + + protected LightValueObject() + { + ValidateInstances(); + } + + [JsonIgnore] + public List InputErrors + { + get { return _inputErrors; } + set { _inputErrors = value; } + } + + /// + /// The method receive the error code to add on errors list of validation. + /// + /// The code error + protected void AddModelValidationErrorCode(string error) + { + _inputErrors.Add(error); + } + + + /// + /// The properties used to return the InputValidator. + /// + [JsonIgnore] + public Validator Validator { get; } = new Validator(); + + /// + /// The method used to input validation of ViewModel. + /// + /// Must be implemented in each derived class. + public abstract void Validate(); + + /// + /// The method used to input validation of ViewModel on FluentValidation. + /// + /// + /// + /// + protected IRuleBuilderInitial RuleFor(Expression> expression) + { + return Validator.RuleFor(expression); + } + + /// + /// Method used for mapping between Model to ViewModel + /// + /// + public void MapFrom(ILightViewModel data) + { + this.DynamicHelperData(data); + } + + /// + /// Method used to map data from Model to ViewModel. + /// + /// + private void DynamicHelperData(dynamic data) + { + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in data.GetType().GetFields()) + { + dynamic value = fieldInfo.GetValue(data); + if (value != null) + { + FieldInfo filed = this.GetFieldByNameAndType(this, fieldInfo.Name, fieldInfo.FieldType.Name); + if (filed != null) + filed.SetValue(this, value); + } + } + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (PropertyInfo propertyInfo in data.GetType().GetProperties()) + { + dynamic value = propertyInfo.GetValue(data); + if (value != null) + { + PropertyInfo field = GetPropertyByNameAndType(this, propertyInfo.Name, propertyInfo.PropertyType.Name); + if (field != null) + field.SetValue(this, value); + } + } + } + + /// + /// From an object it verifies the parameter informed, it has the same name and data type. + /// + /// + /// + /// + /// + private FieldInfo GetFieldByNameAndType(dynamic data, String name, String type) + { + FieldInfo retorno = null; + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in data.GetType().GetFields()) + { + if (fieldInfo.Name.Equals(name) && fieldInfo.FieldType.Name.Equals(type)) + { + retorno = fieldInfo; + break; + } + } + return retorno; + } + /// + /// From an object it verifies the parameter informed, it has the same name and data type. + /// + /// + /// + /// + /// + private PropertyInfo GetPropertyByNameAndType(dynamic data, String name, String type) + { + PropertyInfo retorno = null; + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (PropertyInfo propertyInfo in data.GetType().GetProperties()) + { + if (propertyInfo.Name.Equals(name) && propertyInfo.PropertyType.Name.Equals(type)) + { + retorno = propertyInfo; + break; + } + } + return retorno; + } + /// + /// Method used to create new ViewModel object from a Model. + /// + /// + /// + public LightViewModel MapTo() where U : LightViewModel, ILightViewModel, new() + { + U viewModel = new U(); + + ///By reflection, browse viewModel by identifying all attributes and lists for validation. + foreach (FieldInfo fieldInfo in this.GetType().GetFields()) + { + dynamic value = fieldInfo.GetValue(this); + if (value != null) + { + FieldInfo field = this.GetFieldByNameAndType(viewModel, fieldInfo.Name, fieldInfo.FieldType.Name); + if (field != null) + field.SetValue(viewModel, value); + } + } + + foreach (PropertyInfo fieldInfo in this.GetType().GetProperties()) + { + dynamic value = fieldInfo.GetValue(this); + if (value != null) + { + PropertyInfo field = this.GetPropertyByNameAndType(viewModel, fieldInfo.Name, fieldInfo.PropertyType.Name); + if (field != null) + field.SetValue(viewModel, value); + } + } + return viewModel; + } + + private void ValidateInstances() + { + if (!typeof(T).IsInstanceOfType(this)) + { + throw new TypeInitializationException(this.GetType().FullName, null); + } + } + } +} diff --git a/src/Liquid.Repository/LightRepository.cs b/src/Liquid.Repository/LightRepository.cs index 98e2be3..0a24a62 100644 --- a/src/Liquid.Repository/LightRepository.cs +++ b/src/Liquid.Repository/LightRepository.cs @@ -490,7 +490,7 @@ public static Task RaiseEvent(T model, string dataOperation) } catch (Exception exRegister) { - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(exRegister); + Workbench.Instance.Telemetry.TrackException(exRegister); } return Task.FromResult(default(T)); diff --git a/src/Liquid.Repository/Liquid.Repository.csproj b/src/Liquid.Repository/Liquid.Repository.csproj index 2ffd6f4..953d211 100644 --- a/src/Liquid.Repository/Liquid.Repository.csproj +++ b/src/Liquid.Repository/Liquid.Repository.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Liquid.Runtime/Telemetry/LightTelemetryMiddleware.cs b/src/Liquid.Runtime/Telemetry/LightTelemetryMiddleware.cs index 0a45860..84bcf5d 100644 --- a/src/Liquid.Runtime/Telemetry/LightTelemetryMiddleware.cs +++ b/src/Liquid.Runtime/Telemetry/LightTelemetryMiddleware.cs @@ -99,7 +99,7 @@ public async Task Invoke(HttpContext context) } catch (Exception e) { - ((LightTelemetry)Workbench.Instance.Telemetry).TrackException(e); + Workbench.Instance.Telemetry.TrackException(e); throw; } } diff --git a/test/Liquid.Activation.Tests/LightWorkerTests.cs b/test/Liquid.Activation.Tests/LightWorkerTests.cs new file mode 100644 index 0000000..7e23ee3 --- /dev/null +++ b/test/Liquid.Activation.Tests/LightWorkerTests.cs @@ -0,0 +1,430 @@ +// Copyright (c) Avanade Inc. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using AutoFixture; +using FluentValidation; +using Liquid.Base; +using Liquid.Base.Domain; +using Liquid.Domain; +using Liquid.Domain.Base; +using Liquid.Interfaces; +using Newtonsoft.Json; +using NSubstitute; +using Xunit; + +namespace Liquid.Activation.Tests +{ + public class LightWorkerTests + { + private readonly Fixture _fixture = new Fixture(); + + public LightWorkerTests() + { + Workbench.Instance.Reset(); + Workbench.Instance.AddToCache(WorkbenchServiceType.Telemetry, Substitute.For()); + } + + [Fact] + public void InitializeWhenMockLightWorkerPresentThenQueueAndTopicsAreDiscovered() + { + var sut = new MockLightWorker(); + sut.Initialize(); + + Assert.Contains( + MockLightWorker.TopicList, + _ => _.MethodInfo.ReflectedType == typeof(MockLightWorker) + && _.MethodInfo.Name == nameof(MockLightWorker.TopicMethod)); + + Assert.Contains( + MockLightWorker.QueueList, + _ => _.MethodInfo.ReflectedType == typeof(MockLightWorker) + && _.MethodInfo.Name == nameof(MockLightWorker.QueueMethod)); + + // Given the static nature of LightWorker, we couldn't make this an isolated assertion + // TODO: Refactor LightWorker and then make this isolated + Assert.Throws(() => new MockLightWorker().Initialize()); + } + + [Theory] + [InlineData("anything")] + [InlineData(null)] + [InlineData(1)] + public void InvokeProcessWhenMethodIsNullReturnsExpectedValue(object message) + { + MethodsCollection.Value = _fixture.Create(); + + var method = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.ConstantMethod)); + + var actual = LightWorker.InvokeProcess(method, ToJsonByteStream(message)); + + Assert.Equal(MethodsCollection.Value, actual); + } + + [Fact] + public void InvokeProcessWhenMethodIsEchoMessageIsEmptyReturnsNull() + { + var method = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.EchoMethod)); + + var actual = LightWorker.InvokeProcess(method, Array.Empty()); + + Assert.Null(actual); + } + + [Fact] + public void InvokeProcessWhenMethodHasOneParametersAndMessageIsntValidJsonThrows() + { + var method = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.EchoMethod)); + + var message = ToJsonByteStream("anything"); + + message[0] = _fixture.Create(); + + Assert.ThrowsAny(() => LightWorker.InvokeProcess(method, message)); + } + + [Fact] + public void InvokeProcessWhenMethodHasZeroParametersAndMessageIsntValidReturnsExpectedValue() + { + var method = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.ConstantMethod)); + + var message = ToJsonByteStream("anything"); + + message[0] = _fixture.Create(); + + MethodsCollection.Value = _fixture.Create(); + + var actual = LightWorker.InvokeProcess(method, message); + + Assert.Equal(MethodsCollection.Value, actual); + } + + [Fact] + public void InvokeProcessWhenMessageIsValidJsonParsesItCorrectly() + { + // ARRANGE + var anonymous = new Foobar { Foo = "Bar" }; + var anonymousAsByteStream = ToJsonByteStream(anonymous); + + var method = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.EchoMethod)); + + // ACT + var actual = (Foobar)LightWorker.InvokeProcess(method, anonymousAsByteStream); + + // ASSERT + Assert.Equal(anonymous.Foo, actual.Foo); + } + + [Fact] + public void InvokeProcessWhenMethodHasZeroParametersDoesntParseMessage() + { + // ARRANGE + var mi = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.ConstantMethod)); + + // ACT + var actual = (string)LightWorker.InvokeProcess(mi, null); + + // ASSERT + Assert.Equal(MethodsCollection.Value, actual); + } + + [Fact] + public void InvokeProcessWhenMethodThrowsAsyncThrows() + { + // ARRANGE + var mi = typeof(MethodsCollection).GetMethod(nameof(MethodsCollection.ThrowsAsync)); + + var anonymous = new Foobar { Foo = "Bar" }; + var anonymousAsByteStream = ToJsonByteStream(anonymous); + + // ACT & ASSERT + Assert.ThrowsAsync(() => (Task)LightWorker.InvokeProcess(mi, anonymousAsByteStream)); + } + + [Fact] + public void FactoryWhenNoErrorsReturnsInstanceOfT() + { + var sut = new MockLightWorker(); + Assert.IsAssignableFrom(sut.Factory()); + } + + [Fact] + public void FactoryWhenNoErrorsThenDomainInstanceHasTelemetrySet() + { + var sut = new MockLightWorker(); + var actual = sut.Factory(); + + Assert.NotNull(actual.Telemetry); + } + + [Fact] + public void FactoryWhenNoErrorsThenDomainInstanceHasCacheSet() + { + // ARRANGE + var cache = Substitute.For(); + Workbench.Instance.AddToCache(WorkbenchServiceType.Cache, cache); + + // ACT + var sut = new MockLightWorker(); + var actual = sut.Factory(); + + // ASSERT + Assert.NotNull(actual.Cache); + } + + [Fact] + public void FactoryWhenNoErrorsThenDomainInstanceHasCriticHandlerSet() + { + // ARRANGE + var sut = new MockLightWorker(); + sut.ValidateInput(new MockViewModel()); + + // ACT + var actual = sut.Factory(); + + // ASSERT + Assert.NotNull(actual.CritictHandler); + } + + [Fact] + public void FactoryWhenViewModelHasErrorsThrows() + { + // ARRANGE + var sut = new MockLightWorker(); + var viewModel = new MockViewModel + { + Age = string.Empty, + }; + + sut.ValidateInput(viewModel); + + // ACT & ASSERT + Assert.ThrowsAny(() => sut.Factory()); + } + + [Fact] + public void ValidateInputWhenViewModelHasErrorsThenViewModelErrorsAreSet() + { + // ARRANGE + var sut = new MockLightWorker(); + var viewModel = new MockViewModel + { + Age = string.Empty, + }; + + // ACT + sut.ValidateInput(viewModel); + + // ASSERT + Assert.NotEmpty(viewModel.InputErrors); + } + + [Fact] + public void ValidateInputWhenPropertyHasErrorsThenViewModelErrorsAreSet() + { + var sut = new MockLightWorker(); + var viewModel = new MockViewModel + { + ComplexProperty = new ComplexViewModel + { + DateOfBirth = DateTime.Now + TimeSpan.FromDays(1), + }, + }; + + sut.ValidateInput(viewModel); + + Assert.NotEmpty(viewModel.InputErrors); + } + + [Fact] + public void ValidateInputWhenPropertyIsListAndHasErrorsThenViewModelErrorsAreSet() + { + var sut = new MockLightWorker(); + var viewModel = new MockViewModel + { + List = new List + { + new ComplexViewModel { DateOfBirth = DateTime.Now + TimeSpan.FromDays(1) }, + }, + }; + + sut.ValidateInput(viewModel); + + Assert.NotEmpty(viewModel.InputErrors); + } + + // TODO: There are translation errors in the Critics class:( + [Fact] + public void TerminateWhenCriticsHandlerHasCritics() + { + // ARRANGE + var sut = new MockLightWorker(); + var domain = sut.Factory(); + + domain.CritictHandler.Critics.Add(Substitute.For()); + + var domainResponse = new DomainResponse(); + + // ACT & ASSERT + Assert.Throws(() => sut.Terminate(domainResponse)); + } + + // TODO: There are translation errors in the Critics class:( + [Fact] + public void TerminateWhenNoCriticsReturnsTheDomainResponse() + { + // ARRANGE + var sut = new MockLightWorker(); + var domainResponse = new DomainResponse(); + + // ACT + var actual = sut.Terminate(domainResponse); + + // ASSERT + Assert.Same(actual, domainResponse); + } + + /// + /// Serialize any object to a JSON string and then convert it to a bytestream. + /// + /// The object to serialize. + /// A byestream containing the object as UTF8 bytes. + private byte[] ToJsonByteStream(object obj) + { + var anonymousAsString = JsonConvert.SerializeObject(obj); + var anonymousAsByteStream = Encoding.UTF8.GetBytes(anonymousAsString); + + return anonymousAsByteStream; + } + + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Must be public so LightWorker can access the class")] + public class Foobar + { + public string Foo { get; set; } = "Bar"; + } + + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Must be public so LightWorker can access the class")] + [MessageBus("asd")] + public class MockLightWorker : LightWorker + { + public static List<(MethodInfo MethodInfo, TopicAttribute TopicAttribute)> TopicList => _topics + .Select(kvp => (kvp.Key, kvp.Value)) + .ToList(); + + public static List<(MethodInfo MethodInfo, QueueAttribute QueueAttribute)> QueueList => _queues + .Select(kvp => (kvp.Key, kvp.Value)) + .ToList(); + + [Topic("name", "subscriptionName", 10, true)] + public static void TopicMethod() + { + } + + [Queue("name")] + public static void QueueMethod() + { + } + + // exposed method for testing + public new T Factory() + where T : LightDomain, new() + { + return base.Factory(); + } + + public new void ValidateInput(T viewModel) + where T : LightViewModel, new() + { + base.ValidateInput(viewModel); + } + + public new object Terminate(DomainResponse foo) + { + return base.Terminate(foo); + } + } + + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Must be public so LightWorker can access the class")] + public class ComplexViewModel : LightViewModel + { + public DateTime DateOfBirth { get; set; } = DateTime.Now - TimeSpan.FromDays(1); + + public override void Validate() + { + RuleFor(_ => _.DateOfBirth).LessThan(DateTime.Now); + } + } + + private class MethodsCollection + { + public static string Value { get; set; } = "string"; + + public string ConstantMethod() + { + return Value; + } + + public Foobar EchoMethod(Foobar foobar) + { + return foobar; + } + + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Method must have a parameter because of the test case.")] + public Task ThrowsAsync(Foobar foobar) + { + return Task.FromException(new TestException(string.Empty)); + } + + // Used to test throwing from a method + public class TestException : Exception + { + public TestException(string message) + : base(message) + { + } + + public TestException(string message, Exception innerException) + : base(message, innerException) + { + } + + public TestException() + { + } + } + } + + private class MockDomain : LightService + { + } + + private class MockViewModel : LightViewModel + { + public string Age { get; set; } = "13"; + + public ComplexViewModel ComplexProperty { get; set; } + + public List List { get; set; } + + public override void Validate() + { + RuleFor(_ => _.Age).NotEmpty(); + } + } + } +} diff --git a/test/Liquid.Activation.Tests/Liquid.Activation.Tests.csproj b/test/Liquid.Activation.Tests/Liquid.Activation.Tests.csproj new file mode 100644 index 0000000..44ef7d5 --- /dev/null +++ b/test/Liquid.Activation.Tests/Liquid.Activation.Tests.csproj @@ -0,0 +1,43 @@ + + + + netcoreapp2.2 + + false + + MIT + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Liquid.Base.Tests/WorkbenchTests.cs b/test/Liquid.Base.Tests/WorkbenchTests.cs index 7436fab..eb16597 100644 --- a/test/Liquid.Base.Tests/WorkbenchTests.cs +++ b/test/Liquid.Base.Tests/WorkbenchTests.cs @@ -277,6 +277,11 @@ public void TrackEvent(params object[] events) throw new NotImplementedException(); } + public void TrackException(Exception exception) + { + throw new NotImplementedException(); + } + public void TrackMetric(string metricLabel, double value) { throw new NotImplementedException(); diff --git a/test/Liquid.OnAzure.Tests/AssemblyInfo.cs b/test/Liquid.OnAzure.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..719476c --- /dev/null +++ b/test/Liquid.OnAzure.Tests/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Avanade Inc. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Xunit; + +// TODO: remove once Workbench is removed +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/Liquid.OnAzure.Tests/AzureBlobTests.cs b/test/Liquid.OnAzure.Tests/AzureBlobTests.cs index d05a032..cc056c0 100644 --- a/test/Liquid.OnAzure.Tests/AzureBlobTests.cs +++ b/test/Liquid.OnAzure.Tests/AzureBlobTests.cs @@ -39,8 +39,6 @@ public class AzureBlobTests : IDisposable public AzureBlobTests() { - Workbench.Instance.Reset(); - Workbench.Instance.AddToCache(WorkbenchServiceType.Repository, _fakeLightRepository); _sut = new AzureBlob(new MediaStorageConfiguration @@ -266,6 +264,7 @@ protected virtual void Dispose(bool isDisposing) { _stream?.Dispose(); _lightAttachment?.Dispose(); + Workbench.Instance.Reset(); } } diff --git a/test/Liquid.OnAzure.Tests/Liquid.OnAzure.Tests.csproj b/test/Liquid.OnAzure.Tests/Liquid.OnAzure.Tests.csproj index d7d0600..6652290 100644 --- a/test/Liquid.OnAzure.Tests/Liquid.OnAzure.Tests.csproj +++ b/test/Liquid.OnAzure.Tests/Liquid.OnAzure.Tests.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -24,6 +28,7 @@ + diff --git a/test/Liquid.OnAzure.Tests/ServiceBusTests.cs b/test/Liquid.OnAzure.Tests/ServiceBusTests.cs new file mode 100644 index 0000000..77e5367 --- /dev/null +++ b/test/Liquid.OnAzure.Tests/ServiceBusTests.cs @@ -0,0 +1,368 @@ +// Copyright (c) Avanade Inc. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.Xunit2; +using Liquid.Activation; +using Liquid.Domain.Base; +using Liquid.Interfaces; +using Liquid.Tests; +using Microsoft.Azure.ServiceBus; +using Newtonsoft.Json; +using NSubstitute; +using Xunit; +using static Microsoft.Azure.EventHubs.EventData; + +namespace Liquid.OnAzure.Tests +{ + public class ServiceBusTests : IDisposable + { + private const string QueueConnectionString = "QueueConnection"; + private const string TopicConnectionString = "TopicConnection"; + + private readonly Fixture _fixture = new Fixture(); + + private readonly ILightTelemetry _telemetry = Substitute.For(); + private readonly IQueueClientFactory _queueClientFactory = Substitute.For(); + private readonly IQueueClient _queueClient = Substitute.For(); + private readonly ISubscriptionClientFactory _subscriptionClientFactory = Substitute.For(); + private readonly ISubscriptionClient _subscriptionClient = Substitute.For(); + private readonly IServiceBusConfigurationProvider _configurationProvider = Substitute.For(); + + private readonly ServiceBus _sut; + + public ServiceBusTests() + { + Workbench.Instance.Reset(); + Workbench.Instance.AddToCache(WorkbenchServiceType.Telemetry, _telemetry); + + // ARRANGE IQueueClientFactory + _queueClientFactory + .CreateClient(null, null, default(ReceiveMode)) + .ReturnsForAnyArgs(_queueClient); + + _queueClient + .CompleteAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + // ARRANGE ISubscriptionClientFactory + _subscriptionClientFactory + .CreateClient(null, null, null, default(ReceiveMode)) + .ReturnsForAnyArgs(_subscriptionClient); + + _subscriptionClient + .CompleteAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + // ARRANGE IServiceBusConfigurationProvider + _configurationProvider + .GetConfiguration(QueueConnectionString) + .Returns(_fixture.Create()); + + _configurationProvider + .GetConfiguration(TopicConnectionString) + .Returns(_fixture.Create()); + + // ARRANGE ServiceBus + _sut = new ServiceBus(_queueClientFactory, _subscriptionClientFactory, _configurationProvider); + } + + [Fact] + public void InitializeWhenExistsLightWorkerForQueueThenListenerIsCreated() + { + // ARRANGE + + // ACT + _sut.Initialize(); + + // ASSERT + _queueClient + .Received(1) + .RegisterMessageHandler(Arg.Any>(), Arg.Any()); + } + + [Fact] + public void InitializeWhenExistsLightWorkerForTopicThenListenerIsCreated() + { + // ARRANGE + + // ACT + _sut.Initialize(); + + // ASSERT + _subscriptionClient + .Received(1) + .RegisterMessageHandler(Arg.Any>(), Arg.Any()); + } + + [Theory, AutoData] + public void QueueMessageHandlerWhenMessageIsValidThenNumberOfCallsIsIncremented(string body) + { + // ARRANGE + QueueWorker.WhatToDo = _ => { }; + + var before = QueueWorker.NumberOfCalls; + + _queueClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Do(callbackWithArguments => + { + var func = callbackWithArguments.Arg>(); + var message = CreateMessageFromString(body); + + func(message, default(CancellationToken)); + }); + + // ACT + _sut.Initialize(); + + // ASSERT + var after = QueueWorker.NumberOfCalls; + + Assert.Equal(before + 1, after); + _telemetry.DidNotReceive().TrackException(Arg.Any()); + _queueClient.Received().CompleteAsync(Arg.Any()); + } + + [Theory, AutoData] + public void TopicMessageHandlerWhenMessageIsValidThenNumberOfCallsIsIncrementedAsync(string body) + { + // ARRANGE + TopicWorker.WhatToDo = _ => { }; + + var before = TopicWorker.NumberOfCalls; + + _subscriptionClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Do(callbackWithArguments => + { + var func = callbackWithArguments.Arg>(); + var message = CreateMessageFromString(body); + + func(message, default(CancellationToken)).Wait(); + }); + + // ACT + _sut.Initialize(); + + // ASSERT + var after = TopicWorker.NumberOfCalls; + + Assert.Equal(before + 1, after); + _telemetry.DidNotReceive().TrackException(Arg.Any()); + _subscriptionClient.Received().CompleteAsync(Arg.Any()); + } + + [Theory, AutoData] + public void QueueMessageHandlerWhenMethodThrowsBusinessValidationExceptionThenMessageIsDeadLettered(string body) + { + // ARRANGE + QueueWorker.WhatToDo = _ => throw new BusinessValidationException(new List()); + + _queueClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Do(callbackWithArguments => + { + var func = callbackWithArguments.Arg>(); + var message = CreateMessageFromString(body); + + func(message, default(CancellationToken)).Wait(); + }); + + // ACT + _sut.Initialize(); + + // ASSERT + _queueClient + .Received(1) + .DeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, AutoData] + public void TopicMessageHandlerWhenMethodThrowsBusinessValidationExceptionThenMessageIsDeadLettered(string body) + { + // ARRANGE + TopicWorker.WhatToDo = _ => throw new BusinessValidationException(new List()); + + _subscriptionClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Do(callbackWithArguments => + { + var func = callbackWithArguments.Arg>(); + var message = CreateMessageFromString(body); + + func(message, default(CancellationToken)).GetAwaiter().GetResult(); + }); + + // ACT + _sut.Initialize(); + + // ASSERT + _subscriptionClient + .Received(1) + .DeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, AutoData] + public void TopicMessageHandlerWhenMethodThrowsInvalidInputExceptionThenMessageIsDeadLettered(string body) + { + // ARRANGE + TopicWorker.WhatToDo = _ => throw new InvalidInputException(new List()); + + _subscriptionClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Do(callbackWithArguments => + { + var func = callbackWithArguments.Arg>(); + var message = CreateMessageFromString(body); + + func(message, default(CancellationToken)).GetAwaiter().GetResult(); + }); + + // ACT + _sut.Initialize(); + + // ASSERT + _subscriptionClient + .Received(1) + .DeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public void ProcessQueueWhenErrorSettingUpSubscriptionExceptionIsTracked() + { + // ARRANGE + _queueClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Throw(new Exception()); + + // ACT + _sut.Initialize(); + + // ASSERT + _telemetry.Received().TrackException(Arg.Any()); + } + + [Fact] + public void ProcessSubscriptionWhenErrorSettingUpSubscriptionExceptionIsTracked() + { + // ARRANGE + _subscriptionClient + .When(_ => _.RegisterMessageHandler(Arg.Any>(), Arg.Any())) + .Throw(new Exception()); + + // ACT + _sut.Initialize(); + + // ASSERT + _telemetry.Received().TrackException(Arg.Any()); + } + + [Fact] + public void ExceptionReceivedHandlerWhenArgumentIsNotNullTracksExceptionAndReturnsTaskCompleted() + { + // ARRANGE + var exception = _fixture.Create(); + var action = _fixture.Create(); + var endpoint = _fixture.Create(); + var entityName = _fixture.Create(); + var clientId = _fixture.Create(); + + // ACT + var actual = _sut.ExceptionReceivedHandler(new ExceptionReceivedEventArgs(exception, action, endpoint, entityName, clientId)); + + // ASSERT + _telemetry.Received().TrackException(exception); + Assert.Equal(Task.CompletedTask, actual); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + ServiceBusTestFixture.Reset(); + } + } + + private Message CreateMessageFromString(string body) + { + var payload = JsonConvert.SerializeObject(body); + var message = new Message(Encoding.UTF8.GetBytes(payload)); + + // another hack to enable the tests... + // TODO: Remove once ServiceBus is refactored + var field = typeof(Message.SystemPropertiesCollection) + .GetField("sequenceNumber", BindingFlags.NonPublic | BindingFlags.Instance); + + field.SetValue(message.SystemProperties, 1); + + return message; + } + + [MessageBus(QueueConnectionString)] + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Used by tests and must be public")] + public class QueueWorker : LightWorker + { + public static int NumberOfCalls { get; private set; } = 0; + + public static Action WhatToDo { get; set; } = message => { }; + + [Queue("QueueName")] + public static void MyQueueMethod(string message) + { + NumberOfCalls++; + WhatToDo(message); + } + } + + [MessageBus(TopicConnectionString)] + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Used by tests and must be public")] + public class TopicWorker : LightWorker + { + public static int NumberOfCalls { get; private set; } = 0; + + public static Action WhatToDo { get; set; } = message => { }; + + [Topic("TopicName", "TopicSubscriber")] + public static void MyTopicMethod(string message) + { + NumberOfCalls++; + WhatToDo(message); + } + } + + // To enable tests, we need a reset method + private class ServiceBusTestFixture : ServiceBus + { + /// + /// This is a total hack to enable testing. Don't do this at home! + /// TODO: Remove this hack once the final refactor of is done. + /// + public static void Reset() + { + _queues.Clear(); + _topics.Clear(); + } + } + } +}