Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution Setup - Message Bus Utility #411

Merged
merged 7 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/ci-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,15 @@ services:
ACCEPT_EULA: Y
SA_PASSWORD: DevP@ssw0rd!
ports:
- 1433:1433
- 1433:1433
sb-emulator:
container_name: "servicebus-emulator"
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
volumes:
- "../src/API/WesternStatesWater.WestDaat.Docker/sb-emulator.config.json:/ServiceBus_Emulator/ConfigFiles/Config.json"
ports:
- "5672:5672"
environment:
SQL_SERVER: mssql
MSSQL_SA_PASSWORD: DevP@ssw0rd!
ACCEPT_EULA: "Y"
39 changes: 34 additions & 5 deletions infrastructure/azuredeploy.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ param Product string = 'WestDAAT'
])
param Environment string


var resource_name_dashes_var = '${toLower(Product)}-${toLower(Environment)}'
var resource_name_var = '${toLower(Product)}${toLower(Environment)}'
var serverfarms_ASP_name = 'ASP-${Product}-${toUpper(Environment)}'


var wadedbserver = ((Environment == 'prod')) ? 'wade-production-server.database.windows.net' : 'wade-qa-server.database.windows.net'
var wadedbserver = ((Environment == 'prod'))
? 'wade-production-server.database.windows.net'
: 'wade-qa-server.database.windows.net'
var wadedbname = ((Environment == 'prod')) ? 'WaDE2' : 'WaDE_QA_Server'


resource resource_name 'Microsoft.Cdn/profiles@2020-04-15' = {
name: resource_name_var
location: 'Global'
Expand Down Expand Up @@ -254,6 +253,10 @@ siteConfig: {
name: 'Database:ConnectionString'
value: 'Server=tcp:${wadedbserver},1433;Initial Catalog=${wadedbname};Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'
}
{
name: 'MessageBus:ServiceBusUrl'
value: '${service_bus.name}.servicebus.windows.net'
}
]
}
scmSiteAlsoStopped: false
Expand Down Expand Up @@ -376,7 +379,33 @@ resource sites_fn_sites_azurewebsites_net 'Microsoft.Web/sites/hostNameBindings@
name: '${resource_name_dashes_var}.azurewebsites.net'
location: location
properties: {
siteName: 'wade-westdaat-qa-fn'
hostNameType: 'Verified'
}
}

resource service_bus 'Microsoft.ServiceBus/namespaces@2021-06-01-preview' = {
name: resource_name_dashes_var
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
properties: {
disableLocalAuth: false
zoneRedundant: false
}
}

// Will need to match the following locations:
// AzureNames.Queues list
// sb-emulator.config.json
var queueNames = [
'conservation-application-submitted'
]

resource sbQueues 'Microsoft.ServiceBus/namespaces/queues@2021-06-01-preview' = [
for queueName in queueNames: {
parent: service_bus
name: queueName
}
]
12 changes: 7 additions & 5 deletions src/API/WesternStatesWater.WestDaat.Client.Functions/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@
services.AddHttpContextAccessor();

// Config
services.AddScoped(_ => configuration.GetBlobStorageConfiguration());
services.AddScoped(_ => configuration.GetDatabaseConfiguration());
services.AddScoped(_ => configuration.GetMessageBusConfiguration());
services.AddScoped(_ => configuration.GetNldiConfiguration());
services.AddScoped(_ => configuration.GetSmtpConfiguration());
services.AddScoped(_ => configuration.GetBlobStorageConfiguration());
services.AddScoped(_ => configuration.GetPerformanceConfiguration());
services.AddScoped(_ => configuration.GetSmtpConfiguration());

// Managers
services.AddTransient<IApplicationManager, ConservationManager>();
Expand Down Expand Up @@ -83,11 +84,12 @@

