Adds identity server 4

This commit is contained in:
Ivan Paulovich 2020-07-28 13:27:25 +02:00
parent 6b58e209e3
commit 08094b34c1
857 changed files with 78068 additions and 35091 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
<ProjectGuid>a0517af3-3b35-443a-80dc-ff94f10cf056</ProjectGuid>
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/{Scheme}://{ServiceHost}:{ServicePort}</DockerServiceUrl>
<DockerServiceName>webapi</DockerServiceName>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
version: '3.4'
services:
accounts-api:
ports:
- "5005:5005"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:5005
- ASPNETCORE_HTTPS_PORT=5005
- ASPNETCORE_Kestrel__Certificates__Default__Password=MyCertificatePassword
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
volumes:
- ${USERPROFILE}\.aspnet\https:/https:ro
identity-server:
ports:
- "5000:5000"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:5000
- ASPNETCORE_HTTPS_PORT=5000
- ASPNETCORE_Kestrel__Certificates__Default__Password=MyCertificatePassword
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
volumes:
- ${USERPROFILE}\.aspnet\https:/https:ro
wallet-spa:
stdin_open: true # docker run -i
tty: true # docker run -t
ports:
- "5010:5010"
sql1:
environment:
SA_PASSWORD: "<YourStrong!Passw0rd>"
ACCEPT_EULA: "Y"
ports:
- "1433:1433"

View File

@ -0,0 +1,19 @@
version: '3.4'
services:
accounts-api:
image: ${DOCKER_REGISTRY-}accounts
build:
context: ../accounts-api/
dockerfile: src/WebApi/Dockerfile
identity-server:
image: ${DOCKER_REGISTRY-}identityserver
build:
context: ../
dockerfile: identity-server/Dockerfile
wallet-spa:
image: ${DOCKER_REGISTRY-}wallet
build:
context: ../wallet-spa/
sql1:
image: "mcr.microsoft.com/mssql/server:2019-latest"

View File

@ -12,8 +12,9 @@ jobs:
docker pull mcr.microsoft.com/mssql/server:2017-latest
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
sleep 10
docker exec -i sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourStrong!Passw0rd>' -Q 'ALTER LOGIN SA WITH PASSWORD="<YourNewStrong!Passw0rd>"'
dotnet tool install --global dotnet-ef
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
pushd accounts-api
dotnet tool update --global dotnet-ef --version 3.1.7
dotnet ef database update --project src/Infrastructure --startup-project src/WebApi
popd
dotnet build
dotnet test

View File

@ -1,2 +0,0 @@
*.md
Directory.Build.props

67
.vscode/launch.json vendored
View File

@ -1,67 +0,0 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": "Launch localhost",
"type": "chrome",
"request": "launch",
"url": "https://localhost:5001",
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
},
{
"name": "Attach to url with files served from ./src/WebApi/ClientApp",
"type": "chrome",
"request": "attach",
"port": 9222,
"url": "https://localhost:5001",
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
},
{
"name": "Development",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/src/WebApi",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "Production",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/src/WebApi",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Production"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
}
]
}

View File

@ -3,205 +3,124 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29519.87
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6EE5F03A-7E37-48DB-95BA-3C42942B69AF}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{23ED54A6-81AF-4160-97A6-FD3C25C33E30}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{98E6E94F-804D-4332-8324-4F4DF037DCD3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "src\WebApi\WebApi.csproj", "{EE3837B9-C6EB-4384-B9F9-C441232DBE15}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6505DBE7-AC3B-4575-BCDC-C34F09D9373B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "test\IntegrationTests\IntegrationTests.csproj", "{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{5185498B-241B-49DA-BDA7-F08A2ACA4886}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentTests", "test\ComponentTests\ComponentTests.csproj", "{95F75E96-7060-4612-A78F-C187D0B93331}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A5F185E-DE63-4F81-AF29-2B5F6AEC7885}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{F6826529-32A9-419B-B7E5-63BE9A0FDA93}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
.prettierignore = .prettierignore
CHANGELOG.md = CHANGELOG.md
Directory.Build.props = Directory.Build.props
docker-compose.yml = docker-compose.yml
.github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml
global.json = global.json
LICENSE = LICENSE
nuget.config = nuget.config
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Security", "src\Domain.Security\Domain.Security.csproj", "{670FB303-7E15-46BE-A863-731E2EDD3BBD}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "accounts-api", "accounts-api", "{51331007-CACA-4676-934B-217999A6B1E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Customers", "src\Domain.Customers\Domain.Customers.csproj", "{6523AA08-D2E2-4C20-AE0A-09407C6A1750}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "identity-server", "identity-server", "{5829A64A-52DE-4656-A6C0-C06B1A6195E7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Accounts", "src\Domain.Accounts\Domain.Accounts.csproj", "{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F4408BAF-2D26-4D97-808C-7A96C4A7F636}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "src\Common\Common.csproj", "{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEndTests", "test\EndToEndTests\EndToEndTests.csproj", "{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{B3ABDDAF-9164-4FC4-862D-07A2D9F62F5D}"
ProjectSection(SolutionItems) = preProject
accounts-api\scripts\build.sh = accounts-api\scripts\build.sh
accounts-api\scripts\download-tools.sh = accounts-api\scripts\download-tools.sh
accounts-api\scripts\format.sh = accounts-api\scripts\format.sh
accounts-api\scripts\sql-docker-up.sh = accounts-api\scripts\sql-docker-up.sh
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "accounts-api\src\Application\Application.csproj", "{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "accounts-api\src\WebApi\WebApi.csproj", "{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "accounts-api\src\Infrastructure\Infrastructure.csproj", "{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentTests", "accounts-api\test\ComponentTests\ComponentTests.csproj", "{5900EBD8-D50B-4F1E-B326-3C13298C7B73}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEndTests", "accounts-api\test\EndToEndTests\EndToEndTests.csproj", "{82A0002A-969A-450B-BD42-C3065BD69649}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "accounts-api\test\IntegrationTests\IntegrationTests.csproj", "{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "accounts-api\test\UnitTests\UnitTests.csproj", "{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{02D548DA-5DE0-486E-A5D5-9BEDFFC0CA2A}"
ProjectSection(SolutionItems) = preProject
accounts-api\.vscode\launch.json = accounts-api\.vscode\launch.json
accounts-api\.vscode\settings.json = accounts-api\.vscode\settings.json
accounts-api\.vscode\tasks.json = accounts-api\.vscode\tasks.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServer", "identity-server\IdentityServer.csproj", "{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docker", ".docker", "{FAA0BAC6-0AA8-4908-A287-D550E9F9CBA8}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", ".docker\docker-compose.dcproj", "{A0517AF3-3B35-443A-80DC-FF94F10CF056}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "accounts-api\src\Domain\Domain.csproj", "{0925FCA6-083A-4478-80F3-2391987AAF2C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x64.Build.0 = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Debug|x86.Build.0 = Debug|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|Any CPU.Build.0 = Release|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x64.ActiveCfg = Release|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x64.Build.0 = Release|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x86.ActiveCfg = Release|Any CPU
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2}.Release|x86.Build.0 = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x64.ActiveCfg = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x64.Build.0 = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x86.ActiveCfg = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Debug|x86.Build.0 = Debug|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|Any CPU.Build.0 = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x64.ActiveCfg = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x64.Build.0 = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x86.ActiveCfg = Release|Any CPU
{98E6E94F-804D-4332-8324-4F4DF037DCD3}.Release|x86.Build.0 = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x64.ActiveCfg = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x64.Build.0 = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x86.ActiveCfg = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Debug|x86.Build.0 = Debug|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|Any CPU.Build.0 = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x64.ActiveCfg = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x64.Build.0 = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x86.ActiveCfg = Release|Any CPU
{EE3837B9-C6EB-4384-B9F9-C441232DBE15}.Release|x86.Build.0 = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x64.ActiveCfg = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x64.Build.0 = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x86.ActiveCfg = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Debug|x86.Build.0 = Debug|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|Any CPU.Build.0 = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x64.ActiveCfg = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x64.Build.0 = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x86.ActiveCfg = Release|Any CPU
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6}.Release|x86.Build.0 = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x64.ActiveCfg = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x64.Build.0 = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x86.ActiveCfg = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Debug|x86.Build.0 = Debug|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|Any CPU.Build.0 = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x64.ActiveCfg = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x64.Build.0 = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x86.ActiveCfg = Release|Any CPU
{5185498B-241B-49DA-BDA7-F08A2ACA4886}.Release|x86.Build.0 = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x64.ActiveCfg = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x64.Build.0 = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x86.ActiveCfg = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Debug|x86.Build.0 = Debug|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|Any CPU.Build.0 = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x64.ActiveCfg = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x64.Build.0 = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x86.ActiveCfg = Release|Any CPU
{95F75E96-7060-4612-A78F-C187D0B93331}.Release|x86.Build.0 = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x64.ActiveCfg = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x64.Build.0 = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x86.ActiveCfg = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Debug|x86.Build.0 = Debug|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|Any CPU.Build.0 = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x64.ActiveCfg = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x64.Build.0 = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x86.ActiveCfg = Release|Any CPU
{670FB303-7E15-46BE-A863-731E2EDD3BBD}.Release|x86.Build.0 = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x64.ActiveCfg = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x64.Build.0 = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x86.ActiveCfg = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Debug|x86.Build.0 = Debug|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|Any CPU.Build.0 = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x64.ActiveCfg = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x64.Build.0 = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x86.ActiveCfg = Release|Any CPU
{6523AA08-D2E2-4C20-AE0A-09407C6A1750}.Release|x86.Build.0 = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x64.ActiveCfg = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x64.Build.0 = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x86.ActiveCfg = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Debug|x86.Build.0 = Debug|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|Any CPU.Build.0 = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x64.ActiveCfg = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x64.Build.0 = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x86.ActiveCfg = Release|Any CPU
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E}.Release|x86.Build.0 = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x64.ActiveCfg = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x64.Build.0 = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x86.ActiveCfg = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Debug|x86.Build.0 = Debug|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|Any CPU.Build.0 = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x64.ActiveCfg = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x64.Build.0 = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x86.ActiveCfg = Release|Any CPU
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11}.Release|x86.Build.0 = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x64.ActiveCfg = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x64.Build.0 = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x86.ActiveCfg = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Debug|x86.Build.0 = Debug|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|Any CPU.Build.0 = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x64.ActiveCfg = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x64.Build.0 = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x86.ActiveCfg = Release|Any CPU
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485}.Release|x86.Build.0 = Release|Any CPU
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B}.Release|Any CPU.Build.0 = Release|Any CPU
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082}.Release|Any CPU.Build.0 = Release|Any CPU
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB}.Release|Any CPU.Build.0 = Release|Any CPU
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5900EBD8-D50B-4F1E-B326-3C13298C7B73}.Release|Any CPU.Build.0 = Release|Any CPU
{82A0002A-969A-450B-BD42-C3065BD69649}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82A0002A-969A-450B-BD42-C3065BD69649}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82A0002A-969A-450B-BD42-C3065BD69649}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82A0002A-969A-450B-BD42-C3065BD69649}.Release|Any CPU.Build.0 = Release|Any CPU
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B}.Release|Any CPU.Build.0 = Release|Any CPU
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0}.Release|Any CPU.Build.0 = Release|Any CPU
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6}.Release|Any CPU.Build.0 = Release|Any CPU
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0517AF3-3B35-443A-80DC-FF94F10CF056}.Release|Any CPU.Build.0 = Release|Any CPU
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0925FCA6-083A-4478-80F3-2391987AAF2C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0F658AD1-3154-4381-A1E0-5FBA70C36FC2} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{98E6E94F-804D-4332-8324-4F4DF037DCD3} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{EE3837B9-C6EB-4384-B9F9-C441232DBE15} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{CE9661D0-71FB-418B-9479-CF5C0D43DBE6} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
{5185498B-241B-49DA-BDA7-F08A2ACA4886} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
{95F75E96-7060-4612-A78F-C187D0B93331} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
{670FB303-7E15-46BE-A863-731E2EDD3BBD} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{6523AA08-D2E2-4C20-AE0A-09407C6A1750} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{13F090D5-14D9-4B64-B6E2-7E759BA40B8E} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{1E59A6C0-B3B0-4C7B-BF11-82CE3E733E11} = {6EE5F03A-7E37-48DB-95BA-3C42942B69AF}
{7CDAB6A4-42E3-45A8-AB85-BD2EA978E485} = {6505DBE7-AC3B-4575-BCDC-C34F09D9373B}
{F6826529-32A9-419B-B7E5-63BE9A0FDA93} = {23ED54A6-81AF-4160-97A6-FD3C25C33E30}
{F4408BAF-2D26-4D97-808C-7A96C4A7F636} = {51331007-CACA-4676-934B-217999A6B1E2}
{54ECA5D5-FCB9-4427-98A1-6915E0C2C71B} = {51331007-CACA-4676-934B-217999A6B1E2}
{B3ABDDAF-9164-4FC4-862D-07A2D9F62F5D} = {51331007-CACA-4676-934B-217999A6B1E2}
{80E73E2B-3507-4160-BF51-5B1BD3F94E4B} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
{5286EDF8-AE3D-4E7B-89B3-3AA1CE86C082} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
{C38A01E0-F1C3-40C5-A8A8-5B3ECD5F59AB} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
{5900EBD8-D50B-4F1E-B326-3C13298C7B73} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
{82A0002A-969A-450B-BD42-C3065BD69649} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
{48BBEC7F-651B-4EF2-94E4-E8FFAC3D8E7B} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
{BF05183E-699A-43A8-A5F3-1DB71B0F38B0} = {54ECA5D5-FCB9-4427-98A1-6915E0C2C71B}
{02D548DA-5DE0-486E-A5D5-9BEDFFC0CA2A} = {51331007-CACA-4676-934B-217999A6B1E2}
{01537DBF-3C0F-4B83-A089-0D12E5CA06C6} = {5829A64A-52DE-4656-A6C0-C06B1A6195E7}
{A0517AF3-3B35-443A-80DC-FF94F10CF056} = {FAA0BAC6-0AA8-4908-A287-D550E9F9CBA8}
{0925FCA6-083A-4478-80F3-2391987AAF2C} = {F4408BAF-2D26-4D97-808C-7A96C4A7F636}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0941E413-A73D-485D-8EDE-03CE0C6967E1}

