├── global.json
├── docs
├── layers.xcf
├── clean-architecture-dotnet-core.png
├── clean-architecture-use-cases.png
├── clean-architecture-manga-layers.png
├── clean-architecture-manga-swagger.png
├── clean-architecture-manga-use-cases.png
└── clean-architecture-manga-ports-and-adapters.png
├── src
├── WebApi
│ ├── DependencyInjection
│ │ ├── FeatureFlags
│ │ │ ├── Features.cs
│ │ │ └── FeatureFlagsExtensions.cs
│ │ ├── BusinessExceptionExtensions.cs
│ │ ├── UserInterfaceV2Extensions.cs
│ │ ├── HttpMetricsExtensions.cs
│ │ ├── InMemoryInfrastructureExtensions.cs
│ │ ├── VersioningExtensions.cs
│ │ ├── Authentication
│ │ │ └── UserService.cs
│ │ ├── SQLServerInfrastructureExtensions.cs
│ │ └── ApplicationExtensions.cs
│ ├── appsettings.json
│ ├── appsettings.Development.json
│ ├── Dockerfile
│ ├── UseCases
│ │ ├── V1
│ │ │ ├── CloseAccount
│ │ │ │ ├── CloseAccountRequest.cs
│ │ │ │ ├── CloseAccountPresenter.cs
│ │ │ │ ├── CloseAccountResponse.cs
│ │ │ │ └── AccountsController.cs
│ │ │ ├── GetAccountDetails
│ │ │ │ ├── GetAccountDetailsRequest.cs
│ │ │ │ ├── GetAccountDetailsResponse.cs
│ │ │ │ ├── GetAccountDetailsPresenter.cs
│ │ │ │ └── AccountsController.cs
│ │ │ ├── Login
│ │ │ │ └── LoginController.cs
│ │ │ ├── Register
│ │ │ │ ├── RegisterRequest.cs
│ │ │ │ ├── RegisterResponse.cs
│ │ │ │ ├── RegisterPresenter.cs
│ │ │ │ └── CustomersController.cs
│ │ │ ├── Deposit
│ │ │ │ ├── DepositRequest.cs
│ │ │ │ ├── DepositPresenter.cs
│ │ │ │ ├── DepositResponse.cs
│ │ │ │ └── AccountsController.cs
│ │ │ ├── Withdraw
│ │ │ │ ├── WithdrawRequest.cs
│ │ │ │ ├── WithdrawResponse.cs
│ │ │ │ ├── WithdrawPresenter.cs
│ │ │ │ └── AccountsController.cs
│ │ │ ├── Transfer
│ │ │ │ ├── TransferRequest.cs
│ │ │ │ ├── TransferResponse.cs
│ │ │ │ └── TransferPresenter.cs
│ │ │ └── GetCustomerDetails
│ │ │ │ ├── GetCustomerDetailsResponse.cs
│ │ │ │ ├── CustomersController.cs
│ │ │ │ └── GetCustomerDetailsPresenter.cs
│ │ └── V2
│ │ │ └── GetAccountDetails
│ │ │ ├── GetAccountDetailsRequestV2.cs
│ │ │ ├── GetAccountDetailsPresenterV2.cs
│ │ │ └── AccountsController.cs
│ ├── Program.cs
│ ├── appsettings.Production.json
│ ├── Filters
│ │ ├── BusinessExceptionFilter.cs
│ │ ├── SwaggerDefaultValues.cs
│ │ └── SwaggerDocumentFilter.cs
│ └── ViewModels
│ │ ├── TransactionModel.cs
│ │ └── AccountDetailsModel.cs
├── Application
│ ├── Boundaries
│ │ ├── IUseCaseOutput.cs
│ │ ├── IUseCaseInput.cs
│ │ ├── Deposit
│ │ │ ├── IOutputPort.cs
│ │ │ ├── IUseCase.cs
│ │ │ ├── DepositInput.cs
│ │ │ ├── DepositOutput.cs
│ │ │ └── Transaction.cs
│ │ ├── Transfer
│ │ │ ├── IOutputPort.cs
│ │ │ ├── IUseCase.cs
│ │ │ ├── TransferInput.cs
│ │ │ ├── TransferOutput.cs
│ │ │ └── Transaction.cs
│ │ ├── CloseAccount
│ │ │ ├── IOutputPort.cs
│ │ │ ├── IUseCase.cs
│ │ │ ├── CloseAccountInput.cs
│ │ │ └── CloseAccountOutput.cs
│ │ ├── GetCustomerDetails
│ │ │ ├── GetCustomerDetailsInput.cs
│ │ │ ├── IOutputPort.cs
│ │ │ ├── IUseCase.cs
│ │ │ ├── Transaction.cs
│ │ │ └── GetCustomerDetailsOutput.cs
│ │ ├── GetAccountDetails
│ │ │ ├── IOutputPort.cs
│ │ │ ├── IUseCaseV2.cs
│ │ │ ├── IUseCase.cs
│ │ │ ├── GetAccountDetailsInput.cs
│ │ │ └── Transaction.cs
│ │ ├── Register
│ │ │ ├── IUseCase.cs
│ │ │ ├── IOutputPort.cs
│ │ │ ├── RegisterInput.cs
│ │ │ ├── Transaction.cs
│ │ │ ├── Account.cs
│ │ │ └── Customer.cs
│ │ ├── Withdraw
│ │ │ ├── IUseCase.cs
│ │ │ ├── IOutputPort.cs
│ │ │ ├── WithdrawInput.cs
│ │ │ ├── WithdrawOutput.cs
│ │ │ └── Transaction.cs
│ │ ├── IOutputPortNotFound.cs
│ │ ├── IOutputPortStandard.cs
│ │ └── IUseCase.cs
│ ├── Services
│ │ └── IUnitOfWork.cs
│ ├── Application.csproj
│ └── UseCases
│ │ └── GetAccountDetails.cs
├── Domain
│ ├── Customers
│ │ ├── ValueObjects
│ │ │ ├── InvalidSSNException.cs
│ │ │ ├── EmptyCustomerIdException.cs
│ │ │ ├── SSNShouldNotBeEmptyException.cs
│ │ │ ├── NameShouldNotBeEmptyException.cs
│ │ │ ├── Name.cs
│ │ │ ├── CustomerId.cs
│ │ │ └── SSN.cs
│ │ ├── CustomerNotFoundException.cs
│ │ ├── ICustomerFactory.cs
│ │ ├── ICustomer.cs
│ │ ├── ICustomerRepository.cs
│ │ ├── Customer.cs
│ │ └── AccountCollection.cs
│ ├── Accounts
│ │ ├── Debits
│ │ │ ├── EmptyDebitIdException.cs
│ │ │ ├── IDebit.cs
│ │ │ ├── Debit.cs
│ │ │ └── DebitId.cs
│ │ ├── Credits
│ │ │ ├── EmptyCreditIdException.cs
│ │ │ ├── ICredit.cs
│ │ │ ├── CreditId.cs
│ │ │ └── Credit.cs
│ │ ├── ValueObjects
│ │ │ ├── EmptyAccountIdException.cs
│ │ │ ├── MoneyShouldBePositiveException.cs
│ │ │ ├── AccountId.cs
│ │ │ ├── PositiveMoney.cs
│ │ │ └── Money.cs
│ │ ├── AccountNotFoundException.cs
│ │ ├── IAccountFactory.cs
│ │ ├── IAccount.cs
│ │ └── IAccountRepository.cs
│ ├── Security
│ │ ├── User.cs
│ │ ├── ValueObjects
│ │ │ ├── ExternalUserIdShouldNotBeEmptyException.cs
│ │ │ └── ExternalUserId.cs
│ │ ├── UserNotFoundException.cs
│ │ ├── IUser.cs
│ │ ├── IUserFactory.cs
│ │ ├── Services
│ │ │ └── IUserService.cs
│ │ ├── IUserRepository.cs
│ │ └── SecurityService.cs
│ ├── DomainException.cs
│ ├── stylecop.json
│ └── Domain.csproj
└── Infrastructure
│ ├── InMemoryDataAccess
│ ├── User.cs
│ ├── UnitOfWork.cs
│ ├── Debit.cs
│ ├── Credit.cs
│ ├── Services
│ │ ├── UserService.cs
│ │ └── TestUserService.cs
│ ├── Customer.cs
│ ├── Presenters
│ │ ├── DepositPresenter.cs
│ │ ├── TransferPresenter.cs
│ │ ├── RegisterPresenter.cs
│ │ ├── CloseAccountPresenter.cs
│ │ ├── GetAccountDetailsPresenter.cs
│ │ ├── GetCustomerDetailsPresenter.cs
│ │ └── WithdrawPresenter.cs
│ ├── Account.cs
│ ├── Repositories
│ │ ├── UserRepository.cs
│ │ └── CustomerRepository.cs
│ └── EntityFactory.cs
│ ├── EntityFrameworkDataAccess
│ ├── Entities
│ │ ├── User.cs
│ │ ├── Debit.cs
│ │ ├── Credit.cs
│ │ ├── Customer.cs
│ │ └── Account.cs
│ ├── MangaContext.cs
│ ├── Configuration
│ │ ├── AccountConfiguration.cs
│ │ ├── UserConfiguration.cs
│ │ ├── CustomerConfiguration.cs
│ │ ├── DebitConfiguration.cs
│ │ └── CreditConfiguration.cs
│ ├── UnitOfWork.cs
│ ├── ContextFactory.cs
│ ├── Repositories
│ │ ├── UserRepository.cs
│ │ └── CustomerRepository.cs
│ ├── EntityFactory.cs
│ ├── Migrations
│ │ └── 20191126172117_ValueObjects.cs
│ └── SeedData.cs
│ └── Infrastructure.csproj
├── docker-compose.yml
├── test
├── UnitTests
│ ├── UseCaseTests
│ │ ├── Deposit
│ │ │ ├── NegativeDataSetup.cs
│ │ │ └── PositiveDataSetup.cs
│ │ ├── Transfer
│ │ │ ├── PositiveDataSetup.cs
│ │ │ └── TransferUseCaseTests.cs
│ │ ├── Withdraw
│ │ │ ├── PositiveDataSetup.cs
│ │ │ └── WithdrawTests.cs
│ │ ├── Register
│ │ │ └── PositiveDataSetup.cs
│ │ └── CloseAccount
│ │ │ └── PositiveDataSetup.cs
│ ├── InputValidationTests
│ │ ├── RegisterInputValidationTests.cs
│ │ ├── CloseAccountInputValidationTests.cs
│ │ ├── GetAccountDetailsInputValidationTests.cs
│ │ ├── DepositInputValidationTests.cs
│ │ ├── WithdrawInputValidationTests.cs
│ │ └── TransferInputValidationTests.cs
│ ├── EntitiesTests
│ │ └── CustomerTests.cs
│ ├── UnitTests.csproj
│ ├── PresenterTests
│ │ └── RegisterPresenterTests.cs
│ └── TestFixtures
│ │ └── StandardFixture.cs
├── AcceptanceTests
│ ├── HttpClientExtensions.cs
│ └── ComponentTests.csproj
└── IntegrationTests
│ └── IntegrationTests.csproj
├── sql-docker-up.sh
├── .vscode
├── settings.json
└── launch.json
└── azure-pipelines.yml
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "3.1.100"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/docs/layers.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/layers.xcf
--------------------------------------------------------------------------------
/docs/clean-architecture-dotnet-core.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-dotnet-core.png
--------------------------------------------------------------------------------
/docs/clean-architecture-use-cases.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-use-cases.png
--------------------------------------------------------------------------------
/docs/clean-architecture-manga-layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-layers.png
--------------------------------------------------------------------------------
/docs/clean-architecture-manga-swagger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-swagger.png
--------------------------------------------------------------------------------
/docs/clean-architecture-manga-use-cases.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-use-cases.png
--------------------------------------------------------------------------------
/docs/clean-architecture-manga-ports-and-adapters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piraces/clean-architecture-manga/master/docs/clean-architecture-manga-ports-and-adapters.png
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/FeatureFlags/Features.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection.FeatureFlags
2 | {
3 | public enum Features
4 | {
5 | Transfer,
6 | GetAccountDetailsV2,
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/IUseCaseOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries
2 | {
3 | ///
4 | /// Output Message Marker.
5 | ///
6 | public interface IUseCaseOutput
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/IUseCaseInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries
2 | {
3 | ///
4 | /// Input Message interface marker.
5 | ///
6 | public interface IUseCaseInput
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | webapi:
4 | image: ivanpaulovich/clean-architecture-manga
5 | build:
6 | context: .
7 | dockerfile: src/WebApi/Dockerfile
8 | ports:
9 | - 5500:80
10 | environment:
11 | - ASPNETCORE_ENVIRONMENT=Development
--------------------------------------------------------------------------------
/src/Application/Boundaries/Deposit/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Deposit
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Transfer/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Transfer
2 | {
3 | ///
4 | /// Transfer Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/InvalidSSNException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | internal sealed class InvalidSSNException : DomainException
4 | {
5 | internal InvalidSSNException(string message)
6 | : base(message)
7 | {
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/CloseAccount/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.CloseAccount
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetCustomerDetails/GetCustomerDetailsInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetCustomerDetails
2 | {
3 | ///
4 | /// Get Customer Details Input Message.
5 | ///
6 | public sealed class GetCustomerDetailsInput : IUseCaseInput
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Deposit/NegativeDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.Deposit
2 | {
3 | using Xunit;
4 |
5 | internal sealed class NegativeDataSetup : TheoryData
6 | {
7 | public NegativeDataSetup()
8 | {
9 | Add(-100);
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/EmptyCustomerIdException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | internal sealed class EmptyCustomerIdException : DomainException
4 | {
5 | internal EmptyCustomerIdException(string message)
6 | : base(message)
7 | {
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetAccountDetails/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetAccountDetails
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Transfer/PositiveDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCaseTests.Transfer
2 | {
3 | using Xunit;
4 |
5 | internal sealed class PositiveDataSetup : TheoryData
6 | {
7 | public PositiveDataSetup()
8 | {
9 | Add(100, 600);
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Withdraw/PositiveDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.Withdraw
2 | {
3 | using Xunit;
4 |
5 | internal sealed class PositiveDataSetup : TheoryData
6 | {
7 | public PositiveDataSetup()
8 | {
9 | Add(100, 600);
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetCustomerDetails/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetCustomerDetails
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Deposit/PositiveDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.Deposit
2 | {
3 | using Xunit;
4 |
5 | internal sealed class PositiveDataSetup : TheoryData
6 | {
7 | public PositiveDataSetup()
8 | {
9 | Add(0);
10 | Add(100);
11 | Add(200);
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Register/PositiveDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.Register
2 | {
3 | using Xunit;
4 |
5 | internal sealed class PositiveDataSetup : TheoryData
6 | {
7 | public PositiveDataSetup()
8 | {
9 | Add(0);
10 | Add(100);
11 | Add(200);
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "FeatureManagement": {
10 | "Transfer": true,
11 | "GetAccountDetailsV2": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/CloseAccount/PositiveDataSetup.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.CloseAccount
2 | {
3 | using Xunit;
4 |
5 | internal sealed class PositiveDataSetup : TheoryData
6 | {
7 | public PositiveDataSetup()
8 | {
9 | Add(0.5M);
10 | Add(100M);
11 | Add(200M);
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Domain/Accounts/Debits/EmptyDebitIdException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Debits
2 | {
3 | ///
4 | /// Empty DebitId Exception.
5 | ///
6 | public sealed class EmptyDebitIdException : DomainException
7 | {
8 | internal EmptyDebitIdException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | ///
4 | /// Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Credits/EmptyCreditIdException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Credits
2 | {
3 | ///
4 | /// Empty CreditId Exception.
5 | ///
6 | public sealed class EmptyCreditIdException : DomainException
7 | {
8 | internal EmptyCreditIdException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Deposit/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Deposit
2 | {
3 | ///
4 | /// Deposit Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetAccountDetails/IUseCaseV2.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetAccountDetails
2 | {
3 | ///
4 | /// Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCaseV2 : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Transfer/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Transfer
2 | {
3 | ///
4 | /// Transfer Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Withdraw/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Withdraw
2 | {
3 | ///
4 | /// Withdraw Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/ValueObjects/EmptyAccountIdException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.ValueObjects
2 | {
3 | ///
4 | /// Empty Account Id Exception.
5 | ///
6 | public sealed class EmptyAccountIdException : DomainException
7 | {
8 | internal EmptyAccountIdException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/CloseAccount/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.CloseAccount
2 | {
3 | ///
4 | /// Close Account Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetAccountDetails/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetAccountDetails
2 | {
3 | ///
4 | /// Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetCustomerDetails/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetCustomerDetails
2 | {
3 | ///
4 | /// Use Case Domain-Driven Design Pattern.
5 | ///
6 | public interface IUseCase : IUseCase
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/WebApi/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS base
2 | WORKDIR /app
3 | EXPOSE 80
4 |
5 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build
6 | COPY . .
7 | RUN dotnet test
8 |
9 | FROM build AS publish
10 | WORKDIR /src/WebApi
11 | RUN dotnet publish "WebApi.csproj" -c Release -o /app
12 |
13 | FROM base AS final
14 | WORKDIR /app
15 | COPY --from=publish /app .
16 | ENTRYPOINT ["dotnet", "WebApi.dll"]
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/SSNShouldNotBeEmptyException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | ///
4 | /// SSN Should Not Be Empty Exception.
5 | ///
6 | public sealed class SSNShouldNotBeEmptyException : DomainException
7 | {
8 | internal SSNShouldNotBeEmptyException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Domain/Security/User.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | ///
7 | public abstract class User : IUser
8 | {
9 | ///
10 | public ExternalUserId ExternalUserId { get; set; }
11 |
12 | ///
13 | public CustomerId CustomerId { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/ValueObjects/MoneyShouldBePositiveException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.ValueObjects
2 | {
3 | ///
4 | /// Money Should Be Positive Exception.
5 | ///
6 | public sealed class MoneyShouldBePositiveException : DomainException
7 | {
8 | internal MoneyShouldBePositiveException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/NameShouldNotBeEmptyException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | ///
4 | /// Name Should Not Be Empty Exception.
5 | ///
6 | public sealed class NameShouldNotBeEmptyException : DomainException
7 | {
8 | internal NameShouldNotBeEmptyException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/IOutputPortNotFound.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries
2 | {
3 | ///
4 | /// Not Found Output Port.
5 | ///
6 | public interface IOutputPortNotFound
7 | {
8 | ///
9 | /// Informs the resource was not found.
10 | ///
11 | /// Text description.
12 | void NotFound(string message);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Domain/Security/ValueObjects/ExternalUserIdShouldNotBeEmptyException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security.ValueObjects
2 | {
3 | ///
4 | /// External User Id Should Not Be Empty Exception.
5 | ///
6 | public sealed class ExternalUserIdShouldNotBeEmptyException : DomainException
7 | {
8 | internal ExternalUserIdShouldNotBeEmptyException(string message)
9 | : base(message)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Application/Services/IUnitOfWork.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Services
2 | {
3 | using System.Threading.Tasks;
4 |
5 | ///
6 | /// Unit Of Work. Should only be used by Use Cases.
7 | ///
8 | public interface IUnitOfWork
9 | {
10 | ///
11 | /// Applies all database changes.
12 | ///
13 | /// Number of affected rows.
14 | Task Save();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/CloseAccount/CloseAccountRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.CloseAccount
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// The Close Account Request.
8 | ///
9 | public sealed class CloseAccountRequest
10 | {
11 | ///
12 | /// Gets or sets account ID.
13 | ///
14 | [Required]
15 | public Guid AccountId { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard
8 | {
9 | ///
10 | /// Informs the user is already registered.
11 | ///
12 | /// Custom message.
13 | void CustomerAlreadyRegistered(string message);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Withdraw/IOutputPort.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Withdraw
2 | {
3 | ///
4 | /// Output Port.
5 | ///
6 | public interface IOutputPort
7 | : IOutputPortStandard, IOutputPortNotFound
8 | {
9 | ///
10 | /// Informs it is out of balance.
11 | ///
12 | /// Custom message.
13 | void OutOfBalance(string message);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Debits/IDebit.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Debits
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Debit.
7 | ///
8 | public interface IDebit
9 | {
10 | ///
11 | /// Calculates the sum of two positive amounts.
12 | ///
13 | /// Positive amount.
14 | /// The sum.
15 | PositiveMoney Sum(PositiveMoney amount);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/User.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | public class User : Domain.Security.User
7 | {
8 | public User(CustomerId customerId, ExternalUserId externalUserId)
9 | {
10 | CustomerId = customerId;
11 | ExternalUserId = externalUserId;
12 | }
13 |
14 | protected User()
15 | {
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetAccountDetails
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// The Get Account Details Request.
8 | ///
9 | public sealed class GetAccountDetailsRequest
10 | {
11 | ///
12 | /// Gets or sets account ID.
13 | ///
14 | [Required]
15 | public Guid AccountId { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V2/GetAccountDetails/GetAccountDetailsRequestV2.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V2.GetAccountDetails
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// The Get Account Details Request.
8 | ///
9 | public sealed class GetAccountDetailsRequestV2
10 | {
11 | ///
12 | /// Gets or sets account ID.
13 | ///
14 | [Required]
15 | public Guid AccountId { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Entities/User.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | public class User : Domain.Security.User
7 | {
8 | public User(CustomerId customerId, ExternalUserId externalUserId)
9 | {
10 | CustomerId = customerId;
11 | ExternalUserId = externalUserId;
12 | }
13 |
14 | protected User()
15 | {
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Credits/ICredit.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Credits
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Credit Entity Interface.
7 | ///
8 | public interface ICredit
9 | {
10 | ///
11 | /// Calculates the sum between positive amounts.
12 | ///
13 | /// Positive amount.
14 | /// The positive sum.
15 | PositiveMoney Sum(PositiveMoney amount);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/UnitOfWork.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System.Threading.Tasks;
4 | using Application.Services;
5 |
6 | public sealed class UnitOfWork : IUnitOfWork
7 | {
8 | private readonly MangaContext _context;
9 |
10 | public UnitOfWork(MangaContext context)
11 | {
12 | _context = context;
13 | }
14 |
15 | public async Task Save()
16 | {
17 | return await Task.FromResult(0);
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Domain/DomainException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain
2 | {
3 | using System;
4 |
5 | ///
6 | /// Domain Exception.
7 | ///
8 | public class DomainException : Exception
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// Message.
14 | public DomainException(string businessMessage)
15 | : base(businessMessage)
16 | {
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Login/LoginController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Login
2 | {
3 | using Microsoft.AspNetCore.Authentication;
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | [AllowAnonymous]
8 | [Route("[controller]")]
9 | public sealed class LoginController : Controller
10 | {
11 | [HttpGet]
12 | public IActionResult Login(string returnUrl = "/")
13 | {
14 | return Challenge(new AuthenticationProperties() { RedirectUri = returnUrl });
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Domain/Security/UserNotFoundException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | ///
4 | /// User Not Found Exception.
5 | ///
6 | public sealed class UserNotFoundException : DomainException
7 | {
8 | ///
9 | /// Initializes a new instance of the class.
10 | ///
11 | /// Message.
12 | public UserNotFoundException(string message)
13 | : base(message)
14 | {
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/AccountNotFoundException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts
2 | {
3 | ///
4 | /// Account Not Found Exception.
5 | ///
6 | public sealed class AccountNotFoundException : DomainException
7 | {
8 | ///
9 | /// Initializes a new instance of the class.
10 | ///
11 | /// Message.
12 | public AccountNotFoundException(string message)
13 | : base(message)
14 | {
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Domain/Customers/CustomerNotFoundException.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | ///
4 | /// Customer Not Found Exception.
5 | ///
6 | public sealed class CustomerNotFoundException : DomainException
7 | {
8 | ///
9 | /// Initializes a new instance of the class.
10 | ///
11 | /// Message.
12 | public CustomerNotFoundException(string message)
13 | : base(message)
14 | {
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/BusinessExceptionExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Microsoft.Extensions.DependencyInjection;
4 | using WebApi.Filters;
5 |
6 | public static class BusinessExceptionExtensions
7 | {
8 | public static IServiceCollection AddBusinessExceptionFilter(this IServiceCollection services)
9 | {
10 | services.AddMvc(options =>
11 | {
12 | options.Filters.Add(typeof(BusinessExceptionFilter));
13 | });
14 |
15 | return services;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/IOutputPortStandard.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries
2 | {
3 | ///
4 | /// Standard Output Port.
5 | ///
6 | /// Any IUseCaseOutput.
7 | public interface IOutputPortStandard
8 | where TUseCaseOutput : IUseCaseOutput
9 | {
10 | ///
11 | /// Writes to the Standard Output.
12 | ///
13 | /// The Output Port Message.
14 | void Standard(TUseCaseOutput output);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Register/RegisterRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Register
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 |
5 | ///
6 | /// Registration Request.
7 | ///
8 | public sealed class RegisterRequest
9 | {
10 | ///
11 | /// Gets or sets sSN.
12 | ///
13 | [Required]
14 | public string SSN { get; set; }
15 |
16 | ///
17 | /// Gets or sets initial Amount.
18 | ///
19 | [Required]
20 | public decimal InitialAmount { get; set; }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/WebApi/Program.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi
2 | {
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.Hosting;
5 |
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup();
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/RegisterInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using Application.Boundaries.Register;
4 | using Domain.Accounts.ValueObjects;
5 | using Domain.Customers.ValueObjects;
6 | using Xunit;
7 |
8 | public sealed class RegisterInputValidationTests
9 | {
10 | [Fact]
11 | public void GivenValidData_InputCreated()
12 | {
13 | var actual = new RegisterInput(
14 | new SSN("19860817999"),
15 | new PositiveMoney(10));
16 | Assert.NotNull(actual);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/IUseCase.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries
2 | {
3 | using System.Threading.Tasks;
4 |
5 | ///
6 | /// Use Case.
7 | ///
8 | /// Any Input Message.
9 | public interface IUseCase
10 | where TUseCaseInput : IUseCaseInput
11 | {
12 | ///
13 | /// Executes the Use Case.
14 | ///
15 | /// Input Message.
16 | /// Task.
17 | Task Execute(TUseCaseInput input);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sql-docker-up.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | sudo docker pull mcr.microsoft.com/mssql/server:2017-latest
3 | sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=' -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest
4 | sudo docker exec -it sql1 /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '' -Q 'ALTER LOGIN SA WITH PASSWORD=""'
5 |
6 |
7 | # query
8 |
9 | # sudo docker exec -it sql1 "bash"
10 | # /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P ''
11 | # SELECT Name from sys.Databases
12 | # GO
13 | # USE MangaDB01
14 | # GO
15 | # SELECT * FROM Account
16 | # GO
--------------------------------------------------------------------------------
/src/Domain/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | // ACTION REQUIRED: This file was automatically added to your project, but it
3 | // will not take effect until additional steps are taken to enable it. See the
4 | // following page for additional information:
5 | //
6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md
7 |
8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
9 | "settings": {
10 | "documentationRules": {
11 | "companyName": "Ivan Paulovich"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Deposit/DepositRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Deposit
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// The request to Deposit.
8 | ///
9 | public sealed class DepositRequest
10 | {
11 | ///
12 | /// Gets or sets the Account ID.
13 | ///
14 | [Required]
15 | public Guid AccountId { get; set; }
16 |
17 | ///
18 | /// Gets or sets the amount to Deposit.
19 | ///
20 | [Required]
21 | public decimal Amount { get; set; }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/WebApi/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Warning"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "DefaultConnection": "Server=localhost;User Id=sa;Password=;Database=MangaDB01"
11 | },
12 | "FeatureManagement": {
13 | "Transfer": true,
14 | "GetAccountDetailsV2": true
15 | },
16 | "GitHub": {
17 | "ClientId": "97c352e5dd81b63da60c",
18 | "ClientSecret": "584e60c5b3cb801f778c4e7a30dc457f1275f8f1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/CloseAccount/CloseAccountPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.CloseAccount
2 | {
3 | using Application.Boundaries.CloseAccount;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | public sealed class CloseAccountPresenter : IOutputPort
7 | {
8 | public IActionResult ViewModel { get; private set; }
9 |
10 | public void NotFound(string message)
11 | {
12 | ViewModel = new NotFoundObjectResult(message);
13 | }
14 |
15 | public void Standard(CloseAccountOutput closeAccountOutput)
16 | {
17 | ViewModel = new OkObjectResult(closeAccountOutput.AccountId);
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Withdraw/WithdrawRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Withdraw
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// Withdraw Request.
8 | ///
9 | public class WithdrawRequest
10 | {
11 | ///
12 | /// Gets or sets the Account ID.
13 | ///
14 | [Required]
15 | public Guid AccountId { get; set; }
16 |
17 | ///
18 | /// Gets or sets the amount to withdraw.
19 | ///
20 | [Required]
21 | public decimal Amount { get; set; }
22 | }
23 | }
--------------------------------------------------------------------------------
/test/AcceptanceTests/HttpClientExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace ComponentTests
2 | {
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 |
6 | public static class HttpClientExtensions
7 | {
8 | public static async Task PatchAsync(this HttpClient client, string requestUri, HttpContent content)
9 | {
10 | var method = new HttpMethod("PATCH");
11 | var request = new HttpRequestMessage(method, requestUri)
12 | {
13 | Content = content,
14 | };
15 |
16 | var response = await client.SendAsync(request);
17 | return response;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/CloseAccount/CloseAccountResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.CloseAccount
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 | using Application.Boundaries.CloseAccount;
6 |
7 | ///
8 | /// Close Account Response.
9 | ///
10 | public sealed class CloseAccountResponse
11 | {
12 | public CloseAccountResponse(CloseAccountOutput output)
13 | {
14 | AccountId = output.AccountId.ToGuid();
15 | }
16 |
17 | ///
18 | /// Gets account ID.
19 | ///
20 | [Required]
21 | public Guid AccountId { get; }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Entities/Debit.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | public class Debit : Domain.Accounts.Debits.Debit
8 | {
9 | public Debit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate)
10 | {
11 | AccountId = account.Id;
12 | Amount = amountToWithdraw;
13 | TransactionDate = transactionDate;
14 | }
15 |
16 | protected Debit()
17 | {
18 | }
19 |
20 | public AccountId AccountId { get; protected set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Entities/Credit.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | public class Credit : Domain.Accounts.Credits.Credit
8 | {
9 | public Credit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate)
10 | {
11 | AccountId = account.Id;
12 | Amount = amountToDeposit;
13 | TransactionDate = transactionDate;
14 | }
15 |
16 | protected Credit()
17 | {
18 | }
19 |
20 | public AccountId AccountId { get; protected set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ICustomerFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | using Domain.Customers.ValueObjects;
4 |
5 | ///
6 | /// Customer Entity Factory Domain-Driven Design Pattern.
7 | ///
8 | public interface ICustomerFactory
9 | {
10 | ///
11 | /// Creates a new Customer.
12 | ///
13 | /// SSN.
14 | /// Name.
15 | /// New Customer instance.
16 | ICustomer NewCustomer(SSN ssn, Name name);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Debit.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | public class Debit : Domain.Accounts.Debits.Debit
8 | {
9 | public Debit(
10 | IAccount account,
11 | PositiveMoney amountToWithdraw,
12 | DateTime transactionDate)
13 | {
14 | AccountId = account.Id;
15 | Amount = amountToWithdraw;
16 | TransactionDate = transactionDate;
17 | }
18 |
19 | protected Debit()
20 | {
21 | }
22 |
23 | public AccountId AccountId { get; protected set; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Credit.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | public class Credit : Domain.Accounts.Credits.Credit
8 | {
9 | public Credit(
10 | IAccount account,
11 | PositiveMoney amountToDeposit,
12 | DateTime transactionDate)
13 | {
14 | AccountId = account.Id;
15 | Amount = amountToDeposit;
16 | TransactionDate = transactionDate;
17 | }
18 |
19 | protected Credit()
20 | {
21 | }
22 |
23 | public AccountId AccountId { get; protected set; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Domain/Security/IUser.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | ///
7 | /// User Aggregate Root Domain-Driven Design Pattern.
8 | ///
9 | public interface IUser
10 | {
11 | ///
12 | /// Gets the External UserId.
13 | ///
14 | ExternalUserId ExternalUserId { get; }
15 |
16 | ///
17 | /// Gets the CustomerId.
18 | ///
19 | CustomerId CustomerId { get; }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Services/UserService.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Services
2 | {
3 | using System;
4 | using Domain.Customers.ValueObjects;
5 | using Domain.Security.Services;
6 | using Domain.Security.ValueObjects;
7 |
8 | public sealed class UserService : IUserService
9 | {
10 | public CustomerId? GetCustomerId()
11 | {
12 | return new CustomerId(Guid.NewGuid());
13 | }
14 |
15 | public ExternalUserId GetExternalUserId()
16 | {
17 | return new ExternalUserId("github/ivanpaulovich");
18 | }
19 |
20 | public Name GetUserName()
21 | {
22 | return new Name("Ivan Paulovich");
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "activityBar.background": "#1f6fd0",
4 | "activityBar.activeBorder": "#ee90bb",
5 | "activityBar.foreground": "#e7e7e7",
6 | "activityBar.inactiveForeground": "#e7e7e799",
7 | "activityBarBadge.background": "#ee90bb",
8 | "activityBarBadge.foreground": "#15202b",
9 | "titleBar.activeBackground": "#1857a4",
10 | "titleBar.inactiveBackground": "#1857a499",
11 | "titleBar.activeForeground": "#e7e7e7",
12 | "titleBar.inactiveForeground": "#e7e7e799",
13 | "statusBar.background": "#1857a4",
14 | "statusBarItem.hoverBackground": "#1f6fd0",
15 | "statusBar.foreground": "#e7e7e7"
16 | },
17 | "peacock.color": "#1857a4"
18 | }
--------------------------------------------------------------------------------
/src/Domain/Security/IUserFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | ///
7 | /// User Entity Factory Domain-Driven Design Pattern.
8 | ///
9 | public interface IUserFactory
10 | {
11 | ///
12 | /// Creates new User.
13 | ///
14 | /// Customer object.
15 | /// ExternalUserId.
16 | /// New User instance.
17 | IUser NewUser(CustomerId customer, ExternalUserId externalUserId);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/CloseAccount/CloseAccountInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.CloseAccount
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Close Account Input Message.
7 | ///
8 | public sealed class CloseAccountInput : IUseCaseInput
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// Account Id.
14 | public CloseAccountInput(AccountId accountId)
15 | {
16 | this.AccountId = accountId;
17 | }
18 |
19 | ///
20 | /// Gets AccountId.
21 | ///
22 | public AccountId AccountId { get; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using Domain.Accounts.ValueObjects;
6 | using Domain.Customers;
7 | using Domain.Customers.ValueObjects;
8 |
9 | public class Customer : Domain.Customers.Customer
10 | {
11 | public Customer()
12 | {
13 | }
14 |
15 | public Customer(SSN ssn, Name name)
16 | {
17 | Id = new CustomerId(Guid.NewGuid());
18 | SSN = ssn;
19 | Name = name;
20 | }
21 |
22 | public void LoadAccounts(IEnumerable accounts)
23 | {
24 | Accounts = new AccountCollection();
25 | Accounts.Add(accounts);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/CloseAccount/CloseAccountOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.CloseAccount
2 | {
3 | using Domain.Accounts;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Close Account Output Message.
8 | ///
9 | public sealed class CloseAccountOutput : IUseCaseOutput
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// IAccount object.
15 | public CloseAccountOutput(IAccount account)
16 | {
17 | this.AccountId = account.Id;
18 | }
19 |
20 | ///
21 | /// Gets AccountId.
22 | ///
23 | public AccountId AccountId { get; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetAccountDetails/GetAccountDetailsInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetAccountDetails
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Get Account Details Input Message.
7 | ///
8 | public sealed class GetAccountDetailsInput : IUseCaseInput
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// Account Id.
14 | public GetAccountDetailsInput(AccountId accountId)
15 | {
16 | this.AccountId = accountId;
17 | }
18 |
19 | ///
20 | /// Gets the AccountId.
21 | ///
22 | public AccountId AccountId { get; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/WebApi/Filters/BusinessExceptionFilter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.Filters
2 | {
3 | using Domain;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.AspNetCore.Mvc.Filters;
6 |
7 | public sealed class BusinessExceptionFilter : IExceptionFilter
8 | {
9 | public void OnException(ExceptionContext context)
10 | {
11 | if (context.Exception is DomainException)
12 | {
13 | var problemDetails = new ProblemDetails
14 | {
15 | Status = 400,
16 | Title = "Bad Request",
17 | Detail = context.Exception.Message,
18 | };
19 |
20 | context.Result = new BadRequestObjectResult(problemDetails);
21 | context.Exception = null;
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Domain/Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | enable
6 | true
7 | 8.0
8 | true
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 | Domain.ruleset
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/DepositPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.Deposit;
5 |
6 | public sealed class DepositPresenter : IOutputPort
7 | {
8 | public DepositPresenter()
9 | {
10 | Deposits = new Collection();
11 | NotFounds = new Collection();
12 | }
13 |
14 | public Collection Deposits { get; }
15 |
16 | public Collection NotFounds { get; }
17 |
18 | public void Standard(DepositOutput output)
19 | {
20 | Deposits.Add(output);
21 | }
22 |
23 | public void NotFound(string message)
24 | {
25 | NotFounds.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Domain/Security/Services/IUserService.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security.Services
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 |
6 | ///
7 | /// User Service.
8 | ///
9 | public interface IUserService
10 | {
11 | ///
12 | /// Get Customer Id.
13 | ///
14 | /// CustomerId.
15 | CustomerId? GetCustomerId();
16 |
17 | ///
18 | /// Get External User Id.
19 | ///
20 | /// External User Id.
21 | ExternalUserId GetExternalUserId();
22 |
23 | ///
24 | /// Get User Name.
25 | ///
26 | /// User Name.
27 | Name GetUserName();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Application/Application.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | enable
6 | true
7 | 8.0
8 | true
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 | Application.ruleset
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/TransferPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.Transfer;
5 |
6 | public sealed class TransferPresenter : IOutputPort
7 | {
8 | public TransferPresenter()
9 | {
10 | Transfers = new Collection();
11 | NotFounds = new Collection();
12 | }
13 |
14 | public Collection Transfers { get; }
15 |
16 | public Collection NotFounds { get; }
17 |
18 | public void Standard(TransferOutput output)
19 | {
20 | Transfers.Add(output);
21 | }
22 |
23 | public void NotFound(string message)
24 | {
25 | NotFounds.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/test/UnitTests/EntitiesTests/CustomerTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.EntitiesTests
2 | {
3 | using Domain.Customers;
4 | using Domain.Customers.ValueObjects;
5 | using Xunit;
6 |
7 | public class CustomerTests
8 | {
9 | [Fact]
10 | public void Customer_Should_Be_Registered_With_1_Account()
11 | {
12 | var entityFactory = new Infrastructure.InMemoryDataAccess.EntityFactory();
13 |
14 | // Arrange
15 | ICustomer sut = entityFactory.NewCustomer(
16 | new SSN("198608179922"),
17 | new Name("Ivan Paulovich"));
18 |
19 | var account = entityFactory.NewAccount(sut.Id);
20 |
21 | // Act
22 | sut.Register(account.Id);
23 |
24 | // Assert
25 | Assert.Single(sut.Accounts.GetAccountIds());
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Entities/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using Domain.Accounts.ValueObjects;
6 | using Domain.Customers;
7 | using Domain.Customers.ValueObjects;
8 |
9 | public class Customer : Domain.Customers.Customer
10 | {
11 | public Customer(
12 | SSN ssn,
13 | Name name)
14 | {
15 | Id = new CustomerId(Guid.NewGuid());
16 | SSN = ssn;
17 | Name = name;
18 | }
19 |
20 | protected Customer()
21 | {
22 | }
23 |
24 | public void LoadAccounts(IEnumerable accounts)
25 | {
26 | Accounts = new AccountCollection();
27 | Accounts.Add(accounts);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Transfer/TransferRequest.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Transfer
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// Transfer Request.
8 | ///
9 | public sealed class TransferRequest
10 | {
11 | ///
12 | /// Gets or sets origin Account ID.
13 | ///
14 | [Required]
15 | public Guid OriginAccountId { get; set; }
16 |
17 | ///
18 | /// Gets or sets destination Account ID.
19 | ///
20 | [Required]
21 | public Guid DestinationAccountId { get; set; }
22 |
23 | ///
24 | /// Gets or sets amount Transferred.
25 | ///
26 | [Required]
27 | public decimal Amount { get; set; }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Transfer/TransferResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Transfer
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | public sealed class TransferResponse
7 | {
8 | public TransferResponse(decimal amount, string description, DateTime transactionDate, decimal updatedBalance)
9 | {
10 | Amount = amount;
11 | Description = description;
12 | TransactionDate = transactionDate;
13 | UpdateBalance = updatedBalance;
14 | }
15 |
16 | [Required]
17 | public decimal Amount { get; }
18 |
19 | [Required]
20 | public string Description { get; }
21 |
22 | [Required]
23 | public DateTime TransactionDate { get; }
24 |
25 | [Required]
26 | public decimal UpdateBalance { get; }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/UserInterfaceV2Extensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Domain.Accounts;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using WebApi.UseCases.V2.GetAccountDetails;
6 |
7 | public static class UserInterfaceV2Extensions
8 | {
9 | public static IServiceCollection AddPresentersV2(this IServiceCollection services)
10 | {
11 | services.AddScoped();
12 | services.AddScoped(
13 | ctx => new Application.UseCases.GetAccountDetails(
14 | ctx.GetRequiredService(),
15 | ctx.GetRequiredService()));
16 |
17 | return services;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/RegisterPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.Register;
5 |
6 | public sealed class RegisterPresenter : IOutputPort
7 | {
8 | public RegisterPresenter()
9 | {
10 | Registers = new Collection();
11 | AlreadyRegistered = new Collection();
12 | }
13 |
14 | public Collection Registers { get; }
15 |
16 | public Collection AlreadyRegistered { get; }
17 |
18 | public void Standard(RegisterOutput output)
19 | {
20 | Registers.Add(output);
21 | }
22 |
23 | public void CustomerAlreadyRegistered(string message)
24 | {
25 | AlreadyRegistered.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/CloseAccountPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.CloseAccount;
5 |
6 | public sealed class CloseAccountPresenter : IOutputPort
7 | {
8 | public CloseAccountPresenter()
9 | {
10 | ClosedAccounts = new Collection();
11 | NotFounds = new Collection();
12 | }
13 |
14 | public Collection ClosedAccounts { get; }
15 |
16 | public Collection NotFounds { get; }
17 |
18 | public void Standard(CloseAccountOutput output)
19 | {
20 | ClosedAccounts.Add(output);
21 | }
22 |
23 | public void NotFound(string message)
24 | {
25 | NotFounds.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/CloseAccountInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using System;
4 | using Application.Boundaries.CloseAccount;
5 | using Domain.Accounts.ValueObjects;
6 | using Xunit;
7 |
8 | public sealed class CloseAccountInputValidationTests
9 | {
10 | [Fact]
11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException()
12 | {
13 | var actualEx = Assert.Throws(
14 | () => new AccountId(Guid.Empty));
15 | Assert.Contains("accountId", actualEx.Message);
16 | }
17 |
18 | [Fact]
19 | public void GivenValidData_InputCreated()
20 | {
21 | var actual = new CloseAccountInput(
22 | new AccountId(Guid.NewGuid()));
23 | Assert.NotNull(actual);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Domain/Security/IUserRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | using System.Threading.Tasks;
4 | using Domain.Security.ValueObjects;
5 |
6 | ///
7 | /// User Repository Domain-Driven Design Pattern.
8 | ///
9 | public interface IUserRepository
10 | {
11 | ///
12 | /// Gets User.
13 | ///
14 | /// External UserId.
15 | /// User.
16 | Task Get(ExternalUserId externalUserId);
17 |
18 | ///
19 | /// Adds the User.
20 | ///
21 | /// User instance.
22 | /// Task.
23 | Task Add(IUser user);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/MangaContext.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | public sealed class MangaContext : DbContext
6 | {
7 | public MangaContext(DbContextOptions options)
8 | : base(options)
9 | {
10 | }
11 |
12 | public DbSet Users { get; set; } = null!;
13 |
14 | public DbSet Accounts { get; set; } = null!;
15 |
16 | public DbSet Customers { get; set; } = null!;
17 |
18 | public DbSet Credits { get; set; } = null!;
19 |
20 | public DbSet Debits { get; set; } = null!;
21 |
22 | protected override void OnModelCreating(ModelBuilder builder)
23 | {
24 | builder.ApplyConfigurationsFromAssembly(typeof(MangaContext).Assembly);
25 | SeedData.Seed(builder);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/HttpMetricsExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Microsoft.AspNetCore.Builder;
4 | using Prometheus;
5 |
6 | public static class HttpMetricsExtensions
7 | {
8 | public static IApplicationBuilder UseMangaHttpMetrics(this IApplicationBuilder appBuilder)
9 | {
10 | return appBuilder.UseHttpMetrics(options =>
11 | {
12 | options.RequestDuration.Enabled = false;
13 | options.InProgress.Enabled = false;
14 | options.RequestCount.Counter = Metrics.CreateCounter(
15 | "http_requests_total",
16 | "HTTP Requests Total",
17 | new CounterConfiguration
18 | {
19 | LabelNames = new[] { "controller", "method", "code" },
20 | });
21 | });
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Deposit/DepositPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Deposit
2 | {
3 | using Application.Boundaries.Deposit;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | public sealed class DepositPresenter : IOutputPort
7 | {
8 | public IActionResult ViewModel { get; private set; }
9 |
10 | public void NotFound(string message)
11 | {
12 | ViewModel = new NotFoundObjectResult(message);
13 | }
14 |
15 | public void Standard(DepositOutput depositOutput)
16 | {
17 | var depositResponse = new DepositResponse(
18 | depositOutput.Transaction.Amount.ToMoney().ToDecimal(),
19 | depositOutput.Transaction.Description,
20 | depositOutput.Transaction.TransactionDate,
21 | depositOutput.UpdatedBalance.ToDecimal());
22 | ViewModel = new ObjectResult(depositResponse);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Domain/Customers/ICustomer.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | using Domain.Accounts.ValueObjects;
4 | using Domain.Customers.ValueObjects;
5 |
6 | ///
7 | /// Customer Aggregate Root Domain-Driven Design Pattern.
8 | ///
9 | public interface ICustomer
10 | {
11 | ///
12 | /// Gets the CustomerId.
13 | ///
14 | CustomerId Id { get; }
15 |
16 | ///
17 | /// Gets the Accounts.
18 | ///
19 | AccountCollection Accounts { get; }
20 |
21 | ///
22 | /// Register the Account into the Customer.
23 | ///
24 | /// Account Id.
25 | void Register(AccountId accountId);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/GetAccountDetailsPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.GetAccountDetails;
5 |
6 | public sealed class GetAccountDetailsPresenter : IOutputPort
7 | {
8 | public GetAccountDetailsPresenter()
9 | {
10 | GetAccountDetails = new Collection();
11 | NotFounds = new Collection();
12 | }
13 |
14 | public Collection GetAccountDetails { get; }
15 |
16 | public Collection NotFounds { get; }
17 |
18 | public void Standard(GetAccountDetailsOutput output)
19 | {
20 | GetAccountDetails.Add(output);
21 | }
22 |
23 | public void NotFound(string message)
24 | {
25 | NotFounds.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/GetCustomerDetailsPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.GetCustomerDetails;
5 |
6 | public sealed class GetCustomerDetailsPresenter : IOutputPort
7 | {
8 | public GetCustomerDetailsPresenter()
9 | {
10 | GetCustomerDetails = new Collection();
11 | NotFounds = new Collection();
12 | }
13 |
14 | public Collection GetCustomerDetails { get; }
15 |
16 | public Collection NotFounds { get; }
17 |
18 | public void Standard(GetCustomerDetailsOutput output)
19 | {
20 | GetCustomerDetails.Add(output);
21 | }
22 |
23 | public void NotFound(string message)
24 | {
25 | NotFounds.Add(message);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Withdraw/WithdrawResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Withdraw
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | public sealed class WithdrawResponse
7 | {
8 | public WithdrawResponse(
9 | decimal amount,
10 | string description,
11 | DateTime transactionDate,
12 | decimal updatedBalance)
13 | {
14 | Amount = amount;
15 | Description = description;
16 | TransactionDate = transactionDate;
17 | UpdateBalance = updatedBalance;
18 | }
19 |
20 | [Required]
21 | public decimal Amount { get; }
22 |
23 | [Required]
24 | public string Description { get; }
25 |
26 | [Required]
27 | public DateTime TransactionDate { get; }
28 |
29 | [Required]
30 | public decimal UpdateBalance { get; }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/FeatureFlags/FeatureFlagsExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection.FeatureFlags
2 | {
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.FeatureManagement;
6 |
7 | public static class FeatureFlagsExtensions
8 | {
9 | public static IServiceCollection AddFeatureFlags(this IServiceCollection services, IConfiguration configuration)
10 | {
11 | services.AddFeatureManagement(configuration);
12 |
13 | var featureManager = services.BuildServiceProvider()
14 | .GetRequiredService();
15 |
16 | services.AddMvc()
17 | .ConfigureApplicationPartManager(apm =>
18 | apm.FeatureProviders.Add(
19 | new CustomControllerFeatureProvider(featureManager)));
20 |
21 | return services;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/GetAccountDetailsInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using System;
4 | using Application.Boundaries.GetAccountDetails;
5 | using Domain.Accounts.ValueObjects;
6 | using Xunit;
7 |
8 | public sealed class GetAccountDetailsInputValidationTests
9 | {
10 | [Fact]
11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException()
12 | {
13 | var actualEx = Assert.Throws(
14 | () => new GetAccountDetailsInput(
15 | new AccountId(Guid.Empty)));
16 | Assert.Contains("accountId", actualEx.Message);
17 | }
18 |
19 | [Fact]
20 | public void GivenValidData_InputCreated()
21 | {
22 | var actual = new GetAccountDetailsInput(
23 | new AccountId(Guid.NewGuid()));
24 | Assert.NotNull(actual);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Account.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using Domain.Accounts.Credits;
6 | using Domain.Accounts.Debits;
7 | using Domain.Accounts.ValueObjects;
8 | using Domain.Customers.ValueObjects;
9 |
10 | public class Account : Domain.Accounts.Account
11 | {
12 | public Account(CustomerId customerId)
13 | {
14 | Id = new AccountId(Guid.NewGuid());
15 | CustomerId = customerId;
16 | }
17 |
18 | protected Account()
19 | {
20 | }
21 |
22 | public CustomerId CustomerId { get; protected set; }
23 |
24 | public void Load(IList credits, IList debits)
25 | {
26 | Credits = new CreditsCollection();
27 | Credits.Add(credits);
28 |
29 | Debits = new DebitsCollection();
30 | Debits.Add(debits);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Transfer/TransferPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Transfer
2 | {
3 | using Application.Boundaries.Transfer;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | public sealed class TransferPresenter : IOutputPort
7 | {
8 | public IActionResult ViewModel { get; private set; }
9 |
10 | public void NotFound(string message)
11 | {
12 | ViewModel = new NotFoundObjectResult(message);
13 | }
14 |
15 | public void Standard(TransferOutput transferOutput)
16 | {
17 | var transferResponse = new
18 | {
19 | Amount = transferOutput.Transaction.Amount,
20 | Description = transferOutput.Transaction.Description,
21 | TransactionDate = transferOutput.Transaction.TransactionDate,
22 | UpdatedBalance = transferOutput.UpdatedBalance,
23 | };
24 | ViewModel = new ObjectResult(transferResponse);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | pool:
2 | vmImage: 'windows-2019'
3 |
4 | variables:
5 | buildConfiguration: 'Release'
6 |
7 | steps:
8 | - task: UseDotNet@2
9 | inputs:
10 | packageType: 'sdk'
11 | useGlobalJson: true
12 |
13 | - task: SonarCloudPrepare@1
14 | inputs:
15 | SonarCloud: 'Clean-Architecture'
16 | organization: 'piraces'
17 | scannerMode: 'MSBuild'
18 | projectKey: 'Clean-Architecture-tests'
19 | projectName: 'Clean-Architecture'
20 |
21 | - script: dotnet restore
22 | displayName: 'dotnet restore'
23 |
24 | - script: dotnet build --configuration $(buildConfiguration)
25 | displayName: 'dotnet build $(buildConfiguration)'
26 |
27 | - task: DotNetCoreCLI@2
28 | displayName: 'dotnet test $(buildConfiguration)'
29 | inputs:
30 | command: test
31 | projects: '**/*Tests/*.csproj'
32 | arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
33 |
34 | - task: SonarCloudAnalyze@1
35 |
36 | - task: SonarCloudPublish@1
37 | inputs:
38 | pollingTimeoutSec: '300'
39 |
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/DepositInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using System;
4 | using Application.Boundaries.Deposit;
5 | using Domain.Accounts.ValueObjects;
6 | using Xunit;
7 |
8 | public sealed class DepositInputValidationTests
9 | {
10 | [Fact]
11 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException()
12 | {
13 | var actualEx = Assert.Throws(
14 | () => new DepositInput(
15 | new AccountId(Guid.Empty),
16 | new PositiveMoney(10)));
17 | Assert.Contains("accountId", actualEx.Message);
18 | }
19 |
20 | [Fact]
21 | public void GivenValidData_InputCreated()
22 | {
23 | var actual = new DepositInput(
24 | new AccountId(Guid.NewGuid()),
25 | new PositiveMoney(10));
26 | Assert.NotNull(actual);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Entities/Account.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using Domain.Accounts.Credits;
6 | using Domain.Accounts.Debits;
7 | using Domain.Accounts.ValueObjects;
8 | using Domain.Customers;
9 | using Domain.Customers.ValueObjects;
10 |
11 | public class Account : Domain.Accounts.Account
12 | {
13 | public Account(CustomerId customerId)
14 | {
15 | Id = new AccountId(Guid.NewGuid());
16 | CustomerId = customerId;
17 | }
18 |
19 | protected Account()
20 | {
21 | }
22 |
23 | public CustomerId CustomerId { get; protected set; }
24 |
25 | public void Load(IList credits, IList debits)
26 | {
27 | Credits = new CreditsCollection();
28 | Credits.Add(credits);
29 |
30 | Debits = new DebitsCollection();
31 | Debits.Add(debits);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/WithdrawInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using System;
4 | using Application.Boundaries.Withdraw;
5 | using Domain.Accounts;
6 | using Domain.Accounts.ValueObjects;
7 | using Xunit;
8 |
9 | public sealed class WithdrawInputValidationTests
10 | {
11 | [Fact]
12 | public void GivenEmptyAccountId_InputNotCreated_ThrowsInputValidationException()
13 | {
14 | var actualEx = Assert.Throws(
15 | () => new WithdrawInput(
16 | new AccountId(Guid.Empty),
17 | new PositiveMoney(10)));
18 | Assert.Contains("accountId", actualEx.Message);
19 | }
20 |
21 | [Fact]
22 | public void GivenValidData_InputCreated()
23 | {
24 | var actual = new WithdrawInput(
25 | new AccountId(Guid.NewGuid()),
26 | new PositiveMoney(10));
27 | Assert.NotNull(actual);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Withdraw/WithdrawInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Withdraw
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Withdraw Input Message.
7 | ///
8 | public sealed class WithdrawInput : IUseCaseInput
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// Account Id.
14 | /// Amount.
15 | public WithdrawInput(
16 | AccountId accountId,
17 | PositiveMoney amount)
18 | {
19 | this.AccountId = accountId;
20 | this.Amount = amount;
21 | }
22 |
23 | ///
24 | /// Gets the Account Id.
25 | ///
26 | public AccountId AccountId { get; }
27 |
28 | ///
29 | /// Gets the Amount.
30 | ///
31 | public PositiveMoney Amount { get; }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Deposit/DepositInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Deposit
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Deposit Input Message.
7 | ///
8 | public sealed class DepositInput : IUseCaseInput
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// AccountId.
14 | /// Positive amount to deposit.
15 | public DepositInput(
16 | AccountId accountId,
17 | PositiveMoney amount)
18 | {
19 | this.AccountId = accountId;
20 | this.Amount = amount;
21 | }
22 |
23 | ///
24 | /// Gets AccountId.
25 | ///
26 | public AccountId AccountId { get; }
27 |
28 | ///
29 | /// Gets the Amount.
30 | ///
31 | public PositiveMoney Amount { get; }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Withdraw/WithdrawPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Withdraw
2 | {
3 | using Application.Boundaries.Withdraw;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | public sealed class WithdrawPresenter : IOutputPort
7 | {
8 | public IActionResult ViewModel { get; private set; }
9 |
10 | public void NotFound(string message)
11 | {
12 | ViewModel = new NotFoundObjectResult(message);
13 | }
14 |
15 | public void OutOfBalance(string message)
16 | {
17 | ViewModel = new BadRequestObjectResult(message);
18 | }
19 |
20 | public void Standard(WithdrawOutput withdrawOutput)
21 | {
22 | var withdrawResponse = new WithdrawResponse(
23 | withdrawOutput.Transaction.Amount.ToMoney().ToDecimal(),
24 | withdrawOutput.Transaction.Description,
25 | withdrawOutput.Transaction.TransactionDate,
26 | withdrawOutput.UpdatedBalance.ToDecimal());
27 | ViewModel = new ObjectResult(withdrawResponse);
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/WebApi/ViewModels/TransactionModel.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.ViewModels
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | ///
8 | /// Transaction.
9 | ///
10 | public sealed class TransactionModel
11 | {
12 | public TransactionModel(PositiveMoney amount, string description, DateTime transactionDate)
13 | {
14 | Amount = amount.ToMoney().ToDecimal();
15 | Description = description;
16 | TransactionDate = transactionDate;
17 | }
18 |
19 | ///
20 | /// Gets Amount.
21 | ///
22 | [Required]
23 | public decimal Amount { get; }
24 |
25 | ///
26 | /// Gets Description.
27 | ///
28 | [Required]
29 | public string Description { get; }
30 |
31 | ///
32 | /// Gets Transaction Date.
33 | ///
34 | [Required]
35 | public DateTime TransactionDate { get; }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Configuration/AccountConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration
2 | {
3 | using Domain.Accounts.ValueObjects;
4 | using Domain.Customers.ValueObjects;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
7 |
8 | public class AccountConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | builder.ToTable("Account");
13 |
14 | builder.Property(b => b.Id)
15 | .HasConversion(
16 | v => v.ToGuid(),
17 | v => new AccountId(v))
18 | .IsRequired();
19 |
20 | builder.Property(b => b.CustomerId)
21 | .HasConversion(
22 | v => v.ToGuid(),
23 | v => new CustomerId(v))
24 | .IsRequired();
25 |
26 | builder.Ignore(p => p.Credits)
27 | .Ignore(p => p.Debits);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/RegisterInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | using Domain.Accounts.ValueObjects;
4 | using Domain.Customers.ValueObjects;
5 |
6 | ///
7 | /// Register Input Message.
8 | ///
9 | public sealed class RegisterInput : IUseCaseInput
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// SSN.
15 | /// Positive amount.
16 | public RegisterInput(
17 | SSN ssn,
18 | PositiveMoney initialAmount)
19 | {
20 | this.SSN = ssn;
21 | this.InitialAmount = initialAmount;
22 | }
23 |
24 | ///
25 | /// Gets the SSN.
26 | ///
27 | public SSN SSN { get; }
28 |
29 | ///
30 | /// Gets the Initial Amount.
31 | ///
32 | public PositiveMoney InitialAmount { get; }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/Name.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | ///
4 | /// Name Entity Design Pattern.
5 | ///
6 | public readonly struct Name
7 | {
8 | private readonly string _text;
9 |
10 | ///
11 | /// Initializes a new instance of the struct.
12 | ///
13 | /// Name.
14 | public Name(string text)
15 | {
16 | if (string.IsNullOrWhiteSpace(text))
17 | {
18 | throw new NameShouldNotBeEmptyException($"The {nameof(text)} field is required.");
19 | }
20 |
21 | this._text = text;
22 | }
23 |
24 | ///
25 | /// Converts into string.
26 | ///
27 | /// string.
28 | public override string ToString()
29 | {
30 | return this._text;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Presenters/WithdrawPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Presenters
2 | {
3 | using System.Collections.ObjectModel;
4 | using Application.Boundaries.Withdraw;
5 |
6 | public sealed class WithdrawPresenter : IOutputPort
7 | {
8 | public WithdrawPresenter()
9 | {
10 | Withdrawals = new Collection();
11 | NotFounds = new Collection();
12 | OutOfBalances = new Collection();
13 | }
14 |
15 | public Collection Withdrawals { get; }
16 |
17 | public Collection NotFounds { get; }
18 |
19 | public Collection OutOfBalances { get; }
20 |
21 | public void Standard(WithdrawOutput output)
22 | {
23 | Withdrawals.Add(output);
24 | }
25 |
26 | public void NotFound(string message)
27 | {
28 | NotFounds.Add(message);
29 | }
30 |
31 | public void OutOfBalance(string message)
32 | {
33 | OutOfBalances.Add(message);
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Configuration/UserConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Domain.Security.ValueObjects;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
7 |
8 | public class UserConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | builder.ToTable("User");
13 |
14 | builder.Property(b => b.ExternalUserId)
15 | .HasConversion(
16 | v => v.ToString(),
17 | v => new ExternalUserId(v))
18 | .IsRequired();
19 |
20 | builder.Property(b => b.CustomerId)
21 | .HasConversion(
22 | v => v.ToGuid(),
23 | v => new CustomerId(v))
24 | .IsRequired();
25 |
26 | builder.HasKey(
27 | c => new { c.ExternalUserId, c.CustomerId }
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/UnitOfWork.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using Application.Services;
6 |
7 | public sealed class UnitOfWork : IUnitOfWork, IDisposable
8 | {
9 | private readonly MangaContext _context;
10 | private bool _disposed = false;
11 |
12 | public UnitOfWork(MangaContext context)
13 | {
14 | _context = context;
15 | }
16 |
17 | public async Task Save()
18 | {
19 | int affectedRows = await _context.SaveChangesAsync();
20 | return affectedRows;
21 | }
22 |
23 | public void Dispose()
24 | {
25 | Dispose(true);
26 | GC.SuppressFinalize(this);
27 | }
28 |
29 | private void Dispose(bool disposing)
30 | {
31 | if (!_disposed)
32 | {
33 | if (disposing)
34 | {
35 | _context.Dispose();
36 | }
37 | }
38 |
39 | _disposed = true;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/Domain/Security/ValueObjects/ExternalUserId.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security.ValueObjects
2 | {
3 | ///
4 | /// ExternalUserId Entity Design Pattern.
5 | ///
6 | public readonly struct ExternalUserId
7 | {
8 | private readonly string _text;
9 |
10 | ///
11 | /// Initializes a new instance of the struct.
12 | ///
13 | /// External User Id.
14 | public ExternalUserId(string text)
15 | {
16 | if (string.IsNullOrWhiteSpace(text))
17 | {
18 | throw new ExternalUserIdShouldNotBeEmptyException($"The '{nameof(text)}' field is required.");
19 | }
20 |
21 | this._text = text;
22 | }
23 |
24 | ///
25 | /// Converts into string.
26 | ///
27 | /// String.
28 | public override string ToString()
29 | {
30 | return this._text;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/AcceptanceTests/ComponentTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | false
6 | true
7 |
8 |
9 |
10 | ComponentTests.ruleset
11 |
12 |
13 |
14 | ComponentTests.ruleset
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ICustomerRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | using System.Threading.Tasks;
4 | using Domain.Customers.ValueObjects;
5 |
6 | ///
7 | /// Customer Repository Domain-Driven Design Pattern.
8 | ///
9 | public interface ICustomerRepository
10 | {
11 | ///
12 | /// Gets the Customer by Id.
13 | ///
14 | /// CustomerId.
15 | /// Customer.
16 | Task GetBy(CustomerId customerId);
17 |
18 | ///
19 | /// Adds the Customer.
20 | ///
21 | /// Customer object.
22 | /// Task.
23 | Task Add(ICustomer customer);
24 |
25 | ///
26 | /// Updates the Customer.
27 | ///
28 | /// Customer object.
29 | /// Task.
30 | Task Update(ICustomer customer);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Domain/Customers/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | using Domain.Accounts.ValueObjects;
4 | using Domain.Customers.ValueObjects;
5 |
6 | ///
7 | public abstract class Customer : ICustomer
8 | {
9 | ///
10 | /// Initializes a new instance of the class.
11 | ///
12 | public Customer()
13 | {
14 | this.Accounts = new AccountCollection();
15 | }
16 |
17 | ///
18 | public CustomerId Id { get; protected set; }
19 |
20 | ///
21 | /// Gets or sets Name.
22 | ///
23 | public Name Name { get; protected set; }
24 |
25 | ///
26 | /// Gets or sets SSN.
27 | ///
28 | public SSN SSN { get; protected set; }
29 |
30 | ///
31 | public AccountCollection Accounts { get; protected set; }
32 |
33 | ///
34 | public void Register(AccountId accountId)
35 | {
36 | this.Accounts ??= new AccountCollection();
37 | this.Accounts.Add(accountId);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Configuration/CustomerConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration
2 | {
3 | using Domain.Customers.ValueObjects;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
6 |
7 | public class CustomerConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder
10 | builder)
11 | {
12 | builder.ToTable("Customer");
13 |
14 | builder.Property(b => b.SSN)
15 | .HasConversion(
16 | v => v.ToString(),
17 | v => new SSN(v))
18 | .IsRequired();
19 |
20 | builder.Property(b => b.Name)
21 | .HasConversion(
22 | v => v.ToString(),
23 | v => new Name(v))
24 | .IsRequired();
25 |
26 | builder.Property(b => b.Id)
27 | .HasConversion(
28 | v => v.ToGuid(),
29 | v => new CustomerId(v))
30 | .IsRequired();
31 |
32 | builder.Ignore(p => p.Accounts);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/ContextFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System.IO;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Design;
6 | using Microsoft.Extensions.Configuration;
7 |
8 | public sealed class ContextFactory : IDesignTimeDbContextFactory
9 | {
10 | public MangaContext CreateDbContext(string[] args)
11 | {
12 | string connectionString = ReadDefaultConnectionStringFromAppSettings();
13 |
14 | var builder = new DbContextOptionsBuilder();
15 | builder.UseSqlServer(connectionString);
16 | return new MangaContext(builder.Options);
17 | }
18 |
19 | private string ReadDefaultConnectionStringFromAppSettings()
20 | {
21 | IConfigurationRoot configuration = new ConfigurationBuilder()
22 | .SetBasePath(Directory.GetCurrentDirectory())
23 | .AddJsonFile("appsettings.Production.json")
24 | .Build();
25 |
26 | string connectionString = configuration.GetConnectionString("DefaultConnection");
27 | return connectionString;
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Repositories/UserRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Repositories
2 | {
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Domain.Security;
6 | using Domain.Security.ValueObjects;
7 |
8 | public sealed class UserRepository : IUserRepository
9 | {
10 | private readonly MangaContext _context;
11 |
12 | public UserRepository(MangaContext context)
13 | {
14 | _context = context;
15 | }
16 |
17 | public async Task Add(IUser user)
18 | {
19 | _context.Users.Add((InMemoryDataAccess.User)user);
20 | await Task.CompletedTask;
21 | }
22 |
23 | public async Task Get(ExternalUserId externalUserId)
24 | {
25 | User user = _context.Users
26 | .Where(e => e.ExternalUserId.Equals(externalUserId))
27 | .SingleOrDefault();
28 |
29 | if (user is null)
30 | {
31 | throw new UserNotFoundException($"The user {externalUserId} does not exist or is not processed yet.");
32 | }
33 |
34 | return await Task.FromResult(user);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/WebApi/ViewModels/AccountDetailsModel.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.ViewModels
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 | using Domain.Accounts.ValueObjects;
7 |
8 | ///
9 | /// Account Details.
10 | ///
11 | public sealed class AccountDetailsModel
12 | {
13 | public AccountDetailsModel(
14 | AccountId accountId,
15 | Money currentBalance,
16 | List transactions)
17 | {
18 | AccountId = accountId.ToGuid();
19 | CurrentBalance = currentBalance.ToDecimal();
20 | Transactions = transactions;
21 | }
22 |
23 | ///
24 | /// Gets account ID.
25 | ///
26 | [Required]
27 | public Guid AccountId { get; }
28 |
29 | ///
30 | /// Gets current Balance.
31 | ///
32 | [Required]
33 | public decimal CurrentBalance { get; }
34 |
35 | ///
36 | /// Gets transactions.
37 | ///
38 | [Required]
39 | public List Transactions { get; }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/InMemoryInfrastructureExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Application.Services;
4 | using Domain.Accounts;
5 | using Domain.Customers;
6 | using Domain.Security;
7 | using Infrastructure.InMemoryDataAccess;
8 | using Infrastructure.InMemoryDataAccess.Repositories;
9 | using Microsoft.Extensions.DependencyInjection;
10 |
11 | public static class InMemoryInfrastructureExtensions
12 | {
13 | public static IServiceCollection AddInMemoryPersistence(this IServiceCollection services)
14 | {
15 | services.AddScoped();
16 | services.AddScoped();
17 | services.AddScoped();
18 |
19 | services.AddSingleton();
20 | services.AddScoped();
21 |
22 | services.AddScoped();
23 | services.AddScoped();
24 | services.AddScoped();
25 |
26 | return services;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Configuration/DebitConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Domain.Accounts.Debits;
2 | using Domain.Accounts.ValueObjects;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
5 |
6 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration
7 | {
8 | public class DebitConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | builder.ToTable("Debit");
13 |
14 | builder.Property(debit => debit.Amount)
15 | .HasConversion(
16 | value => value.ToMoney().ToDecimal(),
17 | value => new PositiveMoney(value))
18 | .IsRequired();
19 |
20 | builder.Property(debit => debit.Id)
21 | .HasConversion(
22 | value => value.ToGuid(),
23 | value => new DebitId(value))
24 | .IsRequired();
25 |
26 | builder.Property(debit => debit.AccountId)
27 | .HasConversion(
28 | value => value.ToGuid(),
29 | value => new AccountId(value))
30 | .IsRequired();
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Configuration/CreditConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Domain.Accounts.Credits;
2 | using Domain.Accounts.ValueObjects;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
5 |
6 | namespace Infrastructure.EntityFrameworkDataAccess.Configuration
7 | {
8 | public class CreditConfiguration : IEntityTypeConfiguration
9 | {
10 | public void Configure(EntityTypeBuilder builder)
11 | {
12 | builder.ToTable("Credit");
13 |
14 | builder.Property(credit => credit.Amount)
15 | .HasConversion(
16 | value => value.ToMoney().ToDecimal(),
17 | value => new PositiveMoney(value))
18 | .IsRequired();
19 |
20 | builder.Property(credit => credit.Id)
21 | .HasConversion(
22 | value => value.ToGuid(),
23 | value => new CreditId(value))
24 | .IsRequired();
25 |
26 | builder.Property(credit => credit.AccountId)
27 | .HasConversion(
28 | value => value.ToGuid(),
29 | value => new AccountId(value))
30 | .IsRequired();
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Withdraw/WithdrawOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Withdraw
2 | {
3 | using Domain.Accounts.Debits;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Withdraw Output Message.
8 | ///
9 | public sealed class WithdrawOutput : IUseCaseOutput
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Debit object.
15 | /// Updated balance.
16 | public WithdrawOutput(IDebit debit, Money updatedBalance)
17 | {
18 | Debit debitEntity = (Debit)debit;
19 |
20 | this.Transaction = new Transaction(
21 | debitEntity.Description,
22 | debitEntity.Amount,
23 | debitEntity.TransactionDate);
24 |
25 | this.UpdatedBalance = updatedBalance;
26 | }
27 |
28 | ///
29 | /// Gets the Transaction.
30 | ///
31 | public Transaction Transaction { get; }
32 |
33 | ///
34 | /// Gets the Updated Balance.
35 | ///
36 | public Money UpdatedBalance { get; }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetAccountDetails
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 | using Domain.Accounts.ValueObjects;
7 | using WebApi.ViewModels;
8 |
9 | ///
10 | /// Get Account Details.
11 | ///
12 | public sealed class GetAccountDetailsResponse
13 | {
14 | public GetAccountDetailsResponse(
15 | AccountId accountId,
16 | Money currentBalance,
17 | List transactions)
18 | {
19 | AccountId = accountId.ToGuid();
20 | CurrentBalance = currentBalance.ToDecimal();
21 | Transactions = transactions;
22 | }
23 |
24 | ///
25 | /// Gets account ID.
26 | ///
27 | [Required]
28 | public Guid AccountId { get; }
29 |
30 | ///
31 | /// Gets current Balance.
32 | ///
33 | [Required]
34 | public decimal CurrentBalance { get; }
35 |
36 | ///
37 | /// Gets transactions.
38 | ///
39 | [Required]
40 | public IList Transactions { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/WebApi/Filters/SwaggerDefaultValues.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.Filters
2 | {
3 | using Microsoft.AspNetCore.Mvc.ApiExplorer;
4 | using Microsoft.OpenApi.Models;
5 | using Swashbuckle.AspNetCore.SwaggerGen;
6 |
7 | ///
8 | /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter.
9 | ///
10 | /// This is only required due to bugs in the .
11 | /// Once they are fixed and published, this class can be removed.
12 | public class SwaggerDefaultValues : IOperationFilter
13 | {
14 | ///
15 | /// Applies the filter to the specified operation using the given context.
16 | ///
17 | /// The operation to apply the filter to.
18 | /// The current operation filter context.
19 | public void Apply(OpenApiOperation operation, OperationFilterContext context)
20 | {
21 | var apiDescription = context.ApiDescription;
22 |
23 | operation.Deprecated |= apiDescription.IsDeprecated();
24 |
25 | if (operation.Parameters is null)
26 | {
27 | return;
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Debits/Debit.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Debits
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Debit Entity Design Pattern.
8 | ///
9 | public abstract class Debit : IDebit
10 | {
11 | ///
12 | /// Gets or sets Id.
13 | ///
14 | public DebitId Id { get; protected set; }
15 |
16 | ///
17 | /// Gets or sets Amount.
18 | ///
19 | public PositiveMoney Amount { get; protected set; }
20 |
21 | ///
22 | /// Gets Description.
23 | ///
24 | public string Description { get => "Debit"; }
25 |
26 | ///
27 | /// Gets or sets Transaction Date.
28 | ///
29 | public DateTime TransactionDate { get; protected set; }
30 |
31 | ///
32 | /// Calculates the sum of amounts.
33 | ///
34 | /// Positive amount.
35 | /// The positive sum.
36 | public PositiveMoney Sum(PositiveMoney amount) => this.Amount.Add(amount);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Deposit/DepositOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Deposit
2 | {
3 | using Domain.Accounts.Credits;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Deposit Output Message.
8 | ///
9 | public sealed class DepositOutput : IUseCaseOutput
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Credit object.
15 | /// The updated balance.
16 | public DepositOutput(
17 | ICredit credit,
18 | Money updatedBalance)
19 | {
20 | Credit creditEntity = (Credit)credit;
21 |
22 | this.Transaction = new Transaction(
23 | creditEntity.Description,
24 | creditEntity.Amount,
25 | creditEntity.TransactionDate);
26 |
27 | this.UpdatedBalance = updatedBalance;
28 | }
29 |
30 | ///
31 | /// Gets the Transaction object.
32 | ///
33 | public Transaction Transaction { get; }
34 |
35 | ///
36 | /// Gets the updated balance.
37 | ///
38 | public Money UpdatedBalance { get; }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/VersioningExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | public static class VersioningExtensions
6 | {
7 | public static IServiceCollection AddVersioning(this IServiceCollection services)
8 | {
9 | services.AddApiVersioning(
10 | options =>
11 | {
12 | // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
13 | options.ReportApiVersions = true;
14 | });
15 | services.AddVersionedApiExplorer(
16 | options =>
17 | {
18 | // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
19 | // note: the specified format code will format the version as "'v'major[.minor][-status]"
20 | options.GroupNameFormat = "'v'VVV";
21 |
22 | // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
23 | // can also be used to control the format of the API version in route templates
24 | options.SubstituteApiVersionInUrl = true;
25 | });
26 |
27 | return services;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Deposit/DepositResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Deposit
2 | {
3 | using System;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | ///
7 | /// The response for a successfull Deposit.
8 | ///
9 | public sealed class DepositResponse
10 | {
11 | public DepositResponse(
12 | decimal amount,
13 | string description,
14 | DateTime transactionDate,
15 | decimal updatedBalance)
16 | {
17 | Amount = amount;
18 | Description = description;
19 | TransactionDate = transactionDate;
20 | UpdateBalance = updatedBalance;
21 | }
22 |
23 | ///
24 | /// Gets amount Deposited.
25 | ///
26 | [Required]
27 | public decimal Amount { get; }
28 |
29 | ///
30 | /// Gets description.
31 | ///
32 | [Required]
33 | public string Description { get; }
34 |
35 | ///
36 | /// Gets transaction Date.
37 | ///
38 | [Required]
39 | public DateTime TransactionDate { get; }
40 |
41 | ///
42 | /// Gets updated Balance.
43 | ///
44 | [Required]
45 | public decimal UpdateBalance { get; }
46 | }
47 | }
--------------------------------------------------------------------------------
/test/IntegrationTests/IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | false
6 | true
7 |
8 |
9 |
10 | IntegrationTests.ruleset
11 |
12 |
13 |
14 | IntegrationTests.ruleset
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/UnitTests/UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | false
6 | true
7 |
8 |
9 |
10 | UnitTests.ruleset
11 |
12 |
13 |
14 | UnitTests.ruleset
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Withdraw/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Withdraw
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Text description.
15 | /// Amount.
16 | /// Transaction Date.
17 | public Transaction(
18 | string description,
19 | PositiveMoney amount,
20 | DateTime transactionDate)
21 | {
22 | this.Description = description;
23 | this.Amount = amount;
24 | this.TransactionDate = transactionDate;
25 | }
26 |
27 | ///
28 | /// Gets the Description.
29 | ///
30 | public string Description { get; }
31 |
32 | ///
33 | /// Gets the Amount.
34 | ///
35 | public PositiveMoney Amount { get; }
36 |
37 | ///
38 | /// Gets the Transaction Date.
39 | ///
40 | public DateTime TransactionDate { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/Authentication/UserService.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection.Authentication
2 | {
3 | using System;
4 | using Domain.Customers.ValueObjects;
5 | using Domain.Security.Services;
6 | using Domain.Security.ValueObjects;
7 | using Microsoft.AspNetCore.Http;
8 |
9 | public sealed class UserService : IUserService
10 | {
11 | private readonly IHttpContextAccessor _httpContextAccessor;
12 |
13 | public UserService(IHttpContextAccessor httpContextAccessor)
14 | {
15 | _httpContextAccessor = httpContextAccessor;
16 | }
17 |
18 | public CustomerId? GetCustomerId()
19 | {
20 | return new CustomerId(Guid.NewGuid());
21 | }
22 |
23 | public ExternalUserId GetExternalUserId()
24 | {
25 | var user = _httpContextAccessor.HttpContext.User;
26 |
27 | var login = user.FindFirst(c => c.Type == "urn:github:login")?.Value;
28 | var externalUserId = new ExternalUserId($"{user.Identity.AuthenticationType}/{login}");
29 |
30 | return externalUserId;
31 | }
32 |
33 | public Name GetUserName()
34 | {
35 | var user = _httpContextAccessor.HttpContext.User;
36 |
37 | var username = new Name(user.Identity.Name);
38 |
39 | return username;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Debits/DebitId.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Debits
2 | {
3 | using System;
4 |
5 | ///
6 | /// Debit Value Object Domain-Driven Design Pattern.
7 | ///
8 | public readonly struct DebitId
9 | {
10 | private readonly Guid _debitId;
11 |
12 | ///
13 | /// Initializes a new instance of the struct.
14 | ///
15 | /// DebitId Guid.
16 | public DebitId(Guid debitId)
17 | {
18 | if (debitId == Guid.Empty)
19 | {
20 | throw new EmptyDebitIdException($"{nameof(debitId)} cannot be empty.");
21 | }
22 |
23 | this._debitId = debitId;
24 | }
25 |
26 | ///
27 | /// Converts into string.
28 | ///
29 | /// Serialized string.
30 | public override string ToString()
31 | {
32 | return this._debitId.ToString();
33 | }
34 |
35 | ///
36 | /// Converts into Guid.
37 | ///
38 | /// Guid representation.
39 | public Guid ToGuid() => this._debitId;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Text description.
15 | /// Positive amount.
16 | /// Transaction date.
17 | public Transaction(
18 | string description,
19 | PositiveMoney amount,
20 | DateTime transactionDate)
21 | {
22 | this.Description = description;
23 | this.Amount = amount;
24 | this.TransactionDate = transactionDate;
25 | }
26 |
27 | ///
28 | /// Gets the text description.
29 | ///
30 | public string Description { get; }
31 |
32 | ///
33 | /// Gets the Amount.
34 | ///
35 | public PositiveMoney Amount { get; }
36 |
37 | ///
38 | /// Gets the transaction date.
39 | ///
40 | public DateTime TransactionDate { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/ValueObjects/AccountId.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.ValueObjects
2 | {
3 | using System;
4 |
5 | ///
6 | /// AccountId Entity Design Pattern.
7 | ///
8 | public readonly struct AccountId
9 | {
10 | private readonly Guid _accountId;
11 |
12 | ///
13 | /// Initializes a new instance of the struct.
14 | ///
15 | /// AccountId Guid.
16 | public AccountId(Guid accountId)
17 | {
18 | if (accountId == Guid.Empty)
19 | {
20 | throw new EmptyAccountIdException($"{nameof(accountId)} cannot be empty.");
21 | }
22 |
23 | this._accountId = accountId;
24 | }
25 |
26 | ///
27 | /// Converts into string.
28 | ///
29 | /// String.
30 | public override string ToString()
31 | {
32 | return this._accountId.ToString();
33 | }
34 |
35 | ///
36 | /// Converts into Guid.
37 | ///
38 | /// Guid representation.
39 | public Guid ToGuid() => this._accountId;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/CustomerId.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | using System;
4 |
5 | ///
6 | /// CustomerId Entity Design Pattern.
7 | ///
8 | public readonly struct CustomerId
9 | {
10 | private readonly Guid _customerId;
11 |
12 | ///
13 | /// Initializes a new instance of the struct.
14 | ///
15 | /// Customer Guid.
16 | public CustomerId(Guid customerId)
17 | {
18 | if (customerId == Guid.Empty)
19 | {
20 | throw new EmptyCustomerIdException($"{nameof(customerId)} cannot be empty.");
21 | }
22 |
23 | this._customerId = customerId;
24 | }
25 |
26 | ///
27 | /// Converts into String.
28 | ///
29 | /// String.
30 | public override string ToString()
31 | {
32 | return this._customerId.ToString();
33 | }
34 |
35 | ///
36 | /// Converts into Guid.
37 | ///
38 | /// Guid.
39 | public Guid ToGuid() => this._customerId;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Deposit/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Deposit
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction Output object.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Text Description.
15 | /// Positive amount.
16 | /// Transaction Date.
17 | public Transaction(
18 | string description,
19 | PositiveMoney amount,
20 | DateTime transactionDate)
21 | {
22 | this.Description = description;
23 | this.Amount = amount;
24 | this.TransactionDate = transactionDate;
25 | }
26 |
27 | ///
28 | /// Gets the description.
29 | ///
30 | public string Description { get; }
31 |
32 | ///
33 | /// Gets the amount.
34 | ///
35 | public PositiveMoney Amount { get; }
36 |
37 | ///
38 | /// Gets the transaction date.
39 | ///
40 | public DateTime TransactionDate { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Credits/CreditId.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Credits
2 | {
3 | using System;
4 |
5 | ///
6 | /// CreditId Value Object Domain-Driven Design Pattern.
7 | ///
8 | public readonly struct CreditId
9 | {
10 | private readonly Guid _creditId;
11 |
12 | ///
13 | /// Initializes a new instance of the struct.
14 | ///
15 | /// CreditId.
16 | public CreditId(Guid creditId)
17 | {
18 | if (creditId == Guid.Empty)
19 | {
20 | throw new EmptyCreditIdException($"{nameof(creditId)} cannot be empty.");
21 | }
22 |
23 | this._creditId = creditId;
24 | }
25 |
26 | ///
27 | /// Converts into string.
28 | ///
29 | /// String representation.
30 | public override string ToString()
31 | {
32 | return this._creditId.ToString();
33 | }
34 |
35 | ///
36 | /// Converts into Guid.
37 | ///
38 | /// Guid representation.
39 | public Guid ToGuid() => this._creditId;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetCustomerDetails/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetCustomerDetails
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Text description.
15 | /// Positive amount.
16 | /// Transaction date.
17 | public Transaction(
18 | string description,
19 | PositiveMoney amount,
20 | DateTime transactionDate)
21 | {
22 | this.Description = description;
23 | this.Amount = amount;
24 | this.TransactionDate = transactionDate;
25 | }
26 |
27 | ///
28 | /// Gets the description.
29 | ///
30 | public string Description { get; }
31 |
32 | ///
33 | /// Gets the amount.
34 | ///
35 | public PositiveMoney Amount { get; }
36 |
37 | ///
38 | /// Gets the transaction date.
39 | ///
40 | public DateTime TransactionDate { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/Account.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | using System.Collections.Generic;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Account.
8 | ///
9 | public sealed class Account
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Account Id.
15 | /// Current Balance.
16 | /// Transactions list.
17 | public Account(
18 | AccountId accountId,
19 | Money currentBalance,
20 | List transactions)
21 | {
22 | this.AccountId = accountId;
23 | this.CurrentBalance = currentBalance;
24 | this.Transactions = transactions;
25 | }
26 |
27 | ///
28 | /// Gets the AccountId.
29 | ///
30 | public AccountId AccountId { get; }
31 |
32 | ///
33 | /// Gets the Current Balance.
34 | ///
35 | public Money CurrentBalance { get; }
36 |
37 | ///
38 | /// Gets the Transactions.
39 | ///
40 | public List Transactions { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Repositories/UserRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess.Repositories
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Domain.Security;
7 | using Domain.Security.ValueObjects;
8 | using Microsoft.EntityFrameworkCore;
9 |
10 | public sealed class UserRepository : IUserRepository
11 | {
12 | private readonly MangaContext _context;
13 |
14 | public UserRepository(MangaContext context)
15 | {
16 | _context = context ??
17 | throw new ArgumentNullException(nameof(context));
18 | }
19 |
20 | public async Task Add(IUser user)
21 | {
22 | await _context.Users.AddAsync((EntityFrameworkDataAccess.User)user);
23 | await _context.SaveChangesAsync();
24 | }
25 |
26 | public async Task Get(ExternalUserId externalUserId)
27 | {
28 | var user = await _context
29 | .Users
30 | .Where(a => a.ExternalUserId.Equals(externalUserId))
31 | .SingleOrDefaultAsync();
32 |
33 | if (user is null)
34 | {
35 | throw new UserNotFoundException($"The user {externalUserId} does not exist or is not processed yet.");
36 | }
37 |
38 | return user;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetAccountDetails/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetAccountDetails
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Text description.
15 | /// Positive amount.
16 | /// Transaction date.
17 | public Transaction(
18 | string description,
19 | PositiveMoney amount,
20 | DateTime transactionDate)
21 | {
22 | this.Description = description;
23 | this.Amount = amount;
24 | this.TransactionDate = transactionDate;
25 | }
26 |
27 | ///
28 | /// Gets the text description.
29 | ///
30 | public string Description { get; }
31 |
32 | ///
33 | /// Gets the amount.
34 | ///
35 | public PositiveMoney Amount { get; }
36 |
37 | ///
38 | /// Gets the transaction date.
39 | ///
40 | public DateTime TransactionDate { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/ValueObjects/PositiveMoney.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.ValueObjects
2 | {
3 | ///
4 | /// PositiveMoney Entity Design Pattern.
5 | ///
6 | public readonly struct PositiveMoney
7 | {
8 | private readonly Money _value;
9 |
10 | ///
11 | /// Initializes a new instance of the struct.
12 | ///
13 | /// Decimal amount.
14 | public PositiveMoney(decimal value)
15 | {
16 | if (value < 0)
17 | {
18 | throw new MoneyShouldBePositiveException("The 'Amount' should be positive.");
19 | }
20 |
21 | this._value = new Money(value);
22 | }
23 |
24 | ///
25 | /// Converts into Money Value Object.
26 | ///
27 | /// Money.
28 | public Money ToMoney() => this._value;
29 |
30 | internal PositiveMoney Add(PositiveMoney positiveAmount)
31 | {
32 | return this._value.Add(positiveAmount._value);
33 | }
34 |
35 | internal Money Subtract(PositiveMoney positiveAmount)
36 | {
37 | return this._value.Subtract(positiveAmount._value);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/SQLServerInfrastructureExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Application.Services;
4 | using Domain.Accounts;
5 | using Domain.Customers;
6 | using Domain.Security;
7 | using Infrastructure.EntityFrameworkDataAccess;
8 | using Infrastructure.EntityFrameworkDataAccess.Repositories;
9 | using Microsoft.EntityFrameworkCore;
10 | using Microsoft.Extensions.Configuration;
11 | using Microsoft.Extensions.DependencyInjection;
12 |
13 | public static class SQLServerInfrastructureExtensions
14 | {
15 | public static IServiceCollection AddSQLServerPersistence(
16 | this IServiceCollection services,
17 | IConfiguration configuration)
18 | {
19 | services.AddScoped();
20 | services.AddScoped();
21 | services.AddScoped();
22 |
23 | services.AddDbContext(
24 | options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
25 | services.AddScoped();
26 |
27 | services.AddScoped();
28 | services.AddScoped();
29 |
30 | return services;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetAccountDetails/GetAccountDetailsPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetAccountDetails
2 | {
3 | using System.Collections.Generic;
4 | using Application.Boundaries.GetAccountDetails;
5 | using Microsoft.AspNetCore.Mvc;
6 | using WebApi.ViewModels;
7 |
8 | public sealed class GetAccountDetailsPresenter : IOutputPort
9 | {
10 | public IActionResult ViewModel { get; private set; }
11 |
12 | public void NotFound(string message)
13 | {
14 | ViewModel = new NotFoundObjectResult(message);
15 | }
16 |
17 | public void Standard(GetAccountDetailsOutput getAccountDetailsOutput)
18 | {
19 | List transactions = new List();
20 |
21 | foreach (var item in getAccountDetailsOutput.Transactions)
22 | {
23 | var transaction = new TransactionModel(
24 | item.Amount,
25 | item.Description,
26 | item.TransactionDate);
27 |
28 | transactions.Add(transaction);
29 | }
30 |
31 | var getAccountDetailsResponse = new GetAccountDetailsResponse(
32 | getAccountDetailsOutput.AccountId,
33 | getAccountDetailsOutput.CurrentBalance,
34 | transactions);
35 |
36 | ViewModel = new OkObjectResult(getAccountDetailsResponse);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Register/RegisterResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Register
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 | using Domain.Customers.ValueObjects;
7 | using WebApi.ViewModels;
8 |
9 | ///
10 | /// The response for Registration.
11 | ///
12 | public sealed class RegisterResponse
13 | {
14 | public RegisterResponse(
15 | CustomerId customerId,
16 | SSN ssn,
17 | Name name,
18 | List accounts)
19 | {
20 | CustomerId = customerId.ToGuid();
21 | SSN = ssn.ToString();
22 | Name = name.ToString();
23 | Accounts = accounts;
24 | }
25 |
26 | ///
27 | /// Gets customer ID.
28 | ///
29 | [Required]
30 | public Guid CustomerId { get; }
31 |
32 | ///
33 | /// Gets sSN.
34 | ///
35 | [Required]
36 | public string SSN { get; }
37 |
38 | ///
39 | /// Gets name.
40 | ///
41 | [Required]
42 | public string Name { get; }
43 |
44 | ///
45 | /// Gets accounts.
46 | ///
47 | [Required]
48 | public List Accounts { get; }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/Credits/Credit.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.Credits
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Credit Entity Design Pattern.
8 | ///
9 | public abstract class Credit : ICredit
10 | {
11 | ///
12 | /// Gets or sets Id.
13 | ///
14 | public CreditId Id { get; protected set; }
15 |
16 | ///
17 | /// Gets or sets Amount.
18 | ///
19 | public PositiveMoney Amount { get; protected set; }
20 |
21 | ///
22 | /// Gets Description.
23 | ///
24 | public string Description
25 | {
26 | get { return "Credit"; }
27 | }
28 |
29 | ///
30 | /// Gets or sets Transaction Date.
31 | ///
32 | public DateTime TransactionDate { get; protected set; }
33 |
34 | ///
35 | /// Calculate the sum of positive amounts.
36 | ///
37 | /// Positive amount.
38 | /// The positive sum.
39 | public PositiveMoney Sum(PositiveMoney amount)
40 | {
41 | return this.Amount.Add(amount);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/ValueObjects/Money.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts.ValueObjects
2 | {
3 | ///
4 | /// Money Entity Design Pattern.
5 | ///
6 | public readonly struct Money
7 | {
8 | private readonly decimal _money;
9 |
10 | ///
11 | /// Initializes a new instance of the struct.
12 | ///
13 | /// Decimal amount.
14 | public Money(decimal value)
15 | {
16 | this._money = value;
17 | }
18 |
19 | ///
20 | /// Converts into decimal.
21 | ///
22 | /// decimal amount.
23 | public decimal ToDecimal() => this._money;
24 |
25 | internal bool LessThan(PositiveMoney amount)
26 | {
27 | return this._money < amount.ToMoney()._money;
28 | }
29 |
30 | internal bool IsZero()
31 | {
32 | return this._money == 0;
33 | }
34 |
35 | internal PositiveMoney Add(Money value)
36 | {
37 | return new PositiveMoney(this._money + value.ToDecimal());
38 | }
39 |
40 | internal Money Subtract(Money value)
41 | {
42 | return new Money(this._money - value.ToDecimal());
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Transfer/TransferInput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Transfer
2 | {
3 | using Domain.Accounts.ValueObjects;
4 |
5 | ///
6 | /// Transfer Input Message.
7 | ///
8 | public sealed class TransferInput : IUseCaseInput
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// Origin Account Id.
14 | /// Destination Account Id.
15 | /// Positive amount.
16 | public TransferInput(
17 | AccountId originAccountId,
18 | AccountId destinationAccountId,
19 | PositiveMoney amount)
20 | {
21 | this.OriginAccountId = originAccountId;
22 | this.DestinationAccountId = destinationAccountId;
23 | this.Amount = amount;
24 | }
25 |
26 | ///
27 | /// Gets Origin Account Id.
28 | ///
29 | public AccountId OriginAccountId { get; }
30 |
31 | ///
32 | /// Gets Destination Account Id.
33 | ///
34 | public AccountId DestinationAccountId { get; }
35 |
36 | ///
37 | /// Gets the Amount.
38 | ///
39 | public PositiveMoney Amount { get; }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetCustomerDetails/GetCustomerDetailsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetCustomerDetails
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 | using Domain.Customers.ValueObjects;
7 | using WebApi.ViewModels;
8 |
9 | ///
10 | /// The Customer Details.
11 | ///
12 | public sealed class GetCustomerDetailsResponse
13 | {
14 | public GetCustomerDetailsResponse(
15 | CustomerId customerId,
16 | SSN ssn,
17 | Name name,
18 | List accounts)
19 | {
20 | CustomerId = customerId.ToGuid();
21 | SSN = ssn.ToString();
22 | Name = name.ToString();
23 | Accounts = accounts;
24 | }
25 |
26 | ///
27 | /// Gets customer ID.
28 | ///
29 | [Required]
30 | public Guid CustomerId { get; }
31 |
32 | ///
33 | /// Gets SSN.
34 | ///
35 | [Required]
36 | public string SSN { get; }
37 |
38 | ///
39 | /// Gets name.
40 | ///
41 | [Required]
42 | public string Name { get; }
43 |
44 | ///
45 | /// Gets accounts.
46 | ///
47 | [Required]
48 | public IList Accounts { get; }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Withdraw/WithdrawTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCasesTests.Withdraw
2 | {
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.Withdraw;
6 | using Application.UseCases;
7 | using Domain.Accounts.ValueObjects;
8 | using Infrastructure.InMemoryDataAccess.Presenters;
9 | using UnitTests.TestFixtures;
10 | using Xunit;
11 |
12 | public sealed class WithdrawTests : IClassFixture
13 | {
14 | private readonly StandardFixture _fixture;
15 |
16 | public WithdrawTests(StandardFixture fixture)
17 | {
18 | _fixture = fixture;
19 | }
20 |
21 | [Theory]
22 | [ClassData(typeof(PositiveDataSetup))]
23 | public async Task Withdraw_Valid_Amount(
24 | decimal amount,
25 | decimal expectedBalance)
26 | {
27 | var presenter = new WithdrawPresenter();
28 | var sut = new Withdraw(
29 | _fixture.AccountService,
30 | presenter,
31 | _fixture.AccountRepository,
32 | _fixture.UnitOfWork);
33 |
34 | await sut.Execute(new WithdrawInput(
35 | _fixture.Context.DefaultAccountId,
36 | new PositiveMoney(amount)));
37 |
38 | var actual = presenter.Withdrawals.Last();
39 | Assert.Equal(expectedBalance, actual.UpdatedBalance.ToDecimal());
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/UnitTests/PresenterTests/RegisterPresenterTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.PresenterTests
2 | {
3 | using System.Net;
4 | using Application.Boundaries.Register;
5 | using Domain.Customers;
6 | using Domain.Customers.ValueObjects;
7 | using Domain.Security;
8 | using Domain.Security.ValueObjects;
9 | using Microsoft.AspNetCore.Mvc;
10 | using WebApi.UseCases.V1.Register;
11 | using Xunit;
12 |
13 | public sealed class RegisterPresenterTests
14 | {
15 | [Fact]
16 | public void GivenValidData_Handle_WritesOkObjectResult()
17 | {
18 | var customer = new Infrastructure.InMemoryDataAccess.Customer(
19 | new SSN("198608178888"),
20 | new Name("Ivan Paulovich"));
21 |
22 | var account = new Infrastructure.InMemoryDataAccess.Account(
23 | customer.Id);
24 |
25 | var registerOutput = new RegisterOutput(
26 | new ExternalUserId("github/ivanpaulovich"),
27 | customer,
28 | account);
29 |
30 | var sut = new RegisterPresenter();
31 | sut.Standard(registerOutput);
32 |
33 | var actual = Assert.IsType(sut.ViewModel);
34 | Assert.Equal((int)HttpStatusCode.Created, actual.StatusCode);
35 |
36 | var actualValue = (RegisterResponse)actual.Value;
37 | Assert.Equal(customer.Id.ToGuid(), actualValue.CustomerId);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Domain/Customers/ValueObjects/SSN.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers.ValueObjects
2 | {
3 | using System.Text.RegularExpressions;
4 |
5 | ///
6 | /// SSN Entity Design Pattern.
7 | ///
8 | public readonly struct SSN
9 | {
10 | private const string RegExForValidation = @"^\d{6,8}[-|(\s)]{0,1}\d{4}$";
11 |
12 | private readonly string _text;
13 |
14 | ///
15 | /// Initializes a new instance of the struct.
16 | ///
17 | /// SSN.
18 | public SSN(string text)
19 | {
20 | if (string.IsNullOrWhiteSpace(text))
21 | {
22 | throw new SSNShouldNotBeEmptyException($"The {nameof(text)} field is required.");
23 | }
24 |
25 | Regex regex = new Regex(RegExForValidation);
26 | Match match = regex.Match(text);
27 |
28 | if (!match.Success)
29 | {
30 | throw new InvalidSSNException($"Invalid {nameof(text)} format. Use YYMMDDNNNN.");
31 | }
32 |
33 | this._text = text;
34 | }
35 |
36 | ///
37 | /// Converts into string.
38 | ///
39 | /// string.
40 | public override string ToString() => this._text;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/EntityFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.Credits;
6 | using Domain.Accounts.Debits;
7 | using Domain.Accounts.ValueObjects;
8 | using Domain.Customers;
9 | using Domain.Customers.ValueObjects;
10 | using Domain.Security;
11 | using Domain.Security.ValueObjects;
12 |
13 | ///
14 | /// Entity Factory Domain-Driven Design Pattern.
15 | ///
16 | public sealed class EntityFactory : IUserFactory, ICustomerFactory, IAccountFactory
17 | {
18 | public IAccount NewAccount(CustomerId customerId) => new Account(customerId);
19 |
20 | public ICredit NewCredit(
21 | IAccount account,
22 | PositiveMoney amountToDeposit,
23 | DateTime transactionDate) => new Credit(account, amountToDeposit, transactionDate);
24 |
25 | public ICustomer NewCustomer(
26 | SSN ssn,
27 | Name name) => new Customer(ssn, name);
28 |
29 | public IDebit NewDebit(
30 | IAccount account,
31 | PositiveMoney amountToWithdraw,
32 | DateTime transactionDate) => new Debit(account, amountToWithdraw, transactionDate);
33 |
34 | public IUser NewUser(CustomerId customerId, ExternalUserId externalUserId) => new User(customerId, externalUserId);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/EntityFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts;
5 | using Domain.Accounts.Credits;
6 | using Domain.Accounts.Debits;
7 | using Domain.Accounts.ValueObjects;
8 | using Domain.Customers;
9 | using Domain.Customers.ValueObjects;
10 | using Domain.Security;
11 | using Domain.Security.ValueObjects;
12 |
13 | ///
14 | /// Entity Factory Domain-Driven Design Pattern.
15 | ///
16 | public sealed class EntityFactory : IUserFactory, ICustomerFactory, IAccountFactory
17 | {
18 | public IAccount NewAccount(CustomerId customerId) => new Account(customerId);
19 |
20 | public ICredit NewCredit(
21 | IAccount account,
22 | PositiveMoney amountToDeposit,
23 | DateTime transactionDate) => new Credit(account, amountToDeposit, transactionDate);
24 |
25 | public ICustomer NewCustomer(
26 | SSN ssn,
27 | Name name) => new Customer(ssn, name);
28 |
29 | public IDebit NewDebit(
30 | IAccount account,
31 | PositiveMoney amountToWithdraw,
32 | DateTime transactionDate) => new Debit(account, amountToWithdraw, transactionDate);
33 |
34 | public IUser NewUser(CustomerId customerId, ExternalUserId externalUserId) => new User(customerId, externalUserId);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Infrastructure/Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | enable
6 | true
7 | 8.0
8 | true
9 |
10 |
11 |
12 | Infrastructure.ruleset
13 |
14 |
15 |
16 | Infrastructure.ruleset
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/src/WebApi/bin/Debug/netcoreapp3.0/WebApi.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/src/WebApi",
16 | "stopAtEntry": false,
17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
18 | "serverReadyAction": {
19 | "action": "openExternally",
20 | "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"
21 | },
22 | "env": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | },
25 | "sourceFileMap": {
26 | "/Views": "${workspaceFolder}/Views"
27 | }
28 | },
29 | {
30 | "name": ".NET Core Attach",
31 | "type": "coreclr",
32 | "request": "attach",
33 | "processId": "${command:pickProcess}"
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Repositories/CustomerRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Repositories
2 | {
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Domain.Customers;
6 | using Domain.Customers.ValueObjects;
7 |
8 | public sealed class CustomerRepository : ICustomerRepository
9 | {
10 | private readonly MangaContext _context;
11 |
12 | public CustomerRepository(MangaContext context)
13 | {
14 | _context = context;
15 | }
16 |
17 | public async Task Add(ICustomer customer)
18 | {
19 | _context.Customers.Add((InMemoryDataAccess.Customer)customer);
20 | await Task.CompletedTask;
21 | }
22 |
23 | public async Task GetBy(CustomerId customerId)
24 | {
25 | var customer = _context.Customers
26 | .Where(e => e.Id.Equals(customerId))
27 | .SingleOrDefault();
28 |
29 | if (customer is null)
30 | {
31 | throw new CustomerNotFoundException($"The customer {customerId} does not exist or is not processed yet.");
32 | }
33 |
34 | return await Task.FromResult(customer);
35 | }
36 |
37 | public async Task Update(ICustomer customer)
38 | {
39 | Customer customerOld = _context.Customers
40 | .Where(e => e.Id.Equals(customer.Id))
41 | .SingleOrDefault();
42 |
43 | customerOld = (Customer)customer;
44 | await Task.CompletedTask;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/UnitTests/UseCaseTests/Transfer/TransferUseCaseTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.UseCaseTests.Transfer
2 | {
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.Transfer;
6 | using Application.UseCases;
7 | using Domain.Accounts.ValueObjects;
8 | using Infrastructure.InMemoryDataAccess.Presenters;
9 | using UnitTests.TestFixtures;
10 | using Xunit;
11 |
12 | public sealed class TransferUseCaseTests : IClassFixture
13 | {
14 | private readonly StandardFixture _fixture;
15 |
16 | public TransferUseCaseTests(StandardFixture fixture)
17 | {
18 | _fixture = fixture;
19 | }
20 |
21 | [Theory]
22 | [ClassData(typeof(PositiveDataSetup))]
23 | public async Task Transfer_ChangesBalance_WhenAccountExists(
24 | decimal amount,
25 | decimal expectedOriginBalance)
26 | {
27 | var presenter = new TransferPresenter();
28 | var sut = new Transfer(
29 | _fixture.AccountService,
30 | presenter,
31 | _fixture.AccountRepository,
32 | _fixture.UnitOfWork);
33 |
34 | await sut.Execute(
35 | new TransferInput(
36 | _fixture.Context.DefaultAccountId,
37 | _fixture.Context.SecondAccountId,
38 | new PositiveMoney(amount)));
39 |
40 | var actual = presenter.Transfers.Last();
41 | Assert.Equal(expectedOriginBalance, actual.UpdatedBalance.ToDecimal());
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/UnitTests/InputValidationTests/TransferInputValidationTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.InputValidationTests
2 | {
3 | using System;
4 | using Application.Boundaries.Transfer;
5 | using Domain.Accounts.ValueObjects;
6 | using Xunit;
7 |
8 | public sealed class TransferInputValidationTests
9 | {
10 | [Fact]
11 | public void GivenEmptyOriginAccountId_InputNotCreated_ThrowsInputValidationException()
12 | {
13 | var actualEx = Assert.Throws(
14 | () => new TransferInput(
15 | new AccountId(Guid.Empty),
16 | new AccountId(Guid.NewGuid()),
17 | new PositiveMoney(10)));
18 | Assert.Contains("accountId", actualEx.Message);
19 | }
20 |
21 | [Fact]
22 | public void GivenEmptyDestinationAccountId_InputNotCreated_ThrowsInputValidationException()
23 | {
24 | var actualEx = Assert.Throws(
25 | () => new TransferInput(
26 | new AccountId(Guid.NewGuid()),
27 | new AccountId(Guid.Empty),
28 | new PositiveMoney(10)));
29 | Assert.Contains("accountId", actualEx.Message);
30 | }
31 |
32 | [Fact]
33 | public void GivenValidData_InputCreated()
34 | {
35 | var actual = new TransferInput(
36 | new AccountId(Guid.NewGuid()),
37 | new AccountId(Guid.NewGuid()),
38 | new PositiveMoney(10));
39 | Assert.NotNull(actual);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Infrastructure/InMemoryDataAccess/Services/TestUserService.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.InMemoryDataAccess.Services
2 | {
3 | using System;
4 | using System.Linq;
5 | using Domain.Customers.ValueObjects;
6 | using Domain.Security.Services;
7 | using Domain.Security.ValueObjects;
8 |
9 | public sealed class TestUserService : IUserService
10 | {
11 | private readonly MangaContext _mangaContext;
12 | private readonly Guid _sessionId;
13 |
14 | public TestUserService(MangaContext mangaContext)
15 | {
16 | _sessionId = Guid.NewGuid();
17 | _mangaContext = mangaContext;
18 | }
19 |
20 | public CustomerId? GetCustomerId()
21 | {
22 | var user = _mangaContext.Users
23 | .Where(e => e.ExternalUserId.Equals(GetExternalUserId()))
24 | .SingleOrDefault();
25 |
26 | if (user is null)
27 | {
28 | return null;
29 | }
30 |
31 | var customer = _mangaContext.Customers
32 | .Where(e => e.Id.Equals(user.CustomerId))
33 | .SingleOrDefault();
34 |
35 | if (customer is null)
36 | {
37 | return null;
38 | }
39 |
40 | return customer.Id;
41 | }
42 |
43 | public ExternalUserId GetExternalUserId()
44 | {
45 | return new ExternalUserId($"github/tempuser{_sessionId}");
46 | }
47 |
48 | public Name GetUserName()
49 | {
50 | return new Name($"Temporary User {_sessionId}");
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetCustomerDetails/CustomersController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetCustomerDetails
2 | {
3 | using System.Threading.Tasks;
4 | using Application.Boundaries.GetCustomerDetails;
5 | using FluentMediator;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Mvc;
8 |
9 | [ApiVersion("1.0")]
10 | [Route("api/v{version:apiVersion}/[controller]")]
11 | [ApiController]
12 | public sealed class CustomersController : ControllerBase
13 | {
14 | private readonly IMediator _mediator;
15 | private readonly GetCustomerDetailsPresenter _presenter;
16 |
17 | public CustomersController(
18 | IMediator mediator,
19 | GetCustomerDetailsPresenter presenter)
20 | {
21 | _mediator = mediator;
22 | _presenter = presenter;
23 | }
24 |
25 | ///
26 | /// Get the Customer details.
27 | ///
28 | /// An asynchronous .
29 | [HttpGet(Name = "GetCustomer")]
30 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCustomerDetailsResponse))]
31 | [ProducesResponseType(StatusCodes.Status404NotFound)]
32 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
33 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
34 | public async Task GetCustomer()
35 | {
36 | var input = new GetCustomerDetailsInput();
37 | await _mediator.PublishAsync(input);
38 | return _presenter.ViewModel;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Transfer/TransferOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Transfer
2 | {
3 | using Domain.Accounts.Debits;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transfer Output.
8 | ///
9 | public sealed class TransferOutput : IUseCaseOutput
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Debit object.
15 | /// Updated balance.
16 | /// Origin Account Id.
17 | /// Destination Account Id.
18 | public TransferOutput(
19 | IDebit debit,
20 | Money updatedBalance,
21 | AccountId originAccountId,
22 | AccountId destinationAccountId)
23 | {
24 | Debit debitEntity = (Debit)debit;
25 |
26 | this.Transaction = new Transaction(
27 | originAccountId,
28 | destinationAccountId,
29 | debitEntity.Description,
30 | debitEntity.Amount,
31 | debitEntity.TransactionDate);
32 |
33 | this.UpdatedBalance = updatedBalance;
34 | }
35 |
36 | ///
37 | /// Gets the Transaction.
38 | ///
39 | public Transaction Transaction { get; }
40 |
41 | ///
42 | /// Gets the Updated Balance.
43 | ///
44 | public Money UpdatedBalance { get; }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/IAccountFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts
2 | {
3 | using System;
4 | using Domain.Accounts.Credits;
5 | using Domain.Accounts.Debits;
6 | using Domain.Accounts.ValueObjects;
7 | using Domain.Customers.ValueObjects;
8 |
9 | ///
10 | /// Account Entity Factory Domain-Driven Design Pattern.
11 | ///
12 | public interface IAccountFactory
13 | {
14 | ///
15 | /// Creates a new Account.
16 | ///
17 | /// CustomerId.
18 | /// New Account instance.
19 | IAccount NewAccount(CustomerId customerId);
20 |
21 | ///
22 | /// Creates a new Credit.
23 | ///
24 | /// Account object.
25 | /// Amount to Deposit.
26 | /// Transaction date.
27 | /// New Credit instance.
28 | ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate);
29 |
30 | ///
31 | /// Creates a new Debit.
32 | ///
33 | /// Account object.
34 | /// Amount to Withdraw.
35 | /// Transaction date.
36 | /// New Debit instance.
37 | IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Domain/Security/SecurityService.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Security
2 | {
3 | using System.Threading.Tasks;
4 | using Domain.Customers.ValueObjects;
5 | using Domain.Security.ValueObjects;
6 |
7 | ///
8 | /// Security Domain Service Domain-Driven Design Pattern.
9 | ///
10 | public class SecurityService
11 | {
12 | private readonly IUserFactory _userFactory;
13 | private readonly IUserRepository _userRepository;
14 |
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | /// User Factory.
19 | /// User Repository.
20 | public SecurityService(
21 | IUserFactory userFactory,
22 | IUserRepository userRepository)
23 | {
24 | this._userFactory = userFactory;
25 | this._userRepository = userRepository;
26 | }
27 |
28 | ///
29 | /// Create User Credentials.
30 | ///
31 | /// CustomerId.
32 | /// External User Id.
33 | /// The User.
34 | public async Task CreateUserCredentials(CustomerId customerId, ExternalUserId externalUserId)
35 | {
36 | var user = this._userFactory.NewUser(customerId, externalUserId);
37 | await this._userRepository.Add(user);
38 | return user;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/WebApi/DependencyInjection/ApplicationExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.DependencyInjection
2 | {
3 | using Domain.Accounts;
4 | using Domain.Customers;
5 | using Domain.Security;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | ///
9 | /// Adds Use Cases classes.
10 | ///
11 | public static class ApplicationExtensions
12 | {
13 | ///
14 | /// Adds Use Cases to the ServiceCollection.
15 | ///
16 | /// Service Collection.
17 | /// The modified instance.
18 | public static IServiceCollection AddUseCases(this IServiceCollection services)
19 | {
20 | services.AddScoped();
21 | services.AddScoped();
22 | services.AddScoped();
23 | services.AddScoped();
24 | services.AddScoped();
25 | services.AddScoped();
26 | services.AddScoped();
27 | services.AddScoped();
28 | services.AddScoped();
29 | services.AddScoped();
30 |
31 | return services;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/UnitTests/TestFixtures/StandardFixture.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests.TestFixtures
2 | {
3 | using Domain.Accounts;
4 | using Domain.Customers;
5 | using Domain.Security;
6 | using Infrastructure.InMemoryDataAccess;
7 | using Infrastructure.InMemoryDataAccess.Repositories;
8 | using Infrastructure.InMemoryDataAccess.Services;
9 |
10 | public sealed class StandardFixture
11 | {
12 | public StandardFixture()
13 | {
14 | Context = new MangaContext();
15 | AccountRepository = new AccountRepository(Context);
16 | CustomerRepository = new CustomerRepository(Context);
17 | UserRepository = new UserRepository(Context);
18 | UnitOfWork = new UnitOfWork(Context);
19 | EntityFactory = new EntityFactory();
20 | UserService = new UserService();
21 | CustomerService = new CustomerService(EntityFactory, CustomerRepository);
22 | SecurityService = new SecurityService(EntityFactory, UserRepository);
23 | AccountService = new AccountService(EntityFactory, AccountRepository);
24 | }
25 |
26 | public EntityFactory EntityFactory { get; }
27 |
28 | public MangaContext Context { get; }
29 |
30 | public AccountRepository AccountRepository { get; }
31 |
32 | public CustomerRepository CustomerRepository { get; }
33 |
34 | public UserRepository UserRepository { get; }
35 |
36 | public UnitOfWork UnitOfWork { get; }
37 |
38 | public UserService UserService { get; }
39 |
40 | public CustomerService CustomerService { get; }
41 |
42 | public SecurityService SecurityService { get; }
43 |
44 | public AccountService AccountService { get; }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/IAccount.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts
2 | {
3 | using Domain.Accounts.Credits;
4 | using Domain.Accounts.Debits;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | ///
8 | /// Account Aggregate Root Domain-Driven Design Pattern.
9 | ///
10 | public interface IAccount
11 | {
12 | ///
13 | /// Gets Id.
14 | ///
15 | AccountId Id { get; }
16 |
17 | ///
18 | /// Deposits into account.
19 | ///
20 | /// Factory to create new credits.
21 | /// Amount.
22 | /// Credit created.
23 | ICredit Deposit(IAccountFactory entityFactory, PositiveMoney amountToDeposit);
24 |
25 | ///
26 | /// Withdrawls from account.
27 | ///
28 | /// Factory to create new debits.
29 | /// Amount.
30 | /// Debit created.
31 | IDebit Withdraw(IAccountFactory entityFactory, PositiveMoney amountToWithdraw);
32 |
33 | ///
34 | /// Check if closing account is allowed.
35 | ///
36 | /// True if is allowed.
37 | bool IsClosingAllowed();
38 |
39 | ///
40 | /// Gets the current balance considering credits and debits totals.
41 | ///
42 | /// The current balance.
43 | Money GetCurrentBalance();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/WebApi/Filters/SwaggerDocumentFilter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.Filters
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using Microsoft.OpenApi.Models;
7 | using Swashbuckle.AspNetCore.SwaggerGen;
8 |
9 | public class SwaggerDocumentFilter : IDocumentFilter
10 | {
11 | private readonly List _tags = new List
12 | {
13 | new OpenApiTag
14 | {
15 | Name = "RoutingApi",
16 | Description = "This is a description for the api routes",
17 | },
18 | };
19 |
20 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
21 | {
22 | if (swaggerDoc is null)
23 | {
24 | throw new ArgumentNullException(nameof(swaggerDoc));
25 | }
26 |
27 | swaggerDoc.Tags = GetFilteredTagDefinitions(context);
28 | swaggerDoc.Paths = GetSortedPaths(swaggerDoc);
29 | }
30 |
31 | private List GetFilteredTagDefinitions(DocumentFilterContext context)
32 | {
33 | // Filtering ensures route for tag is present
34 | var currentGroupNames = context.ApiDescriptions
35 | .Select(description => description.GroupName);
36 |
37 | return _tags
38 | .Where(tag => currentGroupNames.Contains(tag.Name))
39 | .ToList();
40 | }
41 |
42 | private OpenApiPaths GetSortedPaths(
43 | OpenApiDocument swaggerDoc)
44 | {
45 | IDictionary dic = swaggerDoc.Paths
46 | .OrderBy(pair => pair.Key)
47 | .ToDictionary(pair => pair.Key, pair => pair.Value);
48 |
49 | return null;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Domain/Customers/AccountCollection.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Customers
2 | {
3 | using System.Collections.Generic;
4 | using System.Collections.ObjectModel;
5 | using Domain.Accounts.ValueObjects;
6 |
7 | ///
8 | /// Accounts First-Class Collection Design Pattern.
9 | ///
10 | public sealed class AccountCollection
11 | {
12 | private readonly IList _accountIds;
13 |
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | public AccountCollection()
18 | {
19 | this._accountIds = new List();
20 | }
21 |
22 | ///
23 | /// Adds Accounts.
24 | ///
25 | /// Accounts list.
26 | public void Add(IEnumerable accounts)
27 | {
28 | foreach (var account in accounts)
29 | {
30 | this.Add(account);
31 | }
32 | }
33 |
34 | ///
35 | /// Add a single account.
36 | ///
37 | /// AccountId.
38 | public void Add(AccountId accountId) => this._accountIds.Add(accountId);
39 |
40 | ///
41 | /// Gets the AccountIds.
42 | ///
43 | /// ReadOnlyCollection.
44 | public IReadOnlyCollection GetAccountIds()
45 | {
46 | IReadOnlyCollection accountIds = new ReadOnlyCollection(this._accountIds);
47 | return accountIds;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Register/RegisterPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Register
2 | {
3 | using System.Collections.Generic;
4 | using Application.Boundaries.Register;
5 | using Microsoft.AspNetCore.Mvc;
6 | using WebApi.ViewModels;
7 |
8 | public sealed class RegisterPresenter : IOutputPort
9 | {
10 | public IActionResult ViewModel { get; private set; }
11 |
12 | public void CustomerAlreadyRegistered(string message)
13 | {
14 | ViewModel = new BadRequestObjectResult(message);
15 | }
16 |
17 | public void Standard(RegisterOutput output)
18 | {
19 | var transactions = new List();
20 |
21 | foreach (var item in output.Account.Transactions)
22 | {
23 | var transaction = new TransactionModel(
24 | item.Amount,
25 | item.Description,
26 | item.TransactionDate);
27 |
28 | transactions.Add(transaction);
29 | }
30 |
31 | var account = new AccountDetailsModel(
32 | output.Account.AccountId,
33 | output.Account.CurrentBalance,
34 | transactions);
35 |
36 | var accounts = new List();
37 | accounts.Add(account);
38 |
39 | var registerResponse = new RegisterResponse(
40 | output.Customer.CustomerId,
41 | output.Customer.SSN,
42 | output.Customer.Name,
43 | accounts);
44 |
45 | ViewModel = new CreatedAtRouteResult(
46 | "GetCustomer",
47 | new
48 | {
49 | customerId = registerResponse.CustomerId,
50 | version = "1.0",
51 | },
52 | registerResponse);
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Migrations/20191126172117_ValueObjects.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | namespace Infrastructure.EntityFrameworkDataAccess.Migrations
5 | {
6 | public partial class ValueObjects : Migration
7 | {
8 | protected override void Up(MigrationBuilder migrationBuilder)
9 | {
10 | migrationBuilder.UpdateData(
11 | table: "Credit",
12 | keyColumn: "Id",
13 | keyValue: new Guid("f5117315-e789-491a-b662-958c37237f9b"),
14 | column: "TransactionDate",
15 | value: new DateTime(2019, 11, 26, 17, 21, 17, 192, DateTimeKind.Utc).AddTicks(890));
16 |
17 | migrationBuilder.UpdateData(
18 | table: "Debit",
19 | keyColumn: "Id",
20 | keyValue: new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f"),
21 | column: "TransactionDate",
22 | value: new DateTime(2019, 11, 26, 17, 21, 17, 192, DateTimeKind.Utc).AddTicks(2670));
23 | }
24 |
25 | protected override void Down(MigrationBuilder migrationBuilder)
26 | {
27 | migrationBuilder.UpdateData(
28 | table: "Credit",
29 | keyColumn: "Id",
30 | keyValue: new Guid("f5117315-e789-491a-b662-958c37237f9b"),
31 | column: "TransactionDate",
32 | value: new DateTime(2019, 11, 24, 22, 39, 24, 636, DateTimeKind.Utc).AddTicks(5740));
33 |
34 | migrationBuilder.UpdateData(
35 | table: "Debit",
36 | keyColumn: "Id",
37 | keyValue: new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f"),
38 | column: "TransactionDate",
39 | value: new DateTime(2019, 11, 24, 22, 39, 24, 636, DateTimeKind.Utc).AddTicks(6780));
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/Repositories/CustomerRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess.Repositories
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Domain.Customers;
7 | using Domain.Customers.ValueObjects;
8 | using Microsoft.EntityFrameworkCore;
9 |
10 | public sealed class CustomerRepository : ICustomerRepository
11 | {
12 | private readonly MangaContext _context;
13 |
14 | public CustomerRepository(MangaContext context)
15 | {
16 | _context = context ??
17 | throw new ArgumentNullException(nameof(context));
18 | }
19 |
20 | public async Task Add(ICustomer customer)
21 | {
22 | await _context.Customers.AddAsync((EntityFrameworkDataAccess.Customer)customer);
23 | await _context.SaveChangesAsync();
24 | }
25 |
26 | public async Task GetBy(CustomerId customerId)
27 | {
28 | var customer = await _context.Customers
29 | .Where(c => c.Id.Equals(customerId))
30 | .SingleOrDefaultAsync();
31 |
32 | if (customer is null)
33 | {
34 | throw new CustomerNotFoundException($"The customer {customerId} does not exist or is not processed yet.");
35 | }
36 |
37 | var accounts = _context.Accounts
38 | .Where(e => e.CustomerId.Equals(customer.Id))
39 | .Select(e => e.Id)
40 | .ToList();
41 |
42 | customer.LoadAccounts(accounts);
43 |
44 | return customer;
45 | }
46 |
47 | public async Task Update(ICustomer customer)
48 | {
49 | _context.Customers.Update((EntityFrameworkDataAccess.Customer)customer);
50 | await _context.SaveChangesAsync();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetAccountDetails/AccountsController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetAccountDetails
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.GetAccountDetails;
6 | using Domain.Accounts.ValueObjects;
7 | using FluentMediator;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | [ApiVersion("1.0")]
12 | [Route("api/v{version:apiVersion}/[controller]")]
13 | [ApiController]
14 | public sealed class AccountsController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 | private readonly GetAccountDetailsPresenter _presenter;
18 |
19 | public AccountsController(
20 | IMediator mediator,
21 | GetAccountDetailsPresenter presenter)
22 | {
23 | _mediator = mediator;
24 | _presenter = presenter;
25 | }
26 |
27 | ///
28 | /// Get an account details.
29 | ///
30 | /// A .
31 | /// An asynchronous .
32 | [HttpGet("{AccountId}", Name = "GetAccount")]
33 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAccountDetailsResponse))]
34 | [ProducesResponseType(StatusCodes.Status404NotFound)]
35 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
36 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
37 | public async Task Get([FromRoute][Required] GetAccountDetailsRequest request)
38 | {
39 | var input = new GetAccountDetailsInput(new AccountId(request.AccountId));
40 | await _mediator.PublishAsync(input);
41 | return _presenter.ViewModel;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/GetCustomerDetails/GetCustomerDetailsPresenter.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.GetCustomerDetails
2 | {
3 | using System.Collections.Generic;
4 | using Application.Boundaries.GetCustomerDetails;
5 | using Microsoft.AspNetCore.Mvc;
6 | using WebApi.ViewModels;
7 |
8 | public sealed class GetCustomerDetailsPresenter : IOutputPort
9 | {
10 | public IActionResult ViewModel { get; private set; }
11 |
12 | public void NotFound(string message)
13 | {
14 | ViewModel = new NotFoundObjectResult(message);
15 | }
16 |
17 | public void Standard(GetCustomerDetailsOutput getCustomerDetailsOutput)
18 | {
19 | List accounts = new List();
20 |
21 | foreach (var account in getCustomerDetailsOutput.Accounts)
22 | {
23 | List transactions = new List();
24 |
25 | foreach (var item in account.Transactions)
26 | {
27 | var transaction = new TransactionModel(
28 | item.Amount,
29 | item.Description,
30 | item.TransactionDate);
31 |
32 | transactions.Add(transaction);
33 | }
34 |
35 | accounts.Add(new AccountDetailsModel(
36 | account.AccountId,
37 | account.CurrentBalance,
38 | transactions));
39 | }
40 |
41 | var getCustomerDetailsResponse = new GetCustomerDetailsResponse(
42 | getCustomerDetailsOutput.CustomerId,
43 | getCustomerDetailsOutput.SSN,
44 | getCustomerDetailsOutput.Name,
45 | accounts);
46 |
47 | ViewModel = new OkObjectResult(getCustomerDetailsResponse);
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/CloseAccount/AccountsController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.CloseAccount
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.CloseAccount;
6 | using Domain.Accounts.ValueObjects;
7 | using FluentMediator;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | [ApiVersion("1.0")]
12 | [Route("api/v{version:apiVersion}/[controller]")]
13 | [ApiController]
14 | public sealed class AccountsController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 | private readonly CloseAccountPresenter _presenter;
18 |
19 | public AccountsController(
20 | IMediator mediator,
21 | CloseAccountPresenter presenter)
22 | {
23 | _mediator = mediator;
24 | _presenter = presenter;
25 | }
26 |
27 | ///
28 | /// Close an Account.
29 | ///
30 | /// The closed account id.
31 | /// Bad request.
32 | /// Error.
33 | /// The request to Close an Account.
34 | /// The account id.
35 | [HttpDelete("{AccountId}")]
36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CloseAccountResponse))]
37 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
39 | public async Task Close([FromRoute][Required] CloseAccountRequest request)
40 | {
41 | var input = new CloseAccountInput(new AccountId(request.AccountId));
42 | await _mediator.PublishAsync(input);
43 | return _presenter.ViewModel;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Register/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Register
2 | {
3 | using System.Collections.Generic;
4 | using Domain.Customers;
5 | using Domain.Customers.ValueObjects;
6 | using Domain.Security.ValueObjects;
7 |
8 | ///
9 | /// Customer.
10 | ///
11 | public sealed class Customer
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// External User Id.
17 | /// Customer object.
18 | /// Accounts list.
19 | public Customer(
20 | ExternalUserId externalUserId,
21 | ICustomer customer,
22 | List accounts)
23 | {
24 | var customerEntity = (Domain.Customers.Customer)customer;
25 | this.ExternalUserId = externalUserId;
26 | this.CustomerId = customerEntity.Id;
27 | this.SSN = customerEntity.SSN;
28 | this.Name = customerEntity.Name;
29 | this.Accounts = accounts;
30 | }
31 |
32 | ///
33 | /// Gets the External User Id.
34 | ///
35 | public ExternalUserId ExternalUserId { get; }
36 |
37 | ///
38 | /// Gets the Customer Id.
39 | ///
40 | public CustomerId CustomerId { get; }
41 |
42 | ///
43 | /// Gets the SSN.
44 | ///
45 | public SSN SSN { get; }
46 |
47 | ///
48 | /// Gets the name.
49 | ///
50 | public Name Name { get; }
51 |
52 | ///
53 | /// Gets the Accounts.
54 | ///
55 | public IReadOnlyList Accounts { get; }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Domain/Accounts/IAccountRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Accounts
2 | {
3 | using System.Threading.Tasks;
4 | using Domain.Accounts.Credits;
5 | using Domain.Accounts.Debits;
6 | using Domain.Accounts.ValueObjects;
7 |
8 | ///
9 | /// Account Repository Domain-Driven Design Pattern.
10 | ///
11 | public interface IAccountRepository
12 | {
13 | ///
14 | /// Gets an Account.
15 | ///
16 | /// AccountId.
17 | /// An Account instance.
18 | Task Get(AccountId id);
19 |
20 | ///
21 | /// Adds an Account.
22 | ///
23 | /// Account object.
24 | /// Credit object.
25 | /// Task.
26 | Task Add(IAccount account, ICredit credit);
27 |
28 | ///
29 | /// Updates an Account.
30 | ///
31 | /// Account object.
32 | /// Credit object.
33 | /// Task.
34 | Task Update(IAccount account, ICredit credit);
35 |
36 | ///
37 | /// Updates the Account.
38 | ///
39 | /// Account object.
40 | /// Debit object.
41 | /// Task.
42 | Task Update(IAccount account, IDebit debit);
43 |
44 | ///
45 | /// Deletes the Account.
46 | ///
47 | /// Account object.
48 | /// Task.
49 | Task Delete(IAccount account);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Deposit/AccountsController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Deposit
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.Deposit;
6 | using Domain.Accounts.ValueObjects;
7 | using FluentMediator;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | [ApiVersion("1.0")]
12 | [Route("api/v{version:apiVersion}/[controller]")]
13 | [ApiController]
14 | public sealed class AccountsController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 | private readonly DepositPresenter _presenter;
18 |
19 | public AccountsController(
20 | IMediator mediator,
21 | DepositPresenter presenter)
22 | {
23 | _mediator = mediator;
24 | _presenter = presenter;
25 | }
26 |
27 | ///
28 | /// Deposit on an account.
29 | ///
30 | /// The updated balance.
31 | /// Bad request.
32 | /// Error.
33 | /// The request to deposit.
34 | /// The updated balance.
35 | [HttpPatch("Deposit")]
36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DepositResponse))]
37 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
39 | public async Task Deposit([FromForm][Required] DepositRequest request)
40 | {
41 | var input = new DepositInput(
42 | new AccountId(request.AccountId),
43 | new PositiveMoney(request.Amount));
44 |
45 | await _mediator.PublishAsync(input);
46 | return _presenter.ViewModel;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Withdraw/AccountsController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Withdraw
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.Withdraw;
6 | using Domain.Accounts.ValueObjects;
7 | using FluentMediator;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | [ApiVersion("1.0")]
12 | [Route("api/v{version:apiVersion}/[controller]")]
13 | [ApiController]
14 | public sealed class AccountsController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 | private readonly WithdrawPresenter _presenter;
18 |
19 | public AccountsController(
20 | IMediator mediator,
21 | WithdrawPresenter presenter)
22 | {
23 | _mediator = mediator;
24 | _presenter = presenter;
25 | }
26 |
27 | ///
28 | /// Withdraw on an account.
29 | ///
30 | /// The updated balance.
31 | /// Bad request.
32 | /// Error.
33 | /// The request to Withdraw.
34 | /// The updated balance.
35 | [HttpPatch("Withdraw")]
36 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WithdrawResponse))]
37 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
38 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
39 | public async Task Withdraw([FromForm][Required] WithdrawRequest request)
40 | {
41 | var input = new WithdrawInput(
42 | new AccountId(request.AccountId),
43 | new PositiveMoney(request.Amount));
44 | await _mediator.PublishAsync(input);
45 | return _presenter.ViewModel;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V2/GetAccountDetails/GetAccountDetailsPresenterV2.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V2.GetAccountDetails
2 | {
3 | using System;
4 | using System.Data;
5 | using Application.Boundaries.GetAccountDetails;
6 | using Microsoft.AspNetCore.Mvc;
7 | using OfficeOpenXml;
8 |
9 | public sealed class GetAccountDetailsPresenterV2 : IOutputPort
10 | {
11 | public IActionResult ViewModel { get; private set; }
12 |
13 | public void NotFound(string message)
14 | {
15 | ViewModel = new NotFoundObjectResult(message);
16 | }
17 |
18 | public void Standard(GetAccountDetailsOutput getAccountDetailsOutput)
19 | {
20 | var dataTable = new DataTable();
21 | dataTable.Columns.Add("Amount", typeof(decimal));
22 | dataTable.Columns.Add("Description", typeof(string));
23 | dataTable.Columns.Add("TransactionDate", typeof(DateTime));
24 |
25 | foreach (var item in getAccountDetailsOutput.Transactions)
26 | {
27 | dataTable.Rows.Add(new object[]
28 | {
29 | item.Amount.ToMoney().ToDecimal(),
30 | item.Description,
31 | item.TransactionDate,
32 | });
33 | }
34 |
35 | byte[] fileContents;
36 |
37 | using (ExcelPackage pck = new ExcelPackage())
38 | {
39 | ExcelWorksheet ws = pck.Workbook.Worksheets.Add(getAccountDetailsOutput.AccountId.ToString());
40 | ws.Cells["A1"].LoadFromDataTable(dataTable, true);
41 | ws.Row(1).Style.Font.Bold = true;
42 | ws.Column(3).Style.Numberformat.Format = "dd/MM/yyyy HH:mm";
43 | fileContents = pck.GetAsByteArray();
44 | }
45 |
46 | ViewModel = new FileContentResult(fileContents, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V1/Register/CustomersController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V1.Register
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.Register;
6 | using Domain.Accounts.ValueObjects;
7 | using Domain.Customers.ValueObjects;
8 | using FluentMediator;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.AspNetCore.Mvc;
11 |
12 | [ApiVersion("1.0")]
13 | [Route("api/v{version:apiVersion}/[controller]")]
14 | [ApiController]
15 | public sealed class CustomersController : ControllerBase
16 | {
17 | private readonly IMediator _mediator;
18 | private readonly RegisterPresenter _presenter;
19 |
20 | public CustomersController(
21 | IMediator mediator,
22 | RegisterPresenter presenter)
23 | {
24 | _mediator = mediator;
25 | _presenter = presenter;
26 | }
27 |
28 | ///
29 | /// Register a customer.
30 | ///
31 | /// The registered customer was create successfully.
32 | /// Bad request.
33 | /// Error.
34 | /// The request to register a customer.
35 | /// The newly registered customer.
36 | [HttpPost]
37 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RegisterResponse))]
38 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
39 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
40 | public async Task Post([FromForm][Required] RegisterRequest request)
41 | {
42 | var input = new RegisterInput(
43 | new SSN(request.SSN),
44 | new PositiveMoney(request.InitialAmount));
45 | await _mediator.PublishAsync(input);
46 | return _presenter.ViewModel;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/GetCustomerDetails/GetCustomerDetailsOutput.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.GetCustomerDetails
2 | {
3 | using System.Collections.Generic;
4 | using Domain.Customers;
5 | using Domain.Customers.ValueObjects;
6 | using Domain.Security.ValueObjects;
7 |
8 | ///
9 | /// Gets Customer Details Output Message.
10 | ///
11 | public sealed class GetCustomerDetailsOutput : IUseCaseOutput
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// External User Id.
17 | /// Customer object.
18 | /// Accounts list.
19 | public GetCustomerDetailsOutput(
20 | ExternalUserId externalUserId,
21 | ICustomer customer,
22 | List accounts)
23 | {
24 | Customer customerEntity = (Customer)customer;
25 | this.ExternalUserId = externalUserId;
26 | this.CustomerId = customerEntity.Id;
27 | this.SSN = customerEntity.SSN;
28 | this.Name = customerEntity.Name;
29 | this.Accounts = accounts;
30 | }
31 |
32 | ///
33 | /// Gets the ExternalUserId.
34 | ///
35 | public ExternalUserId ExternalUserId { get; }
36 |
37 | ///
38 | /// Gets the CustomerId.
39 | ///
40 | public CustomerId CustomerId { get; }
41 |
42 | ///
43 | /// Gets the SSN.
44 | ///
45 | public SSN SSN { get; }
46 |
47 | ///
48 | /// Gets the Name.
49 | ///
50 | public Name Name { get; }
51 |
52 | ///
53 | /// Gets the Accounts.
54 | ///
55 | public IReadOnlyList Accounts { get; }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/WebApi/UseCases/V2/GetAccountDetails/AccountsController.cs:
--------------------------------------------------------------------------------
1 | namespace WebApi.UseCases.V2.GetAccountDetails
2 | {
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Threading.Tasks;
5 | using Application.Boundaries.GetAccountDetails;
6 | using Domain.Accounts.ValueObjects;
7 | using FluentMediator;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 | using Microsoft.FeatureManagement.Mvc;
11 | using WebApi.DependencyInjection.FeatureFlags;
12 |
13 | [FeatureGate(Features.GetAccountDetailsV2)]
14 | [ApiVersion("2.0")]
15 | [Route("api/v{version:apiVersion}/[controller]")]
16 | [ApiController]
17 | public sealed class AccountsController : ControllerBase
18 | {
19 | private readonly IMediator _mediator;
20 | private readonly GetAccountDetailsPresenterV2 _presenter;
21 |
22 | public AccountsController(
23 | IMediator mediator,
24 | GetAccountDetailsPresenterV2 presenter)
25 | {
26 | _mediator = mediator;
27 | _presenter = presenter;
28 | }
29 |
30 | ///
31 | /// Get an account details.
32 | ///
33 | /// A .
34 | /// An asynchronous .
35 | [HttpGet("{AccountId}", Name = "GetAccountV2")]
36 | [ProducesResponseType(StatusCodes.Status200OK)]
37 | [ProducesResponseType(StatusCodes.Status404NotFound)]
38 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
39 | [ProducesResponseType(StatusCodes.Status500InternalServerError)]
40 | public async Task Get([FromRoute][Required] GetAccountDetailsRequestV2 request)
41 | {
42 | var input = new GetAccountDetailsInput(new AccountId(request.AccountId));
43 | await _mediator.PublishAsync(input, "GetAccountDetailsV2");
44 | return _presenter.ViewModel;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Application/Boundaries/Transfer/Transaction.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Boundaries.Transfer
2 | {
3 | using System;
4 | using Domain.Accounts.ValueObjects;
5 |
6 | ///
7 | /// Transaction Message.
8 | ///
9 | public sealed class Transaction
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// Origin Account Id.
15 | /// Destination Account Id.
16 | /// Text description.
17 | /// Positive amount.
18 | /// Transaction date.
19 | public Transaction(
20 | AccountId originAccountId,
21 | AccountId destinationAccountId,
22 | string description,
23 | PositiveMoney amount,
24 | DateTime transactionDate)
25 | {
26 | this.OriginAccountId = originAccountId;
27 | this.DestinationAccountId = destinationAccountId;
28 | this.Description = description;
29 | this.Amount = amount;
30 | this.TransactionDate = transactionDate;
31 | }
32 |
33 | ///
34 | /// Gets the Origin AccountId.
35 | ///
36 | public AccountId OriginAccountId { get; }
37 |
38 | ///
39 | /// Gets the Destination Account Id.
40 | ///
41 | public AccountId DestinationAccountId { get; }
42 |
43 | ///
44 | /// Gets the description.
45 | ///
46 | public string Description { get; }
47 |
48 | ///
49 | /// Gets the Amount.
50 | ///
51 | public PositiveMoney Amount { get; }
52 |
53 | ///
54 | /// Gets the Transaction date.
55 | ///
56 | public DateTime TransactionDate { get; }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Infrastructure/EntityFrameworkDataAccess/SeedData.cs:
--------------------------------------------------------------------------------
1 | namespace Infrastructure.EntityFrameworkDataAccess
2 | {
3 | using System;
4 | using Domain.Accounts.Credits;
5 | using Domain.Accounts.Debits;
6 | using Domain.Accounts.ValueObjects;
7 | using Domain.Customers.ValueObjects;
8 | using Domain.Security.ValueObjects;
9 | using Microsoft.EntityFrameworkCore;
10 |
11 | public static class SeedData
12 | {
13 | public static readonly CustomerId DefaultCustomerId = new CustomerId(new Guid("197d0438-e04b-453d-b5de-eca05960c6ae"));
14 |
15 | public static readonly AccountId DefaultAccountId = new AccountId(new Guid("4c510cfe-5d61-4a46-a3d9-c4313426655f"));
16 |
17 | public static void Seed(ModelBuilder builder)
18 | {
19 | builder.Entity().HasData(
20 | new { Id = DefaultCustomerId, Name = new Name("Test User"), SSN = new SSN("19860817-9999") });
21 |
22 | builder.Entity().HasData(
23 | new { Id = DefaultAccountId, CustomerId = DefaultCustomerId });
24 |
25 | builder.Entity().HasData(
26 | new
27 | {
28 | Id = new CreditId(new Guid("f5117315-e789-491a-b662-958c37237f9b")),
29 | AccountId = DefaultAccountId,
30 | Amount = new PositiveMoney(400),
31 | Description = "Credit",
32 | TransactionDate = DateTime.UtcNow
33 | });
34 |
35 | builder.Entity().HasData(
36 | new
37 | {
38 | Id = new DebitId(new Guid("3d6032df-7a3b-46e6-8706-be971e3d539f")),
39 | AccountId = DefaultAccountId,
40 | Amount = new PositiveMoney(400),
41 | Description = "Debit",
42 | TransactionDate = DateTime.UtcNow
43 | });
44 |
45 | builder.Entity().HasData(
46 | new { CustomerId = DefaultCustomerId, ExternalUserId = new ExternalUserId("github/ivanpaulovich") });
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Application/UseCases/GetAccountDetails.cs:
--------------------------------------------------------------------------------
1 | namespace Application.UseCases
2 | {
3 | using System.Threading.Tasks;
4 | using Application.Boundaries.GetAccountDetails;
5 | using Domain.Accounts;
6 |
7 | ///
8 | /// Get Account Details Use Case Domain-Driven Design Pattern.
9 | ///
10 | public sealed class GetAccountDetails : IUseCase, IUseCaseV2
11 | {
12 | private readonly IOutputPort _outputPort;
13 | private readonly IAccountRepository _accountRepository;
14 |
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | /// Output Port.
19 | /// Account Repository.
20 | public GetAccountDetails(
21 | IOutputPort outputPort,
22 | IAccountRepository accountRepository)
23 | {
24 | this._outputPort = outputPort;
25 | this._accountRepository = accountRepository;
26 | }
27 |
28 | ///
29 | /// Executes the Use Case.
30 | ///
31 | /// Input Message.
32 | /// Task.
33 | public async Task Execute(GetAccountDetailsInput input)
34 | {
35 | IAccount account;
36 |
37 | try
38 | {
39 | account = await this._accountRepository.Get(input.AccountId);
40 | }
41 | catch (AccountNotFoundException ex)
42 | {
43 | this._outputPort.NotFound(ex.Message);
44 | return;
45 | }
46 |
47 | this.BuildOutput(account);
48 | }
49 |
50 | private void BuildOutput(IAccount account)
51 | {
52 | var output = new GetAccountDetailsOutput(account);
53 | this._outputPort.Standard(output);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------