Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit 5c3545b

Browse files
feat: DTOSS-9084 - Docker container for service layer mesh project (#22)
Signed-off-by: Tyrrellion <[email protected]>
1 parent ae8ced3 commit 5c3545b

File tree

11 files changed

+179
-74
lines changed

11 files changed

+179
-74
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ QueueUrl=http://127.0.0.1:10001
1919
FileExtractQueueName=file-extract
2020
FileTransformQueueName=file-transform
2121
StaleHours=12
22+
BlobContainerName=incoming-mesh-files
23+
2224

2325
# API Configuration
2426
API_PORT=7071
27+
MESH_INGEST_PORT=7072
2528

2629
# Event Grid Configuration
2730
EVENT_GRID_TOPIC_URL=https://localhost:60101/api/events
2831
EVENT_GRID_TOPIC_KEY=TheLocal+DevelopmentKey=
2932

3033
# Azurite Configuration
3134
AZURITE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
32-
AZURITE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1
35+
AZURITE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite/devstoreaccount1
3336
AZURITE_BLOB_PORT=10000
3437
AZURITE_QUEUE_PORT=10001
3538
AZURITE_TABLE_PORT=10002

compose.yaml

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,59 @@ services:
88
restart: always
99
environment:
1010
FUNCTIONS_WORKER_RUNTIME: "dotnet-isolated"
11-
AzureWebJobsStorage: "${AZURE_WEB_JOBS_STORAGE}"
11+
AzureWebJobsStorage: "${AZURITE_CONNECTION_STRING}"
1212
AzureWebJobsSecretStorageType: "files"
1313
DatabaseConnectionString: "${DatabaseConnectionString}"
1414
EVENT_GRID_TOPIC_URL: "${EVENT_GRID_TOPIC_URL}"
1515
EVENT_GRID_TOPIC_KEY: "${EVENT_GRID_TOPIC_KEY}"
16+
ASPNETCORE_URLS: "http://0.0.0.0:8080"
1617
ports:
17-
- "${API_PORT}:80"
18+
- "${API_PORT}:8080"
1819
healthcheck:
19-
test: ["CMD-SHELL", "curl -f http://localhost:80/api/health || exit 1"]
20+
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
21+
interval: 30s
22+
timeout: 10s
23+
retries: 3
24+
start_period: 40s
25+
depends_on:
26+
azurite:
27+
condition: service_healthy
28+
db:
29+
condition: service_healthy
30+
networks:
31+
- backend
32+
33+
mesh-ingest:
34+
container_name: "mesh-ingest"
35+
build:
36+
context: ./Src
37+
dockerfile: ServiceLayer.MESH/Dockerfile
38+
platform: linux/amd64
39+
restart: always
40+
environment:
41+
FUNCTIONS_WORKER_RUNTIME: "dotnet-isolated"
42+
AzureWebJobsStorage: "${AZURITE_CONNECTION_STRING}"
43+
AzureWebJobsSecretStorageType: "files"
44+
DatabaseConnectionString: "${DatabaseConnectionString}"
45+
FileDiscoveryTimerExpression: "${FileDiscoveryTimerExpression}"
46+
MeshHandshakeTimerExpression: "${MeshHandshakeTimerExpression}"
47+
FileRetryTimerExpression: "${FileRetryTimerExpression}"
48+
FileExtractQueueName: "${FileExtractQueueName}"
49+
FileTransformQueueName: "${FileTransformQueueName}"
50+
StaleHours: "${StaleHours}"
51+
MeshApiBaseUrl: "http://mesh_sandbox:80/messageexchange"
52+
NbssMailboxId: "${NbssMailboxId}"
53+
MeshPassword: "${MeshPassword}"
54+
MeshSharedKey: "${MeshSharedKey}"
55+
AZURITE_CONNECTION_STRING: "${AZURITE_CONNECTION_STRING}"
56+
MeshStorageAccountUrl: "${AZURITE_CONNECTION_STRING}"
57+
ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT}"
58+
ASPNETCORE_URLS: "http://0.0.0.0:8080"
59+
BlobContainerName: "${BlobContainerName}"
60+
ports:
61+
- "${MESH_INGEST_PORT}:8080"
62+
healthcheck:
63+
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
2064
interval: 30s
2165
timeout: 10s
2266
retries: 3
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace ServiceLayer.Common;
2+
3+
public static class EnvironmentVariables
4+
{
5+
/// <summary>
6+
/// Gets an environment variable by name. Throws if not found.
7+
/// </summary>
8+
/// <param name="key">The name of the environment variable.</param>
9+
/// <returns>The value of the environment variable.</returns>
10+
/// <exception cref="InvalidOperationException">Thrown when the variable is not found or is empty.</exception>
11+
public static string GetRequired(string key)
12+
{
13+
var value = Environment.GetEnvironmentVariable(key);
14+
15+
if (string.IsNullOrEmpty(value))
16+
{
17+
throw new InvalidOperationException($"Environment variable '{key}' is not set or is empty.");
18+
}
19+
20+
return value;
21+
}
22+
}

