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
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
1 change: 1 addition & 0 deletions src/ServiceLayer.Mesh/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class AppConfiguration :
IFileExtractFunctionConfiguration,
IFileExtractQueueClientConfiguration,
IFileTransformQueueClientConfiguration,
IFileTransformFunctionConfiguration,
IFileRetryFunctionConfiguration,
IMeshHandshakeFunctionConfiguration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ServiceLayer.Mesh.Configuration;

public interface IFileTransformFunctionConfiguration
{
int StaleHours { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

public class FileParser : IFileParser
{
public ParsedFile Parse(Stream stream)
{
// TODO - implement this
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using ServiceLayer.Data.Models;
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - NBSS appointment file specific implementation of IFileTransformer. To orchestrate parsing, validation and staging of data (delegated to separate classes)
public class FileTransformer : IFileTransformer
{
private readonly IFileParser _fileParser;
private readonly IValidationRunner _validationRunner;
private readonly IStagingPersister _stagingPersister;

public FileTransformer(IFileParser fileParser, IValidationRunner validationRunner, IStagingPersister stagingPersister)
{
_fileParser = fileParser;
_validationRunner = validationRunner;
_stagingPersister = stagingPersister;
}

public MeshFileType HandlesFileType => MeshFileType.NbssAppointmentEvents;

public async Task<IList<ValidationError>> TransformFileAsync(Stream stream, MeshFile metaData)
{
// TODO - wrap this parsing in a try-catch and return a List<ValidationError> in case of any unforeseen parsing issues (file is totally unlike anything we expect)
var parsed = _fileParser.Parse(stream);

var validationErrors = _validationRunner.Validate(parsed);
if (!validationErrors.Any())
{
await _stagingPersister.WriteStagedData(parsed);
}

return validationErrors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

public interface IFileParser
{
ParsedFile Parse(Stream stream);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - interface for class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table
public interface IStagingPersister
{
Task WriteStagedData(ParsedFile parsedFile);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class FileControlRecord
{
public string? RecordTypeIdentifier { get; set; }

public string? ExtractId { get; set; }

public string? TransferStartDate { get; set; }

public string? TransferStartTime { get; set; }

public string? RecordCount { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class FileDataRecord
{
public int RowNumber { get; set; }
public Dictionary<string, string> Fields { get; } = [];

public string? this[string fieldName] => Fields.GetValueOrDefault(fieldName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

public class ParsedFile
{
public FileControlRecord? FileHeader { get; set; }
public FileControlRecord? FileTrailer { get; set; }
public required List<string> ColumnHeadings { get; set; } = [];
public required List<FileDataRecord> DataRecords { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;

// TODO - class to take validated AppointmentEventsFile and save the records to NbssAppointmentEvents table
public class StagingPersister : IStagingPersister
{
public Task WriteStagedData(ParsedFile parsedFile)
{
// TODO - implement this
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

// TODO - create a whole bunch of implementations of this to perform the validation against NBSS Appointment events files
public interface IFileValidator
{
IEnumerable<ValidationError> Validate(ParsedFile file);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

// TODO - create a whole bunch of implementations of this to perform the validation against NBSS Appointment events records
public interface IRecordValidator
{
IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public interface IValidationRunner
{
IList<ValidationError> Validate(ParsedFile file);
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public class ValidationRunner(
IEnumerable<IFileValidator> fileValidators,
IEnumerable<IRecordValidator> recordValidators)
: IValidationRunner
{
public IList<ValidationError> Validate(ParsedFile file)
{
var errors = new List<ValidationError>();

foreach (var dataRecord in file.DataRecords)
{
foreach (var recordValidator in recordValidators)
{
errors.AddRange(recordValidator.Validate(dataRecord));
}
}

foreach (var validator in fileValidators)
{
errors.AddRange(validator.Validate(file));
}

return errors;
}
}
66 changes: 65 additions & 1 deletion src/ServiceLayer.Mesh/Functions/FileTransformFunction.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
using Google.Protobuf.WellKnownTypes;
using Microsoft.Azure.Functions.Worker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ServiceLayer.Data;
using ServiceLayer.Data.Models;
using ServiceLayer.Mesh.Configuration;
using ServiceLayer.Mesh.Messaging;
using ServiceLayer.Mesh.Storage;

namespace ServiceLayer.Mesh.Functions;

public class FileTransformFunction
public class FileTransformFunction(
ILogger<FileTransformFunction> logger,
ServiceLayerDbContext serviceLayerDbContext,
IMeshFilesBlobStore meshFileBlobStore,
IFileTransformFunctionConfiguration configuration)
{
[Function("FileTransformFunction")]
public async Task Run([QueueTrigger("%FileTransformQueueName%")] FileTransformQueueMessage message)
{
await using var transaction = await serviceLayerDbContext.Database.BeginTransactionAsync();

var file = await serviceLayerDbContext.MeshFiles.FirstOrDefaultAsync(f => f.FileId == message.FileId);

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

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

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

var fileContent = await meshFileBlobStore.DownloadAsync(file);

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

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

private bool IsFileSuitableForTransformation(MeshFile file)
{
// We only want to transform files if they are in a Extracted state,
// or are in a Transforming state and were last touched over 12 hours ago.
var expectedStatuses = new[] { MeshFileStatus.Extracted, MeshFileStatus.Transforming };
if (!expectedStatuses.Contains(file.Status) ||
(file.Status == MeshFileStatus.Transforming && file.LastUpdatedUtc > DateTime.UtcNow.AddHours(-configuration.StaleHours)))
{
logger.LogWarning(
"File with id: {fileId} found in MeshFiles table but is not suitable for transformation. Status: {status}, LastUpdatedUtc: {lastUpdatedUtc}.",
file.FileId,
file.Status,
file.LastUpdatedUtc.ToTimestamp());
return false;
}
return true;
}
}
9 changes: 9 additions & 0 deletions src/ServiceLayer.Mesh/IFileTransformer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ServiceLayer.Data.Models;

namespace ServiceLayer.Mesh;

public interface IFileTransformer
{
MeshFileType HandlesFileType { get; }
Task<IList<ValidationError>> TransformFileAsync(Stream stream, MeshFile metaData);
}
1 change: 1 addition & 0 deletions src/ServiceLayer.Mesh/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
services.AddTransient<IFileTransformQueueClientConfiguration, AppConfiguration>();
services.AddTransient<IMeshHandshakeFunctionConfiguration, AppConfiguration>();
services.AddTransient<IFileRetryFunctionConfiguration, AppConfiguration>();
services.AddTransient<IFileTransformFunctionConfiguration, AppConfiguration>();
});


Expand Down
5 changes: 3 additions & 2 deletions src/ServiceLayer.Mesh/Storage/MeshFilesBlobStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ namespace ServiceLayer.Mesh.Storage;

public class MeshFilesBlobStore(BlobContainerClient blobContainerClient) : IMeshFilesBlobStore
{
public Task<Stream> DownloadAsync(MeshFile file)
public async Task<Stream> DownloadAsync(MeshFile file)
{
throw new NotImplementedException();
var blobClient = blobContainerClient.GetBlobClient(file.BlobPath);
return (await blobClient.DownloadAsync()).Value.Content;
}

public async Task<string> UploadAsync(MeshFile file, byte[] data)
Expand Down
12 changes: 12 additions & 0 deletions src/ServiceLayer.Mesh/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ServiceLayer.Mesh;

public class ValidationError
{
public int? RowNumber { get; set; }

public required string Field { get; set; }

public required string Code { get; set; }

public required string Error { get; set; }
}
Loading
Loading