// Utilities / SDKs
services.AddScoped<IContextUtility, ContextUtility>();
services.AddTransient<IEmailNotificationSdk, EmailNotificationSdk>();
services.AddTransient<IUsgsNldiSdk, UsgsNldiSdk>();
services.AddTransient<IBlobStorageSdk, BlobStorageSdk>();
services.AddTransient<ITemplateResourceSdk, TemplateResourceSdk>();
services.AddTransient<IEmailNotificationSdk, EmailNotificationSdk>();
services.AddTransient<IMessageBusUtility, MessageBusUtility>();
services.AddTransient<ISecurityUtility, SecurityUtility>();
services.AddTransient<ITemplateResourceSdk, TemplateResourceSdk>();
services.AddTransient<IUsgsNldiSdk, UsgsNldiSdk>();

services.AddHttpClient<IUsgsNldiSdk, UsgsNldiSdk>(a =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
using Microsoft.Extensions.Configuration;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;

namespace WesternStatesWater.WestDaat.Common.Configuration
{
public static class ConfigurationHelper
{
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{
{ "Values:AzureWebJobsStorage", "UseDevelopmentStorage=true" },
{
{ "Values:AzureWebJobsStorage", "UseDevelopmentStorage=true" },
{ $"{ConfigurationRootNames.Database}:{nameof(DatabaseConfiguration.ConnectionString)}", "Server=.;Initial Catalog=WaDE2;Integrated Security=true;TrustServerCertificate=true;" },
{ $"{ConfigurationRootNames.UsgsNldiService}:{nameof(UsgsNldiServiceConfiguration.BaseAddress)}", "https://labs.waterdata.usgs.gov/api/nldi/" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxUpstreamMainDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxUpstreamTributaryDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxDownstreamMainDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxDownstreamDiversionDistance)}", "50" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.WaterRightsSearchPageSize)}", "100" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.MaxRecordsDownload)}", "100000" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.DownloadCommandTimeout)}", "120"}
};
$"{ConfigurationRootNames.Database}:{nameof(DatabaseConfiguration.ConnectionString)}",
"Server=.;Initial Catalog=WaDE2;Integrated Security=true;TrustServerCertificate=true;"
},
{ $"{ConfigurationRootNames.UsgsNldiService}:{nameof(UsgsNldiServiceConfiguration.BaseAddress)}", "https://labs.waterdata.usgs.gov/api/nldi/" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxUpstreamMainDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxUpstreamTributaryDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxDownstreamMainDistance)}", "50" },
{ $"{ConfigurationRootNames.Nldi}:{nameof(NldiConfiguration.MaxDownstreamDiversionDistance)}", "50" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.WaterRightsSearchPageSize)}", "100" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.MaxRecordsDownload)}", "100000" },
{ $"{ConfigurationRootNames.Performance}:{nameof(PerformanceConfiguration.DownloadCommandTimeout)}", "120" }
};

