diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3729ff0c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..62aef736 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,13 @@ +categories: + - title: '🚀 Features' + labels: + - 'feature' + - title: '🐛 Bug Fixes' + labels: + - 'bug' + - title: '🧰 Maintenance' + label: 'maintenance' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + $CHANGES diff --git a/.github/workflows/createrelease.yml b/.github/workflows/createrelease.yml new file mode 100644 index 00000000..afe0bffa --- /dev/null +++ b/.github/workflows/createrelease.yml @@ -0,0 +1,110 @@ +name: Release + +on: + release: + types: [published] + +jobs: + build: + name: "Release" + env: + ASPNETCORE_ENVIRONMENT: "Production" + + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v1 + + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + + - name: Setup .NET Core 3.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + + - name: Restore Nuget Packages + run: dotnet restore TransactionProcessor.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json + + - name: Build Code + run: dotnet build TransactionProcessor.sln --configuration Release + + - name: Build Docker Images + run: | + docker build . --file TransactionProcessor/Dockerfile --tag transactionprocessor:latest --tag stuartferguson/transactionprocessor:latest --tag stuartferguson/transactionprocessor:${{ steps.get_version.outputs.VERSION }} + + - name: Publish Images to Docker Hub + run: | + docker login --username=${{ secrets.DOCKER_USERNAME }} --password=${{ secrets.DOCKER_PASSWORD }} + docker push stuartferguson/transactionprocessor:latest + docker push stuartferguson/transactionprocessor:${{ steps.get_version.outputs.VERSION }} + + - name: Publish API + run: dotnet publish "TransactionProcessor\TransactionProcessor.csproj" --configuration Release --output publishOutput + + - name: Setup .NET Core 2.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.0.0 + + - name: Extract Octopus Tools + run: | + mkdir /opt/octo + cd /opt/octo + wget -O /opt/octo/octopus.zip https://download.octopusdeploy.com/octopus-tools/6.12.0/OctopusTools.6.12.0.portable.zip + unzip /opt/octo/octopus.zip + chmod +x /opt/octo/Octo + + - name: Pack Files for Octopus + run: >- + /opt/octo/Octo pack + --outFolder /home/runner/work/TransactionProcessor/TransactionProcessor + --basePath /home/runner/work/TransactionProcessor/TransactionProcessor/publishOutput + --id TransactionProcessor + --version ${{ steps.get_version.outputs.VERSION }} + --format zip + --verbose + --logLevel=verbose + + - name: Push Package to Octopus + run: >- + /opt/octo/Octo push + --server http://sferguson.ddns.net:9001 + --apiKey API-UTN58QCF8HSATACNUBY41XPUC + --package /home/runner/work/TransactionProcessor/TransactionProcessor/TransactionProcessor.${{ steps.get_version.outputs.VERSION }}.zip + --overwrite-mode IgnoreIfExists + + - name: Get Release + id: getrelease + uses: octokit/request-action@v1.x + with: + route: GET /repos/StuartFerguson/TransactionProcessor/releases/tags/${{ steps.get_version.outputs.VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Release Notes + id: buildreleasenotes + uses: gr2m/get-json-paths-action@v1.x + with: + json: ${{ steps.getrelease.outputs.data }} + releasenote: "body" + + - name: Create & Deploy Release in Octopus + run: >- + /opt/octo/Octo create-release + --server http://sferguson.ddns.net:9001 + --apiKey API-UTN58QCF8HSATACNUBY41XPUC + --project "Transaction Processor" + --version ${{ steps.get_version.outputs.VERSION }} + --channel Default + --deployTo Development + --waitfordeployment + --deploymenttimeout 00:05:00 + --releasenotes "${{ steps.buildreleasenotes.outputs.releasenote }}" + + - name: Setup .NET Core 3.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml new file mode 100644 index 00000000..fd89f627 --- /dev/null +++ b/.github/workflows/nightlybuild.yml @@ -0,0 +1,30 @@ +name: Nightly Build + +on: + schedule: + - cron: "* 23 * * *" + +jobs: + build: + name: "Nightly Build" + env: + ASPNETCORE_ENVIRONMENT: "Production" + + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v1 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + + - name: Restore Nuget Packages + run: dotnet restore TransactionProcessor.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json + + - name: Build Code + run: dotnet build TransactionProcessor.sln --configuration Release + + - name: Build Docker Image + run: docker build . --file TransactionProcessor/Dockerfile --tag transactionprocessor:latest diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml new file mode 100644 index 00000000..c7bf4c49 --- /dev/null +++ b/.github/workflows/pullrequest.yml @@ -0,0 +1,32 @@ +name: Build and Test Pull Requests + +on: + pull_request: + branches: + - master + +jobs: + build: + name: "Build and Test Pull Requests" + env: + ASPNETCORE_ENVIRONMENT: "Production" + + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v1 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.0.100 + + - name: Restore Nuget Packages + run: dotnet restore TransactionProcessor.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json + + - name: Build Code + run: dotnet build TransactionProcessor.sln --configuration Release + + - name: Build Docker Image + run: docker build . --file TransactionProcessor/Dockerfile --tag transactionprocessor:latest + diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml new file mode 100644 index 00000000..83d8d984 --- /dev/null +++ b/.github/workflows/release-management.yml @@ -0,0 +1,16 @@ +name: Release Management + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_draft_release: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: toolmantim/release-drafter@v5.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 3e759b75..5ae0db95 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.user *.userosscache *.sln.docstates +*.[Dd]evelopment.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/TransactionProcessor.BusinessLogic/CommandHandlers/CommandRouter.cs b/TransactionProcessor.BusinessLogic/CommandHandlers/CommandRouter.cs new file mode 100644 index 00000000..a33c7f4f --- /dev/null +++ b/TransactionProcessor.BusinessLogic/CommandHandlers/CommandRouter.cs @@ -0,0 +1,64 @@ +namespace TransactionProcessor.BusinessLogic.CommandHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using Commands; + using Services; + using Shared.DomainDrivenDesign.CommandHandling; + + /// + /// + /// + /// + public class CommandRouter : ICommandRouter + { + #region Fields + + /// + /// The transaction domain service + /// + private readonly ITransactionDomainService TransactionDomainService; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The transaction domain service. + public CommandRouter(ITransactionDomainService transactionDomainService) + { + this.TransactionDomainService = transactionDomainService; + } + + #endregion + + #region Methods + + /// + /// Routes the specified command. + /// + /// The command. + /// The cancellation token. + public async Task Route(ICommand command, + CancellationToken cancellationToken) + { + ICommandHandler commandHandler = CreateHandler((dynamic)command); + + await commandHandler.Handle(command, cancellationToken); + } + + /// + /// Creates the handler. + /// + /// The command. + /// + private ICommandHandler CreateHandler(ProcessLogonTransactionCommand command) + { + return new TransactionCommandHandler(this.TransactionDomainService); + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/CommandHandlers/TransactionCommandHandler.cs b/TransactionProcessor.BusinessLogic/CommandHandlers/TransactionCommandHandler.cs new file mode 100644 index 00000000..1da86ca4 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/CommandHandlers/TransactionCommandHandler.cs @@ -0,0 +1,66 @@ +namespace TransactionProcessor.BusinessLogic.CommandHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using Commands; + using Models; + using Services; + using Shared.DomainDrivenDesign.CommandHandling; + + /// + /// + /// + /// + public class TransactionCommandHandler : ICommandHandler + { + #region Fields + + /// + /// The transaction domain service + /// + private readonly ITransactionDomainService TransactionDomainService; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The transaction domain service. + public TransactionCommandHandler(ITransactionDomainService transactionDomainService) + { + this.TransactionDomainService = transactionDomainService; + } + + #endregion + + #region Methods + + /// + /// Handles the specified command. + /// + /// The command. + /// The cancellation token. + public async Task Handle(ICommand command, + CancellationToken cancellationToken) + { + await this.HandleCommand((dynamic)command, cancellationToken); + } + + /// + /// Handles the command. + /// + /// The command. + /// The cancellation token. + private async Task HandleCommand(ProcessLogonTransactionCommand command, + CancellationToken cancellationToken) + { + ProcessLogonTransactionResponse logonResponse = await this.TransactionDomainService.ProcessLogonTransaction(cancellationToken); + + command.Response = logonResponse; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Commands/ProcessLogonTransactionCommand.cs b/TransactionProcessor.BusinessLogic/Commands/ProcessLogonTransactionCommand.cs new file mode 100644 index 00000000..8f5433cd --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Commands/ProcessLogonTransactionCommand.cs @@ -0,0 +1,25 @@ +namespace TransactionProcessor.BusinessLogic.Commands +{ + using System; + using Models; + using Shared.DomainDrivenDesign.CommandHandling; + + /// + /// + /// + /// + public class ProcessLogonTransactionCommand : Command + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The command identifier. + public ProcessLogonTransactionCommand(Guid commandId) : base(commandId) + { + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs new file mode 100644 index 00000000..2cc83f9f --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs @@ -0,0 +1,23 @@ +namespace TransactionProcessor.BusinessLogic.Services +{ + using System.Threading; + using System.Threading.Tasks; + using Models; + + /// + /// + /// + public interface ITransactionDomainService + { + #region Methods + + /// + /// Processes the logon transaction. + /// + /// The cancellation token. + /// + Task ProcessLogonTransaction(CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs new file mode 100644 index 00000000..3785dbca --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -0,0 +1,31 @@ +namespace TransactionProcessor.BusinessLogic.Services +{ + using System.Threading; + using System.Threading.Tasks; + using Models; + + /// + /// + /// + /// + public class TransactionDomainService : ITransactionDomainService + { + #region Methods + + /// + /// Processes the logon transaction. + /// + /// The cancellation token. + /// + public async Task ProcessLogonTransaction(CancellationToken cancellationToken) + { + return new ProcessLogonTransactionResponse + { + ResponseMessage = "SUCCESS", + ResponseCode = 0 + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj new file mode 100644 index 00000000..1e0a1c46 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + + diff --git a/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs b/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs new file mode 100644 index 00000000..d570e116 --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/LogonTransactionRequest.cs @@ -0,0 +1,54 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + + /// + /// + /// + public class LogonTransactionRequest + { + #region Properties + + /// + /// Gets or sets the imei number. + /// + /// + /// The imei number. + /// + public String IMEINumber { get; set; } + + /// + /// Gets or sets the merchant identifier. + /// + /// + /// The merchant identifier. + /// + public Guid MerchantId { get; set; } + + /// + /// Gets or sets the transaction date time. + /// + /// + /// The transaction date time. + /// + public DateTime TransactionDateTime { get; set; } + + /// + /// Gets or sets the transaction number. + /// + /// + /// The transaction number. + /// + public String TransactionNumber { get; set; } + + /// + /// Gets or sets the type of the transaction. + /// + /// + /// The type of the transaction. + /// + public String TransactionType { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs b/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs new file mode 100644 index 00000000..3c59e218 --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/LogonTransactionResponse.cs @@ -0,0 +1,30 @@ +namespace TransactionProcessor.DataTransferObjects +{ + using System; + + /// + /// + /// + public class LogonTransactionResponse + { + #region Properties + + /// + /// Gets or sets the response code. + /// + /// + /// The response code. + /// + public Int32 ResponseCode { get; set; } + + /// + /// Gets or sets the response message. + /// + /// + /// The response message. + /// + public String ResponseMessage { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj b/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj new file mode 100644 index 00000000..f45bdfcc --- /dev/null +++ b/TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.1 + + + diff --git a/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs b/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs new file mode 100644 index 00000000..c394e426 --- /dev/null +++ b/TransactionProcessor.Models/ProcessLogonTransactionResponse.cs @@ -0,0 +1,30 @@ +namespace TransactionProcessor.Models +{ + using System; + + /// + /// + /// + public class ProcessLogonTransactionResponse + { + #region Properties + + /// + /// Gets or sets the response code. + /// + /// + /// The response code. + /// + public Int32 ResponseCode { get; set; } + + /// + /// Gets or sets the response message. + /// + /// + /// The response message. + /// + public String ResponseMessage { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor.Models/TransactionProcessor.Models.csproj b/TransactionProcessor.Models/TransactionProcessor.Models.csproj new file mode 100644 index 00000000..ea83d296 --- /dev/null +++ b/TransactionProcessor.Models/TransactionProcessor.Models.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.0 + + + diff --git a/TransactionProcessor.sln b/TransactionProcessor.sln new file mode 100644 index 00000000..4f3265f2 --- /dev/null +++ b/TransactionProcessor.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29509.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransactionProcessor", "TransactionProcessor\TransactionProcessor.csproj", "{1EBD3402-AF91-4919-A5A1-2E3728918507}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{749ADE74-A6F0-4469-A638-8FD7E82A8667}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{71B30DC4-AB27-4D30-8481-B4C326D074CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.DataTransferObjects", "TransactionProcessor.DataTransferObjects\TransactionProcessor.DataTransferObjects.csproj", "{405F1DE4-9BAA-4B5D-8707-C78B2C3E9040}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.BusinessLogic", "TransactionProcessor.BusinessLogic\TransactionProcessor.BusinessLogic.csproj", "{6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.Models", "TransactionProcessor.Models\TransactionProcessor.Models.csproj", "{97B646C6-7BF7-4F84-A74A-10E50A70CB91}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1EBD3402-AF91-4919-A5A1-2E3728918507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EBD3402-AF91-4919-A5A1-2E3728918507}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EBD3402-AF91-4919-A5A1-2E3728918507}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EBD3402-AF91-4919-A5A1-2E3728918507}.Release|Any CPU.Build.0 = Release|Any CPU + {405F1DE4-9BAA-4B5D-8707-C78B2C3E9040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {405F1DE4-9BAA-4B5D-8707-C78B2C3E9040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {405F1DE4-9BAA-4B5D-8707-C78B2C3E9040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {405F1DE4-9BAA-4B5D-8707-C78B2C3E9040}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0}.Release|Any CPU.Build.0 = Release|Any CPU + {97B646C6-7BF7-4F84-A74A-10E50A70CB91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B646C6-7BF7-4F84-A74A-10E50A70CB91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B646C6-7BF7-4F84-A74A-10E50A70CB91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B646C6-7BF7-4F84-A74A-10E50A70CB91}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1EBD3402-AF91-4919-A5A1-2E3728918507} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {405F1DE4-9BAA-4B5D-8707-C78B2C3E9040} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {6CCAAA44-AC4F-40DA-B5D4-F4390DD729C0} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {97B646C6-7BF7-4F84-A74A-10E50A70CB91} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {193D13DE-424B-4D50-B674-01F9E4CC2CA9} + EndGlobalSection +EndGlobal diff --git a/TransactionProcessor/Common/ConfigureSwaggerOptions.cs b/TransactionProcessor/Common/ConfigureSwaggerOptions.cs new file mode 100644 index 00000000..92353d6c --- /dev/null +++ b/TransactionProcessor/Common/ConfigureSwaggerOptions.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TransactionProcessor.Common +{ + using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Microsoft.OpenApi.Models; + using Swashbuckle.AspNetCore.SwaggerGen; + + /// + /// Configures the Swagger generation options. + /// + /// This allows API versioning to define a Swagger document per API version after the + /// service has been resolved from the service container. + [ExcludeFromCodeCoverage] + public class ConfigureSwaggerOptions : IConfigureOptions + { + #region Fields + + /// + /// The provider + /// + private readonly IApiVersionDescriptionProvider provider; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The provider used to generate Swagger documents. + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; + + #endregion + + #region Methods + + /// + public void Configure(SwaggerGenOptions options) + { + // add a swagger document for each discovered API version + // note: you might choose to skip or document deprecated API versions differently + foreach (ApiVersionDescription description in this.provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, ConfigureSwaggerOptions.CreateInfoForApiVersion(description)); + } + } + + /// + /// Creates the information for API version. + /// + /// The description. + /// + private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + OpenApiInfo info = new OpenApiInfo + { + Title = "Golf Handicapping API", + Version = description.ApiVersion.ToString(), + Description = "A REST Api to manage the golf club handicapping system.", + Contact = new OpenApiContact + { + Name = "Stuart Ferguson", + Email = "golfhandicapping@btinternet.com" + }, + License = new OpenApiLicense + { + Name = "TODO" + } + }; + + if (description.IsDeprecated) + { + info.Description += " This API version has been deprecated."; + } + + return info; + } + + #endregion + } +} diff --git a/TransactionProcessor/Common/SwaggerDefaultValues.cs b/TransactionProcessor/Common/SwaggerDefaultValues.cs new file mode 100644 index 00000000..ca37aabe --- /dev/null +++ b/TransactionProcessor/Common/SwaggerDefaultValues.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TransactionProcessor.Common +{ + using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Abstractions; + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.OpenApi.Models; + using Swashbuckle.AspNetCore.SwaggerGen; + + /// + /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. + /// + /// This is only required due to bugs in the . + /// Once they are fixed and published, this class can be removed. + [ExcludeFromCodeCoverage] + public class SwaggerDefaultValues : IOperationFilter + { + /// + /// Applies the filter to the specified operation using the given context. + /// + /// The operation to apply the filter to. + /// The current operation filter context. + public void Apply(OpenApiOperation operation, + OperationFilterContext context) + { + ApiDescription apiDescription = context.ApiDescription; + ApiVersion apiVersion = apiDescription.GetApiVersion(); + ApiVersionModel model = apiDescription.ActionDescriptor.GetApiVersionModel(ApiVersionMapping.Explicit | ApiVersionMapping.Implicit); + + operation.Deprecated = model.DeprecatedApiVersions.Contains(apiVersion); + + if (operation.Parameters == null) + { + return; + } + + foreach (OpenApiParameter parameter in operation.Parameters) + { + ApiParameterDescription description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + if (parameter.Description == null) + { + parameter.Description = description.ModelMetadata?.Description; + } + + parameter.Required |= description.IsRequired; + } + } + } +} diff --git a/TransactionProcessor/Common/SwaggerJsonConverter.cs b/TransactionProcessor/Common/SwaggerJsonConverter.cs new file mode 100644 index 00000000..28ef4a32 --- /dev/null +++ b/TransactionProcessor/Common/SwaggerJsonConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TransactionProcessor.Common +{ + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + public class SwaggerJsonConverter : JsonConverter + { + #region Properties + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// + /// true if this can read JSON; otherwise, false. + /// + public override Boolean CanRead => false; + + #endregion + + #region Methods + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override Boolean CanConvert(Type objectType) + { + return true; + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + /// + /// + public override Object ReadJson(JsonReader reader, + Type objectType, + Object existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, + Object value, + JsonSerializer serializer) + { + // Disable sending the $type in the serialized json + serializer.TypeNameHandling = TypeNameHandling.None; + + JToken t = JToken.FromObject(value); + t.WriteTo(writer); + } + + #endregion + } +} diff --git a/TransactionProcessor/Controllers/TransactionController.cs b/TransactionProcessor/Controllers/TransactionController.cs new file mode 100644 index 00000000..752a3f27 --- /dev/null +++ b/TransactionProcessor/Controllers/TransactionController.cs @@ -0,0 +1,89 @@ +namespace TransactionProcessor.Controllers +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using BusinessLogic.Commands; + using DataTransferObjects; + using Factories; + using Microsoft.AspNetCore.Mvc; + using Shared.DomainDrivenDesign.CommandHandling; + + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + [Route(TransactionController.ControllerRoute)] + [ApiController] + [ApiVersion("1.0")] + public class TransactionController : ControllerBase + { + #region Fields + + /// + /// The command router + /// + private readonly ICommandRouter CommandRouter; + + /// + /// The model factory + /// + private readonly IModelFactory ModelFactory; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The command router. + /// The model factory. + public TransactionController(ICommandRouter commandRouter, + IModelFactory modelFactory) + { + this.CommandRouter = commandRouter; + this.ModelFactory = modelFactory; + } + + #endregion + + #region Methods + + /// + /// Logons the transaction. + /// + /// The logon transaction request. + /// The cancellation token. + /// + [HttpPost] + [Route("")] + public async Task LogonTransaction([FromBody] LogonTransactionRequest logonTransactionRequest, + CancellationToken cancellationToken) + { + ProcessLogonTransactionCommand command = new ProcessLogonTransactionCommand(Guid.NewGuid()); + + await this.CommandRouter.Route(command, cancellationToken); + + return this.Ok(this.ModelFactory.ConvertFrom(command.Response)); + } + + #endregion + + #region Others + + /// + /// The controller name + /// + public const String ControllerName = "transactions"; + + /// + /// The controller route + /// + private const String ControllerRoute = "api/" + TransactionController.ControllerName; + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor/Dockerfile b/TransactionProcessor/Dockerfile new file mode 100644 index 00000000..344b150d --- /dev/null +++ b/TransactionProcessor/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build +WORKDIR /src +COPY ["TransactionProcessor/NuGet.Config", "."] +COPY ["TransactionProcessor/TransactionProcessor.csproj", "TransactionProcessor/"] +COPY ["TransactionProcessor/NuGet.Config", "TransactionProcessor/"] +COPY ["TransactionProcessor.DataTransferObjects/TransactionProcessor.DataTransferObjects.csproj", "TransactionProcessor.DataTransferObjects/"] +COPY ["TransactionProcessor.BusinessLogic/TransactionProcessor.BusinessLogic.csproj", "TransactionProcessor.BusinessLogic/"] +COPY ["TransactionProcessor.Models/TransactionProcessor.Models.csproj", "TransactionProcessor.Models/"] +RUN dotnet restore "TransactionProcessor/TransactionProcessor.csproj" +COPY . . +WORKDIR "/src/TransactionProcessor" +RUN dotnet build "TransactionProcessor.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "TransactionProcessor.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "TransactionProcessor.dll"] \ No newline at end of file diff --git a/TransactionProcessor/Factories/IModelFactory.cs b/TransactionProcessor/Factories/IModelFactory.cs new file mode 100644 index 00000000..16bde727 --- /dev/null +++ b/TransactionProcessor/Factories/IModelFactory.cs @@ -0,0 +1,22 @@ +namespace TransactionProcessor.Factories +{ + using DataTransferObjects; + using Models; + + /// + /// + /// + public interface IModelFactory + { + #region Methods + + /// + /// Converts from. + /// + /// The process logon transaction response. + /// + LogonTransactionResponse ConvertFrom(ProcessLogonTransactionResponse processLogonTransactionResponse); + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor/Factories/ModelFactory.cs b/TransactionProcessor/Factories/ModelFactory.cs new file mode 100644 index 00000000..af473e19 --- /dev/null +++ b/TransactionProcessor/Factories/ModelFactory.cs @@ -0,0 +1,30 @@ +namespace TransactionProcessor.Factories +{ + using DataTransferObjects; + using Models; + + /// + /// + /// + /// + public class ModelFactory : IModelFactory + { + #region Methods + + /// + /// Converts from. + /// + /// The process logon transaction response. + /// + public LogonTransactionResponse ConvertFrom(ProcessLogonTransactionResponse processLogonTransactionResponse) + { + return new LogonTransactionResponse + { + ResponseMessage = processLogonTransactionResponse.ResponseMessage, + ResponseCode = processLogonTransactionResponse.ResponseCode + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionProcessor/NuGet.Config b/TransactionProcessor/NuGet.Config new file mode 100644 index 00000000..c6bc4ff9 --- /dev/null +++ b/TransactionProcessor/NuGet.Config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TransactionProcessor/Program.cs b/TransactionProcessor/Program.cs new file mode 100644 index 00000000..27e0dfe6 --- /dev/null +++ b/TransactionProcessor/Program.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace TransactionProcessor +{ + using System.Diagnostics.CodeAnalysis; + using System.IO; + using Autofac.Extensions.DependencyInjection; + + [ExcludeFromCodeCoverage] + public class Program + { + public static void Main(string[] args) + { + Program.CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + Console.Title = "Transaction Processor"; + + //At this stage, we only need our hosting file for ip and ports + IConfigurationRoot config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile("hosting.development.json", optional: true) + .AddEnvironmentVariables().Build(); + + IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); + hostBuilder.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseConfiguration(config); + webBuilder.UseKestrel(); + }).UseServiceProviderFactory(new AutofacServiceProviderFactory()); + return hostBuilder; + } + + } +} diff --git a/TransactionProcessor/Startup.cs b/TransactionProcessor/Startup.cs new file mode 100644 index 00000000..eb556714 --- /dev/null +++ b/TransactionProcessor/Startup.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace TransactionProcessor +{ + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Reflection; + using Autofac; + using BusinessLogic.CommandHandlers; + using BusinessLogic.Services; + using Common; + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.Options; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + using NLog.Extensions.Logging; + using Shared.DomainDrivenDesign.CommandHandling; + using Shared.Extensions; + using Shared.General; + using Swashbuckle.AspNetCore.Filters; + using Swashbuckle.AspNetCore.SwaggerGen; + + [ExcludeFromCodeCoverage] + public class Startup + { + public Startup(IWebHostEnvironment webHostEnvironment) + { + IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(webHostEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{webHostEnvironment.EnvironmentName}.json", optional: true).AddEnvironmentVariables(); + + Startup.Configuration = builder.Build(); + Startup.WebHostEnvironment = webHostEnvironment; + } + + public static IConfigurationRoot Configuration { get; set; } + + public static IWebHostEnvironment WebHostEnvironment { get; set; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + this.ConfigureMiddlewareServices(services); + + services.AddSingleton(); + } + + public void ConfigureContainer(ContainerBuilder builder) + { + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + } + + private void ConfigureMiddlewareServices(IServiceCollection services) + { + services.AddApiVersioning( + options => + { + // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions" + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = new HeaderApiVersionReader("api-version"); + }); + + services.AddVersionedApiExplorer( + options => + { + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; + + // note: this option is only necessary when versioning by url segment. the SubstitutionFormat + // can also be used to control the format of the API version in route templates + options.SubstituteApiVersionInUrl = true; + }); + + services.AddTransient, ConfigureSwaggerOptions>(); + + services.AddSwaggerGen(c => + { + // add a custom operation filter which sets default values + c.OperationFilter(); + c.ExampleFilters(); + }); + + services.AddSwaggerExamplesFromAssemblyOf(); + + services.AddControllers().AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + Assembly assembly = this.GetType().GetTypeInfo().Assembly; + services.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, + IApiVersionDescriptionProvider provider) + { + String nlogConfigFilename = "nlog.config"; + + if (env.IsDevelopment()) + { + nlogConfigFilename = $"nlog.{env.EnvironmentName}.config"; + app.UseDeveloperExceptionPage(); + } + + loggerFactory.ConfigureNLog(Path.Combine(env.ContentRootPath, nlogConfigFilename)); + loggerFactory.AddNLog(); + + ILogger logger = loggerFactory.CreateLogger("TransactionProcessor"); + + Logger.Initialise(logger); + + ConfigurationReader.Initialise(Startup.Configuration); + + app.AddRequestLogging(); + app.AddResponseLogging(); + app.AddExceptionHandler(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + app.UseSwagger(); + + app.UseSwaggerUI( + options => + { + // build a swagger endpoint for each discovered API version + foreach (ApiVersionDescription description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + + //if (String.Compare(ConfigurationReader.GetValue("EventStoreSettings", "START_PROJECTIONS"), + // Boolean.TrueString, + // StringComparison.InvariantCultureIgnoreCase) == 0) + //{ + // app.PreWarm(true).Wait(); + //} + //else + //{ + // app.PreWarm(); + //} + } + } +} diff --git a/TransactionProcessor/TransactionProcessor.csproj b/TransactionProcessor/TransactionProcessor.csproj new file mode 100644 index 00000000..b5216dd1 --- /dev/null +++ b/TransactionProcessor/TransactionProcessor.csproj @@ -0,0 +1,47 @@ + + + + netcoreapp3.0 + Linux + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/TransactionProcessor/appsettings.json b/TransactionProcessor/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/TransactionProcessor/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/TransactionProcessor/hosting.json b/TransactionProcessor/hosting.json new file mode 100644 index 00000000..31289e7e --- /dev/null +++ b/TransactionProcessor/hosting.json @@ -0,0 +1,3 @@ +{ + "urls": "http://*:5002" +} \ No newline at end of file diff --git a/TransactionProcessor/nlog.config b/TransactionProcessor/nlog.config new file mode 100644 index 00000000..805ddd3a --- /dev/null +++ b/TransactionProcessor/nlog.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TransactionProcessor/nlog.development.config b/TransactionProcessor/nlog.development.config new file mode 100644 index 00000000..7144c7b6 --- /dev/null +++ b/TransactionProcessor/nlog.development.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file