diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml index d3f469c..fe84bde 100644 --- a/.github/workflows/nightlybuild.yml +++ b/.github/workflows/nightlybuild.yml @@ -55,7 +55,13 @@ jobs: run: docker build . --file FileProcessor/Dockerfile --tag fileprocessor:latest - name: Run Integration Tests - run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" + run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=Nightly + + - name: Run Integration Tests 1 + run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=Nightly1 + + - name: Run Integration Tests 2 + run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=Nightly2 - uses: actions/upload-artifact@v4.4.0 if: ${{ failure() }} diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 2bb6f02..ae8a78f 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -45,6 +45,12 @@ jobs: - name: Run Integration Tests run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=PRTest + - name: Run Integration Tests 1 + run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=PRTest1 + + - name: Run Integration Tests 2 + run: dotnet test "FileProcessor.IntegrationTests\FileProcessor.IntegrationTests.csproj" --filter Category=PRTest2 + - uses: actions/upload-artifact@v4.4.0 if: ${{ failure() }} with: diff --git a/FileProcessor.Client/FileProcessorClient.cs b/FileProcessor.Client/FileProcessorClient.cs index 5fae38c..5271416 100644 --- a/FileProcessor.Client/FileProcessorClient.cs +++ b/FileProcessor.Client/FileProcessorClient.cs @@ -176,12 +176,12 @@ public async Task> UploadFile(String accessToken, ByteArrayContent fileContent = new ByteArrayContent(fileData); fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); formData.Add(fileContent, "file", fileName); - formData.Add(new StringContent(uploadFileRequest.EstateId.ToString()), "request.EstateId"); - formData.Add(new StringContent(uploadFileRequest.MerchantId.ToString()), "request.MerchantId"); - formData.Add(new StringContent(uploadFileRequest.FileProfileId.ToString()), "request.FileProfileId"); - formData.Add(new StringContent(uploadFileRequest.UserId.ToString()), "request.UserId"); + formData.Add(new StringContent(uploadFileRequest.EstateId.ToString()), "EstateId"); + formData.Add(new StringContent(uploadFileRequest.MerchantId.ToString()), "MerchantId"); + formData.Add(new StringContent(uploadFileRequest.FileProfileId.ToString()), "FileProfileId"); + formData.Add(new StringContent(uploadFileRequest.UserId.ToString()), "UserId"); formData.Add(new StringContent(uploadFileRequest.UploadDateTime.ToString("yyyy-MM-dd HH:mm:ss")), - "request.UploadDateTime"); + "UploadDateTime"); httpRequest.Content = formData; httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); diff --git a/FileProcessor.IntegrationTests/AssemblyInfo.cs b/FileProcessor.IntegrationTests/AssemblyInfo.cs index 077691e..7127e87 100644 --- a/FileProcessor.IntegrationTests/AssemblyInfo.cs +++ b/FileProcessor.IntegrationTests/AssemblyInfo.cs @@ -16,5 +16,5 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM. [assembly: Guid("25d4d6fc-b809-4acc-a910-249a03e5f07a")] -[assembly: LevelOfParallelism(2)] +[assembly: LevelOfParallelism(1)] [assembly: Parallelizable(ParallelScope.Fixtures)] \ No newline at end of file diff --git a/FileProcessor.IntegrationTests/Common/Setup.cs b/FileProcessor.IntegrationTests/Common/Setup.cs index 2831299..634f49d 100644 --- a/FileProcessor.IntegrationTests/Common/Setup.cs +++ b/FileProcessor.IntegrationTests/Common/Setup.cs @@ -1,11 +1,5 @@ namespace FileProcessor.IntegrationTests.Common { - using System; - using System.Data; - using System.Data.SqlClient; - using System.Net; - using System.Threading; - using System.Threading.Tasks; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; using Ductus.FluentDocker.Services.Extensions; @@ -15,6 +9,13 @@ using Shared.IntegrationTesting; using Shared.Logger; using Shouldly; + using System; + using System.Data; + using System.Data.SqlClient; + using System.IO; + using System.Net; + using System.Threading; + using System.Threading.Tasks; using ILogger = Microsoft.Extensions.Logging.ILogger; [Binding] @@ -29,6 +30,10 @@ public class Setup public static async Task GlobalSetup(DockerHelper dockerHelper) { + Environment.SetEnvironmentVariable("FLUENTDOCKER_LOGLEVEL", "trace"); + Environment.SetEnvironmentVariable("FLUENTDOCKER_NOSUDO", "true"); + Environment.SetEnvironmentVariable("FLUENTDOCKER_PATH", "/usr/bin/docker"); + ShouldlyConfiguration.DefaultTaskTimeout = TimeSpan.FromMinutes(1); dockerHelper.SqlCredentials = Setup.SqlCredentials; dockerHelper.DockerCredentials = Setup.DockerCredentials; diff --git a/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature b/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature index 8ddadcd..4ad2f96 100644 --- a/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature +++ b/FileProcessor.IntegrationTests/Features/GetFileImportDetails.feature @@ -81,7 +81,8 @@ Background: | Deposit1 | 300.00 | Today | Test Merchant 1 | Test Estate 1 | | Deposit1 | 300.00 | Today | Test Merchant 2 | Test Estate 1 | -@PRTest +@PRTest1 +@Nightly1 Scenario: Get File Import Log Details Given I have a file named 'SafarcomTopup1.txt' with the following contents | Column1 | Column2 | Column3 | diff --git a/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature b/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature index 5b6e0f8..02f1a81 100644 --- a/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature +++ b/FileProcessor.IntegrationTests/Features/ProcessTopupCSV.feature @@ -73,7 +73,8 @@ Background: Given I make the following manual merchant deposits | Reference | Amount | DateTime | MerchantName | EstateName | | Deposit1 | 300.00 | Today | Test Merchant 1 | Test Estate 1 | - + +@Nightly Scenario: Process Safaricom Topup File with 1 detail row Given I have a file named 'SafarcomTopup.txt' with the following contents | Column1 | Column2 | Column3 | @@ -86,6 +87,7 @@ Scenario: Process Safaricom Topup File with 1 detail row #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 1 transaction should be returned +@Nightly Scenario: Process Safaricom Topup File with 2 detail rows Given I have a file named 'SafarcomTopup.txt' with the following contents | Column1 | Column2 | Column3 | @@ -99,6 +101,7 @@ Scenario: Process Safaricom Topup File with 2 detail rows #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 2 transaction should be returned +@Nightly Scenario: Process 2 Safaricom Topup Files Given I have a file named 'SafarcomTopup1.txt' with the following contents | Column1 | Column2 | Column3 | @@ -122,6 +125,7 @@ Scenario: Process 2 Safaricom Topup Files #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 3 transaction should be returned @PRTest +@Nightly Scenario: Process Duplicate Safaricom Topup File with 1 detail row Given I have a file named 'SafarcomTopup1.txt' with the following contents | Column1 | Column2 | Column3 | @@ -146,7 +150,7 @@ Scenario: Process Duplicate Safaricom Topup File with 1 detail row | Test Estate 1 | Test Merchant 1 | B2A59ABF-293D-4A6B-B81B-7007503C3476 | ABA59ABF-293D-4A6B-B81B-7007503C3476 | # Wrong Format?? - +@Nightly Scenario: Process Safaricom Topup File with Upload Date Time Given I have a file named 'SafarcomTopup.txt' with the following contents | Column1 | Column2 | Column3 | diff --git a/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature b/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature index 85aa834..ca835da 100644 --- a/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature +++ b/FileProcessor.IntegrationTests/Features/ProcessVoucherCSV.feature @@ -74,7 +74,8 @@ Background: Given I make the following manual merchant deposits | Reference | Amount | DateTime | MerchantName | EstateName | | Deposit1 | 300.00 | Today | Test Merchant 1 | Test Estate 1 | - + +@Nightly2 Scenario: Process Voucher File with 1 detail row for recipient email Given I have a file named 'VoucherIssue.txt' with the following contents | Column1 | Column2 | Column3 | Column4 | @@ -87,6 +88,7 @@ Scenario: Process Voucher File with 1 detail row for recipient email #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 1 transaction should be returned +@Nightly2 Scenario: Process Voucher File with 1 detail row for recipient mobile Given I have a file named 'VoucherIssue.txt' with the following contents | Column1 | Column2 | Column3 | Column4 | @@ -99,6 +101,7 @@ Scenario: Process Voucher File with 1 detail row for recipient mobile #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 1 transaction should be returned +@Nightly2 Scenario: Process Voucher File with 2 detail rows Given I have a file named 'VoucherIssue.txt' with the following contents | Column1 | Column2 | Column3 | Column4 | @@ -112,6 +115,7 @@ Scenario: Process Voucher File with 2 detail rows #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 2 transaction should be returned +@Nightly2 Scenario: Process 2 Voucher Files Given I have a file named 'VoucherIssue1.txt' with the following contents | Column1 | Column2 | Column3 | Column4 | @@ -135,7 +139,8 @@ Scenario: Process 2 Voucher Files #When As merchant "Test Merchant 1" on Estate "Test Estate 1" I get my transactions 4 transaction should be returned -@PRTest +@PRTest2 +@Nightly2 Scenario: Process Duplicate Voucher Topup File with 1 detail row Given I have a file named 'VoucherIssue1.txt' with the following contents | Column1 | Column2 | Column3 | Column4 | diff --git a/FileProcessor.IntegrationTests/FileProcessor.IntegrationTests.csproj b/FileProcessor.IntegrationTests/FileProcessor.IntegrationTests.csproj index 109ecf6..b368cc9 100644 --- a/FileProcessor.IntegrationTests/FileProcessor.IntegrationTests.csproj +++ b/FileProcessor.IntegrationTests/FileProcessor.IntegrationTests.csproj @@ -75,9 +75,6 @@ Always - - Always - diff --git a/FileProcessor.IntegrationTests/xunit.runner.json b/FileProcessor.IntegrationTests/xunit.runner.json deleted file mode 100644 index 7b19a6c..0000000 --- a/FileProcessor.IntegrationTests/xunit.runner.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "maxParallelThreads" : 1 -} diff --git a/FileProcessor.Tests/ModelFactoryTests.cs b/FileProcessor.Tests/ModelFactoryTests.cs index 97aa23a..6f19cc5 100644 --- a/FileProcessor.Tests/ModelFactoryTests.cs +++ b/FileProcessor.Tests/ModelFactoryTests.cs @@ -24,9 +24,7 @@ public void ModelFactory_ConvertFrom_FileImportLogList_IsConverted() { List importLogs = TestData.FileImportLogModels; - ModelFactory modelFactory = new ModelFactory(); - - FileImportLogList result = modelFactory.ConvertFrom(importLogs); + FileImportLogList result = ModelFactory.ConvertFrom(importLogs); this.VerifyFileImportLogList(importLogs, result); } @@ -41,9 +39,7 @@ public void ModelFactory_ConvertFrom_FileImportLogList_WithNoFiles_IsConverted() fileImportLog.Files = new List(); } - ModelFactory modelFactory = new ModelFactory(); - - FileImportLogList result = modelFactory.ConvertFrom(importLogs); + FileImportLogList result = ModelFactory.ConvertFrom(importLogs); this.VerifyFileImportLogList(importLogs, result); } @@ -53,9 +49,7 @@ public void ModelFactory_ConvertFrom_FileImportLog_IsConverted() { FileProcessor.Models.FileImportLog importLog = TestData.FileImportLogModel1; - ModelFactory modelFactory = new ModelFactory(); - - var result = modelFactory.ConvertFrom(importLog); + DataTransferObjects.Responses.FileImportLog result = ModelFactory.ConvertFrom(importLog); this.VerifyFileImportLog(importLog, result); } @@ -66,9 +60,7 @@ public void ModelFactory_ConvertFrom_FileImportLog_WithNoFiles_IsConverted() FileProcessor.Models.FileImportLog importLog = TestData.FileImportLogModel1; importLog.Files = new List(); - ModelFactory modelFactory = new ModelFactory(); - - var result = modelFactory.ConvertFrom(importLog); + DataTransferObjects.Responses.FileImportLog result = ModelFactory.ConvertFrom(importLog); this.VerifyFileImportLog(importLog, result); } @@ -76,10 +68,9 @@ public void ModelFactory_ConvertFrom_FileImportLog_WithNoFiles_IsConverted() [Fact] public void ModelFactory_ConvertFrom_FileDetails_IsConverted() { - ModelFactory modelFactory = new ModelFactory(); FileDetails fileDetails = TestData.FileDetailsModel; - var result = modelFactory.ConvertFrom(fileDetails); + DataTransferObjects.Responses.FileDetails result = ModelFactory.ConvertFrom(fileDetails); this.VerifyFileDetails(fileDetails, result); } diff --git a/FileProcessor/Bootstrapper/MiddlewareRegistry.cs b/FileProcessor/Bootstrapper/MiddlewareRegistry.cs index d4553c1..13e6c00 100644 --- a/FileProcessor/Bootstrapper/MiddlewareRegistry.cs +++ b/FileProcessor/Bootstrapper/MiddlewareRegistry.cs @@ -3,6 +3,7 @@ using EventStore.Client; using Lamar; using Microsoft.AspNetCore.Authentication.JwtBearer; + using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.IdentityModel.Tokens; @@ -118,6 +119,14 @@ public MiddlewareRegistry() options.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy(); options.SerializerOptions.PropertyNameCaseInsensitive = true; // optional, but safer }); + + this.Configure(options => + { + // Allow very large values (adjust to what you need) + options.ValueLengthLimit = int.MaxValue; // form value length + options.MultipartBodyLengthLimit = long.MaxValue; // multipart body length + options.MemoryBufferThreshold = int.MaxValue; // buffer threshold + }); } #endregion diff --git a/FileProcessor/Bootstrapper/MiscRegistry.cs b/FileProcessor/Bootstrapper/MiscRegistry.cs index 6746936..566e01b 100644 --- a/FileProcessor/Bootstrapper/MiscRegistry.cs +++ b/FileProcessor/Bootstrapper/MiscRegistry.cs @@ -23,7 +23,6 @@ public class MiscRegistry : ServiceRegistry public MiscRegistry() { this.AddSingleton(); - this.AddSingleton(); this.AddSingleton(); bool logRequests = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogRequests", true); diff --git a/FileProcessor/Common/IModelFactory.cs b/FileProcessor/Common/IModelFactory.cs deleted file mode 100644 index 205bd58..0000000 --- a/FileProcessor/Common/IModelFactory.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace FileProcessor.Common -{ - using System.Collections.Generic; - using DataTransferObjects.Responses; - using FileImportLogResponse = DataTransferObjects.Responses.FileImportLog; - using FileDetailsResponse = DataTransferObjects.Responses.FileDetails; - - /// - /// - /// - public interface IModelFactory - { - #region Methods - - /// - /// Converts from. - /// - /// The import logs. - /// - FileImportLogList ConvertFrom(List importLogs); - - /// - /// Converts from. - /// - /// The file import log. - /// - FileImportLogResponse ConvertFrom(Models.FileImportLog fileImportLog); - - /// - /// Converts from. - /// - /// The file details. - /// - FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails); - - #endregion - } -} \ No newline at end of file diff --git a/FileProcessor/Common/ModelFactory.cs b/FileProcessor/Common/ModelFactory.cs index f01441b..a8bae8a 100644 --- a/FileProcessor/Common/ModelFactory.cs +++ b/FileProcessor/Common/ModelFactory.cs @@ -9,11 +9,7 @@ namespace FileProcessor.Common using FileDetailsResponse = DataTransferObjects.Responses.FileDetails; using FileLineResponse = DataTransferObjects.Responses.FileLine; - /// - /// - /// - /// - public class ModelFactory : IModelFactory + public static class ModelFactory { #region Methods @@ -22,13 +18,13 @@ public class ModelFactory : IModelFactory /// /// The file import logs. /// - public FileImportLogList ConvertFrom(List fileImportLogs) + public static FileImportLogList ConvertFrom(List fileImportLogs) { FileImportLogList result = new FileImportLogList(); result.FileImportLogs = new List(); foreach (Models.FileImportLog fileImportLog in fileImportLogs) { - result.FileImportLogs.Add(this.ConvertFrom(fileImportLog)); + result.FileImportLogs.Add(ConvertFrom(fileImportLog)); } return result; @@ -39,7 +35,7 @@ public FileImportLogList ConvertFrom(List fileImportLogs) /// /// The file import log. /// - public FileImportLogResponse ConvertFrom(Models.FileImportLog fileImportLog) + public static FileImportLogResponse ConvertFrom(Models.FileImportLog fileImportLog) { FileImportLogResponse fileImportLogResponse = new FileImportLogResponse { @@ -53,7 +49,7 @@ public FileImportLogResponse ConvertFrom(Models.FileImportLog fileImportLog) foreach (ImportLogFile importLogFile in fileImportLog.Files) { - FileImportLogFile fileImportLogFile = this.ConvertFrom(importLogFile); + FileImportLogFile fileImportLogFile = ConvertFrom(importLogFile); fileImportLogFile.FileImportLogId = fileImportLog.FileImportLogId; fileImportLogResponse.Files.Add(fileImportLogFile); } @@ -61,7 +57,7 @@ public FileImportLogResponse ConvertFrom(Models.FileImportLog fileImportLog) return fileImportLogResponse; } - public FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails) + public static FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails) { FileDetailsResponse fileDetailsResponse = new FileDetailsResponse { @@ -85,7 +81,7 @@ public FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails) { LineData = fileDetailsFileLine.LineData, LineNumber = fileDetailsFileLine.LineNumber, - ProcessingResult = this.TranslateProcessingResult(fileDetailsFileLine.ProcessingResult), + ProcessingResult = TranslateProcessingResult(fileDetailsFileLine.ProcessingResult), TransactionId = fileDetailsFileLine.TransactionId, RejectionReason = fileDetailsFileLine.RejectedReason }); @@ -110,7 +106,7 @@ public FileDetailsResponse ConvertFrom(Models.FileDetails fileDetails) /// /// The processing result. /// - private FileLineProcessingResult TranslateProcessingResult(ProcessingResult processingResult) + private static FileLineProcessingResult TranslateProcessingResult(ProcessingResult processingResult) { switch(processingResult) { @@ -134,7 +130,7 @@ private FileLineProcessingResult TranslateProcessingResult(ProcessingResult proc /// /// The import log file. /// - public FileImportLogFile ConvertFrom(ImportLogFile importLogFile) + public static FileImportLogFile ConvertFrom(ImportLogFile importLogFile) { FileImportLogFile fileImportLogFile = new FileImportLogFile { diff --git a/FileProcessor/Controllers/DomainEventController.cs b/FileProcessor/Controllers/DomainEventController.cs deleted file mode 100644 index 1c97a05..0000000 --- a/FileProcessor/Controllers/DomainEventController.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using SimpleResults; - -namespace FileProcessor.Controllers -{ - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using Microsoft.AspNetCore.Mvc; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Shared.DomainDrivenDesign.EventSourcing; - using Shared.EventStore.Aggregate; - using Shared.EventStore.EventHandling; - using Shared.General; - using Shared.Logger; - using Shared.Serialisation; - - [Route(DomainEventController.ControllerRoute)] - [ApiController] - [ExcludeFromCodeCoverage] - [ApiExplorerSettings(IgnoreApi = true)] - public class DomainEventController : ControllerBase - { - #region Fields - - /// - /// The domain event handler resolver - /// - private readonly IDomainEventHandlerResolver DomainEventHandlerResolver; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The domain event handler resolver. - public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver) - { - this.DomainEventHandlerResolver = domainEventHandlerResolver; - } - - #endregion - - #region Methods - - /// - /// Posts the event asynchronous. - /// - /// The domain event. - /// The cancellation token. - /// - [HttpPost] - public async Task PostEventAsync([FromBody] Object request, - CancellationToken cancellationToken) - { - var domainEvent = await this.GetDomainEvent(request); - - cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); - - List eventHandlers = this.GetDomainEventHandlers(domainEvent); - - try - { - Logger.LogWarning($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); - - if (eventHandlers == null || eventHandlers.Any() == false) - { - // Log a warning out - Logger.LogInformation($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); - return this.Ok(); - } - - List tasks = new List(); - foreach (IDomainEventHandler domainEventHandler in eventHandlers) - { - tasks.Add(domainEventHandler.Handle(domainEvent, cancellationToken)); - } - - Task.WaitAll(tasks.ToArray()); - - Logger.LogWarning($"Finished processing event - ID [{domainEvent.EventId}]"); - - return this.Ok(); - } - catch (Exception ex) - { - String domainEventData = JsonConvert.SerializeObject(domainEvent); - Logger.LogError(new Exception($" Failed to Process Event, Event Data received [{domainEventData}]", ex)); - - throw; - } - } - - /// - /// Callbacks the specified cancellation token. - /// - /// The cancellation token. - /// The event identifier. - private void Callback(CancellationToken cancellationToken, - Guid eventId) - { - if (cancellationToken.IsCancellationRequested) //I think this would always be true anyway - { - Logger.LogInformation($"Cancel request for EventId {eventId}"); - cancellationToken.ThrowIfCancellationRequested(); - } - } - - private List GetDomainEventHandlers(IDomainEvent domainEvent) - { - - if (this.Request.Headers.ContainsKey("EventHandler")) - { - var eventHandler = this.Request.Headers["EventHandler"]; - var eventHandlerType = this.Request.Headers["EventHandlerType"]; - var resolver = Startup.Container.GetInstance(eventHandlerType); - // We are being told by the caller to use a specific handler - var allhandlersResult = resolver.GetDomainEventHandlers(domainEvent); - if (allhandlersResult.IsFailed) - return new List(); - var handlers = allhandlersResult.Data.Where(h => h.GetType().Name.Contains(eventHandler)); - - return handlers.ToList(); - - } - - Result> eventHandlersResult = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); - if (eventHandlersResult.IsFailed) - return new List(); - return eventHandlersResult.Data; - } - - private async Task GetDomainEvent(Object domainEvent) - { - String eventType = this.Request.Headers["eventType"].ToString(); - - Type type = TypeMap.GetType(eventType); - - if (type == null) - throw new Exception($"Failed to find a domain event with type {eventType}"); - - JsonIgnoreAttributeIgnorerContractResolver jsonIgnoreAttributeIgnorerContractResolver = new JsonIgnoreAttributeIgnorerContractResolver(); - JsonSerializerSettings jsonSerialiserSettings = new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - TypeNameHandling = TypeNameHandling.All, - Formatting = Formatting.Indented, - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - ContractResolver = jsonIgnoreAttributeIgnorerContractResolver - }; - - if (type.IsSubclassOf(typeof(DomainEvent))) - { - String json = JsonConvert.SerializeObject(domainEvent, jsonSerialiserSettings); - - DomainEventFactory domainEventFactory = new(); - String validatedJson = this.ValidateEvent(json); - return domainEventFactory.CreateDomainEvent(validatedJson, type); - } - - return null; - } - - private String ValidateEvent(String domainEventJson) - { - JObject domainEvent = JObject.Parse(domainEventJson); - - if (domainEvent.ContainsKey("eventId") == false || domainEvent["eventId"].ToObject() == Guid.Empty) - { - throw new ArgumentException("Domain Event must contain an Event Id"); - } - - return domainEventJson; - } - - #endregion - - #region Others - - /// - /// The controller name - /// - public const String ControllerName = "domainevents"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + DomainEventController.ControllerName; - - #endregion - } -} diff --git a/FileProcessor/Controllers/FileController.cs b/FileProcessor/Controllers/FileController.cs deleted file mode 100644 index 04c47ef..0000000 --- a/FileProcessor/Controllers/FileController.cs +++ /dev/null @@ -1,146 +0,0 @@ -using FileProcessor.BusinessLogic.Requests; -using Shared.EventStore.Aggregate; -using Shared.Results; -using Shared.Results.Web; -using SimpleResults; - -namespace FileProcessor.Controllers -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Linq; - using System.Net.Http.Headers; - using System.Threading; - using System.Threading.Tasks; - using Common; - using DataTransferObjects; - using Models; - using MediatR; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Shared.General; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - [Route(FileController.ControllerRoute)] - [ApiController] - [Authorize] - public class FileController : ControllerBase - { - #region Fields - - /// - /// The mediator - /// - private readonly IMediator Mediator; - - - private readonly IModelFactory ModelFactory; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The mediator. - /// The manager. - public FileController(IMediator mediator, IModelFactory modelFactory) - { - this.Mediator = mediator; - this.ModelFactory = modelFactory; - } - - #endregion - - #region Methods - - /// - /// Uploads the file. - /// - /// The request. - /// The form collection. - /// The cancellation token. - /// - [Route("")] - [HttpPost] - [DisableRequestSizeLimit] - [Consumes("multipart/form-data")] - public async Task UploadFile([FromForm] UploadFileRequest request, - [FromForm] IFormCollection formCollection, - CancellationToken cancellationToken) - { - IFormFile file = formCollection.Files.First(); - - String temporaryFileLocation = ConfigurationReader.GetValue("AppSettings", "TemporaryFileLocation"); - String fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); - - String fullPath = Path.Combine(temporaryFileLocation, fileName); - - using (FileStream stream = new FileStream(fullPath, FileMode.Create)) - { - await file.CopyToAsync(stream, cancellationToken); - } - - if (request.UploadDateTime == DateTime.MinValue) - { - request.UploadDateTime = DateTime.Now; - } - - // Create a command with the file in it - FileCommands.UploadFileCommand command = - new (request.EstateId, request.MerchantId, request.UserId, fullPath, request.FileProfileId, request.UploadDateTime); - - Result result= await this.Mediator.Send(command, cancellationToken); - - Shared.Logger.Logger.LogDebug($"Day is {request.UploadDateTime.Day}"); - Shared.Logger.Logger.LogDebug($"Month is {request.UploadDateTime.Month}"); - Shared.Logger.Logger.LogDebug($"Year is {request.UploadDateTime.Year}"); - - return ResponseFactory.FromResult(result, (r) => r); - } - - /// - /// Gets the file. - /// - /// The file identifier. - /// The estate identifier. - /// The cancellation token. - /// - [HttpGet] - [Route("{fileId}")] - public async Task GetFile([FromRoute] Guid fileId, - [FromQuery] Guid estateId, - CancellationToken cancellationToken) { - //FileDetails fileDetailsModel = await this.Manager.GetFile(fileId, estateId, cancellationToken); - FileQueries.GetFileQuery query = new FileQueries.GetFileQuery(fileId, estateId); - - Result result = await this.Mediator.Send(query, cancellationToken); - - return ResponseFactory.FromResult(result, (r) => this.ModelFactory.ConvertFrom(r)); - - } - - #endregion - - #region Others - - /// - /// The controller name - /// - public const String ControllerName = "files"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + FileController.ControllerName; - - #endregion - } -} \ No newline at end of file diff --git a/FileProcessor/Controllers/FileImportLogController.cs b/FileProcessor/Controllers/FileImportLogController.cs deleted file mode 100644 index 9c6c9f8..0000000 --- a/FileProcessor/Controllers/FileImportLogController.cs +++ /dev/null @@ -1,114 +0,0 @@ -using FileProcessor.BusinessLogic.Requests; -using FileProcessor.DataTransferObjects.Responses; -using MediatR; -using Microsoft.AspNetCore.Http; -using Shared.EventStore.Aggregate; -using Shared.Results; -using Shared.Results.Web; -using SimpleResults; - -namespace FileProcessor.Controllers -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; - using Common; - using Models; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using static Microsoft.EntityFrameworkCore.DbLoggerCategory; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - [Route(FileImportLogController.ControllerRoute)] - [ApiController] - [Authorize] - public class FileImportLogController : ControllerBase - { - #region Fields - - private readonly IMediator Mediator; - - /// - /// The model factory - /// - private readonly IModelFactory ModelFactory; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The manager. - /// The model factory. - public FileImportLogController(IMediator mediator, - IModelFactory modelFactory) - { - this.Mediator = mediator; - this.ModelFactory = modelFactory; - } - - #endregion - - #region Methods - - /// - /// Gets the import logs. - /// - /// The estate identifier. - /// The start date time. - /// The end date time. - /// The merchant identifier. - /// The cancellation token. - /// - [HttpGet] - public async Task GetImportLogs([FromQuery] Guid estateId, - [FromQuery] DateTime startDateTime, - [FromQuery] DateTime endDateTime, - [FromQuery] Guid? merchantId, - CancellationToken cancellationToken) { - FileQueries.GetImportLogsQuery query = new(estateId, startDateTime, endDateTime, merchantId); - - Result> result = await this.Mediator.Send(query, cancellationToken); - - return ResponseFactory.FromResult(result, (r) => this.ModelFactory.ConvertFrom(r)); - } - - [HttpGet] - [Route("{fileImportLogId}")] - public async Task GetImportLog([FromRoute] Guid fileImportLogId, - [FromQuery] Guid estateId, - [FromQuery] Guid? merchantId, - CancellationToken cancellationToken) - { - FileQueries.GetImportLogQuery query = new(fileImportLogId, estateId, merchantId); - - Result result = await this.Mediator.Send(query, cancellationToken); - - return ResponseFactory.FromResult(result, (r) => this.ModelFactory.ConvertFrom(r)); - } - - #endregion - - #region Others - - /// - /// The controller name - /// - public const String ControllerName = "fileImportLogs"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + FileImportLogController.ControllerName; - - #endregion - } -} \ No newline at end of file diff --git a/FileProcessor/Endpoints/DomainEventEndpoints.cs b/FileProcessor/Endpoints/DomainEventEndpoints.cs new file mode 100644 index 0000000..b1a9f08 --- /dev/null +++ b/FileProcessor/Endpoints/DomainEventEndpoints.cs @@ -0,0 +1,18 @@ +using FileProcessor.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FileProcessor.Endpoints; + +public static class DomainEventEndpoints +{ + private const string Route = "api/domainevents"; + + public static void MapDomainEventEndpoint(this IEndpointRouteBuilder app) + { + app.MapPost(Route, DomainEventHandlers.HandleDomainEvent) + .ExcludeFromDescription() // hides from Swagger + .WithName("DomainEvent"); + } +} \ No newline at end of file diff --git a/FileProcessor/Endpoints/FileEndpoints.cs b/FileProcessor/Endpoints/FileEndpoints.cs new file mode 100644 index 0000000..0a7d08a --- /dev/null +++ b/FileProcessor/Endpoints/FileEndpoints.cs @@ -0,0 +1,26 @@ +using FileProcessor.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FileProcessor.Endpoints; + +public static class FileEndpoints +{ + public const string BaseRoute = "api/files"; + + public static void MapFileEndpoints(this IEndpointRouteBuilder app) + { + RouteGroupBuilder group = app.MapGroup(BaseRoute) + .WithTags("Files") + .RequireAuthorization(); + + group.MapPost("/", FileHandlers.UploadFileAsync) + .DisableAntiforgery() + .Accepts("multipart/form-data") + .WithName("UploadFile"); + + group.MapGet("/{fileId:guid}", FileHandlers.GetFileAsync) + .WithName("GetFile"); + } +} \ No newline at end of file diff --git a/FileProcessor/Endpoints/FileImportLogEndpoints.cs b/FileProcessor/Endpoints/FileImportLogEndpoints.cs new file mode 100644 index 0000000..dce14b6 --- /dev/null +++ b/FileProcessor/Endpoints/FileImportLogEndpoints.cs @@ -0,0 +1,24 @@ +using FileProcessor.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FileProcessor.Endpoints; + +public static class FileImportLogEndpoints +{ + public const string BaseRoute = "api/fileImportLogs"; + + public static void MapFileImportLogEndpoints(this IEndpointRouteBuilder app) + { + RouteGroupBuilder group = app.MapGroup(BaseRoute) + .WithTags("File Import Logs") + .RequireAuthorization(); + + group.MapGet("/", FileImportLogHandlers.GetImportLogsAsync) + .WithName("GetImportLogs"); + + group.MapGet("/{fileImportLogId:guid}", FileImportLogHandlers.GetImportLogAsync) + .WithName("GetImportLog"); + } +} \ No newline at end of file diff --git a/FileProcessor/FileProcessor.csproj b/FileProcessor/FileProcessor.csproj index 7239252..bf5a1b8 100644 --- a/FileProcessor/FileProcessor.csproj +++ b/FileProcessor/FileProcessor.csproj @@ -39,4 +39,8 @@ + + + + diff --git a/FileProcessor/Handlers/DomainEventHandlers.cs b/FileProcessor/Handlers/DomainEventHandlers.cs new file mode 100644 index 0000000..82c42d7 --- /dev/null +++ b/FileProcessor/Handlers/DomainEventHandlers.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared.DomainDrivenDesign.EventSourcing; +using Shared.EventStore.Aggregate; +using Shared.EventStore.EventHandling; +using Shared.Exceptions; +using Shared.General; +using Shared.Logger; +using Shared.Serialisation; +using SimpleResults; +using static FileProcessor.Common.ModelFactory; +using ModelFactory = FileProcessor.Common.ModelFactory; + +namespace FileProcessor.Handlers +{ + public static class DomainEventHandlers + { + public static async Task HandleDomainEvent(HttpRequest request, + object body, + IDomainEventHandlerResolver resolver, + CancellationToken cancellationToken) + { + IDomainEvent domainEvent = await GetDomainEvent(request, body); + + cancellationToken.Register(() => Callback(cancellationToken, domainEvent.EventId)); + + try + { + Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); + + Result> eventHandlersResult = resolver.GetDomainEventHandlers(domainEvent); + + if (eventHandlersResult.IsFailed) + { + Logger.LogWarning($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); + return Results.Ok(); + } + + var eventHandlers = eventHandlersResult.Data; + var tasks = eventHandlers.Select(h => h.Handle(domainEvent, cancellationToken)); + await Task.WhenAll(tasks); + + Logger.LogInformation("Finished processing event - ID [{domainEvent.EventId}]"); + + return Results.Ok(); + } + catch (Exception ex) + { + string domainEventData = JsonConvert.SerializeObject(domainEvent); + Logger.LogError($"Failed to process event. Data received [{domainEventData}]", ex); + throw; + } + } + + private static void Callback(CancellationToken cancellationToken, Guid eventId) + { + if (cancellationToken.IsCancellationRequested) + { + Logger.LogInformation($"Cancel request for EventId {eventId}"); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private static async Task GetDomainEvent(HttpRequest request, object domainEvent) + { + string eventType = request.Headers["eventType"].ToString(); + + Type type = TypeMap.GetType(eventType); + + if (type == null) + throw new NotFoundException($"Failed to find a domain event with type {eventType}"); + + var resolver = new JsonIgnoreAttributeIgnorerContractResolver(); + var settings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.All, + Formatting = Formatting.Indented, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + ContractResolver = resolver + }; + + if (type.IsSubclassOf(typeof(DomainEvent))) + { + string json = JsonConvert.SerializeObject(domainEvent, settings); + + var factory = new DomainEventFactory(); + string validatedJson = ValidateEvent(json); + return factory.CreateDomainEvent(validatedJson, type); + } + + return null; + } + + private static string ValidateEvent(string domainEventJson) + { + var domainEvent = JObject.Parse(domainEventJson); + + if (!domainEvent.ContainsKey("eventId") || + domainEvent["eventId"]!.ToObject() == Guid.Empty) + { + throw new ArgumentException("Domain Event must contain an Event Id"); + } + + return domainEventJson; + } + } +} diff --git a/FileProcessor/Handlers/FileHandlers.cs b/FileProcessor/Handlers/FileHandlers.cs new file mode 100644 index 0000000..b90a74a --- /dev/null +++ b/FileProcessor/Handlers/FileHandlers.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using FileProcessor.BusinessLogic.Requests; +using FileProcessor.Common; +using FileProcessor.DataTransferObjects; +using FileProcessor.Models; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shared.General; +using Shared.Results.Web; +using SimpleResults; + +namespace FileProcessor.Handlers; + +public static class FileHandlers +{ + public static async Task UploadFileAsync(IMediator mediator, + [FromForm] UploadFileRequest request, + [FromForm] IFormCollection formCollection, + CancellationToken cancellationToken) + { + IFormFile file = formCollection.Files.First(); + + string temporaryFileLocation = ConfigurationReader.GetValue("AppSettings", "TemporaryFileLocation"); + string fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); + string fullPath = Path.Combine(temporaryFileLocation, fileName); + + await using (FileStream stream = new(fullPath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + if (request.UploadDateTime == DateTime.MinValue) + request.UploadDateTime = DateTime.Now; + + FileCommands.UploadFileCommand command = new(request.EstateId, + request.MerchantId, + request.UserId, + fullPath, + request.FileProfileId, + request.UploadDateTime); + + Result result = await mediator.Send(command, cancellationToken); + + Shared.Logger.Logger.LogDebug($"Day is {request.UploadDateTime.Day}"); + Shared.Logger.Logger.LogDebug($"Month is {request.UploadDateTime.Month}"); + Shared.Logger.Logger.LogDebug($"Year is {request.UploadDateTime.Year}"); + + return ResponseFactory.FromResult(result, r => r); + } + + public static async Task GetFileAsync(IMediator mediator, + [FromRoute] Guid fileId, + [FromQuery] Guid estateId, + CancellationToken cancellationToken) + { + FileQueries.GetFileQuery query = new(fileId, estateId); + Result result = await mediator.Send(query, cancellationToken); + + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); + } +} \ No newline at end of file diff --git a/FileProcessor/Handlers/FileImportLogHandlers.cs b/FileProcessor/Handlers/FileImportLogHandlers.cs new file mode 100644 index 0000000..7bfdfde --- /dev/null +++ b/FileProcessor/Handlers/FileImportLogHandlers.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FileProcessor.BusinessLogic.Requests; +using FileProcessor.Common; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shared.Results.Web; +using SimpleResults; + +namespace FileProcessor.Handlers; + +public static class FileImportLogHandlers +{ + public static async Task GetImportLogsAsync(IMediator mediator, + [FromQuery] Guid estateId, + [FromQuery] DateTime startDateTime, + [FromQuery] DateTime endDateTime, + [FromQuery] Guid? merchantId, + CancellationToken cancellationToken) + { + FileQueries.GetImportLogsQuery query = new(estateId, startDateTime, endDateTime, merchantId); + Result> result = await mediator.Send(query, cancellationToken); + + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); + } + + public static async Task GetImportLogAsync(IMediator mediator, + [FromRoute] Guid fileImportLogId, + [FromQuery] Guid estateId, + [FromQuery] Guid? merchantId, + CancellationToken cancellationToken) + { + FileQueries.GetImportLogQuery query = new(fileImportLogId, estateId, merchantId); + Result result = await mediator.Send(query, cancellationToken); + + return ResponseFactory.FromResult(result, ModelFactory.ConvertFrom); + } +} \ No newline at end of file diff --git a/FileProcessor/Program.cs b/FileProcessor/Program.cs index a0ac4eb..54b503f 100644 --- a/FileProcessor/Program.cs +++ b/FileProcessor/Program.cs @@ -71,7 +71,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) webBuilder.UseStartup(); webBuilder.UseConfiguration(config); webBuilder.UseKestrel(); + webBuilder.ConfigureKestrel(serverOptions => + { + // Remove Kestrel max request body size limit (set to null -> no limit) + serverOptions.Limits.MaxRequestBodySize = null; + }); }); + return hostBuilder; } diff --git a/FileProcessor/Startup.cs b/FileProcessor/Startup.cs index 1cd145b..e8bcc36 100644 --- a/FileProcessor/Startup.cs +++ b/FileProcessor/Startup.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using FileProcessor.Endpoints; using ImTools; namespace FileProcessor @@ -126,9 +127,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); + app.UseEndpoints(endpoints => { + endpoints.MapDomainEventEndpoint(); + endpoints.MapFileEndpoints(); + endpoints.MapFileImportLogEndpoints(); + endpoints.MapHealthChecks("health", new HealthCheckOptions() { Predicate = _ => true,