View File

@ -29,37 +29,80 @@ We also support the React client:
## Build & Run
Run the following commands:
Spin up SQL Server:
```sh
pushd src/WebApi/ClientApp
cd .docker
docker-compose build
docker-compose up -d sql1
dotnet tool update --global dotnet-ef --version 3.1.7
dotnet ef database update --project ../accounts-api/src/Infrastructure --startup-project ../accounts-api/src/WebApi
docker-compose up -d
```
Then the following containers should be runnning:
| Application | Port | Protocol |
|------------------ | ----- |--------- |
| Wallet SPA | 5010 | HTTPS |
| Accounts API | 5005 | HTTPS |
| Identity Server | 5000 | HTTPS |
| SQL Server | 1433 | TCP |
Browse to `https://localhost:5010` then click on Log In. Trust the [self-signed certificate](https://stackoverflow.com/questions/21397809/create-a-trusted-self-signed-ssl-cert-for-localhost-for-use-with-express-node).
If you are prefer dotnet commands then start each service individually:
<details>
<summary>Expand to get the dotnet run steps.</summary>
### Generate Self Signed Certificate
```sh
dotnet dev-certs https --clean
dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\aspnetapp.pfx -p MyCertificatePassword
```
### Spin up SQL Server in a Docker container
```sh
docker pull mcr.microsoft.com/mssql/server:2017-latest
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
```
### Create and Seed Accounts Database
```sh
dotnet tool update --global dotnet-ef --version 3.1.6
dotnet ef database update --project accounts-api/src/Infrastructure --startup-project accounts-api/src/WebApi
```
### Running Services
#### Identity Server
```sh
dotnet run --project identity-server/src/IdentityServer.csproj
```
#### Account API
```sh
dotnet run --project accounts-api/src/WebApi/WebApi.csproj
```
#### Wallett SPA
```sh
pushd wallet-spa/src/ClientApp
npm install
popd
dotnet run --project src/WebApi/WebApi.csproj --launch-profile Development
dotnet run --project wallet-spa/src/WalletSPA.csproj --launch-profile WalletSPA
```
Then authenticate into the API by browsing to `https://localhost:5001/api/v1/Login/Google?returnUrl=%2Fswagger%2Findex.html`.
- App: `http://localhost:5001`
- Swagger: `http://localhost:5001/swagger/index.html`
or try the Docker approach:
```sh
docker build -t my-app . -f src/WebApi/Dockerfile
docker run -p 6001:80 my-app
```
- App: `http://localhost:6001`
- Swagger: `http://localhost:6001/swagger/index.html`
## Production Environment Setup
```sh
dotnet ef migrations add "InitialCreate" -o "DataAccess/Migrations" --project src/Infrastructure --startup-project src/Infrastructure
```
```sh
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
```
</details>
## Motivation

74
accounts-api/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,74 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": "Launch IdentityServer",
"type": "chrome",
"request": "launch",
"url": "https://localhost:5000",
"webRoot": "${workspaceFolder}/src/IdentityServer"
},
{
"name": "Launch localhost",
"type": "chrome",
"request": "launch",
"url": "https://localhost:5005",
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
},
{
"name": "Attach to url with files served from ./src/WebApi/ClientApp",
"type": "chrome",
"request": "attach",
"port": 9222,
"url": "https://localhost:5005",
"webRoot": "${workspaceFolder}/src/WebApi/ClientApp"
},
{
"name": "Development",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/src/WebApi",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "Production",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/src/WebApi",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Production"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
}
]
}

0
scripts/build.sh → accounts-api/scripts/build.sh Executable file → Normal file
View File

View File

0
scripts/format.sh → accounts-api/scripts/format.sh Executable file → Normal file
View File

View File

@ -0,0 +1,20 @@
#!/bin/bash
docker stop sql1
docker rm sql1
docker pull mcr.microsoft.com/mssql/server:2017-latest
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
sleep 10
docker exec -it sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourStrong!Passw0rd>' -Q 'ALTER LOGIN SA WITH PASSWORD="<YourNewStrong!Passw0rd>"'
dotnet tool install --global dotnet-ef
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
# query
# sudo docker exec -it sql1 "bash"
# /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
# SELECT Name from sys.Databases
# GO
# USE MangaDB01
# GO
# SELECT * FROM Account
# GO

View File

@ -0,0 +1,17 @@
#!/bin/bash
sudo docker pull mcr.microsoft.com/mssql/server:2017-latest
sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest
sleep 10
dotnet tool install --global dotnet-ef
dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure
# query
# sudo docker exec -it sql1 "bash"
# /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
# SELECT Name from sys.Databases
# GO
# USE MangaDB01
# GO
# SELECT * FROM Account
# GO

View File

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
<LangVersion>8.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Messages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Messages.Designer.cs">
<DependentUpon>Messages.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
namespace Application.Services
{
using System.Threading.Tasks;
using Domain.ValueObjects;
/// <summary>
/// </summary>
public interface ICurrencyExchange
{
Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency);
}
}

View File

@ -0,0 +1,18 @@
// <copyright file="IUserService.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.Services
{
/// <summary>
/// User Service.
/// </summary>
public interface IUserService
{
/// <summary>
/// Gets the Current User Id.
/// </summary>
/// <returns>User.</returns>
string GetCurrentUserId();
}
}

View File