src/ServiceLayer.Mesh/Configuration/AppConfiguration.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using ServiceLayer.Common;
2+
13
namespace ServiceLayer.Mesh.Configuration;
24

35
public class AppConfiguration :
@@ -19,12 +21,7 @@ public class AppConfiguration :
1921

2022
private static string GetRequired(string key)
2123
{
22-
var value = Environment.GetEnvironmentVariable(key);
23-
24-
if (string.IsNullOrEmpty(value))
25-
{
26-
throw new InvalidOperationException($"Environment variable '{key}' is not set or is empty.");
27-
}
24+
var value = EnvironmentVariables.GetRequired(key);
2825

2926
return value;
3027
}

src/ServiceLayer.Mesh/DockerFile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS installer-env
2+
WORKDIR /src/dotnet-function-app
3+
4+
COPY ./dotnet-mesh-client/application/DotNetMeshClient/NHS.Mesh.Client/NHS.Mesh.Client.csproj ../dotnet-mesh-client/application/DotNetMeshClient/NHS.Mesh.Client/
5+
COPY ./ServiceLayer.Common/ServiceLayer.Common.csproj ../ServiceLayer.Common/
6+
COPY ./ServiceLayer.Mesh/ServiceLayer.Mesh.csproj .
7+
RUN dotnet restore
8+
9+
COPY ./dotnet-mesh-client/ ../dotnet-mesh-client/
10+
COPY ./ServiceLayer.Common/ ../ServiceLayer.Common/
11+
COPY ./ServiceLayer.Mesh/ .
12+
13+
RUN dotnet publish -c Release -o /home/site/wwwroot
14+
15+
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated9.0 AS production
16+
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
17+
AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
18+
ASPNETCORE_ENVIRONMENT=Production
19+
20+
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
21+
USER appuser
22+
23+
COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]

src/ServiceLayer.Mesh/Functions/FileDiscoveryFunction.cs

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,51 @@
77
using ServiceLayer.Mesh.Configuration;
88
using ServiceLayer.Mesh.Messaging;
99