public static DatabaseConfiguration GetDatabaseConfiguration(this IConfiguration config)
{
Expand All @@ -42,10 +47,21 @@ public static BlobStorageConfiguration GetBlobStorageConfiguration(this IConfigu
{
return config.GetSection(ConfigurationRootNames.Blob).Get<BlobStorageConfiguration>() ?? new BlobStorageConfiguration();
}

public static PerformanceConfiguration GetPerformanceConfiguration(this IConfiguration config)
{
return config.GetSection(ConfigurationRootNames.Performance).Get<PerformanceConfiguration>() ?? new PerformanceConfiguration();
}

public static MessageBusConfiguration GetMessageBusConfiguration(this IConfiguration config)
{
return config.GetSection(ConfigurationRootNames.MessageBus).Get<MessageBusConfiguration>() ?? new MessageBusConfiguration();
}

public static TokenCredential TokenCredential => new ChainedTokenCredential(
new AzureCliCredential(), // When Local
new DefaultAzureCredential() // When Azure
);
}

public static class ConfigurationRootNames
Expand All @@ -56,5 +72,7 @@ public static class ConfigurationRootNames
public const string Nldi = "Nldi";
public const string Blob = "BlobStorage";
public const string Performance = "Performance";
public const string MessageBus = "MessageBus";
public const string Environment = "Environment";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace WesternStatesWater.WestDaat.Common.Configuration;

public class MessageBusConfiguration
{
public string ServiceBusUrl { get; set; }

public bool UseDevelopmentEmulator { get; set; }
}
13 changes: 13 additions & 0 deletions src/API/WesternStatesWater.WestDaat.Common/Constants/AzureNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace WesternStatesWater.WestDaat.Common.Constants;

/// <summary>
/// List of Azure Service Bus queues used by the application.
/// Will need to match the following locations:
/// azuredeploy.json (bicep file)
/// sb-emulator.config.json (service bus emulator config).
/// </summary>
public static class Queues
{
public const string SmokeTest = "smoke-test"; // Local only
public const string ConservationApplicationSubmitted = "conservation-application-submitted";
}
15 changes: 13 additions & 2 deletions src/API/WesternStatesWater.WestDaat.Docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ services:
image: mcr.microsoft.com/mssql/server:2019-latest
container_name: mssql
environment:
ACCEPT_EULA: 'Y'
ACCEPT_EULA: "Y"
SA_PASSWORD: DevP@ssw0rd!
ports:
- 1433:1433
- 1433:1433
sb-emulator:
container_name: "servicebus-emulator"
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
volumes:
- "./sb-emulator.config.json:/ServiceBus_Emulator/ConfigFiles/Config.json"
ports:
- "5672:5672"
environment:
SQL_SERVER: mssql
MSSQL_SA_PASSWORD: DevP@ssw0rd!
ACCEPT_EULA: "Y"
21 changes: 21 additions & 0 deletions src/API/WesternStatesWater.WestDaat.Docker/sb-emulator.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"UserConfig": {
"Namespaces": [
{
"Name": "westdaat",
"Queues": [
{
"Name": "smoke-test"
},
{
"Name": "conservation-application-submitted"
}
],
"Topics": []
}
],
"Logging": {
"Type": "File"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Azure.Messaging.ServiceBus;
using WesternStatesWater.Shared.DataContracts;
using WesternStatesWater.WestDaat.Common.Configuration;
using WesternStatesWater.WestDaat.Common.Constants;
using WesternStatesWater.WestDaat.Utilities;

namespace WesternStatesWater.WestDaat.Tests.UtilitiesTests;

[TestClass]
public class MessageBusUtilityTests : UtilitiesTestBase
{
[TestMethod]
public async Task SendMessageAsyncTest_ShouldReceiveMessage()
{
// Arrange
var messageBusUtility = new MessageBusUtility(Configuration.GetMessageBusConfiguration());
var messageObject = new SmokeTestRequest { Message = "Look Ma! It's working!" };

try
{
// Act
await messageBusUtility.SendMessageAsync(Queues.SmokeTest, messageObject);
}
catch (ServiceBusException e)
{
throw new AssertFailedException(
$"Failed to send message to queue '{Queues.SmokeTest}'. "
+ "Make sure the queue exists and the Azure Service Bus Emulator docker container is running.", e);
}
}

class SmokeTestRequest : RequestBase
{
public string Message { get; set; }

Check warning on line 34 in src/API/WesternStatesWater.WestDaat.Tests.Utilities.Tests/MessageBusUtilityTests.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Message' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 34 in src/API/WesternStatesWater.WestDaat.Tests.Utilities.Tests/MessageBusUtilityTests.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Message' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
Configuration = new ConfigurationBuilder()
.AddInMemoryCollection(ConfigurationHelper.DefaultConfiguration)
.AddInMemoryCollection(DefaultTestConfiguration)

Check warning on line 15 in src/API/WesternStatesWater.WestDaat.Tests.Utilities.Tests/UtilitiesTestBase.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'Dictionary<string, string>' cannot be used for parameter 'initialData' of type 'IEnumerable<KeyValuePair<string, string?>>' in 'IConfigurationBuilder MemoryConfigurationBuilderExtensions.AddInMemoryCollection(IConfigurationBuilder configurationBuilder, IEnumerable<KeyValuePair<string, string?>>? initialData)' due to differences in the nullability of reference types.

Check warning on line 15 in src/API/WesternStatesWater.WestDaat.Tests.Utilities.Tests/UtilitiesTestBase.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'Dictionary<string, string>' cannot be used for parameter 'initialData' of type 'IEnumerable<KeyValuePair<string, string?>>' in 'IConfigurationBuilder MemoryConfigurationBuilderExtensions.AddInMemoryCollection(IConfigurationBuilder configurationBuilder, IEnumerable<KeyValuePair<string, string?>>? initialData)' due to differences in the nullability of reference types.
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddUserSecrets(Assembly.GetExecutingAssembly(), true)
Expand All @@ -22,7 +22,8 @@

public static Dictionary<string, string> DefaultTestConfiguration => new()
{

{ $"{ConfigurationRootNames.MessageBus}:{nameof(MessageBusConfiguration.UseDevelopmentEmulator)}", "true" },
{ $"{ConfigurationRootNames.MessageBus}:{nameof(MessageBusConfiguration.ServiceBusUrl)}", "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" },
};

private ILoggerFactory? _loggerFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using WesternStatesWater.Shared.DataContracts;

namespace WesternStatesWater.WestDaat.Utilities;

/// <summary>
/// Message bus utility that handles interacting with an external message bus. When
/// running in a local development environment, the Azure Service Bus Emulator is used.
/// </summary>
public interface IMessageBusUtility
{
Task SendMessageAsync<T>(string queueOrTopicName, T messageObject) where T : RequestBase;
}
42 changes: 42 additions & 0 deletions src/API/WesternStatesWater.WestDaat.Utilities/MessageBusUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Concurrent;
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using WesternStatesWater.Shared.DataContracts;
using WesternStatesWater.WestDaat.Common.Configuration;

namespace WesternStatesWater.WestDaat.Utilities;

public class MessageBusUtility : IMessageBusUtility, IAsyncDisposable
{
private readonly ServiceBusClient _serviceBusClient;
private readonly ConcurrentDictionary<string, ServiceBusSender> _messageBusSenderCache;

public MessageBusUtility(MessageBusConfiguration messageBusConfig)
{
_serviceBusClient = messageBusConfig.UseDevelopmentEmulator
? new ServiceBusClient(messageBusConfig.ServiceBusUrl)
: new ServiceBusClient(messageBusConfig.ServiceBusUrl, ConfigurationHelper.TokenCredential);

_messageBusSenderCache = new ConcurrentDictionary<string, ServiceBusSender>();
}

public async Task SendMessageAsync<T>(string queueOrTopicName, T messageObject) where T : RequestBase
{
var message = JsonSerializer.Serialize<RequestBase>(messageObject);

var sender = GetServiceBusSender(queueOrTopicName);

await sender.SendMessageAsync(new ServiceBusMessage(message));
}

private ServiceBusSender GetServiceBusSender(string queueOrTopicName)
{
return _messageBusSenderCache.GetOrAdd(queueOrTopicName, a => _serviceBusClient.CreateSender(a));
}

public async ValueTask DisposeAsync()
{
await _serviceBusClient.DisposeAsync();
GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WesternStatesWater.WestDaat.Common\WesternStatesWater.WestDaat.Common.csproj" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.2" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.22.1" />
<PackageReference Include="CsvHelper" Version="28.0.0" />
<PackageReference Include="GeoJSON.Text" Version="1.0.2" />
Expand All @@ -22,6 +23,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\WesternStatesWater.WestDaat.Contracts.Client\WesternStatesWater.WestDaat.Contracts.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\GeoConnexJsonLDResource.Designer.cs">
Expand Down
Loading