@ -0,0 +1,37 @@
// <copyright file="CloseAccountInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.CloseAccount
{
using System;
using Domain.ValueObjects;
using Services;
/// <summary>
/// Close Account Input Message.
/// </summary>
internal sealed class CloseAccountInput
{
/// <summary>
/// Initializes a new instance of the <see cref="CloseAccountInput" /> class.
/// </summary>
/// <param name="accountId">Account Id.</param>
internal CloseAccountInput(Guid accountId)
{
this.ModelState = new Notification();
if (accountId != Guid.Empty)
{
this.AccountId = new AccountId(accountId);
}
else
{
this.ModelState.Add(nameof(accountId), "AccountId is required.");
}
}
internal AccountId AccountId { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,94 @@
// <copyright file="CloseAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.CloseAccount
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.ValueObjects;
using Services;
/// <inheritdoc />
public sealed class CloseAccountUseCase : ICloseAccountUseCase
{
private readonly IAccountRepository _accountRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserService _userService;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="CloseAccountUseCase" /> class.
/// </summary>
/// <param name="accountRepository">Account Repository.</param>
/// <param name="userService">User Service.</param>
/// <param name="unitOfWork"></param>
public CloseAccountUseCase(
IAccountRepository accountRepository,
IUserService userService,
IUnitOfWork unitOfWork)
{
this._accountRepository = accountRepository;
this._userService = userService;
this._unitOfWork = unitOfWork;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(Guid accountId)
{
var input = new CloseAccountInput(accountId);
if (input.ModelState.IsValid)
{
string externalUserId = this._userService
.GetCurrentUserId();
return this.CloseAccountInternal(input.AccountId, externalUserId);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task CloseAccountInternal(AccountId accountId, string externalUserId)
{
IAccount account = await this._accountRepository
.Find(accountId, externalUserId)
.ConfigureAwait(false);
if (account is Account closingAccount)
{
if (!closingAccount.IsClosingAllowed())
{
this._outputPort?.HasFunds();
return;
}
await this.Close(closingAccount)
.ConfigureAwait(false);
this._outputPort?.Ok(closingAccount);
return;
}
this._outputPort?.NotFound();
}
private async Task Close(Account closeAccount)
{
await this._accountRepository
.Delete(closeAccount.AccountId)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,33 @@
// <copyright file="ICloseAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.CloseAccount
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Close Account
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface ICloseAccountUseCase
{
/// <summary>
/// Executes the use case.
/// </summary>
/// <param name="accountId">Account Id.</param>
/// <returns>Task.</returns>
Task Execute(Guid accountId);
/// <summary>
/// Sets the Output Port.
/// </summary>
/// <param name="outputPort">Output Port</param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,35 @@
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.CloseAccount
{
using Domain;
using Services;
/// <summary>
/// Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
/// <summary>
/// Account closed successfully.
/// </summary>
void Ok(Account account);
/// <summary>
/// Account not found.
/// </summary>
void NotFound();
/// <summary>
/// Account has funds.
/// </summary>
void HasFunds();
}
}

View File

@ -0,0 +1,61 @@
// <copyright file="DepositInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Deposit
{
using System;
using Domain.ValueObjects;
using Services;
/// <summary>
/// Deposit Input Message.
/// </summary>
internal sealed class DepositInput
{
/// <summary>
/// Initializes a new instance of the <see cref="DepositInput" /> class.
/// </summary>
/// <param name="accountId">AccountId.</param>
/// <param name="amount">Positive amount to deposit.</param>
/// <param name="currency">Currency from amount.</param>
internal DepositInput(Guid accountId, decimal amount, string currency)
{
this.ModelState = new Notification();
if (accountId != Guid.Empty)
{
this.AccountId = new AccountId(accountId);
}
else
{
this.ModelState.Add(nameof(accountId), "AccountId is required.");
}
if (currency == Currency.Dollar.Code ||
currency == Currency.Euro.Code ||
currency == Currency.BritishPound.Code ||
currency == Currency.Canadian.Code ||
currency == Currency.Real.Code ||
currency == Currency.Krona.Code)
{
if (amount > 0)
{
this.Amount = new PositiveMoney(amount, new Currency(currency));
}
else
{
this.ModelState.Add(nameof(amount), "Amount should be positive.");
}
}
else
{
this.ModelState.Add(nameof(currency), "Currency is required.");
}
}
internal AccountId AccountId { get; }
internal PositiveMoney Amount { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,98 @@
// <copyright file="DepositUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Deposit
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.Credits;
using Domain.ValueObjects;
using Services;
/// <inheritdoc />
public sealed class DepositUseCase : IDepositUseCase
{
private readonly IAccountFactory _accountFactory;
private readonly IAccountRepository _accountRepository;
private readonly ICurrencyExchange _currencyExchange;
private readonly IUnitOfWork _unitOfWork;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="DepositUseCase" /> class.
/// </summary>
/// <param name="accountRepository">Account Repository.</param>
/// <param name="unitOfWork">Unit Of Work.</param>
/// <param name="accountFactory"></param>
/// <param name="currencyExchange"></param>
public DepositUseCase(
IAccountRepository accountRepository,
IUnitOfWork unitOfWork,
IAccountFactory accountFactory,
ICurrencyExchange currencyExchange)
{
this._accountRepository = accountRepository;
this._unitOfWork = unitOfWork;
this._accountFactory = accountFactory;
this._currencyExchange = currencyExchange;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(Guid accountId, decimal amount, string currency)
{
var input = new DepositInput(accountId, amount, currency);
if (input.ModelState.IsValid)
{
return this.DepositInternal(input.AccountId, input.Amount);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task DepositInternal(AccountId accountId, PositiveMoney amount)
{
IAccount account = await this._accountRepository
.GetAccount(accountId)
.ConfigureAwait(false);
if (account is Account depositAccount)
{
PositiveMoney amountInAccountCurrency =
await this._currencyExchange
.Convert(amount, depositAccount.Currency)
.ConfigureAwait(false);
Credit credit = this._accountFactory
.NewCredit(depositAccount, amountInAccountCurrency, DateTime.Now);
await this.Deposit(depositAccount, credit)
.ConfigureAwait(false);
this._outputPort?.Ok(credit, depositAccount);
return;
}
this._outputPort?.NotFound();
}
private async Task Deposit(Account account, Credit credit)
{
account.Deposit(credit);
await this._accountRepository
.Update(account, credit)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,35 @@
// <copyright file="IDepositUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Deposit
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Deposit
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IDepositUseCase
{
/// <summary>
/// Executes the Use Case.
/// </summary>
/// <param name="accountId">AccountId.</param>
/// <param name="amount">Positive amount to deposit.</param>
/// <param name="currency">Currency from amount.</param>
/// <returns>Task.</returns>
Task Execute(Guid accountId, decimal amount, string currency);
/// <summary>
/// Sets the Output Port.
/// </summary>
/// <param name="outputPort">Output Port</param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,31 @@
// <copyright file="IDepositOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Deposit
{
using Domain;
using Domain.Credits;
using Services;
/// <summary>
/// Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
/// <summary>
/// Deposited.
/// </summary>
void Ok(Credit credit, Account account);
/// <summary>
/// Not found.
/// </summary>
void NotFound();
}
}

View File

@ -0,0 +1,37 @@
// <copyright file="GetAccountInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccount
{
using System;
using Domain.ValueObjects;
using Services;
/// <summary>
/// Get Account Details Input Message.
/// </summary>
internal sealed class GetAccountInput
{
/// <summary>
/// Initializes a new instance of the <see cref="GetAccountInput" /> class.
/// </summary>
/// <param name="accountId">Account Id.</param>
internal GetAccountInput(Guid accountId)
{
this.ModelState = new Notification();
if (accountId != Guid.Empty)
{
this.AccountId = new AccountId(accountId);
}
else
{
this.ModelState.Add(nameof(accountId), "AccountId is required.");
}
}
internal AccountId AccountId { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,56 @@
// <copyright file="GetAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccount
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.ValueObjects;
/// <inheritdoc />
public sealed class GetAccountUseCase : IGetAccountUseCase
{
private readonly IAccountRepository _accountRepository;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="GetAccountUseCase" /> class.
/// </summary>
/// <param name="accountRepository">Account Repository.</param>
public GetAccountUseCase(IAccountRepository accountRepository) => this._accountRepository = accountRepository;
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(Guid accountId)
{
var input = new GetAccountInput(accountId);
if (input.ModelState.IsValid)
{
return this.GetAccountInternal(input.AccountId);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task GetAccountInternal(AccountId accountId)
{
IAccount account = await this._accountRepository
.GetAccount(accountId)
.ConfigureAwait(false);
if (account is Account getAccount)
{
this._outputPort?.Ok(getAccount);
return;
}
this._outputPort?.NotFound();
}
}
}

View File

@ -0,0 +1,32 @@
// <copyright file="IGetAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccount
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Gets the Account
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IGetAccountUseCase
{
/// <summary>
/// Executes the Use Case
/// </summary>
/// <param name="accountId">Account Id.</param>
Task Execute(Guid accountId);
/// <summary>
/// Executes the Use Case.
/// </summary>
/// <param name="outputPort"></param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,30 @@
// <copyright file="IGetAccountOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccount
{
using Domain;
using Services;
/// <summary>
/// Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
/// <summary>
/// Account closed.
/// </summary>
void Ok(Account account);
/// <summary>
/// Account closed.
/// </summary>
void NotFound();
}
}

View File

@ -0,0 +1,54 @@
// <copyright file="GetAccountsUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccounts
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Domain;
using Services;
/// <inheritdoc />
public sealed class GetAccountsUseCase : IGetAccountsUseCase
{
private readonly IAccountRepository _accountRepository;
private readonly IUserService _userService;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="GetAccountsUseCase" /> class.
/// </summary>
/// <param name="userService">User Service.</param>
/// <param name="accountRepository">Customer Repository.</param>
public GetAccountsUseCase(
IUserService userService,
IAccountRepository accountRepository)
{
this._userService = userService;
this._accountRepository = accountRepository;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public async Task Execute()
{
string externalUserId = this._userService
.GetCurrentUserId();
await this.GetAccountsInternal(externalUserId)
.ConfigureAwait(false);
}
private async Task GetAccountsInternal(string externalUserId)
{
IList<Account>? accounts = await this._accountRepository
.GetAccounts(externalUserId)
.ConfigureAwait(false);
this._outputPort?.Ok(accounts);
}
}
}

View File

@ -0,0 +1,20 @@
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.GetAccounts
{
using System.Collections.Generic;
using Domain;
/// <summary>
/// Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Listed accounts.
/// </summary>
void Ok(IList<Account> accounts);
}
}

View File

@ -0,0 +1,30 @@
// <copyright file="IOpenAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.OpenAccount
{
using System.Threading.Tasks;
/// <summary>
/// Open Account
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IOpenAccountUseCase
{
/// <summary>
/// Executes the Use Case
/// </summary>
Task Execute(decimal amount, string currency);
/// <summary>
/// Sets the Output Port.
/// </summary>
/// <param name="outputPort">Output Port</param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,30 @@
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.OpenAccount
{
using Domain;
using Services;
/// <summary>
/// Open Account Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Account open.
/// </summary>
void Ok(Account account);
/// <summary>
/// Resource not found.
/// </summary>
void NotFound();
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
}
}

View File

@ -0,0 +1,47 @@
// <copyright file="OpenAccountInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.OpenAccount
{
using Domain.ValueObjects;
using Services;
/// <summary>
/// Open Account Input Message.
/// </summary>
internal sealed class OpenAccountInput
{
/// <summary>
/// Initializes a new instance of the <see cref="OpenAccountInput" /> class.
/// </summary>
public OpenAccountInput(decimal amount, string currency)
{
this.ModelState = new Notification();
if (currency == Currency.Dollar.Code ||
currency == Currency.Euro.Code ||
currency == Currency.BritishPound.Code ||
currency == Currency.Canadian.Code ||
currency == Currency.Real.Code ||
currency == Currency.Krona.Code)
{
if (amount > 0)
{
this.Amount = new PositiveMoney(amount, new Currency(currency));
}
else
{
this.ModelState.Add(nameof(amount), "Amount should be positive.");
}
}
else
{
this.ModelState.Add(nameof(currency), "Currency is required.");
}
}
internal PositiveMoney Amount { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,82 @@
// <copyright file="OpenAccountUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.OpenAccount
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.Credits;
using Domain.ValueObjects;
using Services;
/// <inheritdoc />
public sealed class OpenAccountUseCase : IOpenAccountUseCase
{
private readonly IAccountFactory _accountFactory;
private readonly IAccountRepository _accountRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserService _userService;
private IOutputPort? _outputPort;
public OpenAccountUseCase(
IAccountRepository accountRepository,
IUnitOfWork unitOfWork,
IUserService userService,
IAccountFactory accountFactory)
{
this._accountRepository = accountRepository;
this._unitOfWork = unitOfWork;
this._userService = userService;
this._accountFactory = accountFactory;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(decimal amount, string currency)
{
var input = new OpenAccountInput(amount, currency);
if (input.ModelState.IsValid)
{
return this.OpenAccountInternal(input.Amount);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task OpenAccountInternal(PositiveMoney amountToDeposit)
{
string externalUserId = this._userService
.GetCurrentUserId();
Account account = this._accountFactory
.NewAccount(externalUserId, amountToDeposit.Currency);
Credit credit = this._accountFactory
.NewCredit(account, amountToDeposit, DateTime.Now);
await this.Deposit(account, credit)
.ConfigureAwait(false);
this._outputPort?.Ok(account);
}
private async Task Deposit(Account account, Credit credit)
{
account.Deposit(credit);
await this._accountRepository
.Add(account, credit)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,39 @@
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Transfer
{
using Domain;
using Domain.Credits;
using Domain.Debits;
using Services;
/// <summary>
/// Transfer Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
/// <summary>
/// Resource not found.
/// </summary>
void NotFound();
/// <summary>
/// </summary>
/// <param name="originAccount"></param>
/// <param name="debit"></param>
/// <param name="destinationAccount"></param>
/// <param name="credit"></param>
void Ok(Account originAccount, Debit debit, Account destinationAccount, Credit credit);
/// <summary>
/// </summary>
void OutOfFunds();
}
}

View File

@ -0,0 +1,31 @@
// <copyright file="ITransferUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Transfer
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Transfer
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface ITransferUseCase
{
/// <summary>
/// Executes the use case.
/// </summary>
Task Execute(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency);
/// <summary>
/// Sets the Output Port.
/// </summary>
/// <param name="outputPort">Output Port</param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,68 @@
// <copyright file="TransferInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Transfer
{
using System;
using Domain.ValueObjects;
using Services;
/// <summary>
/// Transfer Input Message.
/// </summary>
internal sealed class TransferInput
{
/// <summary>
/// Initializes a new instance of the <see cref="TransferInput" /> class.
/// </summary>
internal TransferInput(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency)
{
this.ModelState = new Notification();
if (originAccountId != Guid.Empty)
{
this.OriginAccountId = new AccountId(originAccountId);
}
else
{
this.ModelState.Add(nameof(originAccountId), "Origin AccountId is required.");
}
if (destinationAccountId != Guid.Empty)
{
this.DestinationAccountId = new AccountId(destinationAccountId);
}
else
{
this.ModelState.Add(nameof(destinationAccountId), "Destination AccountId is required.");
}
if (currency == Currency.Dollar.Code ||
currency == Currency.Euro.Code ||
currency == Currency.BritishPound.Code ||
currency == Currency.Canadian.Code ||
currency == Currency.Real.Code ||
currency == Currency.Krona.Code)
{
if (amount > 0)
{
this.TransferAmount = new PositiveMoney(amount, new Currency(currency));
}
else
{
this.ModelState.Add(nameof(amount), "Amount should be positive.");
}
}
else
{
this.ModelState.Add(nameof(currency), "Currency is required.");
}
}
internal AccountId OriginAccountId { get; }
internal AccountId DestinationAccountId { get; }
internal PositiveMoney TransferAmount { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,138 @@
// <copyright file="TransferUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Transfer
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
using Services;
/// <inheritdoc />
public sealed class TransferUseCase : ITransferUseCase
{
private readonly IAccountFactory _accountFactory;
private readonly IAccountRepository _accountRepository;
private readonly ICurrencyExchange _currencyExchange;
private readonly IUnitOfWork _unitOfWork;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="TransferUseCase" /> class.
/// </summary>
/// <param name="accountRepository">Account Repository.</param>
/// <param name="unitOfWork">Unit Of Work.</param>
/// <param name="accountFactory"></param>
/// <param name="currencyExchange"></param>
public TransferUseCase(
IAccountRepository accountRepository,
IUnitOfWork unitOfWork,
IAccountFactory accountFactory,
ICurrencyExchange currencyExchange)
{
this._accountRepository = accountRepository;
this._unitOfWork = unitOfWork;
this._accountFactory = accountFactory;
this._currencyExchange = currencyExchange;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(Guid originAccountId, Guid destinationAccountId, decimal amount, string currency)
{
var input = new TransferInput(
originAccountId,
destinationAccountId,
amount,
currency);
if (input.ModelState.IsValid)
{
return this.TransferInternal(input.OriginAccountId, input.DestinationAccountId, input.TransferAmount);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task TransferInternal(AccountId originAccountId, AccountId destinationAccountId,
PositiveMoney transferAmount)
{
IAccount originAccount = await this._accountRepository
.GetAccount(originAccountId)
.ConfigureAwait(false);
IAccount destinationAccount = await this._accountRepository
.GetAccount(destinationAccountId)
.ConfigureAwait(false);
if (originAccount is Account withdrawAccount && destinationAccount is Account depositAccount)
{
PositiveMoney localCurrencyAmount =
await this._currencyExchange
.Convert(transferAmount, withdrawAccount.Currency)
.ConfigureAwait(false);
Debit debit = this._accountFactory
.NewDebit(withdrawAccount, localCurrencyAmount, DateTime.Now);
if (withdrawAccount.GetCurrentBalance().Amount - debit.Amount.Amount < 0)
{
this._outputPort?.OutOfFunds();
return;
}
await this.Withdraw(withdrawAccount, debit)
.ConfigureAwait(false);
PositiveMoney destinationCurrencyAmount =
await this._currencyExchange
.Convert(transferAmount, depositAccount.Currency)
.ConfigureAwait(false);
Credit credit = this._accountFactory
.NewCredit(depositAccount, destinationCurrencyAmount, DateTime.Now);
await this.Deposit(depositAccount, credit)
.ConfigureAwait(false);
this._outputPort?.Ok(withdrawAccount, debit, depositAccount, credit);
return;
}
this._outputPort?.NotFound();
}
private async Task Deposit(Account account, Credit credit)
{
account.Deposit(credit);
await this._accountRepository
.Update(account, credit)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
private async Task Withdraw(Account account, Debit debit)
{
account.Withdraw(debit);
await this._accountRepository
.Update(account, debit)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,37 @@
// <copyright file="IOutputPort.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Withdraw
{
using Domain;
using Domain.Debits;
using Services;
/// <summary>
/// Output Port.
/// </summary>
public interface IOutputPort
{
/// <summary>
/// Informs it is out of balance.
/// </summary>
void OutOfFunds();
/// <summary>
/// Invalid input.
/// </summary>
void Invalid(Notification notification);
/// <summary>
/// Resource not closed.
/// </summary>
void NotFound();
/// <summary>
/// </summary>
/// <param name="debit"></param>
/// <param name="account"></param>
void Ok(Debit debit, Account account);
}
}

View File

@ -0,0 +1,34 @@
// <copyright file="IWithdrawUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Withdraw
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Withdraw
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#use-case">
/// Use
/// Case Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IWithdrawUseCase
{
/// <summary>
/// Executes the use case.
/// </summary>
/// <param name="accountId">AccountId.</param>
/// <param name="amount">Positive amount to withdraw.</param>
/// <param name="currency">Currency from amount.</param>
Task Execute(Guid accountId, decimal amount, string currency);
/// <summary>
/// Sets the Output Port.
/// </summary>
/// <param name="outputPort">Output Port</param>
void SetOutputPort(IOutputPort outputPort);
}
}

View File

@ -0,0 +1,58 @@
// <copyright file="WithdrawInput.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Withdraw
{
using System;
using Domain.ValueObjects;
using Services;
/// <summary>
/// Withdraw Input Message.
/// </summary>
internal sealed class WithdrawInput
{
/// <summary>
/// Initializes a new instance of the <see cref="WithdrawInput" /> class.
/// </summary>
internal WithdrawInput(Guid accountId, decimal amount, string currency)
{
this.ModelState = new Notification();
if (accountId != Guid.Empty)
{
this.AccountId = new AccountId(accountId);
}
else
{
this.ModelState.Add(nameof(accountId), "AccountId is required.");
}
if (currency == Currency.Dollar.Code ||
currency == Currency.Euro.Code ||
currency == Currency.BritishPound.Code ||
currency == Currency.Canadian.Code ||
currency == Currency.Real.Code ||
currency == Currency.Krona.Code)
{
if (amount > 0)
{
this.Amount = new PositiveMoney(amount, new Currency(currency));
}
else
{
this.ModelState.Add(nameof(amount), "Amount should be positive.");
}
}
else
{
this.ModelState.Add(nameof(currency), "Currency is required.");
}
}
internal AccountId AccountId { get; }
internal PositiveMoney Amount { get; }
internal Notification ModelState { get; }
}
}

View File

@ -0,0 +1,111 @@
// <copyright file="WithdrawUseCase.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Application.UseCases.Withdraw
{
using System;
using System.Threading.Tasks;
using Domain;
using Domain.Debits;
using Domain.ValueObjects;
using Services;
/// <inheritdoc />
public sealed class WithdrawUseCase : IWithdrawUseCase
{
private readonly IAccountFactory _accountFactory;
private readonly IAccountRepository _accountRepository;
private readonly ICurrencyExchange _currencyExchange;
private readonly IUnitOfWork _unitOfWork;
private readonly IUserService _userService;
private IOutputPort? _outputPort;
/// <summary>
/// Initializes a new instance of the <see cref="WithdrawUseCase" /> class.
/// </summary>
/// <param name="accountRepository">Account Repository.</param>
/// <param name="unitOfWork">Unit Of Work.</param>
/// <param name="accountFactory"></param>
/// <param name="userService"></param>
/// <param name="currencyExchange"></param>
public WithdrawUseCase(
IAccountRepository accountRepository,
IUnitOfWork unitOfWork,
IAccountFactory accountFactory,
IUserService userService,
ICurrencyExchange currencyExchange)
{
this._accountRepository = accountRepository;
this._unitOfWork = unitOfWork;
this._accountFactory = accountFactory;
this._userService = userService;
this._currencyExchange = currencyExchange;
}
/// <inheritdoc />
public void SetOutputPort(IOutputPort outputPort) => this._outputPort = outputPort;
/// <inheritdoc />
public Task Execute(Guid accountId, decimal amount, string currency)
{
var input = new WithdrawInput(accountId, amount, currency);
if (input.ModelState.IsValid)
{
return this.WithdrawInternal(input.AccountId, input.Amount);
}
this._outputPort?.Invalid(input.ModelState);
return Task.CompletedTask;
}
private async Task WithdrawInternal(AccountId accountId, PositiveMoney withdrawAmount)
{
string externalUserId = this._userService
.GetCurrentUserId();
IAccount account = await this._accountRepository
.Find(accountId, externalUserId)
.ConfigureAwait(false);
if (account is Account withdrawAccount)
{
PositiveMoney localCurrencyAmount =
await this._currencyExchange
.Convert(withdrawAmount, withdrawAccount.Currency)
.ConfigureAwait(false);
Debit debit = this._accountFactory
.NewDebit(withdrawAccount, localCurrencyAmount, DateTime.Now);
if (withdrawAccount.GetCurrentBalance().Amount - debit.Amount.Amount < 0)
{
this._outputPort?.OutOfFunds();
return;
}
await this.Withdraw(withdrawAccount, debit)
.ConfigureAwait(false);
this._outputPort?.Ok(debit, withdrawAccount);
return;
}
this._outputPort?.NotFound();
}
private async Task Withdraw(Account account, Debit debit)
{
account.Withdraw(debit);
await this._accountRepository
.Update(account, debit)
.ConfigureAwait(false);
await this._unitOfWork
.Save()
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,69 @@
// <copyright file="Account.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain
{
using Credits;
using Debits;
using ValueObjects;
/// <inheritdoc />
public class Account : IAccount
{
public Account(AccountId accountId, string externalUserId, Currency currency)
{
this.AccountId = accountId;
this.Currency = currency;
this.ExternalUserId = externalUserId;
}
/// <summary>
/// Gets the ExternalUserId.
/// </summary>
public string ExternalUserId { get; }
/// <summary>
/// Gets the Credits List.
/// </summary>
public CreditsCollection CreditsCollection { get; } = new CreditsCollection();
/// <summary>
/// Gets the Debits List.
/// </summary>
public DebitsCollection DebitsCollection { get; } = new DebitsCollection();
/// <summary>
/// Gets the Currency.
/// </summary>
public Currency Currency { get; }
/// <inheritdoc />
public AccountId AccountId { get; }
/// <inheritdoc />
public void Deposit(Credit credit) => this.CreditsCollection.Add(credit);
/// <inheritdoc />
public void Withdraw(Debit debit) => this.DebitsCollection.Add(debit);
/// <inheritdoc />
public bool IsClosingAllowed() => this.GetCurrentBalance()
.IsZero();
/// <inheritdoc />
public Money GetCurrentBalance()
{
PositiveMoney totalCredits = this.CreditsCollection
.GetTotal();
PositiveMoney totalDebits = this.DebitsCollection
.GetTotal();
Money totalAmount = totalCredits
.Subtract(totalDebits);
return totalAmount;
}
}
}

View File

@ -0,0 +1,38 @@
// <copyright file="AccountNull.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain
{
using System;
using Credits;
using Debits;
using ValueObjects;
/// <inheritdoc />
public sealed class AccountNull : IAccount
{
public static AccountNull Instance { get; } = new AccountNull();
/// <inheritdoc />
public AccountId AccountId => new AccountId(Guid.Empty);
/// <inheritdoc />
public void Deposit(Credit credit)
{
// Null Pattern
}
/// <inheritdoc />
public void Withdraw(Debit debit)
{
// Null Pattern
}
/// <inheritdoc />
public bool IsClosingAllowed() => false;
/// <inheritdoc />
public Money GetCurrentBalance() => new Money(0, new Currency());
}
}

View File

@ -0,0 +1,66 @@
// <copyright file="Credit.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Credits
{
using System;
using ValueObjects;
/// <summary>
/// Credit
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public class Credit : ICredit
{
public Credit(CreditId creditId, AccountId accountId, DateTime transactionDate, decimal value, string currency)
{
this.CreditId = creditId;
this.AccountId = accountId;
this.TransactionDate = transactionDate;
this.Amount = new PositiveMoney(value, new Currency(currency));
}
/// <summary>
/// Gets Description.
/// </summary>
public static string Description => "Credit";
/// <summary>
/// Gets or sets Transaction Date.
/// </summary>
public DateTime TransactionDate { get; }
/// <summary>
/// Gets or sets AccountId.
/// </summary>
public AccountId AccountId { get; }
public Account? Account { get; set; }
public decimal Value => this.Amount.Amount;
public string Currency => this.Amount.Currency.Code;
/// <summary>
/// Gets or sets Id.
/// </summary>
public CreditId CreditId { get; }
/// <summary>
/// Gets or sets Amount.
/// </summary>
public PositiveMoney Amount { get; }
/// <summary>
/// Calculate the sum of positive amounts.
/// </summary>
/// <param name="amount">Positive amount.</param>
/// <returns>The positive sum.</returns>
public Money Sum(Money amount) => this.Amount.Add(amount);
}
}

View File

@ -0,0 +1,24 @@
// <copyright file="CreditNull.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Credits
{
using System;
using ValueObjects;
/// <summary>
/// Credit
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public sealed class CreditNull : ICredit
{
public static CreditNull Instance { get; } = new CreditNull();
public CreditId CreditId { get; } = new CreditId(Guid.Empty);
public PositiveMoney Amount { get; } = new PositiveMoney(0, new Currency(string.Empty));
}
}

View File

@ -0,0 +1,38 @@
// <copyright file="CreditsCollection.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Credits
{
using System.Collections.Generic;
using System.Linq;
using ValueObjects;
/// <summary>
/// Credits
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Design-Patterns#first-class-collections">
/// First-Class
/// Collection Design Pattern
/// </see>
/// .
/// </summary>
public sealed class CreditsCollection : List<Credit>
{
/// <summary>
/// Gets Total amount.
/// </summary>
/// <returns>Positive amount.</returns>
public PositiveMoney GetTotal()
{
if (this.Count == 0)
{
return new PositiveMoney(0, new Currency(string.Empty));
}
PositiveMoney total = new PositiveMoney(0, this.First().Amount.Currency);
return this.Aggregate(total, (current, credit) =>
new PositiveMoney(current.Amount + credit.Amount.Amount, current.Currency));
}
}
}

View File

@ -0,0 +1,21 @@
// <copyright file="ICredit.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Credits
{
using ValueObjects;
/// <summary>
/// Credit Entity Interface.
/// </summary>
public interface ICredit
{
CreditId CreditId { get; }
/// <summary>
/// Gets the Amount.
/// </summary>
PositiveMoney Amount { get; }
}
}

View File

@ -0,0 +1,66 @@
// <copyright file="Debit.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Debits
{
using System;
using ValueObjects;
/// <summary>
/// Debit
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public class Debit : IDebit
{
public Debit(DebitId DebitId, AccountId accountId, DateTime transactionDate, decimal value, string currency)
{
this.DebitId = DebitId;
this.AccountId = accountId;
this.TransactionDate = transactionDate;
this.Amount = new PositiveMoney(value, new Currency(currency));
}
/// <summary>
/// Gets Description.
/// </summary>
public static string Description => "Debit";
/// <summary>
/// Gets or sets Transaction Date.
/// </summary>
public DateTime TransactionDate { get; }
/// <summary>
/// Gets the AccountId.
/// </summary>
public AccountId AccountId { get; }
public Account? Account { get; set; }
public decimal Value => this.Amount.Amount;
public string Currency => this.Amount.Currency.Code;
/// <summary>
/// Gets or sets Id.
/// </summary>
public DebitId DebitId { get; }
/// <summary>
/// Gets or sets Amount.
/// </summary>
public PositiveMoney Amount { get; }
/// <summary>
/// Calculate the sum of positive amounts.
/// </summary>
/// <param name="amount">Positive amount.</param>
/// <returns>The positive sum.</returns>
public Money Sum(Money amount) => this.Amount.Add(amount);
}
}

View File

@ -0,0 +1,24 @@
// <copyright file="DebitNull.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Debits
{
using System;
using ValueObjects;
/// <summary>
/// Debit
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public sealed class DebitNull : IDebit
{
public static DebitNull Instance { get; } = new DebitNull();
public DebitId DebitId { get; } = new DebitId(Guid.Empty);
public PositiveMoney Amount { get; } = new PositiveMoney(0, new Currency(string.Empty));
}
}

View File

@ -0,0 +1,38 @@
// <copyright file="DebitsCollection.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Debits
{
using System.Collections.Generic;
using System.Linq;
using ValueObjects;
/// <summary>
/// Debits
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Design-Patterns#first-class-collections">
/// First-Class
/// Collection Design Pattern
/// </see>
/// .
/// </summary>
public sealed class DebitsCollection : List<Debit>
{
/// <summary>
/// Gets Total amount.
/// </summary>
/// <returns>Total.</returns>
public PositiveMoney GetTotal()
{
if (this.Count == 0)
{
return new PositiveMoney(0, new Currency(string.Empty));
}
PositiveMoney total = new PositiveMoney(0, this.First().Amount.Currency);
return this.Aggregate(total, (current, credit) =>
new PositiveMoney(current.Amount + credit.Amount.Amount, current.Currency));
}
}
}

View File

@ -0,0 +1,21 @@
// <copyright file="IDebit.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.Debits
{
using ValueObjects;
/// <summary>
/// Debit.
/// </summary>
public interface IDebit
{
DebitId DebitId { get; }
/// <summary>
/// Gets the Amount.
/// </summary>
PositiveMoney Amount { get; }
}
}

View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
<LangVersion>8.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Messages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Messages.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Messages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,51 @@
// <copyright file="IAccount.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain
{
using Credits;
using Debits;
using ValueObjects;
/// <summary>
/// Account
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#aggregate-root">
/// Aggregate
/// Root Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IAccount
{
/// <summary>
/// Gets Id.
/// </summary>
AccountId AccountId { get; }
/// <summary>
/// Deposits into account.
/// </summary>
/// <returns>Credit created.</returns>
void Deposit(Credit credit);
/// <summary>
/// Withdraws from account.
/// </summary>
/// <returns>Debit created.</returns>
void Withdraw(Debit debit);
/// <summary>
/// Check if closing account is allowed.
/// </summary>
/// <returns>True if is allowed.</returns>
bool IsClosingAllowed();
/// <summary>
/// Gets the current balance considering credits and debits totals.
/// </summary>
/// <returns>The current balance.</returns>
Money GetCurrentBalance();
}
}

View File

@ -0,0 +1,49 @@
// <copyright file="IAccountFactory.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain
{
using System;
using Credits;
using Debits;
using ValueObjects;
/// <summary>
/// Account
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity-factory">
/// Entity
/// Factory Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IAccountFactory
{
/// <summary>
/// Creates a new Account.
/// </summary>
/// <param name="externalUserId">ExternalUserId.</param>
/// <param name="currency">Currency</param>
/// <returns>New Account instance.</returns>
Account NewAccount(string externalUserId, Currency currency);
/// <summary>
/// Creates a new Credit.
/// </summary>
/// <param name="account">Account object.</param>
/// <param name="amountToDeposit">Amount to Deposit.</param>
/// <param name="transactionDate">Transaction date.</param>
/// <returns>New Credit instance.</returns>
Credit NewCredit(Account account, PositiveMoney amountToDeposit, DateTime transactionDate);
/// <summary>
/// Creates a new Debit.
/// </summary>
/// <param name="account">Account object.</param>
/// <param name="amountToWithdraw">Amount to Withdraw.</param>
/// <param name="transactionDate">Transaction date.</param>
/// <returns>New Debit instance.</returns>
Debit NewDebit(Account account, PositiveMoney amountToWithdraw, DateTime transactionDate);
}
}

View File

@ -0,0 +1,74 @@
// <copyright file="IAccountRepository.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Credits;
using Debits;
using ValueObjects;
/// <summary>
/// Account
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#repository">
/// Repository
/// Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public interface IAccountRepository
{
/// <summary>
/// </summary>
/// <param name="accountId"></param>
/// <returns></returns>
Task<IAccount> GetAccount(AccountId accountId);
/// <summary>
/// </summary>
/// <param name="externalUserId"></param>
/// <returns></returns>
Task<IList<Account>> GetAccounts(string externalUserId);
/// <summary>
/// Adds an Account.
/// </summary>
/// <param name="account">Account object.</param>
/// <param name="credit">Credit object.</param>
/// <returns>Task.</returns>
Task Add(Account account, Credit credit);
/// <summary>
/// Updates an Account.
/// </summary>
/// <param name="account">Account object.</param>
/// <param name="credit">Credit object.</param>
/// <returns>Task.</returns>
Task Update(Account account, Credit credit);
/// <summary>
/// Updates the Account.
/// </summary>
/// <param name="account">Account object.</param>
/// <param name="debit">Debit object.</param>
/// <returns>Task.</returns>
Task Update(Account account, Debit debit);
/// <summary>
/// Deletes the Account.
/// </summary>
/// <param name="accountId">Account Id.</param>
/// <returns>Task.</returns>
Task Delete(AccountId accountId);
/// <summary>
/// Finds an Account.
/// </summary>
/// <param name="accountId">Account Id.</param>
/// <param name="externalUserId">External User Id.</param>
/// <returns></returns>
Task<IAccount> Find(AccountId accountId, string externalUserId);
}
}

View File

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Domain {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Messages {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Messages() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Domain.Messages", typeof(Messages).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Account has not enough funds..
/// </summary>
internal static string AccountHasNotEnoughFunds {
get {
return ResourceManager.GetString("AccountHasNotEnoughFunds", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,38 @@
// <copyright file="AccountId.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// AccountId
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct AccountId : IEquatable<AccountId>
{
public Guid Id { get; }
public AccountId(Guid id) =>
this.Id = id;
public override bool Equals(object? obj) =>
obj is AccountId o && this.Equals(o);
public bool Equals(AccountId other) => this.Id == other.Id;
public override int GetHashCode() =>
HashCode.Combine(this.Id);
public static bool operator ==(AccountId left, AccountId right) => left.Equals(right);
public static bool operator !=(AccountId left, AccountId right) => !(left == right);
public override string ToString() => this.Id.ToString();
}
}

View File

@ -0,0 +1,39 @@
// <copyright file="CreditId.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// CreditId
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
/// Value
/// Object Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct CreditId : IEquatable<CreditId>
{
public Guid Id { get; }
public CreditId(Guid id) =>
this.Id = id;
public override bool Equals(object? obj) =>
obj is CreditId o && this.Equals(o);
public bool Equals(CreditId other) => this.Id == other.Id;
public override int GetHashCode() =>
HashCode.Combine(this.Id);
public static bool operator ==(CreditId left, CreditId right) => left.Equals(right);
public static bool operator !=(CreditId left, CreditId right) => !(left == right);
public override string ToString() => this.Id.ToString();
}
}

View File

@ -0,0 +1,71 @@
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// Currency
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
/// Value Object
/// Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct Currency : IEquatable<Currency>
{
public string Code { get; }
public Currency(string code) =>
this.Code = code;
public override bool Equals(object? obj) =>
obj is Currency o && this.Equals(o);
public bool Equals(Currency other) => this.Code == other.Code;
public override int GetHashCode() =>
HashCode.Combine(this.Code);
public static bool operator ==(Currency left, Currency right) => left.Equals(right);
public static bool operator !=(Currency left, Currency right) => !(left == right);
/// <summary>
/// Dollar.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency Dollar = new Currency("USD");
/// <summary>
/// Euro.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency Euro = new Currency("EUR");
/// <summary>
/// British Pound.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency BritishPound = new Currency("GBP");
/// <summary>
/// Canadian Dollar.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency Canadian = new Currency("CAD");
/// <summary>
/// Brazilian Real.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency Real = new Currency("BRL");
/// <summary>
/// Swedish Krona.
/// </summary>
/// <returns>Currency.</returns>
public static readonly Currency Krona = new Currency("SEK");
public override string ToString() => this.Code;
}
}

View File

@ -0,0 +1,39 @@
// <copyright file="DebitId.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// Debit
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
/// Value
/// Object Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct DebitId : IEquatable<DebitId>
{
public Guid Id { get; }
public DebitId(Guid id) =>
this.Id = id;
public override bool Equals(object? obj) =>
obj is DebitId o && this.Equals(o);
public bool Equals(DebitId other) => this.Id == other.Id;
public override int GetHashCode() =>
HashCode.Combine(this.Id);
public static bool operator ==(DebitId left, DebitId right) => left.Equals(right);
public static bool operator !=(DebitId left, DebitId right) => !(left == right);
public override string ToString() => this.Id.ToString();
}
}

View File

@ -0,0 +1,43 @@
// <copyright file="Money.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// Money
/// <see href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity">
/// Entity
/// Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct Money : IEquatable<Money>
{
public decimal Amount { get; }
public Currency Currency { get; }
public Money(decimal amount, Currency currency) =>
(this.Amount, this.Currency) = (amount, currency);
public override bool Equals(object? obj) =>
obj is Money o && this.Equals(o);
public bool Equals(Money other) =>
this.Amount == other.Amount &&
this.Currency == other.Currency;
public override int GetHashCode() =>
HashCode.Combine(this.Amount, this.Currency);
public static bool operator ==(Money left, Money right) => left.Equals(right);
public static bool operator !=(Money left, Money right) => !(left == right);
public bool IsZero() => this.Amount == 0;
public override string ToString() => string.Format($"{this.Amount} {this.Currency}");
}
}

View File

@ -0,0 +1,47 @@
// <copyright file="PositiveMoney.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Domain.ValueObjects
{
using System;
/// <summary>
/// PositiveMoney
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#value-object">
/// Value Object
/// Design Pattern
/// </see>
/// .
/// </summary>
public readonly struct PositiveMoney : IEquatable<PositiveMoney>
{
public decimal Amount { get; }
public Currency Currency { get; }
public PositiveMoney(decimal amount, Currency currency) =>
(this.Amount, this.Currency) = (amount, currency);
public override bool Equals(object? obj) =>
obj is PositiveMoney o && this.Equals(o);
public bool Equals(PositiveMoney other) =>
this.Amount == other.Amount &&
this.Currency == other.Currency;
public override int GetHashCode() =>
HashCode.Combine(this.Amount, this.Currency);
public static bool operator ==(PositiveMoney left, PositiveMoney right) => left.Equals(right);
public static bool operator !=(PositiveMoney left, PositiveMoney right) => !(left == right);
public Money Subtract(PositiveMoney totalDebits) =>
new Money(Math.Round(this.Amount - totalDebits.Amount, 2), this.Currency);
public Money Add(Money amount) => new Money(Math.Round(this.Amount + amount.Amount, 2), this.Currency);
public override string ToString() => string.Format($"{this.Amount} {this.Currency}");
}
}

View File

@ -0,0 +1,36 @@
namespace Infrastructure.CurrencyExchange
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Application.Services;
using Domain.ValueObjects;
/// <summary>
/// Fake implementation of the Exchange Service using hardcoded rates
/// </summary>
public sealed class CurrencyExchangeFake : ICurrencyExchange
{
private readonly Dictionary<Currency, decimal> _usdRates = new Dictionary<Currency, decimal>
{
{Currency.Dollar, 1m},
{Currency.Euro, 0.89021m},
{Currency.Canadian, 1.35737m},
{Currency.BritishPound, 0.80668m},
{Currency.Krona, 9.31944m},
{Currency.Real, 5.46346m}
};
public Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency)
{
// hardcoded rates from https://www.xe.com/currency/usd-us-dollar
decimal usdAmount = this._usdRates[originalAmount.Currency] / originalAmount.Amount;
decimal destinationAmount = this._usdRates[destinationCurrency] / usdAmount;
return Task.FromResult(
new PositiveMoney(
destinationAmount,
destinationCurrency));
}
}
}

View File

@ -0,0 +1,75 @@
namespace Infrastructure.CurrencyExchange
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;
using Application.Services;
using Domain.ValueObjects;
using Newtonsoft.Json.Linq;
/// <summary>
/// Real implementation of the Exchange Service using external data source
/// </summary>
public sealed class CurrencyExchangeService : ICurrencyExchange
{
public const string HttpClientName = "Fixer";
[SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")]
private const string _exchangeUrl = "https://api.exchangeratesapi.io/latest?base=USD";
private readonly IHttpClientFactory _httpClientFactory;
private readonly Dictionary<Currency, decimal> _usdRates = new Dictionary<Currency, decimal>();
public CurrencyExchangeService(IHttpClientFactory httpClientFactory) =>
this._httpClientFactory = httpClientFactory;
/// <summary>
/// Converts allowed currencies into USD.
/// </summary>
/// <returns>Money.</returns>
public async Task<PositiveMoney> Convert(PositiveMoney originalAmount, Currency destinationCurrency)
{
HttpClient httpClient = this._httpClientFactory.CreateClient(HttpClientName);
Uri requestUri = new Uri(_exchangeUrl);
HttpResponseMessage response = await httpClient.GetAsync(requestUri)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string responseJson = await response
.Content
.ReadAsStringAsync()
.ConfigureAwait(false);
this.ParseCurrencies(responseJson);
decimal usdAmount = this._usdRates[originalAmount.Currency] / originalAmount.Amount;
decimal destinationAmount = this._usdRates[destinationCurrency] / usdAmount;
return new PositiveMoney(
destinationAmount,
destinationCurrency);
}
private void ParseCurrencies(string responseJson)
{
var rates = JObject.Parse(responseJson);
decimal eur = rates["rates"]![Currency.Euro.Code]!.Value<decimal>();
decimal cad = rates["rates"]![Currency.Canadian.Code]!.Value<decimal>();
decimal gbh = rates["rates"]![Currency.BritishPound.Code]!.Value<decimal>();
decimal sek = rates["rates"]![Currency.Krona.Code]!.Value<decimal>();
decimal brl = rates["rates"]![Currency.Real.Code]!.Value<decimal>();
this._usdRates.Add(Currency.Dollar, 1);
this._usdRates.Add(Currency.Euro, eur);
this._usdRates.Add(Currency.Canadian, cad);
this._usdRates.Add(Currency.BritishPound, gbh);
this._usdRates.Add(Currency.Krona, sek);
this._usdRates.Add(Currency.Real, brl);
}
}
}

View File

@ -0,0 +1,57 @@
// <copyright file="AccountConfiguration.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess.Configuration
{
using System;
using Domain;
using Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
/// <summary>
/// Account Configuration.
/// </summary>
public sealed class AccountConfiguration : IEntityTypeConfiguration<Account>
{
/// <summary>
/// Configure Account.
/// </summary>
/// <param name="builder">Builder.</param>
public void Configure(EntityTypeBuilder<Account> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ToTable("Account");
builder.Property(b => b.AccountId)
.HasConversion(
v => v.Id,
v => new AccountId(v))
.IsRequired();
builder.Property(credit => credit.Currency)
.HasConversion(
value => value.Code,
value => new Currency(value))
.IsRequired();
builder.Property(b => b.ExternalUserId)
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
builder.HasMany(x => x.CreditsCollection)
.WithOne(b => b.Account!)
.HasForeignKey(b => b.AccountId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(x => x.DebitsCollection)
.WithOne(b => b.Account!)
.HasForeignKey(b => b.AccountId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

@ -0,0 +1,58 @@
// <copyright file="CreditConfiguration.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess.Configuration
{
using System;
using Domain.Credits;
using Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
/// <summary>
/// Credit Configuration.
/// </summary>
public sealed class CreditConfiguration : IEntityTypeConfiguration<Credit>
{
/// <summary>
/// Configure Credit.
/// </summary>
/// <param name="builder">Builder.</param>
public void Configure(EntityTypeBuilder<Credit> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ToTable("Credit");
builder.Ignore(e => e.Amount);
builder.Property(credit => credit.Value)
.IsRequired();
builder.Property(credit => credit.Currency)
.IsRequired();
builder.Property(credit => credit.CreditId)
.HasConversion(
value => value.Id,
value => new CreditId(value))
.IsRequired();
builder.Property(credit => credit.AccountId)
.HasConversion(
value => value.Id,
value => new AccountId(value))
.IsRequired();
builder.Property(credit => credit.TransactionDate)
.IsRequired();
builder.Property(b => b.AccountId)
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
}
}
}

View File

@ -0,0 +1,58 @@
// <copyright file="DebitConfiguration.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess.Configuration
{
using System;
using Domain.Debits;
using Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
/// <summary>
/// Debit Configuration.
/// </summary>
public sealed class DebitConfiguration : IEntityTypeConfiguration<Debit>
{
/// <summary>
/// Configure Debit.
/// </summary>
/// <param name="builder">Builder.</param>
public void Configure(EntityTypeBuilder<Debit> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ToTable("Debit");
builder.Ignore(e => e.Amount);
builder.Property(debit => debit.Value)
.IsRequired();
builder.Property(debit => debit.Currency)
.IsRequired();
builder.Property(debit => debit.DebitId)
.HasConversion(
value => value.Id,
value => new DebitId(value))
.IsRequired();
builder.Property(debit => debit.AccountId)
.HasConversion(
value => value.Id,
value => new AccountId(value))
.IsRequired();
builder.Property(debit => debit.TransactionDate)
.IsRequired();
builder.Property(b => b.AccountId)
.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
}
}
}

View File

@ -0,0 +1,45 @@
// <copyright file="ContextFactory.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess
{
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
/// <summary>
/// ContextFactory.
/// </summary>
public sealed class ContextFactory : IDesignTimeDbContextFactory<MangaContext>
{
/// <summary>
/// Instantiate a MangaContext.
/// </summary>
/// <param name="args">Command line args.</param>
/// <returns>Manga Context.</returns>
public MangaContext CreateDbContext(string[] args)
{
string connectionString = ReadDefaultConnectionStringFromAppSettings();
var builder = new DbContextOptionsBuilder<MangaContext>();
Console.WriteLine(connectionString);
builder.UseSqlServer(connectionString);
builder.EnableSensitiveDataLogging();
return new MangaContext(builder.Options);
}
private static string ReadDefaultConnectionStringFromAppSettings()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Production.json")
.Build();
string connectionString = configuration.GetValue<string>("PersistenceModule:DefaultConnection");
return connectionString;
}
}
}

View File

@ -0,0 +1,43 @@
// <copyright file="EntityFactory.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess
{
using System;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
/// <summary>
/// <see
/// href="https://github.com/ivanpaulovich/clean-architecture-manga/wiki/Domain-Driven-Design-Patterns#entity-factory">
/// Entity
/// Factory Domain-Driven Design Pattern
/// </see>
/// .
/// </summary>
public sealed class EntityFactory : IAccountFactory
{
/// <inheritdoc />
public Account NewAccount(string externalUserId, Currency currency)
=> new Account(new AccountId(Guid.NewGuid()), externalUserId, currency);
/// <inheritdoc />
public Credit NewCredit(
Account account,
PositiveMoney amountToDeposit,
DateTime transactionDate) =>
new Credit(new CreditId(Guid.NewGuid()), account.AccountId, transactionDate,
amountToDeposit.Amount, amountToDeposit.Currency.Code);
/// <inheritdoc />
public Debit NewDebit(
Account account,
PositiveMoney amountToWithdraw,
DateTime transactionDate) =>
new Debit(new DebitId(Guid.NewGuid()), account.AccountId, transactionDate, amountToWithdraw.Amount,
amountToWithdraw.Currency.Code);
}
}

View File

@ -0,0 +1,55 @@
// <copyright file="MangaContext.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess
{
using System;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Microsoft.EntityFrameworkCore;
/// <inheritdoc />
public sealed class MangaContext : DbContext
{
/// <summary>
/// </summary>
/// <param name="options"></param>
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
public MangaContext(DbContextOptions options)
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
: base(options)
{
}
/// <summary>
/// Gets or sets Accounts
/// </summary>
public DbSet<Account> Accounts { get; set; }
/// <summary>
/// Gets or sets Credits
/// </summary>
public DbSet<Credit> Credits { get; set; }
/// <summary>
/// Gets or sets Debits
/// </summary>
public DbSet<Debit> Debits { get; set; }
/// <summary>
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder is null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MangaContext).Assembly);
SeedData.Seed(modelBuilder);
}
}
}

View File

@ -0,0 +1,71 @@
// <copyright file="MangaContextFake.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess
{
using System;
using System.Collections.ObjectModel;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
/// <summary>
/// </summary>
public sealed class MangaContextFake
{
/// <summary>
/// </summary>
public MangaContextFake()
{
var credit = new Credit(
new CreditId(Guid.NewGuid()),
SeedData.DefaultAccountId,
DateTime.Now,
800,
Currency.Dollar.Code);
var debit = new Debit(
new DebitId(Guid.NewGuid()),
SeedData.DefaultAccountId,
DateTime.Now,
300,
Currency.Dollar.Code);
var account = new Account(
SeedData.DefaultAccountId,
SeedData.DefaultExternalUserId,
Currency.Dollar);
account.CreditsCollection.Add(credit);
account.DebitsCollection.Add(debit);
this.Accounts.Add(account);
this.Credits.Add(credit);
this.Debits.Add(debit);
var account2 = new Account(
SeedData.SecondAccountId,
SeedData.SecondExternalUserId,
Currency.Dollar);
this.Accounts.Add(account2);
}
/// <summary>
/// Gets or sets Accounts.
/// </summary>
public Collection<Account> Accounts { get; } = new Collection<Account>();
/// <summary>
/// Gets or sets Credits.
/// </summary>
public Collection<Credit> Credits { get; } = new Collection<Credit>();
/// <summary>
/// Gets or sets Debits.
/// </summary>
public Collection<Debit> Debits { get; } = new Collection<Debit>();
}
}

View File

@ -0,0 +1,140 @@
// <auto-generated />
using System;
using Infrastructure.DataAccess;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Infrastructure.DataAccess.Migrations
{
[DbContext(typeof(MangaContext))]
[Migration("20200821064304_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.6")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Domain.Accounts.Account", b =>
{
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ExternalUserId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("AccountId");
b.ToTable("Account");
b.HasData(
new
{
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
ExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae"
});
});
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
{
b.Property<Guid>("CreditId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("TransactionDate")
.HasColumnType("datetime2");
b.Property<decimal>("Value")
.HasColumnType("decimal(18,2)");
b.HasKey("CreditId");
b.HasIndex("AccountId");
b.ToTable("Credit");
b.HasData(
new
{
CreditId = new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795),
Value = 400m
});
});
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
{
b.Property<Guid>("DebitId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("TransactionDate")
.HasColumnType("datetime2");
b.Property<decimal>("Value")
.HasColumnType("decimal(18,2)");
b.HasKey("DebitId");
b.HasIndex("AccountId");
b.ToTable("Debit");
b.HasData(
new
{
DebitId = new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301),
Value = 400m
});
});
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
{
b.HasOne("Domain.Accounts.Account", "Account")
.WithMany("CreditsCollection")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
{
b.HasOne("Domain.Accounts.Account", "Account")
.WithMany("DebitsCollection")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,113 @@
namespace Infrastructure.DataAccess.Migrations
{
using System;
using Microsoft.EntityFrameworkCore.Migrations;
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"Account",
table => new
{
AccountId = table.Column<Guid>(nullable: false),
ExternalUserId = table.Column<string>(nullable: false),
Currency = table.Column<string>(nullable: false)
},
constraints: table => { table.PrimaryKey("PK_Account", x => x.AccountId); });
migrationBuilder.CreateTable(
"Credit",
table => new
{
CreditId = table.Column<Guid>(nullable: false),
TransactionDate = table.Column<DateTime>(nullable: false),
AccountId = table.Column<Guid>(nullable: false),
Value = table.Column<decimal>(nullable: false),
Currency = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Credit", x => x.CreditId);
table.ForeignKey(
"FK_Credit_Account_AccountId",
x => x.AccountId,
"Account",
"AccountId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"Debit",
table => new
{
DebitId = table.Column<Guid>(nullable: false),
TransactionDate = table.Column<DateTime>(nullable: false),
AccountId = table.Column<Guid>(nullable: false),
Value = table.Column<decimal>(nullable: false),
Currency = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Debit", x => x.DebitId);
table.ForeignKey(
"FK_Debit_Account_AccountId",
x => x.AccountId,
"Account",
"AccountId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
"Account",
new[] {"AccountId", "Currency", "ExternalUserId"},
new object[]
{
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD", "197d0438-e04b-453d-b5de-eca05960c6ae"
});
migrationBuilder.InsertData(
"Credit",
new[] {"CreditId", "AccountId", "Currency", "TransactionDate", "Value"},
new object[]
{
new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD",
new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795), 400m
});
migrationBuilder.InsertData(
"Debit",
new[] {"DebitId", "AccountId", "Currency", "TransactionDate", "Value"},
new object[]
{
new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"), "USD",
new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301), 400m
});
migrationBuilder.CreateIndex(
"IX_Credit_AccountId",
"Credit",
"AccountId");
migrationBuilder.CreateIndex(
"IX_Debit_AccountId",
"Debit",
"AccountId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"Credit");
migrationBuilder.DropTable(
"Debit");
migrationBuilder.DropTable(
"Account");
}
}
}

View File

@ -0,0 +1,137 @@
// <auto-generated />
namespace Infrastructure.DataAccess.Migrations
{
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
[DbContext(typeof(MangaContext))]
internal class MangaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.6")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Domain.Accounts.Account", b =>
{
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ExternalUserId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("AccountId");
b.ToTable("Account");
b.HasData(
new
{
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
ExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae"
});
});
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
{
b.Property<Guid>("CreditId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("TransactionDate")
.HasColumnType("datetime2");
b.Property<decimal>("Value")
.HasColumnType("decimal(18,2)");
b.HasKey("CreditId");
b.HasIndex("AccountId");
b.ToTable("Credit");
b.HasData(
new
{
CreditId = new Guid("7bf066ba-379a-4e72-a59b-9755fda432ce"),
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 92, DateTimeKind.Utc).AddTicks(7795),
Value = 400m
});
});
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
{
b.Property<Guid>("DebitId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AccountId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("TransactionDate")
.HasColumnType("datetime2");
b.Property<decimal>("Value")
.HasColumnType("decimal(18,2)");
b.HasKey("DebitId");
b.HasIndex("AccountId");
b.ToTable("Debit");
b.HasData(
new
{
DebitId = new Guid("31ade963-bd69-4afb-9df7-611ae2cfa651"),
AccountId = new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"),
Currency = "USD",
TransactionDate = new DateTime(2020, 8, 21, 6, 43, 4, 93, DateTimeKind.Utc).AddTicks(301),
Value = 400m
});
});
modelBuilder.Entity("Domain.Accounts.Credits.Credit", b =>
{
b.HasOne("Domain.Accounts.Account", "Account")
.WithMany("CreditsCollection")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Domain.Accounts.Debits.Debit", b =>
{
b.HasOne("Domain.Accounts.Account", "Account")
.WithMany("DebitsCollection")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,137 @@
// <copyright file="AccountRepository.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess.Repositories
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
/// <inheritdoc />
public sealed class AccountRepository : IAccountRepository
{
private readonly MangaContext _context;
/// <summary>
/// </summary>
/// <param name="context"></param>
public AccountRepository(MangaContext context) => this._context = context ??
throw new ArgumentNullException(
nameof(context));
/// <inheritdoc />
public async Task Add(Account account, Credit credit)
{
await this._context
.Accounts
.AddAsync(account)
.ConfigureAwait(false);
await this._context
.Credits
.AddAsync(credit)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete(AccountId accountId)
{
Account account = await this._context
.Accounts
.FindAsync(accountId)
.ConfigureAwait(false);
if (account != null)
{
this._context.Accounts.Remove(account);
}
}
/// <inheritdoc />
public async Task<IAccount> GetAccount(AccountId accountId)
{
Account account = await this._context
.Accounts
.Where(e => e.AccountId == accountId)
.Select(e => e)
.SingleOrDefaultAsync()
.ConfigureAwait(false);
if (account is Account findAccount)
{
return await this.LoadTransactions(accountId, findAccount).ConfigureAwait(false);
}
return AccountNull.Instance;
}
/// <inheritdoc />
public async Task Update(Account account, Credit credit) => await this._context
.Credits
.AddAsync(credit)
.ConfigureAwait(false);
/// <inheritdoc />
public async Task Update(Account account, Debit debit) => await this._context
.Debits
.AddAsync(debit)
.ConfigureAwait(false);
public async Task<IAccount> Find(AccountId accountId, string externalUserId)
{
Account account = await this._context
.Accounts
.Where(e => e.ExternalUserId == externalUserId && e.AccountId == accountId)
.Select(e => e)
.SingleOrDefaultAsync()
.ConfigureAwait(false);
if (account is Account findAccount)
{
return await this.LoadTransactions(accountId, findAccount).ConfigureAwait(false);
}
return AccountNull.Instance;
}
public async Task<IList<Account>> GetAccounts(string externalUserId)
{
var accounts = await this._context
.Accounts
.Where(e => e.ExternalUserId == externalUserId)
.ToListAsync()
.ConfigureAwait(false);
return accounts;
}
private async Task<IAccount> LoadTransactions(AccountId accountId, Account findAccount)
{
List<Credit> credits = await this._context
.Credits
.Where(e => e.AccountId.Equals(accountId))
.ToListAsync()
.ConfigureAwait(false);
List<Debit> debits = await this._context
.Debits
.Where(e => e.AccountId.Equals(accountId))
.ToListAsync()
.ConfigureAwait(false);
findAccount.CreditsCollection
.AddRange(credits);
findAccount.DebitsCollection
.AddRange(debits);
return findAccount;
}
}
}

View File

@ -0,0 +1,138 @@
// <copyright file="AccountRepositoryFake.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess.Repositories
{
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
/// <inheritdoc />
public sealed class AccountRepositoryFake : IAccountRepository
{
private readonly MangaContextFake _context;
/// <summary>
/// </summary>
/// <param name="context"></param>
public AccountRepositoryFake(MangaContextFake context) => this._context = context;
/// <inheritdoc />
public async Task Add(Account account, Credit credit)
{
this._context
.Accounts
.Add(account);
this._context
.Credits
.Add(credit);
await Task.CompletedTask
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Delete(AccountId accountId)
{
Account accountOld = this._context
.Accounts
.SingleOrDefault(e => e.AccountId.Equals(accountId));
this._context
.Accounts
.Remove(accountOld);
await Task.CompletedTask
.ConfigureAwait(false);
}
public async Task<IAccount> Find(AccountId accountId, string externalUserId)
{
Account account = this._context
.Accounts
.Where(e => e.ExternalUserId == externalUserId && e.AccountId.Equals(accountId))
.Select(e => e)
.SingleOrDefault();
if (account == null)
{
return AccountNull.Instance;
}
return await Task.FromResult(account)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IAccount> GetAccount(AccountId accountId)
{
Account account = this._context
.Accounts
.SingleOrDefault(e => e.AccountId.Equals(accountId));
if (account == null)
{
return AccountNull.Instance;
}
return await Task.FromResult(account)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IList<Account>> GetAccounts(string externalUserId)
{
var accounts = this._context
.Accounts
.Where(e => e.ExternalUserId == externalUserId)
.ToList();
return await Task.FromResult(accounts)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Update(Account account, Credit credit)
{
Account accountOld = this._context
.Accounts
.SingleOrDefault(e => e.AccountId.Equals(account.AccountId));
if (accountOld != null)
{
this._context.Accounts.Remove(accountOld);
}
this._context.Accounts.Add(account);
this._context.Credits.Add(credit);
await Task.CompletedTask
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Update(Account account, Debit debit)
{
Account accountOld = this._context
.Accounts
.SingleOrDefault(e => e.AccountId.Equals(account.AccountId));
if (accountOld != null)
{
this._context.Accounts.Remove(accountOld);
this._context.Accounts.Add(account);
}
this._context.Debits.Add(debit);
await Task.CompletedTask
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,73 @@
// <copyright file="SeedData.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.DataAccess
{
using System;
using Domain;
using Domain.Credits;
using Domain.Debits;
using Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// </summary>
public static class SeedData
{
public static readonly string DefaultExternalUserId = "197d0438-e04b-453d-b5de-eca05960c6ae";
public static readonly AccountId DefaultAccountId =
new AccountId(new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"));
public static readonly AccountId SecondAccountId =
new AccountId(new Guid("E82D2EA6-E9D3-444D-A22F-9D65F2F2C65E"));
public static readonly string SecondExternalUserId = "C70E69BF-EDC7-48E3-BF33-B424F7464C5F";
public static readonly CreditId DefaultCreditId =
new CreditId(new Guid("7BF066BA-379A-4E72-A59B-9755FDA432CE"));
public static readonly DebitId DefaultDebitId =
new DebitId(new Guid("31ADE963-BD69-4AFB-9DF7-611AE2CFA651"));
public static void Seed(ModelBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Entity<Account>()
.HasData(
new
{
AccountId = DefaultAccountId,
ExternalUserId = DefaultExternalUserId,
Currency = Currency.Dollar
});
builder.Entity<Credit>()
.HasData(
new
{
CreditId = DefaultCreditId,
AccountId = DefaultAccountId,
TransactionDate = DateTime.UtcNow,
Value = 400m,
Currency = Currency.Dollar.Code
});
builder.Entity<Debit>()
.HasData(
new
{
DebitId = DefaultDebitId,
AccountId = DefaultAccountId,
TransactionDate = DateTime.UtcNow,
Value = 400m,
Currency = Currency.Dollar.Code
});
}
}
}

View File

@ -0,0 +1,35 @@
// <copyright file="ExternalUserService.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.ExternalAuthentication
{
using System.Security.Claims;
using Application.Services;
using Microsoft.AspNetCore.Http;
/// <inheritdoc />
public sealed class ExternalUserService : IUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// </summary>
/// <param name="httpContextAccessor"></param>
public ExternalUserService(
IHttpContextAccessor httpContextAccessor) =>
this._httpContextAccessor = httpContextAccessor;
/// <inheritdoc />
public string GetCurrentUserId()
{
ClaimsPrincipal user = this._httpContextAccessor
.HttpContext
.User;
string id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value!;
return id;
}
}
}

View File

@ -0,0 +1,16 @@
// <copyright file="TestUserService.cs" company="Ivan Paulovich">
// Copyright © Ivan Paulovich. All rights reserved.
// </copyright>
namespace Infrastructure.ExternalAuthentication
{
using Application.Services;
using DataAccess;
/// <inheritdoc />
public sealed class TestUserService : IUserService
{
/// <inheritdoc />
public string GetCurrentUserId() => SeedData.DefaultExternalUserId;
}
}

View File

@ -0,0 +1,81 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<NoWarn>$(NoWarn);CA1062;1591</NoWarn>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
<LangVersion>8.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.Production.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.Production.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="3.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.12.0.21095">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SecurityCodeScan" Version="3.5.3" PrivateAssets="all" />
<PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0-beta2.final">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Messages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Messages.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Messages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="DataAccess\Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,5 @@
{
"PersistenceModule": {
"DefaultConnection": "Server=192.168.1.75;User Id=sa;Password=<YourStrong!Passw0rd>;Data source=db01"
}
}

View File

@ -0,0 +1,25 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
COPY ["src/Application/Application.csproj", "src/Application/"]
COPY ["src/Domain/Domain.csproj", "src/Domain/"]
COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
RUN dotnet restore "src/WebApi/WebApi.csproj"
COPY . .
WORKDIR "/src/src/WebApi"
RUN dotnet build "WebApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApi.dll"]

View File

@ -0,0 +1,35 @@
namespace WebApi.Modules
{
using Application.UseCases.CloseAccount;
using Application.UseCases.Deposit;
using Application.UseCases.GetAccount;
using Application.UseCases.GetAccounts;
using Application.UseCases.OpenAccount;
using Application.UseCases.Transfer;
using Application.UseCases.Withdraw;
using Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Adds Use Cases classes.
/// </summary>
public static class ApplicationExtensions
{
/// <summary>
/// Adds Use Cases to the ServiceCollection.
/// </summary>
/// <param name="services">Service Collection.</param>
/// <returns>The modified instance.</returns>
public static IServiceCollection AddUseCases(this IServiceCollection services)
{
services.AddScoped<ICloseAccountUseCase, CloseAccountUseCase>();
services.AddScoped<IDepositUseCase, DepositUseCase>();
services.AddScoped<IGetAccountUseCase, GetAccountUseCase>();
services.AddScoped<IGetAccountsUseCase, GetAccountsUseCase>();
services.AddScoped<IOpenAccountUseCase, OpenAccountUseCase>();
services.AddScoped<ITransferUseCase, TransferUseCase>();
services.AddScoped<IWithdrawUseCase, WithdrawUseCase>();
return services;
}
}
}

Some files were not shown because too many files have changed in this diff Show More