From 26608674b576ba6abe87607570a7d76ae48947ed Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 22 Oct 2026 21:53:13 +0100 Subject: [PATCH] refactor: reduce Codacy complexity --- .../Services/FileProcessorDomainService.cs | 454 +++++++++--------- .../FileProfileAggregate.cs | 98 ++-- FileProcessor/Handlers/FileHandlers.cs | 59 ++- 3 files changed, 342 insertions(+), 269 deletions(-) diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index 19de7b4..f1b92ad 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -1,4 +1,4 @@ -using SecurityService.DataTransferObjects; +using SecurityService.DataTransferObjects; using Shared.EventStore.Helpers; using TransactionProcessor.DataTransferObjects.Responses.Merchant; @@ -59,15 +59,15 @@ public class FileProcessorDomainService : IFileProcessorDomainService private readonly IFileProcessorManager FileProcessorManager; private readonly IAggregateRepository FileImportLogAggregateRepository; - + private readonly IAggregateRepository FileAggregateRepository; - + private readonly ITransactionProcessorClient TransactionProcessorClient; - + private readonly ISecurityServiceClient SecurityServiceClient; - + private readonly Func FileFormatHandlerResolver; - + private readonly IFileSystem FileSystem; private readonly Int32 MaximumDetailRecordCount; @@ -170,99 +170,136 @@ public async Task UploadFile(FileCommands.UploadFileCommand command, // The import log is created only after validation and duplicate checks succeed. Guid importLogId = Helpers.CalculateFileImportLogAggregateId(importLogDateTime.Date, command.EstateId); - Result result = await ApplyFileImportLogUpdates(async (fileImportLogAggregate) => { - IFileInfo file = this.FileSystem.FileInfo.New(command.FilePath); - if (file.Exists == false) { - return Result.NotFound($"File {file.FullName} not found"); - } + Result result = await ApplyFileImportLogUpdates( + fileImportLogAggregate => this.RegisterUploadedFile(command, importLogDateTime, fileImportLogAggregate, cancellationToken), + importLogId, + cancellationToken, + false); - String originalName = String.IsNullOrWhiteSpace(command.OriginalFileName) - ? file.Name - : command.OriginalFileName; + return result; + } - // Read the original bytes so the server validates the same content hash as the uploader. - Byte[] fileContent; - await using (Stream fs = file.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) { - using MemoryStream memoryStream = new(); - await fs.CopyToAsync(memoryStream, cancellationToken); - fileContent = memoryStream.ToArray(); - } - Guid generatedFileId = CreateGuidFromFileData(fileContent); + private async Task RegisterUploadedFile(FileCommands.UploadFileCommand command, + DateTime importLogDateTime, + FileImportLogAggregate fileImportLogAggregate, + CancellationToken cancellationToken) + { + IFileInfo file = this.FileSystem.FileInfo.New(command.FilePath); + if (file.Exists == false) + return Result.NotFound($"File {file.FullName} not found"); - if (generatedFileId != command.FileId) { - return Result.Invalid("File content does not match the file id provided"); - } + String originalName = String.IsNullOrWhiteSpace(command.OriginalFileName) ? file.Name : command.OriginalFileName; + Result contentResult = await this.ValidateUploadedFileContent(file, command.FileId, cancellationToken); + if (contentResult.IsFailed) + return contentResult; - Result existingFileResult = await this.FileAggregateRepository.GetLatestVersion(command.FileId, cancellationToken); - if (existingFileResult?.IsSuccess == true && existingFileResult.Data?.IsCreated == true) - { - FileDetails existingFile = existingFileResult.Data.GetFile(); - if (existingFile.EstateId != command.EstateId || - existingFile.MerchantId != command.MerchantId || - existingFile.UserId != command.UserId || - existingFile.FileProfileId != command.FileProfileId) - { - return Result.Invalid($"File Id [{command.FileId}] is already registered with different upload details"); - } - - this.DeleteDuplicateUploadFile(command.FilePath); - return Result.Success(); - } - if (existingFileResult?.IsFailed == true && existingFileResult.Status != ResultStatus.NotFound) - return ResultHelpers.CreateFailure(existingFileResult); + Result existingFileResult = await this.HandleExistingFile(command, cancellationToken); + if (existingFileResult.IsFailed) + return ResultHelpers.CreateFailure(existingFileResult); + if (existingFileResult.Data) + return Result.Success(); - // An identical file already on the current import log is accepted without appending another event. - Boolean fileAlreadyRegistered = fileImportLogAggregate.ContainsFile(command.FileId); - if (fileAlreadyRegistered) - { - Result duplicateResult = fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(command.FileId, - command.MerchantId, - command.UserId, - command.FileProfileId, - originalName, - command.FilePath, - command.FileUploadedDateTime)); - if (duplicateResult.IsSuccess) - this.DeleteDuplicateUploadFile(command.FilePath); - - return duplicateResult; - } + if (fileImportLogAggregate.ContainsFile(command.FileId)) + return this.RegisterDuplicateImportLogFile(command, originalName, fileImportLogAggregate); - Result getFileProfileResult = await this.FileProcessorManager.GetFileProfile(command.FileProfileId, cancellationToken); - if (getFileProfileResult.IsFailed) - return ResultHelpers.CreateFailure(getFileProfileResult); + return await this.RegisterNewImportLogFile(command, + originalName, + importLogDateTime, + fileImportLogAggregate, + cancellationToken); + } - if (getFileProfileResult.Data == null) - return Result.NotFound($"No file profile found with Id {command.FileProfileId}"); + private async Task ValidateUploadedFileContent(IFileInfo file, + Guid fileId, + CancellationToken cancellationToken) + { + // Validate the same original bytes used by the uploader to calculate the file id. + Byte[] fileContent; + await using (Stream stream = file.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) + using (MemoryStream memoryStream = new()) + { + await stream.CopyToAsync(memoryStream, cancellationToken); + fileContent = memoryStream.ToArray(); + } - Guid fileProfileVersionId = getFileProfileResult.Data.FileProfileVersionId == Guid.Empty - ? getFileProfileResult.Data.FileProfileId - : getFileProfileResult.Data.FileProfileVersionId; + return CreateGuidFromFileData(fileContent) == fileId + ? Result.Success() + : Result.Invalid("File content does not match the file id provided"); + } - if (fileImportLogAggregate.IsCreated == false) - { - Result createResult = fileImportLogAggregate.CreateImportLog(command.EstateId, importLogDateTime); - if (createResult.IsFailed) - return createResult; - } + private async Task> HandleExistingFile(FileCommands.UploadFileCommand command, + CancellationToken cancellationToken) + { + Result existingFileResult = await this.FileAggregateRepository.GetLatestVersion(command.FileId, cancellationToken); + if (existingFileResult?.IsSuccess == true && existingFileResult.Data?.IsCreated == true) + { + FileDetails existingFile = existingFileResult.Data.GetFile(); + if (existingFile.EstateId != command.EstateId || + existingFile.MerchantId != command.MerchantId || + existingFile.UserId != command.UserId || + existingFile.FileProfileId != command.FileProfileId) + return Result.Invalid($"File Id [{command.FileId}] is already registered with different upload details"); + + this.DeleteDuplicateUploadFile(command.FilePath); + return Result.Success(true); + } + + if (existingFileResult?.IsFailed == true && existingFileResult.Status != ResultStatus.NotFound) + return ResultHelpers.CreateFailure(existingFileResult); + + return Result.Success(false); + } + + private Result RegisterDuplicateImportLogFile(FileCommands.UploadFileCommand command, + String originalName, + FileImportLogAggregate fileImportLogAggregate) + { + Result result = fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(command.FileId, + command.MerchantId, + command.UserId, + command.FileProfileId, + originalName, + command.FilePath, + command.FileUploadedDateTime)); + if (result.IsSuccess) + this.DeleteDuplicateUploadFile(command.FilePath); - Result stateResult = fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(command.FileId, - command.MerchantId, - command.UserId, - command.FileProfileId, - originalName, - command.FilePath, - command.FileUploadedDateTime, - fileProfileVersionId)); - if (stateResult.IsFailed) - return stateResult; - - return Result.Success(); - }, importLogId, cancellationToken, false); - return result; } + private async Task RegisterNewImportLogFile(FileCommands.UploadFileCommand command, + String originalName, + DateTime importLogDateTime, + FileImportLogAggregate fileImportLogAggregate, + CancellationToken cancellationToken) + { + Result profileResult = await this.FileProcessorManager.GetFileProfile(command.FileProfileId, cancellationToken); + if (profileResult.IsFailed) + return ResultHelpers.CreateFailure(profileResult); + if (profileResult.Data == null) + return Result.NotFound($"No file profile found with Id {command.FileProfileId}"); + + Guid fileProfileVersionId = profileResult.Data.FileProfileVersionId == Guid.Empty + ? profileResult.Data.FileProfileId + : profileResult.Data.FileProfileVersionId; + + if (fileImportLogAggregate.IsCreated == false) + { + Result createResult = fileImportLogAggregate.CreateImportLog(command.EstateId, importLogDateTime); + if (createResult.IsFailed) + return createResult; + } + + return fileImportLogAggregate.AddImportedFile(new AddImportedFileRequest(command.FileId, + command.MerchantId, + command.UserId, + command.FileProfileId, + originalName, + command.FilePath, + command.FileUploadedDateTime, + fileProfileVersionId)); + } + public static Guid CreateGuidFromFileData(String fileContents) { return CreateGuidFromFileData(Encoding.UTF8.GetBytes(fileContents)); @@ -350,13 +387,13 @@ await MoveAcceptedFileToFailedDirectory(command.FileId, cancellationToken); return result; } - + Result processResult = await this.ProcessFile(command.FileId, command.FileProfileId, command.FileProfileVersionId, command.FilePath, cancellationToken); - + return processResult; } @@ -618,7 +655,7 @@ private Result> BuildTransactionMetadata(FileCommands out String operatorName) { // need to now parse the line (based on the file format), this builds the metadata Dictionary transactionMetadata = this.ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); - + if (transactionMetadata.Any() == false) { // Line failed to parse so record this operatorName = null; @@ -629,7 +666,7 @@ private Result> BuildTransactionMetadata(FileCommands transactionMetadata.Add("FileId", command.FileId.ToString()); transactionMetadata.Add("FileLineNumber", fileLine.LineNumber.ToString()); transactionMetadata.Add("EventId", command.EventId.ToString()); - + operatorName = fileProfile.OperatorName; if (transactionMetadata.ContainsKey("OperatorName")) { // extract the value @@ -668,8 +705,8 @@ private async Task> SendSaleTransaction(FileDeta AdditionalTransactionMetadata = transactionMetadata, TransactionSource = 2, // File based transaction }; - - + + // Send request to transaction processor processingContext.Stage = FileLineProcessingStage.TransactionDispatchStarted; processingContext.TransactionDispatchAttempted = true; @@ -821,144 +858,30 @@ private async Task ProcessFile(Guid fileId, Guid fileProfileVersionId, String fileName, CancellationToken cancellationToken) { - IFileInfo inProgressFile = null; - FileProfileModel fileProfile = null; - Result fileProfileResult = await this.GetFileProfileForProcessing(fileId, fileProfileId, fileProfileVersionId, cancellationToken); if (fileProfileResult.IsFailed) return ResultHelpers.CreateFailure(fileProfileResult); - fileProfile = fileProfileResult.Data; - // Check the processed/failed directories exist - if (this.FileSystem.Directory.Exists(fileProfile.ProcessedDirectory) == false) { - Logger.LogInformation($"Creating Directory {fileProfile.ProcessedDirectory} as not found"); - this.FileSystem.Directory.CreateDirectory(fileProfile.ProcessedDirectory); - } - - if (this.FileSystem.Directory.Exists(fileProfile.FailedDirectory) == false) { - Logger.LogInformation($"Creating Directory {fileProfile.FailedDirectory} as not found"); - this.FileSystem.Directory.CreateDirectory(fileProfile.FailedDirectory); - } - - inProgressFile = this.FileSystem.FileInfo.New(fileName); - - if (inProgressFile.Exists == false) { - // We also want to check the processed and failed folders incase this is an event replay - IFileInfo failedFileInfo = - this.FileSystem.FileInfo.New($"{fileProfile.FailedDirectory}/{inProgressFile.Name}"); - IFileInfo processedFileInfo = - this.FileSystem.FileInfo.New($"{fileProfile.ProcessedDirectory}/{inProgressFile.Name}"); - - (Boolean, Boolean) fileStatus = (failedFileInfo.Exists, processedFileInfo.Exists); - - IFileInfo fileInfo = fileStatus switch - { - (true, false) => failedFileInfo, - (false, true) => processedFileInfo, - _ => null - }; + FileProfileModel fileProfile = fileProfileResult.Data; - if (fileInfo == null) { - return Result.NotFound($"File {inProgressFile.FullName} not found"); - } + Result inProgressFileResult = this.ResolveInProgressFile(fileName, fileProfile); + if (inProgressFileResult.IsFailed) + return ResultHelpers.CreateFailure(inProgressFileResult); - // Overwrite the inprogress file info object with the file found in the failed folder - inProgressFile = fileInfo; - } + IFileInfo inProgressFile = inProgressFileResult.Data; Boolean fileWasRejected = false; Result result = await ApplyFileUpdates(async (FileAggregate fileAggregate) => { - if (fileAggregate.GetFile().IsRejected) { fileWasRejected = true; return Result.Success(); } - String fileContent = null; - //Open file for Read\Write - using (Stream fs = inProgressFile.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) { - //Create object of StreamReader by passing FileStream object on which it needs to operates on - using (StreamReader sr = new StreamReader(fs)) { - //Use ReadToEnd method to read all the content from file - fileContent = await sr.ReadToEndAsync(cancellationToken); - } - } - - { - String[] fileLines = fileContent.Split(fileProfile.LineTerminator); - - IFileFormatHandler fileFormatHandler = this.FileFormatHandlerResolver(fileProfile.FileFormatHandler); - Int32 detailRecordCount = fileLines.Count(fileLine => - { - String trimmedLine = fileLine.Trim(); - return String.IsNullOrEmpty(trimmedLine) == false && fileFormatHandler.FileLineCanBeIgnored(trimmedLine) == false; - }); - - if (detailRecordCount > this.MaximumDetailRecordCount) - { - Result rejectionResult = fileAggregate.RejectFile(detailRecordCount, - this.MaximumDetailRecordCount, - MaximumDetailRecordsExceededCode, - $"File contains {detailRecordCount} detail records; the maximum permitted is {this.MaximumDetailRecordCount}", - DateTime.UtcNow); - if (rejectionResult.IsFailed) - return rejectionResult; - - fileWasRejected = true; - return Result.Success(); - } - - FileValidationResult validationResult; - using (StringReader validationReader = new(fileContent)) - validationResult = fileFormatHandler.ValidateFile(validationReader, fileProfile.LineTerminator); - - if (validationResult == null) - { - Result unavailableResult = fileAggregate.RejectFile(detailRecordCount, - this.MaximumDetailRecordCount, - "ValidationUnavailable", - "File validation did not return a result", - DateTime.UtcNow); - if (unavailableResult.IsFailed) - return unavailableResult; - - fileWasRejected = true; - return Result.Success(); - } - - if (validationResult.IsValid == false) - { - String rejectionReason = String.Join("; ", validationResult.Errors.Select(FormatValidationError)); - Result rejectionResult = fileAggregate.RejectFile(detailRecordCount, - this.MaximumDetailRecordCount, - validationResult.Errors.First().Code, - rejectionReason, - DateTime.UtcNow); - if (rejectionResult.IsFailed) - return rejectionResult; - - fileWasRejected = true; - return Result.Success(); - } - - HashSet previouslyPersistedLines = fileAggregate.GetFile().FileLines - .Select(fileLine => fileLine.LineData) - .ToHashSet(StringComparer.Ordinal); - - foreach (String fileLine in fileLines) { - String trimmedFileLine = fileLine.Trim(); - if (previouslyPersistedLines.Contains(trimmedFileLine)) - continue; - - Result stateResult = fileAggregate.AddFileLine(trimmedFileLine); - if (stateResult.IsFailed) - return stateResult; - } - } - - return Result.Success(); + Result contentResult = await this.ProcessFileContents(fileAggregate, inProgressFile, fileProfile, cancellationToken); + fileWasRejected = fileAggregate.GetFile().IsRejected; + return contentResult; }, fileId, cancellationToken); if (result.IsSuccess) { @@ -982,6 +905,107 @@ private async Task ProcessFile(Guid fileId, return result; } + private Result ResolveInProgressFile(String fileName, FileProfileModel fileProfile) + { + EnsureDirectoryExists(fileProfile.ProcessedDirectory); + EnsureDirectoryExists(fileProfile.FailedDirectory); + + IFileInfo inProgressFile = this.FileSystem.FileInfo.New(fileName); + if (inProgressFile.Exists) + return Result.Success(inProgressFile); + + IFileInfo failedFileInfo = this.FileSystem.FileInfo.New($"{fileProfile.FailedDirectory}/{inProgressFile.Name}"); + IFileInfo processedFileInfo = this.FileSystem.FileInfo.New($"{fileProfile.ProcessedDirectory}/{inProgressFile.Name}"); + IFileInfo replayFile = (failedFileInfo.Exists, processedFileInfo.Exists) switch + { + (true, false) => failedFileInfo, + (false, true) => processedFileInfo, + _ => null + }; + + return replayFile == null + ? Result.NotFound($"File {inProgressFile.FullName} not found") + : Result.Success(replayFile); + } + + private void EnsureDirectoryExists(String directory) + { + if (this.FileSystem.Directory.Exists(directory)) + return; + + Logger.LogInformation($"Creating Directory {directory} as not found"); + this.FileSystem.Directory.CreateDirectory(directory); + } + + private async Task ProcessFileContents(FileAggregate fileAggregate, + IFileInfo inProgressFile, + FileProfileModel fileProfile, + CancellationToken cancellationToken) + { + String fileContent; + using (Stream fs = inProgressFile.Open(FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read)) + using (StreamReader reader = new(fs)) + fileContent = await reader.ReadToEndAsync(cancellationToken); + + String[] fileLines = fileContent.Split(fileProfile.LineTerminator); + IFileFormatHandler fileFormatHandler = this.FileFormatHandlerResolver(fileProfile.FileFormatHandler); + Int32 detailRecordCount = fileLines.Count(fileLine => + { + String trimmedLine = fileLine.Trim(); + return String.IsNullOrEmpty(trimmedLine) == false && fileFormatHandler.FileLineCanBeIgnored(trimmedLine) == false; + }); + + if (detailRecordCount > this.MaximumDetailRecordCount) + return this.RejectFile(fileAggregate, + detailRecordCount, + MaximumDetailRecordsExceededCode, + $"File contains {detailRecordCount} detail records; the maximum permitted is {this.MaximumDetailRecordCount}"); + + FileValidationResult validationResult; + using (StringReader validationReader = new(fileContent)) + validationResult = fileFormatHandler.ValidateFile(validationReader, fileProfile.LineTerminator); + + if (validationResult == null) + return this.RejectFile(fileAggregate, detailRecordCount, "ValidationUnavailable", "File validation did not return a result"); + + if (validationResult.IsValid == false) + return this.RejectFile(fileAggregate, + detailRecordCount, + validationResult.Errors.First().Code, + String.Join("; ", validationResult.Errors.Select(FormatValidationError))); + + return PersistFileLines(fileAggregate, fileLines); + } + + private Result RejectFile(FileAggregate fileAggregate, Int32 detailRecordCount, String errorCode, String reason) + { + return fileAggregate.RejectFile(detailRecordCount, + this.MaximumDetailRecordCount, + errorCode, + reason, + DateTime.UtcNow); + } + + private static Result PersistFileLines(FileAggregate fileAggregate, IEnumerable fileLines) + { + HashSet previouslyPersistedLines = fileAggregate.GetFile().FileLines + .Select(fileLine => fileLine.LineData) + .ToHashSet(StringComparer.Ordinal); + + foreach (String fileLine in fileLines) + { + String trimmedFileLine = fileLine.Trim(); + if (previouslyPersistedLines.Contains(trimmedFileLine)) + continue; + + Result stateResult = fileAggregate.AddFileLine(trimmedFileLine); + if (stateResult.IsFailed) + return stateResult; + } + + return Result.Success(); + } + private async Task> GetFileProfileForProcessing(Guid fileId, Guid fileProfileId, Guid fileProfileVersionId, @@ -1058,7 +1082,7 @@ private async Task> GetToken(CancellationToken cancellatio Logger.LogWarning($"Failed to get token: {getTokenResult.Message}"); return ResultHelpers.CreateFailure(getTokenResult); } - + return getTokenResult.Data; } diff --git a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs index a2d27df..ab7f32e 100644 --- a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs +++ b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs @@ -173,48 +173,76 @@ public static Result UpdateProfile(this FileProfileAggregate aggregate, Guid fil { return validationResult; } - - String lineTerminator = profile.LineTerminator; - if (request.LineTerminator.HasValue) { - Result lineTerminatorResult = request.LineTerminator.Value.ToLineTerminator(); - if (lineTerminatorResult.IsFailed) { - return ResultHelpers.CreateFailure(lineTerminatorResult); - } - - lineTerminator = lineTerminatorResult.Data; - } - - String name = request.Name == null ? profile.Name : request.Name.Trim(); - String listeningDirectory = request.ListeningDirectory == null ? profile.ListeningDirectory : request.ListeningDirectory.Trim(); - String requestType = request.RequestType == null ? profile.RequestType : request.RequestType.Trim(); - String operatorName = request.OperatorName == null ? profile.OperatorName : request.OperatorName.Trim(); - String fileFormatHandler = request.FileFormatHandler == null ? profile.FileFormatHandler : request.FileFormatHandler.Trim(); + Result lineTerminatorResult = ResolveLineTerminator(profile, request); + if (lineTerminatorResult.IsFailed) + return ResultHelpers.CreateFailure(lineTerminatorResult); - if (IsDomainEventRequired(profile.Name, name) == false && - IsDomainEventRequired(profile.ListeningDirectory, listeningDirectory) == false && - IsDomainEventRequired(profile.RequestType, requestType) == false && - IsDomainEventRequired(profile.OperatorName, operatorName) == false && - IsDomainEventRequired(profile.LineTerminator, lineTerminator) == false && - IsDomainEventRequired(profile.FileFormatHandler, fileFormatHandler) == false) - { + FileProfileUpdate update = CreateUpdate(profile, request, lineTerminatorResult.Data); + if (HasProfileChanged(profile, update) == false) return Result.Success(); - } - Int32 nextVersion = profile.Versions.Count == 0 ? 1 : profile.Versions.Max(version => version.FileProfileVersion) + 1; - aggregate.ApplyAndAppend(new FileProfileVersionCreatedEvent(aggregate.AggregateId, - fileProfileId, - Guid.NewGuid(), - nextVersion, - name, - listeningDirectory, - requestType, - operatorName, - lineTerminator, - fileFormatHandler)); + aggregate.ApplyAndAppend(CreateVersionEvent(aggregate, fileProfileId, profile, update)); return Result.Success(); } + private static Result ResolveLineTerminator(FileProfileState profile, UpdateFileProfileRequest request) + { + if (request.LineTerminator.HasValue == false) + return Result.Success(profile.LineTerminator); + + Result result = request.LineTerminator.Value.ToLineTerminator(); + return result.IsFailed ? ResultHelpers.CreateFailure(result) : result; + } + + private static FileProfileUpdate CreateUpdate(FileProfileState profile, + UpdateFileProfileRequest request, + String lineTerminator) + { + return new FileProfileUpdate( + request.Name == null ? profile.Name : request.Name.Trim(), + request.ListeningDirectory == null ? profile.ListeningDirectory : request.ListeningDirectory.Trim(), + request.RequestType == null ? profile.RequestType : request.RequestType.Trim(), + request.OperatorName == null ? profile.OperatorName : request.OperatorName.Trim(), + lineTerminator, + request.FileFormatHandler == null ? profile.FileFormatHandler : request.FileFormatHandler.Trim()); + } + + private static Boolean HasProfileChanged(FileProfileState profile, FileProfileUpdate update) + { + return IsDomainEventRequired(profile.Name, update.Name) || + IsDomainEventRequired(profile.ListeningDirectory, update.ListeningDirectory) || + IsDomainEventRequired(profile.RequestType, update.RequestType) || + IsDomainEventRequired(profile.OperatorName, update.OperatorName) || + IsDomainEventRequired(profile.LineTerminator, update.LineTerminator) || + IsDomainEventRequired(profile.FileFormatHandler, update.FileFormatHandler); + } + + private static FileProfileVersionCreatedEvent CreateVersionEvent(FileProfileAggregate aggregate, + Guid fileProfileId, + FileProfileState profile, + FileProfileUpdate update) + { + Int32 nextVersion = profile.Versions.Count == 0 ? 1 : profile.Versions.Max(version => version.FileProfileVersion) + 1; + return new FileProfileVersionCreatedEvent(aggregate.AggregateId, + fileProfileId, + Guid.NewGuid(), + nextVersion, + update.Name, + update.ListeningDirectory, + update.RequestType, + update.OperatorName, + update.LineTerminator, + update.FileFormatHandler); + } + + private sealed record FileProfileUpdate(String Name, + String ListeningDirectory, + String RequestType, + String OperatorName, + String LineTerminator, + String FileFormatHandler); + private static String? TrimWhitespaceExceptLineBreaks(String value) { if (String.IsNullOrEmpty(value)) diff --git a/FileProcessor/Handlers/FileHandlers.cs b/FileProcessor/Handlers/FileHandlers.cs index 18c05ab..feb25a0 100644 --- a/FileProcessor/Handlers/FileHandlers.cs +++ b/FileProcessor/Handlers/FileHandlers.cs @@ -26,23 +26,8 @@ public static async Task UploadFile(IMediator mediator, CancellationToken cancellationToken) { IFormCollection formCollection = await httpRequest.ReadFormAsync(cancellationToken); - if (formCollection?.Files == null || formCollection.Files.Count != 1) - return Results.BadRequest("Exactly one file must be supplied."); - - IFormFile file = formCollection.Files[0]; - if (file.Length <= 0) - return Results.BadRequest("The uploaded file must not be empty."); - - if (file.Length > UploadFileSettings.GetMaximumFileSizeBytes()) - return Results.StatusCode(StatusCodes.Status413PayloadTooLarge); - - String fileName = SanitizeFileName(file.FileName); - if (String.IsNullOrWhiteSpace(fileName)) - return Results.BadRequest("The uploaded file must have a valid filename."); - - String fileExtension = Path.GetExtension(fileName); - if (UploadFileSettings.GetAllowedFileExtensions().Contains(fileExtension, StringComparer.OrdinalIgnoreCase) == false) - return Results.BadRequest($"Files with extension [{fileExtension}] are not supported."); + if (TryValidateUpload(formCollection, out ValidatedUpload upload, out IResult validationError) == false) + return validationError; string temporaryFileLocation = ConfigurationReader.GetValue("AppSettings", "TemporaryFileLocation"); string temporaryFileName = Guid.NewGuid().ToString("N"); @@ -57,7 +42,7 @@ public static async Task UploadFile(IMediator mediator, await using (FileStream stream = new(fullPath, FileMode.CreateNew)) { temporaryFileCreated = true; - await file.CopyToAsync(stream, cancellationToken); + await upload.File.CopyToAsync(stream, cancellationToken); } if (request.UploadDateTime == DateTime.MinValue) @@ -70,7 +55,7 @@ public static async Task UploadFile(IMediator mediator, fullPath, request.FileProfileId, request.UploadDateTime, - fileName); + upload.FileName); Result result = await mediator.Send(command, cancellationToken); uploadAccepted = result.IsSuccess; @@ -101,6 +86,42 @@ public static async Task UploadFile(IMediator mediator, } } + private static Boolean TryValidateUpload(IFormCollection formCollection, + out ValidatedUpload upload, + out IResult validationError) + { + upload = null; + validationError = null; + if (formCollection?.Files == null || formCollection.Files.Count != 1) + return SetValidationError(Results.BadRequest("Exactly one file must be supplied."), out validationError); + + IFormFile file = formCollection.Files[0]; + if (file.Length <= 0) + return SetValidationError(Results.BadRequest("The uploaded file must not be empty."), out validationError); + + if (file.Length > UploadFileSettings.GetMaximumFileSizeBytes()) + return SetValidationError(Results.StatusCode(StatusCodes.Status413PayloadTooLarge), out validationError); + + String fileName = SanitizeFileName(file.FileName); + if (String.IsNullOrWhiteSpace(fileName)) + return SetValidationError(Results.BadRequest("The uploaded file must have a valid filename."), out validationError); + + String fileExtension = Path.GetExtension(fileName); + if (UploadFileSettings.GetAllowedFileExtensions().Contains(fileExtension, StringComparer.OrdinalIgnoreCase) == false) + return SetValidationError(Results.BadRequest($"Files with extension [{fileExtension}] are not supported."), out validationError); + + upload = new ValidatedUpload(file, fileName); + return true; + } + + private static Boolean SetValidationError(IResult error, out IResult validationError) + { + validationError = error; + return false; + } + + private sealed record ValidatedUpload(IFormFile File, String FileName); + private static String SanitizeFileName(String fileName) { if (String.IsNullOrWhiteSpace(fileName))