Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
83e0f79
Add Mesh client to sln file
ianfnelson May 12, 2025
1950834
wip
ianfnelson May 12, 2025
5009f6c
feat: ExtractFunction attempts to upload file to blob storage
alex-clayton-1 May 12, 2025
f5adb31
Queue client implementations
ianfnelson May 13, 2025
939f9ee
renames
ianfnelson May 13, 2025
e14e3f8
sketching out storage impl
ianfnelson May 13, 2025
71c2f53
tweak
ianfnelson May 13, 2025
d682007
fix namespace
ianfnelson May 13, 2025
0a4ce6b
fix file name
ianfnelson May 13, 2025
4460867
fix compilation errors
ianfnelson May 13, 2025
5044497
fix: fixing unit tests for discovery function
SamTyrrellNHS May 13, 2025
da5d59b
comment
ianfnelson May 13, 2025
0d3a21d
feat: adding unit tests for file Extract function
SamTyrrellNHS May 13, 2025
4f64ffd
feat: channging env var name
SamTyrrellNHS May 13, 2025
e70e497
feat: Implemented FileExtract Function
alex-clayton-1 May 13, 2025
26cddd0
introduce app configuration
ianfnelson May 13, 2025
9317192
fix test compilation
ianfnelson May 13, 2025
c609736
configuration of queue clients and extract function
ianfnelson May 13, 2025
f4e9ccb
extract function configuration
ianfnelson May 13, 2025
239523e
fix
ianfnelson May 13, 2025
7e9de12
feat: Improved logging in FileExtractFunction
alex-clayton-1 May 13, 2025
b503cff
refactor: Split out functionality in FileExtractFunction into separat…
alex-clayton-1 May 13, 2025
ddd700d
style: Fixed formatting issues
alex-clayton-1 May 13, 2025
f464619
test: Fixed two of the FileExtractFunctionTests
alex-clayton-1 May 13, 2025
a11ea60
added environment variables to env example
ianfnelson May 14, 2025
cdfea60
test: Updated unit tests for FileExtractFunction
alex-clayton-1 May 14, 2025
6e70a42
feat: Updated FileExtractFunction to log warning and continue when ac…
alex-clayton-1 May 14, 2025
3bc6b99
feat: Removed unnecessary log warning
alex-clayton-1 May 14, 2025
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
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
# Database Configuration
DATABASE_USER=SA
DATABASE_PASSWORD=YourStrong@Passw0rd
DATABASE_NAME=PathwayCoordinator
DATABASE_NAME=ServiceLayer
DATABASE_HOST=db
AZURE_WEB_JOBS_STORAGE=UseDevelopmentStorage=true
DatabaseConnectionString=Server=${DATABASE_HOST};Database=${DATABASE_NAME};User Id=${DATABASE_USER};Password=${DATABASE_PASSWORD};TrustServerCertificate=True
AzureWebJobsStorage=UseDevelopmentStorage=true
FUNCTIONS_WORKER_RUNTIME=dotnet-isolated
MailboxId=X26ABC1
MeshSharedKey=TestKey
MeshPassword=password
BSSMailBox=X26ABC1
NbssMailboxId=X26ABC1
MeshApiBaseUrl=http://localhost:8700/messageexchange
ASPNETCORE_ENVIRONMENT=Development
DiscoveryTimerExpression=*/5 * * * *
FileDiscoveryTimerExpression=*/5 * * * *
QueueUrl=http://127.0.0.1:10001
FileExtractQueueName=file-extract
FileTransformQueueName=file-transform

# API Configuration
API_PORT=7071
Expand Down
26 changes: 26 additions & 0 deletions src/ServiceLayer.Mesh/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace ServiceLayer.Mesh.Configuration;

