diff --git a/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj b/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj
new file mode 100644
index 00000000..231a00d8
--- /dev/null
+++ b/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Squadron.AzureServiceBus.Tests
+ Squadron.AzureServiceBus.Tests
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs b/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs
new file mode 100644
index 00000000..d5006200
--- /dev/null
+++ b/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Threading.Tasks;
+using Azure.Messaging.ServiceBus;
+using FluentAssertions;
+using Xunit;
+
+namespace Squadron.AzureServiceBus.Tests;
+
+public class AzureServiceBusCustomConfigTests(
+ AzureServiceBusResources azureServiceBusResource) :
+ IClassFixture>
+{
+ [Fact]
+ public async Task Send_And_Receive()
+ {
+ var sender = azureServiceBusResource.Client
+ .CreateSender("custom.topic");
+ var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("custom_message"));
+ await sender.SendMessageAsync(serviceBusMessage);
+
+ var receiver = azureServiceBusResource.Client
+ .CreateReceiver("custom.queue");
+ var message = await receiver.ReceiveMessageAsync();
+ var receivedMessage = message.Body.ToString();
+
+ receivedMessage.Should().Be("custom_message");
+ }
+}
+
+public class CustomConfig : AzureServiceBusConfig
+{
+ protected override Queue[] CreateQueues()
+ {
+ return [new Queue("custom.queue")];
+ }
+
+ protected override Topic[] CreateTopics()
+ {
+ var subscription = new Subscription(
+ "custom.subscription",
+ new SubscriptionProperties(ForwardTo: "custom.queue"));
+
+ return [new Topic("custom.topic", Subscriptions: [subscription])];
+ }
+}
diff --git a/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs b/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs
new file mode 100644
index 00000000..1d17b653
--- /dev/null
+++ b/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using FluentAssertions;
+using Xunit;
+
+namespace Squadron.AzureServiceBus.Tests;
+
+public class AzureServiceBusResourceTests(
+ AzureServiceBusResources azureServiceBusResource) :
+ IClassFixture
+{
+ [Fact]
+ public async Task Send_And_Receive()
+ {
+ var receiver = azureServiceBusResource.Client
+ .CreateReceiver(AzureServiceBusStatus.QueueName);
+ var message = await receiver.ReceiveMessageAsync();
+ var receivedMessage = message.Body.ToString();
+
+ receivedMessage.Should().Be("status_check");
+ }
+}
\ No newline at end of file
diff --git a/src/AzureServiceBus.Tests/xunit.runner.json b/src/AzureServiceBus.Tests/xunit.runner.json
new file mode 100644
index 00000000..bd5fcdd2
--- /dev/null
+++ b/src/AzureServiceBus.Tests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "appDomain": "denied",
+ "parallelizeAssembly": true
+}
\ No newline at end of file
diff --git a/src/AzureServiceBus/AzureServiceBus.csproj b/src/AzureServiceBus/AzureServiceBus.csproj
new file mode 100644
index 00000000..7a739284
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBus.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+ Squadron.AzureServiceBus
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AzureServiceBus/AzureServiceBusConfig.cs b/src/AzureServiceBus/AzureServiceBusConfig.cs
new file mode 100644
index 00000000..6942e671
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusConfig.cs
@@ -0,0 +1,102 @@
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Squadron;
+
+public class AzureServiceBusConfig
+{
+ private static readonly JsonSerializerOptions _options = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ protected virtual Queue[] CreateQueues()
+ {
+ return [];
+ }
+
+ protected virtual Topic[] CreateTopics()
+ {
+ return [];
+ }
+
+ public string Build()
+ {
+ var fileName = Path.GetTempFileName();
+
+ Queue[] queues = CreateQueues()
+ .Concat([new Queue(AzureServiceBusStatus.QueueName)])
+ .ToArray();
+
+ var config = new
+ {
+ UserConfig = new UserConfig(
+ [new Namespace(queues, CreateTopics())],
+ new Logging())
+ };
+
+ var serializedConfig = JsonSerializer.Serialize(config, options: _options);
+ File.WriteAllText(fileName, serializedConfig);
+
+ return fileName;
+ }
+}
+
+public record UserConfig(
+ Namespace[] Namespaces,
+ Logging Logging);
+public record Namespace(
+ Queue[] Queues,
+ Topic[] Topics,
+ string Name = "sbemulatorns");
+public record Queue(
+ string Name,
+ QueueProperties? Properties = null);
+public record QueueProperties(
+ bool DeadLetteringOnMessageExpiration = true,
+ string DefaultMessageTimeToLive= "PT1H",
+ string DuplicateDetectionHistoryTimeWindow = "PT1M",
+ string ForwardDeadLetteredMessagesTo = "",
+ string ForwardTo = "",
+ string LockDuration= "PT1M",
+ int MaxDeliveryCount = 5,
+ bool RequiresDuplicateDetection = false,
+ bool RequiresSession = false);
+public record Topic(
+ string Name,
+ TopicProperties? Properties = null,
+ Subscription[]? Subscriptions = null);
+public record TopicProperties(
+ string DefaultMessageTimeToLive = "PT1H",
+ string DuplicateDetectionHistoryTimeWindow = "PT1M",
+ bool RequiresDuplicateDetection = false);
+public record Subscription(
+ string Name,
+ SubscriptionProperties? Properties = null,
+ SubscriptionRule[]? Rules = null);
+public record SubscriptionProperties(
+ bool DeadLetteringOnMessageExpiration = true,
+ string DefaultMessageTimeToLive = "PT1H",
+ string LockDuration = "PT1M",
+ int MaxDeliveryCount = 5,
+ string ForwardDeadLetteredMessagesTo = "",
+ string ForwardTo = "",
+ bool RequiresSession = false);
+public record SubscriptionRule(
+ string Name,
+ TopicRuleProperties? Properties = null);
+public record TopicRuleProperties(
+ string FilterType,
+ CorrelationFilter CorrelationFilter);
+public record CorrelationFilter(
+ string ContentType,
+ string CorrelationId,
+ string Label,
+ string MessageId,
+ string ReplyTo,
+ string ReplyToSessionId,
+ string SessionId,
+ string To);
+public record Logging(string Type = "Console");
diff --git a/src/AzureServiceBus/AzureServiceBusConstants.cs b/src/AzureServiceBus/AzureServiceBusConstants.cs
new file mode 100644
index 00000000..23b1a0e2
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusConstants.cs
@@ -0,0 +1,7 @@
+namespace Squadron;
+
+internal class AzureServiceBusConstants
+{
+ internal static string SqlServerResourceName { get; } = "asb_sql_server";
+ internal static string AzureServiceBusEmulatorResourceName { get; } = "asb_emulator";
+}
diff --git a/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs b/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs
new file mode 100644
index 00000000..5ee5c067
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs
@@ -0,0 +1,18 @@
+namespace Squadron;
+
+public class AzureServiceBusDefaultOptions : ComposeResourceOptions
+ where TConfig : AzureServiceBusConfig, new()
+{
+ public override void Configure(ComposeResourceBuilder builder)
+ {
+ builder.AddContainer(
+ AzureServiceBusConstants.SqlServerResourceName);
+
+ builder
+ .AddContainer>(
+ AzureServiceBusConstants.AzureServiceBusEmulatorResourceName)
+ .AddLink(AzureServiceBusConstants.SqlServerResourceName,
+ new EnvironmentVariableMapping("SQL_SERVER", "#NAME#"),
+ new EnvironmentVariableMapping("MSSQL_SA_PASSWORD", "#MSSQL_SA_PASSWORD#"));
+ }
+}
\ No newline at end of file
diff --git a/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs b/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs
new file mode 100644
index 00000000..f354afa3
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Squadron;
+
+public class AzureServiceBusEmulatorDefaultOptions :
+ ContainerResourceOptions,
+ IComposableResourceOption
+ where TConfig : AzureServiceBusConfig, new()
+{
+ public Type ResourceType => typeof(AzureServiceBusEmulatorResource);
+
+ ///
+ /// Configure resource options for AzureServiceBusEmulatorDefaultOptions
+ ///
+ ///
+ public override void Configure(ContainerResourceBuilder builder)
+ {
+ var azureServiceBusConfig = new TConfig();
+ var configFile = azureServiceBusConfig.Build();
+
+ builder
+ .Name("asb_emulator")
+ .Image("mcr.microsoft.com/azure-messaging/servicebus-emulator")
+ .InternalPort(5672)
+ .WaitTimeout(120)
+ .AddEnvironmentVariable("ACCEPT_EULA=Y")
+ .AddVolume($"{configFile}:/ServiceBus_Emulator/ConfigFiles/Config.json")
+ .PreferLocalImage();
+ }
+}
diff --git a/src/AzureServiceBus/AzureServiceBusResource.cs b/src/AzureServiceBus/AzureServiceBusResource.cs
new file mode 100644
index 00000000..8e9924e5
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusResource.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Threading.Tasks;
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+using Xunit;
+
+namespace Squadron;
+
+///
+public class AzureServiceBusEmulatorResource
+ : AzureServiceBusEmulatorResource, TConfig>
+ where TConfig : AzureServiceBusConfig, new();
+
+///
+/// Represents an AzureServiceBus resource that can be used by unit tests.
+///
+///
+public class AzureServiceBusEmulatorResource :
+ ContainerResource,
+ IAsyncLifetime,
+ IComposableResource
+ where TOptions : ContainerResourceOptions, new()
+{
+ ///
+ /// Connection string to access the Azure Service Bus
+ ///
+ public string ConnectionString { get; private set; }
+
+ ///
+ /// Azure Service Bus client
+ ///
+ public ServiceBusClient Client { get; private set; }
+
+ ///
+ public override async Task InitializeAsync()
+ {
+ await base.InitializeAsync();
+
+ ConnectionString =
+ $"Endpoint=sb://{Manager.Instance.Address}:{Manager.Instance.HostPort}" +
+ ";SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;";
+
+ Client = new ServiceBusClient(ConnectionString);
+ await Initializer.WaitAsync(new AzureServiceBusStatus(Client));
+ }
+}
\ No newline at end of file
diff --git a/src/AzureServiceBus/AzureServiceBusResources.cs b/src/AzureServiceBus/AzureServiceBusResources.cs
new file mode 100644
index 00000000..b308e5a1
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusResources.cs
@@ -0,0 +1,25 @@
+using Azure.Messaging.ServiceBus;
+using SharpCompress;
+
+namespace Squadron;
+
+public class AzureServiceBusResources :
+ AzureServiceBusResources;
+
+public class AzureServiceBusResources :
+ ComposeResource>
+ where TConfig : AzureServiceBusConfig, new()
+{
+ private readonly Lazy> _azureServiceBusEmulatorResource;
+
+ public AzureServiceBusResources()
+ {
+ _azureServiceBusEmulatorResource = new Lazy>(() =>
+ (Managers[AzureServiceBusConstants.AzureServiceBusEmulatorResourceName].Resource
+ as AzureServiceBusEmulatorResource)!);
+ }
+
+ public string ConnectionString => _azureServiceBusEmulatorResource.Value.ConnectionString!;
+
+ public ServiceBusClient Client => _azureServiceBusEmulatorResource.Value.Client!;
+}
diff --git a/src/AzureServiceBus/AzureServiceBusStatus.cs b/src/AzureServiceBus/AzureServiceBusStatus.cs
new file mode 100644
index 00000000..07a8a8cd
--- /dev/null
+++ b/src/AzureServiceBus/AzureServiceBusStatus.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Messaging.ServiceBus;
+
+namespace Squadron;
+
+public class AzureServiceBusStatus(ServiceBusClient client) :
+ IResourceStatusProvider
+{
+ public static string QueueName = "squadron.status.check";
+
+ public async Task IsReadyAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var sender = client.CreateSender(QueueName);
+ var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("status_check"));
+ await sender.SendMessageAsync(serviceBusMessage, cancellationToken);
+
+ return new Status { IsReady = true };
+ }
+ catch (Exception ex)
+ {
+ return new Status { IsReady = false, Message = "Not ready" };
+ }
+ }
+}
diff --git a/src/Compose.Tests/NetworkTests.cs b/src/Compose.Tests/NetworkTests.cs
index 48b6ea11..8f49a31d 100644
--- a/src/Compose.Tests/NetworkTests.cs
+++ b/src/Compose.Tests/NetworkTests.cs
@@ -32,14 +32,14 @@ public async Task TwoContainer_Network_BothInSameNetwork()
string connectionString = mongoResource.GetComposeExports()["CONNECTIONSTRING_INTERNAL"];
string containerName = GetNameFromConnectionString(connectionString);
- IList response = (await _dockerClient.Containers.ListContainersAsync(
- new ContainersListParameters()));
+ IList response = await _dockerClient.Containers.ListContainersAsync(
+ new ContainersListParameters());
- ContainerListResponse container = response.Where(c => c.Names.Contains($"/{containerName}")).Single();
+ ContainerListResponse container = response.FirstOrDefault(c => c.Names.Contains($"/{containerName}"));
- string networkName = container.NetworkSettings.Networks.Keys.Where(n => n.Contains("squa_network")).Single();
+ string networkName = container?.NetworkSettings.Networks.Keys.FirstOrDefault(n => n.Contains("squa_network"));
- NetworkResponse network = (await _dockerClient.Networks.ListNetworksAsync()).Where(n => n.Name == networkName).SingleOrDefault();
+ NetworkResponse network = (await _dockerClient.Networks.ListNetworksAsync()).FirstOrDefault(n => n.Name == networkName);
network.Should().NotBeNull();
}
diff --git a/src/Compose/ComposeResource.cs b/src/Compose/ComposeResource.cs
index 21c9c6da..ff3a0630 100644
--- a/src/Compose/ComposeResource.cs
+++ b/src/Compose/ComposeResource.cs
@@ -1,83 +1,78 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Xunit;
-namespace Squadron
+namespace Squadron;
+
+public class ComposeResource : IAsyncLifetime
+ where TOptions : ComposeResourceOptions, new()
{
- public class ComposeResource : IAsyncLifetime
- where TOptions : ComposeResourceOptions, new()
- {
- public ComposeResourceSettings Settings { get; set; }
+ public ComposeResourceSettings Settings { get; set; }
- protected Dictionary Managers { get; set; }
- = new Dictionary();
+ protected Dictionary Managers { get; set; }
+ = new Dictionary();
+
+ public virtual async Task InitializeAsync()
+ {
+ var options = new TOptions();
+ var builder = ComposeResourceBuilder.New();
+ options.Configure(builder);
+ Settings = builder.Build();
- public async Task InitializeAsync()
+ foreach (var name in BuildStartOrder())
{
- var options = new TOptions();
- var builder = ComposeResourceBuilder.New();
- options.Configure(builder);
- Settings = builder.Build();
+ var mgr = new ComposeResourceManager(Settings, name);
+ Managers.Add(name, mgr);
- foreach (var name in BuildStartOrder())
+ foreach (ComposeResourceLink link in mgr.ResourceSettings.Links)
{
- var mgr = new ComposeResourceManager();
- mgr.ResourceSettings = Settings.Resources.First(x => x.Name == name);
- Managers.Add(name, mgr);
-
- var variables = new List();
- foreach (ComposeResourceLink link in mgr.ResourceSettings.Links)
+ foreach (EnvironmentVariableMapping map in link.EnvironmentVariables)
{
- foreach (EnvironmentVariableMapping map in link.EnvironmentVariables)
- {
- Dictionary exports = Managers[link.Name].Exports;
- variables.Add($"{map.Name}={GetVariableValue(map.Value, exports)}");
- }
+ Dictionary exports = Managers[link.Name].Exports;
+ mgr.AddEnvironmentVariable($"{map.Name}={GetVariableValue(map.Value, exports)}");
}
- mgr.EnvironmentVariables = Settings.GlobalEnvionmentVariables.Concat(variables);
- await mgr.StartAsync();
}
+
+ await mgr.StartAsync();
}
+ }
- public TResource GetResource(string name)
- {
- ComposeResourceManager manager = Managers[name];
- return (TResource)manager.Resource;
- }
+ public TResource GetResource(string name)
+ {
+ ComposeResourceManager manager = Managers[name];
+ return (TResource)manager.Resource;
+ }
- private string GetVariableValue(string template, Dictionary exports)
+ private string GetVariableValue(string template, Dictionary exports)
+ {
+ var value = template;
+ foreach (KeyValuePair export in exports)
{
- var value = template;
- foreach (KeyValuePair export in exports)
- {
- value = value.Replace($"#{export.Key}#", export.Value);
- }
- return value;
+ value = value.Replace($"#{export.Key}#", export.Value);
}
+ return value;
+ }
- public async Task DisposeAsync()
- {
- var stopTasks = new List();
-
- foreach (KeyValuePair mgr in Managers)
- {
- stopTasks.Add(mgr.Value.StopAsync());
- }
- await Task.WhenAll(stopTasks);
- }
+ public async Task DisposeAsync()
+ {
+ var stopTasks = new List();
- private IEnumerable BuildStartOrder()
+ foreach (KeyValuePair mgr in Managers)
{
- //TODO: Build dependency graph
- return Settings.Resources.Select(x => x.Name);
+ stopTasks.Add(mgr.Value.StopAsync());
}
+ await Task.WhenAll(stopTasks);
+ }
- private ComposableResourceSettings GetResourceSetting(string name)
- {
- return Settings.Resources.FirstOrDefault(x => x.Name == name);
- }
+ private IEnumerable BuildStartOrder()
+ {
+ //TODO: Build dependency graph
+ return Settings.Resources.Select(x => x.Name);
+ }
+
+ private ComposableResourceSettings GetResourceSetting(string name)
+ {
+ return Settings.Resources.FirstOrDefault(x => x.Name == name);
}
-}
+}
\ No newline at end of file
diff --git a/src/Compose/ComposeResourceBuilder.cs b/src/Compose/ComposeResourceBuilder.cs
index 6e291afc..60238a2d 100644
--- a/src/Compose/ComposeResourceBuilder.cs
+++ b/src/Compose/ComposeResourceBuilder.cs
@@ -2,101 +2,95 @@
using System.Collections.Generic;
using System.Linq;
-namespace Squadron
+namespace Squadron;
+
+public interface IComposableResourceBuilder
+{
+ ComposableResourceSettings Build();
+}
+
+public class ComposableResourceBuilder : IComposableResourceBuilder
+ where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new()
{
- public interface IComposableResourceBuilder
+ private readonly string _name;
+ private readonly ComposableResourceType _resourceType;
+ private List _links = new List();
+ private Action _onStarted = null;
+
+ public static ComposableResourceBuilder New(
+ string name,
+ ComposableResourceType resourceType)
+ => new ComposableResourceBuilder(name, resourceType);
+
+ private ComposableResourceBuilder(string name, ComposableResourceType resourceType)
{
- ComposableResourceSettings Build();
+ _name = name;
+ _resourceType = resourceType;
}
- public class ComposableResourceBuilder : IComposableResourceBuilder
- where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new()
+ public ComposableResourceBuilder AddLink(
+ string name,
+ params EnvironmentVariableMapping[] mappings)
{
- private readonly string _name;
- private readonly ComposableResourceType _resourceType;
- private List _links = new List();
- private Action _onStarted = null;
-
- public static ComposableResourceBuilder New(
- string name,
- ComposableResourceType resourceType)
- => new ComposableResourceBuilder(name, resourceType);
-
- private ComposableResourceBuilder(string name, ComposableResourceType resourceType)
+ _links.Add(new ComposeResourceLink
{
- _name = name;
- _resourceType = resourceType;
- }
+ Name = name,
+ EnvironmentVariables = new List(mappings)
+ });
- public ComposableResourceBuilder AddLink(
- string name,
- params EnvironmentVariableMapping[] mappings)
- {
- _links.Add(new ComposeResourceLink
- {
- Name = name,
- EnvironmentVariables = new List(mappings)
- });
-
- return this;
- }
- public ComposableResourceBuilder WithOnStarted(
- Action onStarted)
- {
- _onStarted = onStarted;
- return this;
- }
+ return this;
+ }
+ public ComposableResourceBuilder WithOnStarted(
+ Action onStarted)
+ {
+ _onStarted = onStarted;
+ return this;
+ }
- public ComposableResourceSettings Build()
- {
- var setting = new ComposableResourceSettings(
- _name,
- _resourceType,
- new List(_links),
- new TResourceOptions(),
- _onStarted
- );
-
- return setting;
- }
+ public ComposableResourceSettings Build()
+ {
+ return new ComposableResourceSettings(
+ _name,
+ _resourceType,
+ new List(_links),
+ new TResourceOptions(),
+ _onStarted);
}
+}
- public class ComposeResourceBuilder
- {
- public static ComposeResourceBuilder New() => new ComposeResourceBuilder();
+public class ComposeResourceBuilder
+{
+ public static ComposeResourceBuilder New() => new ComposeResourceBuilder();
- private List _settingsBuilder =
- new List();
+ private List _settingsBuilder =
+ new List();
- private List _globaEnvironmentVariables = new List();
+ private List _globaEnvironmentVariables = new List();
- public ComposeResourceSettings Build()
- {
- return new ComposeResourceSettings(
- new List(_globaEnvironmentVariables),
- _settingsBuilder.Select(x => x.Build()).ToList()
- );
- }
-
- public ComposableResourceBuilder AddContainer(
- string name
- )
- where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new()
- {
+ public ComposeResourceSettings Build()
+ {
+ return new ComposeResourceSettings(
+ new List(_globaEnvironmentVariables),
+ _settingsBuilder.Select(x => x.Build()).ToList());
+ }
- var crb = ComposableResourceBuilder.New(
- name,
- ComposableResourceType.Container);
- _settingsBuilder.Add(crb);
- return crb;
- }
+ public ComposableResourceBuilder AddContainer(
+ string name)
+ where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new()
+ {
- public ComposeResourceBuilder AddGlobalEnvironmentVariable(string name, string value)
- {
- _globaEnvironmentVariables.Add($"{name}={value}");
- return this;
- }
+ var crb = ComposableResourceBuilder.New(
+ name,
+ ComposableResourceType.Container);
+ _settingsBuilder.Add(crb);
+ return crb;
}
-}
+
+ public ComposeResourceBuilder AddGlobalEnvironmentVariable(string name, string value)
+ {
+ _globaEnvironmentVariables.Add($"{name}={value}");
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/Compose/ComposeResourceManager.cs b/src/Compose/ComposeResourceManager.cs
index 52868d18..ec65b2c9 100644
--- a/src/Compose/ComposeResourceManager.cs
+++ b/src/Compose/ComposeResourceManager.cs
@@ -1,60 +1,76 @@
-using System;
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
-namespace Squadron
+namespace Squadron;
+
+public class ComposeResourceManager
{
- public class ComposeResourceManager
- {
- public ComposableResourceSettings ResourceSettings { get; internal set; }
- public ContainerResourceSettings ContainerSettings { get; private set; }
- public Dictionary Exports { get; private set; }
+ private readonly ComposeResourceSettings _settings;
+ private readonly List _environmentVariables = new List();
- public IComposableResource Resource { get; set; }
- public IEnumerable EnvironmentVariables { get; internal set; }
+ public ComposeResourceManager(ComposeResourceSettings settings, string name)
+ {
+ _settings = settings;
+ ResourceSettings = settings.Resources.First(x => x.Name == name);
+ _environmentVariables.AddRange(settings.GlobalEnvionmentVariables);
+ }
- internal async Task StartAsync()
- {
- if (ResourceSettings.Type == ComposableResourceType.Container)
- {
- var builder = ContainerResourceBuilder.New();
- ResourceSettings.ContainerOptions.Configure(builder);
- BuildResourceInstance();
- Resource.SetEnvironmentVariables(EnvironmentVariables.ToList());
-
- // Give over the networks if resource is not generic
- if(!IsResourceGenericType())
- {
- IList networks = builder.Build().Networks;
- Resource.SetNetworks(networks);
- }
-
- await Resource.InitializeAsync();
- Exports = Resource.GetComposeExports();
- }
- }
+ public ComposableResourceSettings ResourceSettings { get; internal set; }
+ public ContainerResourceSettings ContainerSettings { get; private set; }
+ public Dictionary Exports { get; private set; }
+ public IComposableResource Resource { get; set; }
+ public IReadOnlyList EnvironmentVariables => _environmentVariables;
+
+ public void AddEnvironmentVariable(params string[] variable)
+ {
+ _environmentVariables.AddRange(variable);
+ }
- private void BuildResourceInstance()
+ internal async Task StartAsync()
+ {
+ if (ResourceSettings.Type == ComposableResourceType.Container)
{
- var composableOptions = (IComposableResourceOption)ResourceSettings.ContainerOptions;
- Type activateType = composableOptions.ResourceType;
+ var builder = ContainerResourceBuilder.New();
+ ResourceSettings.ContainerOptions.Configure(builder);
+ BuildResourceInstance();
+ Resource.SetEnvironmentVariables(EnvironmentVariables.ToList());
- if (IsResourceGenericType())
+ // Give over the networks if resource is not generic
+ var networks = new List();
+ if(!IsResourceGenericType())
{
- activateType = composableOptions.ResourceType
- .MakeGenericType(ResourceSettings.ContainerOptions.GetType());
+ networks.AddRange(builder.Build().Networks);
}
- Resource = (IComposableResource)Activator.CreateInstance(activateType);
+
+ networks.Add(_settings.Identifier);
+ Resource.SetNetworks(networks);
+
+ await Resource.InitializeAsync();
+ Exports = Resource.GetComposeExports();
}
-
- private bool IsResourceGenericType() =>
- ((IComposableResourceOption)ResourceSettings.ContainerOptions)
- .ResourceType.IsGenericType;
+ }
+
+ private void BuildResourceInstance()
+ {
+ var composableOptions = (IComposableResourceOption)ResourceSettings.ContainerOptions;
+ Type activateType = composableOptions.ResourceType;
- internal async Task StopAsync()
+ if (IsResourceGenericType())
{
- await Resource.DisposeAsync();
+ activateType = composableOptions.ResourceType
+ .MakeGenericType(ResourceSettings.ContainerOptions.GetType());
}
+ Resource = (IComposableResource)Activator.CreateInstance(activateType);
+ }
+
+ private bool IsResourceGenericType() =>
+ ((IComposableResourceOption)ResourceSettings.ContainerOptions)
+ .ResourceType.IsGenericTypeDefinition;
+
+ internal async Task StopAsync()
+ {
+ await Resource.DisposeAsync();
}
-}
+}
\ No newline at end of file
diff --git a/src/Compose/ComposeResourceSettings.cs b/src/Compose/ComposeResourceSettings.cs
index 3c428ccc..7b8fedaf 100644
--- a/src/Compose/ComposeResourceSettings.cs
+++ b/src/Compose/ComposeResourceSettings.cs
@@ -1,21 +1,21 @@
-using System;
+using System;
using System.Collections.Generic;
-namespace Squadron
+namespace Squadron;
+
+public class ComposeResourceSettings
{
- public class ComposeResourceSettings
- {
- public IReadOnlyList GlobalEnvionmentVariables { get; internal set; }
+ public string Identifier { get; } = $"compose_{Guid.NewGuid():N}";
+ public IReadOnlyList GlobalEnvionmentVariables { get; internal set; }
- public IReadOnlyList Resources { get; set; }
+ public IReadOnlyList Resources { get; set; }
- internal ComposeResourceSettings(
- IReadOnlyList globalEnvionmentVariables,
- IReadOnlyList resources)
- {
- GlobalEnvionmentVariables = globalEnvionmentVariables ??
- throw new ArgumentNullException(nameof(globalEnvionmentVariables));
- Resources = resources ?? throw new ArgumentNullException(nameof(resources));
- }
+ internal ComposeResourceSettings(
+ IReadOnlyList globalEnvionmentVariables,
+ IReadOnlyList resources)
+ {
+ GlobalEnvionmentVariables = globalEnvionmentVariables ??
+ throw new ArgumentNullException(nameof(globalEnvionmentVariables));
+ Resources = resources ?? throw new ArgumentNullException(nameof(resources));
}
-}
+}
\ No newline at end of file
diff --git a/src/SqlServer/SqlServerDefaultOptions.cs b/src/SqlServer/SqlServerDefaultOptions.cs
index 1e0c1d7b..e7ccf2a6 100644
--- a/src/SqlServer/SqlServerDefaultOptions.cs
+++ b/src/SqlServer/SqlServerDefaultOptions.cs
@@ -1,29 +1,32 @@
using System;
-namespace Squadron
+namespace Squadron;
+
+///
+/// Default SqlServer resource options
+///
+public class SqlServerDefaultOptions :
+ ContainerResourceOptions,
+ IComposableResourceOption
{
+ public Type ResourceType => typeof(SqlServerResource);
+
///
- /// Default SqlServer resource options
+ /// Configure resource options
///
- public class SqlServerDefaultOptions : ContainerResourceOptions
+ ///
+ public override void Configure(ContainerResourceBuilder builder)
{
- ///
- /// Configure resource options
- ///
- ///
- public override void Configure(ContainerResourceBuilder builder)
- {
- var password = "_Qtp" + Guid.NewGuid().ToString("N");
- builder
- .Name("mssql")
- .Image("mcr.microsoft.com/mssql/server:2019-latest")
- .InternalPort(1433)
- .WaitTimeout(60)
- .Username("sa")
- .Password(password)
- .AddEnvironmentVariable("ACCEPT_EULA=Y")
- .AddEnvironmentVariable($"SA_PASSWORD={password}")
- .PreferLocalImage();
- }
+ var password = "_Qtp" + Guid.NewGuid().ToString("N");
+ builder
+ .Name("mssql")
+ .Image("mcr.microsoft.com/mssql/server:2019-latest")
+ .InternalPort(1433)
+ .WaitTimeout(60)
+ .Username("sa")
+ .Password(password)
+ .AddEnvironmentVariable("ACCEPT_EULA=Y")
+ .AddEnvironmentVariable($"SA_PASSWORD={password}")
+ .PreferLocalImage();
}
-}
+}
\ No newline at end of file
diff --git a/src/SqlServer/SqlServerResource.cs b/src/SqlServer/SqlServerResource.cs
index 47ce924f..73b2a551 100644
--- a/src/SqlServer/SqlServerResource.cs
+++ b/src/SqlServer/SqlServerResource.cs
@@ -6,193 +6,193 @@
using System.Threading.Tasks;
using Xunit;
-namespace Squadron
+namespace Squadron;
+
+///
+public partial class SqlServerResource :
+ SqlServerResource;
+
+///
+/// Represents a SqlServer database resource that can be used by unit tests.
+///
+///
+public partial class SqlServerResource :
+ ContainerResource,
+ IAsyncLifetime,
+ IComposableResource
+ where TOptions : ContainerResourceOptions, new()
{
- ///
- public partial class SqlServerResource : SqlServerResource { }
+ private readonly SemaphoreSlim _sync = new SemaphoreSlim(1,1);
+ private readonly HashSet _databases = new HashSet();
+ private string _serverConnectionString;
- ///
- /// Represents a SqlServer database resource that can be used by unit tests.
- ///
- ///
- public partial class SqlServerResource
- : ContainerResource,
- IAsyncLifetime
- where TOptions : ContainerResourceOptions, new()
+ ///
+ public override async Task InitializeAsync()
{
- ///
- /// Sync lock
- ///
- protected readonly SemaphoreSlim _sync = new SemaphoreSlim(1,1);
- ///
- /// The databases
- ///
- protected readonly HashSet _databases = new HashSet();
- ///
- /// The SqlServer connection string
- ///
- protected string _serverConnectionString;
-
- ///
- public override async Task InitializeAsync()
- {
- await base.InitializeAsync();
+ await base.InitializeAsync();
- _serverConnectionString = CreateServerConnectionString();
+ _serverConnectionString = CreateServerConnectionString();
- await Initializer.WaitAsync(
- new SqlServerStatus(_serverConnectionString));
- }
+ await Initializer.WaitAsync(
+ new SqlServerStatus(_serverConnectionString));
+ }
+
+ ///
+ /// Creates a connection string targeting the given
+ ///
+ /// Database name
+ public string CreateConnectionString(string databaseName)
+ => CreateDatabaseConnectionString(databaseName);
- ///
- /// Creates a connection string targeting the given
- ///
- /// Database name
- public string CreateConnectionString(string databaseName)
- => CreateDatabaseConnectionString(databaseName);
-
- ///
- /// Create a empty database
- ///
- /// Optional: the database name.
- /// Database connection string.
- public Task CreateDatabaseAsync(string databaseName = null)
+ ///
+ /// Create a empty database
+ ///
+ /// Optional: the database name.
+ /// Database connection string.
+ public Task CreateDatabaseAsync(string databaseName = null)
+ {
+ var name = databaseName ?? UniqueNameGenerator.Create("db");
+ var script = $"CREATE DATABASE {name};";
+ return CreateDatabaseAsync(script, name);
+ }
+
+ ///
+ /// Create a database from an SQL script
+ ///
+ /// The SQL script content
+ /// The database name.
+ ///
+ /// is null or
+ /// or
+ /// is null or
+ ///
+ /// Database connection string.
+ public Task CreateDatabaseAsync(string sqlScript, string databaseName)
+ {
+ if (string.IsNullOrEmpty(sqlScript))
{
- var name = databaseName ?? UniqueNameGenerator.Create("db");
- var script = $"CREATE DATABASE {name};";
- return CreateDatabaseAsync(script, name);
+ throw new ArgumentException(
+ "The sql script cannot be null or empty.", nameof(sqlScript));
}
- ///
- /// Create a database from an SQL script
- ///
- /// The SQL script content
- /// The database name.
- ///
- /// is null or
- /// or
- /// is null or
- ///
- /// Database connection string.
- public Task CreateDatabaseAsync(string sqlScript, string databaseName)
+ if (string.IsNullOrEmpty(databaseName))
{
- if (string.IsNullOrEmpty(sqlScript))
- {
- throw new ArgumentException(
- "The sql script cannot be null or empty.", nameof(sqlScript));
- }
-
- if (string.IsNullOrEmpty(databaseName))
- {
- throw new ArgumentException(
- "The database name cannot be null or empty.", nameof(databaseName));
- }
-
- return CreateDatabaseInternalAsync(sqlScript, databaseName);
+ throw new ArgumentException(
+ "The database name cannot be null or empty.", nameof(databaseName));
}
- private async Task CreateDatabaseInternalAsync(string sqlScript, string databaseName)
+ return CreateDatabaseInternalAsync(sqlScript, databaseName);
+ }
+
+ private async Task CreateDatabaseInternalAsync(string sqlScript, string databaseName)
+ {
+ await _sync.WaitAsync();
+ try
{
- await _sync.WaitAsync();
- try
- {
- FileInfo scriptFile = CreateSqlFile(sqlScript);
- var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}");
+ FileInfo scriptFile = CreateSqlFile(sqlScript);
+ var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}");
- await Manager.CopyToContainerAsync(copyContext);
+ await Manager.CopyToContainerAsync(copyContext);
- await Manager.InvokeCommandAsync(
- ChmodCommand.ReadWrite($"/tmp/{scriptFile.Name}"));
+ await Manager.InvokeCommandAsync(
+ ChmodCommand.ReadWrite($"/tmp/{scriptFile.Name}"));
- await Manager.InvokeCommandAsync(
- SqlCommand.ExecuteFile(copyContext.Destination, Settings));
+ await Manager.InvokeCommandAsync(
+ SqlCommand.ExecuteFile(copyContext.Destination, Settings));
- _databases.Add(databaseName);
+ _databases.Add(databaseName);
- return CreateDatabaseConnectionString(databaseName);
- }
- finally
- {
- _sync.Release();
- }
+ return CreateDatabaseConnectionString(databaseName);
}
-
- ///
- /// Executes the specified on the currently running server.
- ///
- ///
- /// The SQL that shall be executed on the currently running server.
- ///
- ///
- /// is null or
- ///
- ///
- /// There is currently no SQL Server running.
- ///
- public Task ExecuteSqlAsync(string sql)
+ finally
{
- if (string.IsNullOrEmpty(sql))
- {
- throw new ArgumentException("The sql script cannot be null or empty.", nameof(sql));
- }
-
- if (string.IsNullOrEmpty(_serverConnectionString))
- {
- throw new InvalidOperationException("There is currently no databse deployed.");
- }
+ _sync.Release();
+ }
+ }
- return ExecuteSqlInternalAsync(sql);
+ ///
+ /// Executes the specified on the currently running server.
+ ///
+ ///
+ /// The SQL that shall be executed on the currently running server.
+ ///
+ ///
+ /// is null or
+ ///
+ ///
+ /// There is currently no SQL Server running.
+ ///
+ public Task ExecuteSqlAsync(string sql)
+ {
+ if (string.IsNullOrEmpty(sql))
+ {
+ throw new ArgumentException("The sql script cannot be null or empty.", nameof(sql));
}
- private async Task ExecuteSqlInternalAsync(string sql)
+ if (string.IsNullOrEmpty(_serverConnectionString))
{
- await _sync.WaitAsync();
- try
- {
- await Manager.InvokeCommandAsync(
- SqlCommand.ExecuteQuery(sql, Settings));
- }
- finally
- {
- _sync.Release();
- }
+ throw new InvalidOperationException("There is currently no databse deployed.");
}
- ///
- /// Creates the database connection string.
- ///
- /// Name of the database.
- ///
- protected string CreateDatabaseConnectionString(string databaseName)
- => $"{_serverConnectionString}Database={databaseName}";
+ return ExecuteSqlInternalAsync(sql);
+ }
+
+ public override Dictionary GetComposeExports()
+ {
+ Dictionary exports = base.GetComposeExports();
+ exports.Add("CONNECTIONSTRING", _serverConnectionString);
+ exports.Add("MSSQL_SA_PASSWORD", Settings.Password);
+ return exports;
+ }
- private string CreateServerConnectionString()
- => new StringBuilder()
- .Append($"Data Source={Manager.Instance.Address},{Manager.Instance.HostPort};")
- .Append("Integrated Security=False;")
- .Append($"User ID={Settings.Username};")
- .Append($"Password={Settings.Password};")
- .Append("MultipleActiveResultSets=True;")
- .Append("Encrypt=no;")
- .ToString();
-
- internal async Task DeployAndExecute(string sqlScript)
+ private async Task ExecuteSqlInternalAsync(string sql)
+ {
+ await _sync.WaitAsync();
+ try
{
- FileInfo scriptFile = CreateSqlFile(sqlScript);
- var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}");
-
- await Manager.CopyToContainerAsync(copyContext);
await Manager.InvokeCommandAsync(
- SqlCommand.ExecuteFile(copyContext.Destination, Settings));
-
- File.Delete(scriptFile.FullName);
+ SqlCommand.ExecuteQuery(sql, Settings));
}
-
- private FileInfo CreateSqlFile(string content)
+ finally
{
- var scriptFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".sql");
- File.WriteAllText(scriptFile, content);
- return new FileInfo(scriptFile);
+ _sync.Release();
}
}
-}
+
+ ///
+ /// Creates the database connection string.
+ ///
+ /// Name of the database.
+ ///
+ protected string CreateDatabaseConnectionString(string databaseName)
+ => $"{_serverConnectionString}Database={databaseName}";
+
+ private string CreateServerConnectionString()
+ => new StringBuilder()
+ .Append($"Data Source={Manager.Instance.Address},{Manager.Instance.HostPort};")
+ .Append("Integrated Security=False;")
+ .Append($"User ID={Settings.Username};")
+ .Append($"Password={Settings.Password};")
+ .Append("MultipleActiveResultSets=True;")
+ .Append("Encrypt=no;")
+ .ToString();
+
+ internal async Task DeployAndExecute(string sqlScript)
+ {
+ FileInfo scriptFile = CreateSqlFile(sqlScript);
+ var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}");
+
+ await Manager.CopyToContainerAsync(copyContext);
+ await Manager.InvokeCommandAsync(
+ SqlCommand.ExecuteFile(copyContext.Destination, Settings));
+
+ File.Delete(scriptFile.FullName);
+ }
+
+ private FileInfo CreateSqlFile(string content)
+ {
+ var scriptFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".sql");
+ File.WriteAllText(scriptFile, content);
+ return new FileInfo(scriptFile);
+ }
+}
\ No newline at end of file
diff --git a/src/Squadron.sln b/src/Squadron.sln
index 9873b15b..33987553 100644
--- a/src/Squadron.sln
+++ b/src/Squadron.sln
@@ -94,6 +94,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gitea", "Gitea\Gitea.csproj
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gitea.Tests", "Gitea.Tests\Gitea.Tests.csproj", "{8FFFFA62-88DC-44BB-A451-E74455AFC8D0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureServiceBus", "AzureServiceBus\AzureServiceBus.csproj", "{5EEC5295-84E9-43CB-97F9-65ACECBE45D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureServiceBus.Tests", "AzureServiceBus.Tests\AzureServiceBus.Tests.csproj", "{BFAFE620-C81C-4158-87CB-BC2A53DF16B6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -644,6 +648,30 @@ Global
{8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x64.Build.0 = Release|Any CPU
{8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x86.ActiveCfg = Release|Any CPU
{8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x86.Build.0 = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x64.Build.0 = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x86.Build.0 = Debug|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x64.ActiveCfg = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x64.Build.0 = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x86.ActiveCfg = Release|Any CPU
+ {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x86.Build.0 = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x64.Build.0 = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x86.Build.0 = Debug|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x64.ActiveCfg = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x64.Build.0 = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x86.ActiveCfg = Release|Any CPU
+ {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE