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();
+ }
+ }
+ }
+}