public class AppConfiguration :
IFileDiscoveryFunctionConfiguration,
IFileExtractFunctionConfiguration,
IFileExtractQueueClientConfiguration,
IFileTransformQueueClientConfiguration
{
public string NbssMeshMailboxId => GetRequired("NbssMailboxId");

public string FileExtractQueueName => GetRequired("FileExtractQueueName");

public string FileTransformQueueName => GetRequired("FileTransformQueueName");

private static string GetRequired(string key)
{
var value = Environment.GetEnvironmentVariable(key);

if (string.IsNullOrEmpty(value))
{
throw new InvalidOperationException($"Environment variable '{key}' is not set or is empty.");
}

return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileDiscoveryFunctionConfiguration
{
string NbssMeshMailboxId { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileExtractFunctionConfiguration
{
string NbssMeshMailboxId { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileExtractQueueClientConfiguration
{
string FileExtractQueueName { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileTransformQueueClientConfiguration
{
string FileTransformQueueName { get; }
}
3 changes: 1 addition & 2 deletions src/ServiceLayer.Mesh/Data/DesignTimeDbContextFactory.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using ServiceLayer.Mesh.Data;

namespace ParticipantManager.API.Data;
namespace ServiceLayer.Mesh.Data;

public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ServiceLayerDbContext>
{
Expand Down
69 changes: 0 additions & 69 deletions src/ServiceLayer.Mesh/Functions/DiscoveryFunction.cs

This file was deleted.

59 changes: 59 additions & 0 deletions src/ServiceLayer.Mesh/Functions/FileDiscoveryFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NHS.MESH.Client.Contracts.Services;
using ServiceLayer.Mesh.Configuration;
using ServiceLayer.Mesh.Data;
using ServiceLayer.Mesh.Messaging;
using ServiceLayer.Mesh.Models;

namespace ServiceLayer.Mesh.Functions
{
public class FileDiscoveryFunction(
ILogger<FileDiscoveryFunction> logger,
IFileDiscoveryFunctionConfiguration configuration,
IMeshInboxService meshInboxService,
ServiceLayerDbContext serviceLayerDbContext,
IFileExtractQueueClient fileExtractQueueClient)
{
[Function("FileDiscoveryFunction")]
public async Task Run([TimerTrigger("%FileDiscoveryTimerExpression%")] TimerInfo myTimer)
{
logger.LogInformation("{functionName} started at: {time}", nameof(FileDiscoveryFunction), DateTime.UtcNow);

var response = await meshInboxService.GetMessagesAsync(configuration.NbssMeshMailboxId);

foreach (var messageId in response.Response.Messages)
{
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();

var existing = await serviceLayerDbContext.MeshFiles
.AnyAsync(f => f.FileId == messageId);

if (!existing)
{
var file = new MeshFile
{
FileId = messageId,
FileType = MeshFileType.NbssAppointmentEvents,
MailboxId = configuration.NbssMeshMailboxId,
Status = MeshFileStatus.Discovered,
FirstSeenUtc = DateTime.UtcNow,
LastUpdatedUtc = DateTime.UtcNow
};

serviceLayerDbContext.MeshFiles.Add(file);

await serviceLayerDbContext.SaveChangesAsync();
await transaction.CommitAsync();

await fileExtractQueueClient.EnqueueFileExtractAsync(file);
}
else
{
await transaction.RollbackAsync();
}
}
}
}
}
124 changes: 124 additions & 0 deletions src/ServiceLayer.Mesh/Functions/FileExtractFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using Google.Protobuf.WellKnownTypes;
using Microsoft.Azure.Functions.Worker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NHS.MESH.Client.Contracts.Services;
using ServiceLayer.Mesh.Configuration;
using ServiceLayer.Mesh.Data;
using ServiceLayer.Mesh.Messaging;
using ServiceLayer.Mesh.Models;
using ServiceLayer.Mesh.Storage;

namespace ServiceLayer.Mesh.Functions;

public class FileExtractFunction(
ILogger<FileExtractFunction> logger,
IFileExtractFunctionConfiguration configuration,
IMeshInboxService meshInboxService,
ServiceLayerDbContext serviceLayerDbContext,
IFileTransformQueueClient fileTransformQueueClient,
IFileExtractQueueClient fileExtractQueueClient,
IMeshFilesBlobStore meshFileBlobStore)
{
[Function("FileExtractFunction")]
public async Task Run([QueueTrigger("%FileExtractQueueName%")] FileExtractQueueMessage message)
{
logger.LogInformation("{functionName} started at: {time}", nameof(FileDiscoveryFunction), DateTime.UtcNow);

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

var file = await GetFileAsync(message.FileId);
if (file == null)
{
return;
}

if (!IsFileSuitableForExtraction(file))
{
return;
}

await UpdateFileStatusForExtraction(file);
await transaction.CommitAsync();

try
{
await ProcessFileExtraction(file, message);
}
catch (Exception ex)
{
await HandleExtractionError(file, message, ex);
}
}

private async Task<MeshFile?> GetFileAsync(string fileId)
{
var file = await serviceLayerDbContext.MeshFiles
.FirstOrDefaultAsync(f => f.FileId == fileId);

if (file == null)
{
logger.LogWarning("File with id: {fileId} not found in MeshFiles table.", fileId);
}

return file;
}

private bool IsFileSuitableForExtraction(MeshFile file)
{
// We only want to extract files if they are in a Discovered state,
// or are in an Extracting state and were last touched over 12 hours ago.
var expectedStatuses = new[] { MeshFileStatus.Discovered, MeshFileStatus.Extracting };
if (!expectedStatuses.Contains(file.Status) ||
(file.Status == MeshFileStatus.Extracting && file.LastUpdatedUtc > DateTime.UtcNow.AddHours(-12)))
{
logger.LogWarning(
"File with id: {fileId} found in MeshFiles table but is not suitable for extraction. Status: {status}, LastUpdatedUtc: {lastUpdatedUtc}.",
file.FileId,
file.Status,
file.LastUpdatedUtc.ToTimestamp());
return false;
}
return true;
}

private async Task UpdateFileStatusForExtraction(MeshFile file)
{
file.Status = MeshFileStatus.Extracting;
file.LastUpdatedUtc = DateTime.UtcNow;
await serviceLayerDbContext.SaveChangesAsync();
}

private async Task ProcessFileExtraction(MeshFile file, FileExtractQueueMessage message)
{
var meshResponse = await meshInboxService.GetMessageByIdAsync(configuration.NbssMeshMailboxId, file.FileId);
if (!meshResponse.IsSuccessful)
{
throw new InvalidOperationException($"Mesh extraction failed: {meshResponse.Error}");
}

var blobPath = await meshFileBlobStore.UploadAsync(file, meshResponse.Response.FileAttachment.Content);

var meshAcknowledgementResponse = await meshInboxService.AcknowledgeMessageByIdAsync(configuration.NbssMeshMailboxId, message.FileId);
if (!meshAcknowledgementResponse.IsSuccessful)
{
logger.LogWarning("Mesh acknowledgement failed: {error}.\nThis is not a fatal error so processing will continue.", meshAcknowledgementResponse.Error);
}

file.BlobPath = blobPath;
file.Status = MeshFileStatus.Extracted;
file.LastUpdatedUtc = DateTime.UtcNow;
await serviceLayerDbContext.SaveChangesAsync();

await fileTransformQueueClient.EnqueueFileTransformAsync(file);
}

private async Task HandleExtractionError(MeshFile file, FileExtractQueueMessage message, Exception ex)
{
logger.LogError(ex, "An exception occurred during file extraction for fileId: {fileId}", message.FileId);
file.Status = MeshFileStatus.FailedExtract;
file.LastUpdatedUtc = DateTime.UtcNow;
await serviceLayerDbContext.SaveChangesAsync();
await fileExtractQueueClient.SendToPoisonQueueAsync(message);
}
}
6 changes: 6 additions & 0 deletions src/ServiceLayer.Mesh/Functions/FileRetryFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Functions;

public class FileRetryFunction
{

}
6 changes: 6 additions & 0 deletions src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Functions;

public class FileTransformFunction
{

}
6 changes: 6 additions & 0 deletions src/ServiceLayer.Mesh/Functions/MeshHandshakeFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Functions;

public class MeshHandshakeFunction
{

}
25 changes: 25 additions & 0 deletions src/ServiceLayer.Mesh/Messaging/FileExtractQueueClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Azure.Storage.Queues;
using Microsoft.Extensions.Logging;
using ServiceLayer.Mesh.Configuration;
using ServiceLayer.Mesh.Models;

namespace ServiceLayer.Mesh.Messaging;

public class FileExtractQueueClient(
ILogger<FileExtractQueueClient> logger,
IFileExtractQueueClientConfiguration configuration,
QueueServiceClient queueServiceClient)
: QueueClientBase(logger, queueServiceClient), IFileExtractQueueClient
{
public async Task EnqueueFileExtractAsync(MeshFile file)
{
await SendJsonMessageAsync(new FileExtractQueueMessage { FileId = file.FileId });
}

public async Task SendToPoisonQueueAsync(FileExtractQueueMessage message)
{
await base.SendToPoisonQueueAsync(message);
}

protected override string QueueName => configuration.FileExtractQueueName;
}
6 changes: 6 additions & 0 deletions src/ServiceLayer.Mesh/Messaging/FileExtractQueueMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Messaging;

public class FileExtractQueueMessage
{
public required string FileId { get; set; }
}
Loading
Loading