10-
namespace ServiceLayer.Mesh.Functions
10+
namespace ServiceLayer.Mesh.Functions;
11+
12+
public class FileDiscoveryFunction(
13+
ILogger<FileDiscoveryFunction> logger,
14+
IFileDiscoveryFunctionConfiguration configuration,
15+
IMeshInboxService meshInboxService,
16+
ServiceLayerDbContext serviceLayerDbContext,
17+
IFileExtractQueueClient fileExtractQueueClient)
1118
{
12-
public class FileDiscoveryFunction(
13-
ILogger<FileDiscoveryFunction> logger,
14-
IFileDiscoveryFunctionConfiguration configuration,
15-
IMeshInboxService meshInboxService,
16-
ServiceLayerDbContext serviceLayerDbContext,
17-
IFileExtractQueueClient fileExtractQueueClient)
19+
[Function("FileDiscoveryFunction")]
20+
public async Task Run([TimerTrigger("%FileDiscoveryTimerExpression%")] TimerInfo myTimer)
1821
{
19-
[Function("FileDiscoveryFunction")]
20-
public async Task Run([TimerTrigger("%FileDiscoveryTimerExpression%")] TimerInfo myTimer)
22+
logger.LogInformation("{functionName} started at: {time}", nameof(FileDiscoveryFunction), DateTime.UtcNow);
23+
24+
var response = await meshInboxService.GetMessagesAsync(configuration.NbssMeshMailboxId);
25+
26+
foreach (var messageId in response.Response.Messages)
2127
{
22-
logger.LogInformation("{functionName} started at: {time}", nameof(FileDiscoveryFunction), DateTime.UtcNow);
28+
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();
2329

24-
var response = await meshInboxService.GetMessagesAsync(configuration.NbssMeshMailboxId);
30+
var existing = await serviceLayerDbContext.MeshFiles
31+
.AnyAsync(f => f.FileId == messageId);
2532

26-
foreach (var messageId in response.Response.Messages)
33+
if (!existing)
2734
{
28-
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();
35+
var file = new MeshFile
36+
{
37+
FileId = messageId,
38+
FileType = MeshFileType.NbssAppointmentEvents,
39+
MailboxId = configuration.NbssMeshMailboxId,
40+
Status = MeshFileStatus.Discovered,
41+
FirstSeenUtc = DateTime.UtcNow,
42+
LastUpdatedUtc = DateTime.UtcNow
43+
};
2944

30-
var existing = await serviceLayerDbContext.MeshFiles
31-
.AnyAsync(f => f.FileId == messageId);
45+
serviceLayerDbContext.MeshFiles.Add(file);
3246

33-
if (!existing)
34-
{
35-
var file = new MeshFile
36-
{
37-
FileId = messageId,
38-
FileType = MeshFileType.NbssAppointmentEvents,
39-
MailboxId = configuration.NbssMeshMailboxId,
40-
Status = MeshFileStatus.Discovered,
41-
FirstSeenUtc = DateTime.UtcNow,
42-
LastUpdatedUtc = DateTime.UtcNow
43-
};
44-
45-
serviceLayerDbContext.MeshFiles.Add(file);
46-
47-
await serviceLayerDbContext.SaveChangesAsync();
48-
await transaction.CommitAsync();
49-
50-
await fileExtractQueueClient.EnqueueFileExtractAsync(file);
51-
}
52-
else
53-
{
54-
await transaction.RollbackAsync();
55-
}
47+
await serviceLayerDbContext.SaveChangesAsync();
48+
await transaction.CommitAsync();
49+
50+
await fileExtractQueueClient.EnqueueFileExtractAsync(file);
51+
}
52+
else
53+
{
54+
await transaction.RollbackAsync();
5655
}
5756
}
5857
}

src/ServiceLayer.Mesh/Functions/FileExtractFunction.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,12 @@ public class FileExtractFunction(
2323
[Function("FileExtractFunction")]
2424
public async Task Run([QueueTrigger("%FileExtractQueueName%")] FileExtractQueueMessage message)
2525
{
26-
logger.LogInformation("{functionName} started at: {time}", nameof(FileDiscoveryFunction), DateTime.UtcNow);
26+
logger.LogInformation("{functionName} started.", nameof(FileExtractFunction));
2727

2828
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();
2929

3030
var file = await GetFileAsync(message.FileId);
31-
if (file == null)
32-
{
33-
return;
34-
}
35-
36-
if (!IsFileSuitableForExtraction(file))
31+
if (file == null || !IsFileSuitableForExtraction(file))
3732
{
3833
return;
3934
}

src/ServiceLayer.Mesh/Functions/FileRetryFunction.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ public async Task Run([TimerTrigger("%FileRetryTimerExpression%")] TimerInfo myT
2222

2323
var staleDateTimeUtc = DateTime.UtcNow.AddHours(-configuration.StaleHours);
2424

25-
await Task.WhenAll(
26-
RetryStaleExtractions(staleDateTimeUtc),
27-
RetryStaleTransformations(staleDateTimeUtc));
25+
await RetryStaleExtractions(staleDateTimeUtc);
26+
await RetryStaleTransformations(staleDateTimeUtc);
2827
}
2928

3029
private async Task RetryStaleExtractions(DateTime staleDateTimeUtc)

src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public async Task Run([QueueTrigger("%FileTransformQueueName%")] FileTransformQu
4444
var parsedfile = fileParser.Parse(fileContent);
4545

4646
// TODO - take dependency on IEnumerable<IFileTransformer>.
47-
// After initial common checks against database, find the appropriate implementation of IFileTransformer to handle the functionality that differs between file type.
47+
// After initial common checks against database, find the appropriate implementation of IFileTransformer
48+
// to handle the functionality that differs between file types.
4849
}
4950

5051
private async Task UpdateFileStatusForTransformation(MeshFile file)

src/ServiceLayer.Mesh/Program.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,52 @@
88
using ServiceLayer.Mesh.Configuration;
99
using ServiceLayer.Mesh.Messaging;
1010
using ServiceLayer.Data;
11+
using ServiceLayer.Mesh.Storage;
12+
using ServiceLayer.Common;
1113
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;
1214

1315
var host = new HostBuilder()
1416
.ConfigureFunctionsWebApplication()
1517
.ConfigureServices(services =>
1618
{
17-
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
19+
var environment = EnvironmentVariables.GetRequired("ASPNETCORE_ENVIRONMENT");
1820
var isLocalEnvironment = environment == "Development";
1921

2022
// MESH Client config
2123
services
22-
.AddMeshClient(_ => _.MeshApiBaseUrl = Environment.GetEnvironmentVariable("MeshApiBaseUrl"))
23-
.AddMailbox(Environment.GetEnvironmentVariable("NbssMailboxId"), new NHS.MESH.Client.Configuration.MailboxConfiguration
24+
.AddMeshClient(_ => _.MeshApiBaseUrl = EnvironmentVariables.GetRequired("MeshApiBaseUrl"))
25+
.AddMailbox(EnvironmentVariables.GetRequired("NbssMailboxId"), new NHS.MESH.Client.Configuration.MailboxConfiguration
2426
{
25-
Password = Environment.GetEnvironmentVariable("MeshPassword"),
26-
SharedKey = Environment.GetEnvironmentVariable("MeshSharedKey"),
27+
Password = EnvironmentVariables.GetRequired("MeshPassword"),
28+
SharedKey = EnvironmentVariables.GetRequired("MeshSharedKey"),
2729
}).Build();
2830

2931
// EF Core DbContext
3032
services.AddDbContext<ServiceLayerDbContext>(options =>
3133
{
32-
var connectionString = Environment.GetEnvironmentVariable("DatabaseConnectionString");
34+
var connectionString = EnvironmentVariables.GetRequired("DatabaseConnectionString");
3335
if (string.IsNullOrEmpty(connectionString))
3436
throw new InvalidOperationException("The connection string has not been initialized.");
3537

3638
options.UseSqlServer(connectionString);
3739
});
3840

41+
var queueClientOptions = new QueueClientOptions
42+
{
43+
MessageEncoding = QueueMessageEncoding.Base64
44+
};
45+
3946
// Register QueueClients as singletons
4047
services.AddSingleton(provider =>
4148
{
4249
if (isLocalEnvironment)
4350
{
44-
var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
45-
return new QueueServiceClient(connectionString);
51+
var connectionString = EnvironmentVariables.GetRequired("AzureWebJobsStorage");
52+
return new QueueServiceClient(connectionString, queueClientOptions);
4653
}
4754

48-
var meshStorageAccountUrl = Environment.GetEnvironmentVariable("MeshStorageAccountUrl");
49-
return new QueueServiceClient(new Uri(meshStorageAccountUrl), new DefaultAzureCredential());
55+
var meshStorageAccountUrl = EnvironmentVariables.GetRequired("MeshStorageAccountUrl");
56+
return new QueueServiceClient(new Uri(meshStorageAccountUrl), new DefaultAzureCredential(), queueClientOptions);
5057
});
5158

5259
services.AddSingleton<IFileExtractQueueClient, FileExtractQueueClient>();
@@ -56,10 +63,12 @@
5663
services.AddSingleton(provider =>
5764
{
5865
return new BlobContainerClient(
59-
Environment.GetEnvironmentVariable("AzureWebJobsStorage"),
60-
Environment.GetEnvironmentVariable("BlobContainerName"));
66+
EnvironmentVariables.GetRequired("AzureWebJobsStorage"),
67+
EnvironmentVariables.GetRequired("BlobContainerName"));
6168
});
6269

70+
services.AddSingleton<IMeshFilesBlobStore, MeshFilesBlobStore>();
71+
6372
services.AddTransient<IFileDiscoveryFunctionConfiguration, AppConfiguration>();
6473
services.AddTransient<IFileExtractFunctionConfiguration, AppConfiguration>();
6574
services.AddTransient<IFileExtractQueueClientConfiguration, AppConfiguration>();

0 commit comments

Comments
 (0)