Repository: atsapura/CardManagement
Branch: master
Commit: 412af96601d1
Files: 45
Total size: 161.2 KB
Directory structure:
gitextract_g_zz3ke9/
├── .dockerignore
├── .gitignore
├── CardManagement/
│ ├── BalanceOperation.fs
│ ├── CardActions.fs
│ ├── CardDomain.fs
│ ├── CardDomainCommandModels.fs
│ ├── CardDomainQueryModels.fs
│ ├── CardManagement.fsproj
│ ├── CardProgramBuilder.fs
│ └── CardWorkflow.fs
├── CardManagement.Api/
│ └── CardManagement.Api/
│ ├── CardManagement.Api.fsproj
│ ├── Dockerfile
│ ├── OptionConverter.fs
│ ├── Program.fs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── appsettings.Development.json
│ └── appsettings.json
├── CardManagement.Common/
│ ├── CardManagement.Common.fsproj
│ ├── Common.fs
│ ├── CommonTypes.fs
│ ├── Country.fs
│ ├── ErrorMessages.fs
│ └── Errors.fs
├── CardManagement.Console/
│ ├── CardManagement.Console.fsproj
│ ├── Program.fs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── CardManagement.Data/
│ ├── CardDataPipeline.fs
│ ├── CardDomainEntities.fs
│ ├── CardManagement.Data.fsproj
│ ├── CardMongoConfiguration.fs
│ ├── CommandRepository.fs
│ ├── DomainToEntityMapping.fs
│ ├── EntityToDomainMapping.fs
│ └── QueryRepository.fs
├── CardManagement.Infrastructure/
│ ├── AppConfiguration.fs
│ ├── CardApi.fs
│ ├── CardManagement.Infrastructure.fsproj
│ ├── CardProgramInterpreter.fs
│ └── Logging.fs
├── CardManagement.sln
├── README.md
├── SampleCalls.http
├── article/
│ └── Fighting.Complexity.md
└── docker/
└── docker-compose.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.dockerignore
.env
.git
.gitignore
.vs
.vscode
*/bin
*/obj
**/.toolstarget
================================================
FILE: .gitignore
================================================
/.vs
[Bb]in
[Oo]bj
[Dd]ebug
[Rr]elease
*.user
*.suo
/.vscode
/.ionide
================================================
FILE: CardManagement/BalanceOperation.fs
================================================
namespace CardManagement
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module BalanceOperation =
open CardDomain
open System
open CardManagement.Common
let isDecrease change =
match change with
| Increase _ -> false
| Decrease _ -> true
let spentAtDate (date: DateTimeOffset) cardNumber operations =
let date = date.Date
let operationFilter { CardNumber = number; BalanceChange = change; Timestamp = timestamp } =
isDecrease change && number = cardNumber && timestamp.Date = date
let spendings = List.filter operationFilter operations
List.sumBy (fun s -> -s.BalanceChange.ToDecimal()) spendings |> Money
================================================
FILE: CardManagement/CardActions.fs
================================================
namespace CardManagement
(*
This module contains business logic only. It doesn't know anything about data access layer,
it doesn't deal with composition, the only thing we have here is functions, which represent
business data transformations.
Note that all the functions here are pure and total: they don't deal with any kind of external state,
they have no side effects, and they can successfully process ANY kind of input - they don't throw exceptions.
Since they deal with domain types (which earlier we designed in a way that invalid data can't be represented with them),
we don't need to do input validation here. The only error we return in here is `OperationNotAllowedError`,
which means that user provided valid data but wants to do something that is not allowed, e.g. pay with expired card.
*)
module CardActions =
open System
open CardDomain
open CardManagement.Common.Errors
open CardManagement.Common
let private isExpired (currentDate: DateTimeOffset) (month: Month, year: Year) =
(int year.Value, month.ToNumber() |> int) < (currentDate.Year, currentDate.Month)
let private setDailyLimitNotAllowed = operationNotAllowed "Set daily limit"
let private processPaymentNotAllowed = operationNotAllowed "Process payment"
let private cardExpiredMessage (cardNumber: CardNumber) =
sprintf "Card %s is expired" cardNumber.Value
let private cardDeactivatedMessage (cardNumber: CardNumber) =
sprintf "Card %s is deactivated" cardNumber.Value
let isCardExpired (currentDate: DateTimeOffset) card =
isExpired currentDate card.Expiration
let deactivate card =
match card.AccountDetails with
| Deactivated -> card
| Active _ -> { card with AccountDetails = Deactivated }
let activate (cardAccountInfo: AccountInfo) card =
match card.AccountDetails with
| Active _ -> card
| Deactivated -> { card with AccountDetails = Active cardAccountInfo }
let setDailyLimit (currentDate: DateTimeOffset) limit card =
if isCardExpired currentDate card then
cardExpiredMessage card.CardNumber |> setDailyLimitNotAllowed
else
match card.AccountDetails with
| Deactivated -> cardDeactivatedMessage card.CardNumber |> setDailyLimitNotAllowed
| Active accInfo -> { card with AccountDetails = Active { accInfo with DailyLimit = limit } } |> Ok
let processPayment (currentDate: DateTimeOffset) (spentToday: Money) card (paymentAmount: MoneyTransaction) =
if isCardExpired currentDate card then
cardExpiredMessage card.CardNumber |> processPaymentNotAllowed
else
match card.AccountDetails with
| Deactivated -> cardDeactivatedMessage card.CardNumber |> processPaymentNotAllowed
| Active accInfo ->
if paymentAmount.Value > accInfo.Balance.Value then
sprintf "Insufficent funds on card %s" card.CardNumber.Value
|> processPaymentNotAllowed
else
match accInfo.DailyLimit with
| Limit limit when limit < spentToday + paymentAmount ->
sprintf "Daily limit is exceeded for card %s with daily limit %M. Today was spent %M"
card.CardNumber.Value limit.Value spentToday.Value
|> processPaymentNotAllowed
(*
We could use here the ultimate wild card case like this:
| _ ->
but it's dangerous because if a new case appears in `DailyLimit` type,
we won't get a compile error here, which would remind us to process this
new case in here. So this is a safe way to do the same thing.
*)
| Limit _ | Unlimited ->
let newBalance = accInfo.Balance - paymentAmount
let updatedCard = { card with AccountDetails = Active { accInfo with Balance = newBalance } }
let balanceOperation =
{ Timestamp = currentDate
CardNumber = card.CardNumber
NewBalance = newBalance
BalanceChange = Decrease paymentAmount }
Ok (updatedCard, balanceOperation)
let topUp (currentDate: DateTimeOffset) card (topUp : MoneyTransaction) =
let topUpNotAllowed = operationNotAllowed "Top up"
if isCardExpired currentDate card then
cardExpiredMessage card.CardNumber |> topUpNotAllowed
else
match card.AccountDetails with
| Deactivated -> cardDeactivatedMessage card.CardNumber |> topUpNotAllowed
| Active accInfo ->
let newBalance = accInfo.Balance + topUp
let updatedCard = { card with AccountDetails = Active { accInfo with Balance = newBalance } }
let balanceOperation =
{ Timestamp = currentDate
NewBalance = newBalance
CardNumber = card.CardNumber
BalanceChange = Increase topUp }
Ok (updatedCard, balanceOperation)
================================================
FILE: CardManagement/CardDomain.fs
================================================
namespace CardManagement
(*
This file contains our domain types.
There are several important goals to pursue when you do domain modeling:
- Tell AS MUCH as you can with your type: expected states, descriptive naming and so on.
- Make invalid state unrepresentable using private constructors and built in validation.
- Make illegal operations impossible: e.g. if deactivated credit card can't be used for payment,
hide all the information, which is needed to complete an operation.
*)
module CardDomain =
open System.Text.RegularExpressions
open CardManagement.Common.Errors
open CardManagement.Common
open System
let private cardNumberRegex = new Regex("^[0-9]{16}$", RegexOptions.Compiled)
(*
Technically card number is represented with a string.
But it has certain validation rules which we don't want to be violated,
so instead of throwing exception like one would do in C#, we create separate type,
make it's constructor private and expose a factory method which returns `Result` with
possible `ValidationError`. So whenever we have an instance of `CardNumber`, we can be
certain, that the value inside is valid.
*)
type CardNumber = private CardNumber of string
with
member this.Value = match this with CardNumber s -> s
static member create fieldName str =
match str with
| (null|"") -> validationError fieldName "card number can't be empty"
| str ->
if cardNumberRegex.IsMatch(str) then CardNumber str |> Ok
else validationError fieldName "Card number must be a 16 digits string"
(*
Again, technically daily limit is represented with `decimal`. But `decimal` isn't
quite what we need here. It can be negative, which is not a valid value for daily limit.
It can also be a zero, and it may mean that there's no daily limit or it may mean that
no purchase can be made at all. We could also use `Nullable<decimal>`, but then we would be
in danger of `NullReferenceException` or someone could along the way use construction `?? 0`.
In any case this is much easier to read:
*)
[<Struct>]
type DailyLimit =
private
| Limit of Money
| Unlimited
with
static member ofDecimal dec =
if dec > 0m then Money dec |> Limit
else Unlimited
member this.ToDecimalOption() =
match this with
| Unlimited -> None
| Limit limit -> Some limit.Value
(*
Since we made our constructor private, we can't pattern match it directly from outside,
so we expose this Active Pattern to be able to see what's inside, but without a possibility
of direct creation of this type.
In a nutshell it's sort of `{ get; private set; }` for the whole type.
*)
let (|Limit|Unlimited|) limit =
match limit with
| Limit dec -> Limit dec
| Unlimited -> Unlimited
type UserId = System.Guid
type AccountInfo =
{ HolderId: UserId
Balance: Money
DailyLimit: DailyLimit }
with
static member Default userId =
{ HolderId = userId
Balance = Money 0m
DailyLimit = Unlimited }
(*
This bit is important. As you can see, `AccountInfo` type is holding information about
the money you have, which is clearly mandatory when you need to process a payment.
Now, we don't want anyone to be able to process a payment with deactivated card,
so we just don't provide this information when the card isn't active.
Now this important business rule can't be violated by accident.
*)
type CardAccountInfo =
| Active of AccountInfo
| Deactivated
(*
We could use `DateTime` type to represent an expiration date. But `DateTime` contains way
more information then we need. Which would rise a lot of questions:
- What do we do with the time?
- What about timezone?
- What about day of month?
Now it's clear that expiration is about just month and year.
*)
type Card =
{ CardNumber: CardNumber
Name: LetterString
HolderId: UserId
Expiration: (Month * Year)
AccountDetails: CardAccountInfo }
type CardDetails =
{ Card: Card
HolderAddress: Address
HolderId: UserId
HolderName: LetterString }
type UserInfo =
{ Name: LetterString
Id: UserId
Address: Address }
type User =
{ UserInfo : UserInfo
Cards: Card list }
[<Struct>]
type BalanceChange =
| Increase of increase: MoneyTransaction
| Decrease of decrease: MoneyTransaction
with
member this.ToDecimal() =
match this with
| Increase i -> i.Value
| Decrease d -> -d.Value
[<Struct>]
type BalanceOperation =
{ CardNumber: CardNumber
Timestamp: DateTimeOffset
BalanceChange: BalanceChange
NewBalance: Money }
================================================
FILE: CardManagement/CardDomainCommandModels.fs
================================================
namespace CardManagement
(*
This module contains command models, validated commands and validation functions.
In C# common pattern is to throw exception if input is invalid and pass it further if it's ok.
Problem with that approach is if we forget to validate, the code will compile and a program
either won't crash at all, or it will in some unexpected place. So we have to cover that with unit tests.
Here however we use different types for validated entities.
So even if we try to miss validation, the code won't even compile.
*)
module CardDomainCommandModels =
open CardManagement.Common
open CardDomain
open CardManagement.Common.Errors
open FsToolkit.ErrorHandling
type ActivateCommand = { CardNumber: CardNumber }
type DeactivateCommand = { CardNumber: CardNumber }
type SetDailyLimitCommand =
{ CardNumber: CardNumber
DailyLimit: DailyLimit }
type ProcessPaymentCommand =
{ CardNumber: CardNumber
PaymentAmount: MoneyTransaction }
type TopUpCommand =
{ CardNumber: CardNumber
TopUpAmount: MoneyTransaction }
[<CLIMutable>]
type ActivateCardCommandModel =
{ CardNumber: string }
[<CLIMutable>]
type DeactivateCardCommandModel =
{ CardNumber: string }
[<CLIMutable>]
type SetDailyLimitCardCommandModel =
{ CardNumber: string
Limit: decimal }
[<CLIMutable>]
type ProcessPaymentCommandModel =
{ CardNumber: string
PaymentAmount: decimal }
[<CLIMutable>]
type TopUpCommandModel =
{ CardNumber: string
TopUpAmount: decimal }
[<CLIMutable>]
type CreateAddressCommandModel =
{ Country: string
City: string
PostalCode: string
AddressLine1: string
AddressLine2: string }
[<CLIMutable>]
type CreateUserCommandModel =
{ Name: string
Address: CreateAddressCommandModel }
[<CLIMutable>]
type CreateCardCommandModel =
{ CardNumber : string
Name: string
ExpirationMonth: uint16
ExpirationYear: uint16
UserId: UserId }
(*
This is a brief API description made with just type aliases.
As you can see, every public function here returns a `Result` with possible `ValidationError`.
No other error can occur in here.
*)
type ValidateActivateCardCommand = ActivateCardCommandModel -> ValidationResult<ActivateCommand>
type ValidateDeactivateCardCommand = DeactivateCardCommandModel -> ValidationResult<DeactivateCommand>
type ValidateSetDailyLimitCommand = SetDailyLimitCardCommandModel -> ValidationResult<SetDailyLimitCommand>
type ValidateProcessPaymentCommand = ProcessPaymentCommandModel -> ValidationResult<ProcessPaymentCommand>
type ValidateTopUpCommand = TopUpCommandModel -> ValidationResult<TopUpCommand>
type ValidateCreateAddressCommand = CreateAddressCommandModel -> ValidationResult<Address>
type ValidateCreateUserCommand = CreateUserCommandModel -> ValidationResult<UserInfo>
type ValidateCreateCardCommand = CreateCardCommandModel -> ValidationResult<Card>
let private validateCardNumber = CardNumber.create "cardNumber"
let validateActivateCardCommand : ValidateActivateCardCommand =
fun cmd ->
result {
let! number = cmd.CardNumber |> validateCardNumber
return { ActivateCommand.CardNumber = number }
}
let validateDeactivateCardCommand : ValidateDeactivateCardCommand =
fun cmd ->
result {
let! number = cmd.CardNumber |> validateCardNumber
return { DeactivateCommand.CardNumber = number }
}
let validateSetDailyLimitCommand : ValidateSetDailyLimitCommand =
fun cmd ->
result {
let! number = cmd.CardNumber |> validateCardNumber
let limit = DailyLimit.ofDecimal cmd.Limit
return
{ CardNumber = number
DailyLimit = limit }
}
let validateProcessPaymentCommand : ValidateProcessPaymentCommand =
fun cmd ->
result {
let! number = cmd.CardNumber |> validateCardNumber
let! amount = cmd.PaymentAmount |> MoneyTransaction.create
return
{ ProcessPaymentCommand.CardNumber = number
PaymentAmount = amount }
}
let validateTopUpCommand : ValidateTopUpCommand =
fun cmd ->
result {
let! number = cmd.CardNumber |> validateCardNumber
let! amount = cmd.TopUpAmount |> MoneyTransaction.create
return
{ TopUpCommand.CardNumber = number
TopUpAmount = amount }
}
let validateCreateAddressCommand : ValidateCreateAddressCommand =
fun cmd ->
result {
let! country = parseCountry cmd.Country
let! city = LetterString.create "city" cmd.City
let! postalCode = PostalCode.create "postalCode" cmd.PostalCode
return
{ Address.Country = country
City = city
PostalCode = postalCode
AddressLine1 = cmd.AddressLine1
AddressLine2 = cmd.AddressLine2}
}
let validateCreateUserCommand userId : ValidateCreateUserCommand =
fun cmd ->
result {
let! name = LetterString.create "name" cmd.Name
let! address = validateCreateAddressCommand cmd.Address
return
{ UserInfo.Id = userId
Name = name
Address = address }
}
let validateCreateCardCommand : ValidateCreateCardCommand =
fun cmd ->
result {
let! name = LetterString.create "name" cmd.Name
let! number = CardNumber.create "cardNumber" cmd.CardNumber
let! month = Month.create "expirationMonth" cmd.ExpirationMonth
let! year = Year.create "expirationYear" cmd.ExpirationYear
return
{ Card.CardNumber = number
Name = name
HolderId = cmd.UserId
Expiration = month,year
AccountDetails =
AccountInfo.Default cmd.UserId
|> Active }
}
================================================
FILE: CardManagement/CardDomainQueryModels.fs
================================================
namespace CardManagement
(*
This module contains mappings of our domain types to something that user/client will see.
Since JSON and a lot of popular languages now do not support Discriminated Unions, which
we heavily use in our domain, we have to convert our domain types to something represented
by common types.
*)
module CardDomainQueryModels =
open System
open CardDomain
open CardManagement.Common
type AddressModel =
{ Country: string
City: string
PostalCode: string
AddressLine1: string
AddressLine2: string }
type BasicCardInfoModel =
{ CardNumber: string
Name: string
ExpirationMonth: uint16
ExpirationYear: uint16 }
type CardInfoModel =
{ BasicInfo: BasicCardInfoModel
Balance: decimal option
DailyLimit: decimal option
IsActive: bool }
type CardDetailsModel =
{ CardInfo: CardInfoModel
HolderName: string
HolderAddress: AddressModel }
type UserModel =
{ Id: Guid
Name: string
Address: AddressModel
Cards: CardInfoModel list }
let toBasicInfoToModel (basicCard: Card) =
{ CardNumber = basicCard.CardNumber.Value
Name = basicCard.Name.Value
ExpirationMonth = (fst basicCard.Expiration).ToNumber()
ExpirationYear = (snd basicCard.Expiration).Value }
let toCardInfoModel card =
let (balance, dailyLimit, isActive) =
match card.AccountDetails with
| Active accInfo ->
(accInfo.Balance.Value |> Some, accInfo.DailyLimit.ToDecimalOption(), true)
| Deactivated -> (None, None, false)
{ BasicInfo = card |> toBasicInfoToModel
Balance = balance
DailyLimit = dailyLimit
IsActive = isActive }
let toAddressModel (address: Address) =
{ Country = address.Country.ToString()
City = address.City.Value
PostalCode = address.PostalCode.Value
AddressLine1 = address.AddressLine1
AddressLine2 = address.AddressLine2 }
let toCardDetailsModel (cardDetails: CardDetails) =
{ CardInfo = cardDetails.Card |> toCardInfoModel
HolderName = cardDetails.HolderName.Value
HolderAddress = cardDetails.HolderAddress |> toAddressModel }
let toUserModel (user: User) =
{ Id = user.UserInfo.Id
Name = user.UserInfo.Name.Value
Address = user.UserInfo.Address |> toAddressModel
Cards = user.Cards |> List.map toCardInfoModel }
================================================
FILE: CardManagement/CardManagement.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="CardDomain.fs" />
<Compile Include="BalanceOperation.fs" />
<Compile Include="CardActions.fs" />
<Compile Include="CardDomainQueryModels.fs" />
<Compile Include="CardDomainCommandModels.fs" />
<Compile Include="CardProgramBuilder.fs" />
<Compile Include="CardWorkflow.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsToolkit.ErrorHandling" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CardManagement.Common\CardManagement.Common.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement/CardProgramBuilder.fs
================================================
namespace CardManagement
module CardProgramBuilder =
open CardDomain
open System
open CardManagement.Common
open Errors
(*
Ok, this requires some explanation.
So we've created a lot of functions for validation, logic and model mapping.
All of those functions are pure, so combining them in here is totally fine.
But we have to interact with DB and functions for that are in another layer, we don't have access to them.
However we have to make business logic decisions based on output of those functions,
so we have to emulate or inject them or whatever.
In OOP they use DI frameworks for that, but since the ultimate goal is to move as many errors
in compile time as possible, using classic IoC container would be a step in the opposite direction.
How do we solve this? At first I chose the most obvious way and just passed all the dependencies
in the functions. I kept this code in `obsolete-dependency-managing` branch, see `CardPipeline.fs` file.
Another option (this one) is to use Interpreter pattern.
The idea is that we divide our composition code in 2 parts: execution tree and interpreter for that tree.
Execution tree is a set of sequentual instructions, like this:
- validate input card number, if it's valid
- get me a card by that number. If there's one
- activate it.
- save result.
- map it to model and return.
Now, this tree doesn't know what database we use, what library we use to call it,
it doesn't even know whether we use sync or async calls to do that.
All it knows is a name of operation, input parameter type and return type.
Basically a signature, but without any side effect information, e.g. `Card` instead of `Task<Card>` or `Async<Card>`.
But since we are building a tree structure, instead of using interfaces or plain function signatures,
we use union type with a tuple inside every case.
We use 1 union for 1 bounded context (in our case the whole app is 1 context).
This union represents all the possible dependencies we use in this bounded context.
Every case replresent a placeholder for a dependency.
First element of a tuple inside the case is an input parameter of dependency.
A second tuple is a function, which receives an output parameter of that dependency
and returns the rest of our execution tree branch.
*)
type Program<'a> =
| GetCard of CardNumber * (Card option -> Program<'a>)
| GetCardWithAccountInfo of CardNumber * ((Card*AccountInfo) option -> Program<'a>)
| CreateCard of (Card*AccountInfo) * (Result<unit, DataRelatedError> -> Program<'a>)
| ReplaceCard of Card * (Result<unit, DataRelatedError> -> Program<'a>)
| GetUser of UserId * (User option -> Program<'a>)
| CreateUser of UserInfo * (Result<unit, DataRelatedError> -> Program<'a>)
| GetBalanceOperations of (CardNumber * DateTimeOffset * DateTimeOffset) * (BalanceOperation list -> Program<'a>)
| SaveBalanceOperation of BalanceOperation * (Result<unit, DataRelatedError> -> Program<'a>)
| Stop of 'a
// This bind function allows you to pass a continuation for current node of your expression tree
// the code is basically a boiler plate, as you can see.
let rec bind f instruction =
match instruction with
| GetCard (x, next) -> GetCard (x, (next >> bind f))
| GetCardWithAccountInfo (x, next) -> GetCardWithAccountInfo (x, (next >> bind f))
| CreateCard (x, next) -> CreateCard (x, (next >> bind f))
| ReplaceCard (x, next) -> ReplaceCard (x, (next >> bind f))
| GetUser (x, next) -> GetUser (x,(next >> bind f))
| CreateUser (x, next) -> CreateUser (x,(next >> bind f))
| GetBalanceOperations (x, next) -> GetBalanceOperations (x,(next >> bind f))
| SaveBalanceOperation (x, next) -> SaveBalanceOperation (x,(next >> bind f))
| Stop x -> f x
// this is a set of basic functions. Use them in your expression tree builder to represent dependency call
let stop x = Stop x
let getCardByNumber number = GetCard (number, stop)
let getCardWithAccountInfo number = GetCardWithAccountInfo (number, stop)
let createNewCard (card, acc) = CreateCard ((card, acc), stop)
let replaceCard card = ReplaceCard (card, stop)
let getUserById id = GetUser (id, stop)
let createNewUser user = CreateUser (user, stop)
let getBalanceOperations (number, fromDate, toDate) = GetBalanceOperations ((number, fromDate, toDate), stop)
let saveBalanceOperation op = SaveBalanceOperation (op, stop)
// These are builders for computation expressions. Using CEs will make building execution trees very easy
type SimpleProgramBuilder() =
member __.Bind (x, f) = bind f x
member __.Return x = Stop x
member __.Zero () = Stop ()
member __.ReturnFrom x = x
type ProgramBuilder() =
member __.Bind (x, f) = bind f x
member this.Bind (x, f) =
match x with
| Ok x -> this.ReturnFrom (f x)
| Error e -> this.Return (Error e)
member this.Bind((x: Program<Result<_,_>>), f) =
let f x =
match x with
| Ok x -> this.ReturnFrom (f x)
| Error e -> this.Return (Error e )
this.Bind(x, f)
member __.Return x = Stop x
member __.Zero () = Stop ()
member __.ReturnFrom x = x
let program = ProgramBuilder()
let simpleProgram = SimpleProgramBuilder()
// This is example of using a computation expression `program` from above
let expectDataRelatedErrorProgram (prog: Program<Result<_,DataRelatedError>>) =
program {
let! result = prog //here we retrieve return value from our program `prog`. Like async/await in C#
return expectDataRelatedError result
}
================================================
FILE: CardManagement/CardWorkflow.fs
================================================
namespace CardManagement
(* Finally this is our composition of domain functions. In here we build those execution trees.
If you want to see, how we inject dependencies in here, go to
`CardManagement.Infrastructure.CardProgramInterpreter`. *)
module CardWorkflow =
open CardDomain
open System
open CardDomainCommandModels
open CardManagement.Common
open CardDomainQueryModels
open Errors
open CardProgramBuilder
let private noneToError (a: 'a option) id =
let error = EntityNotFound (sprintf "%sEntity" typeof<'a>.Name, id)
Result.ofOption error a
let private tryGetCard cardNumber =
program {
let! card = getCardByNumber cardNumber
let! card = noneToError card cardNumber.Value |> expectDataRelatedError
return Ok card
}
let processPayment (currentDate: DateTimeOffset, payment) =
program {
(* You can see these `expectValidationError` and `expectDataRelatedErrors` functions here.
What they do is map different errors into `Error` type, since every execution branch
must return the same type, in this case `Result<'a, Error>`.
They also help you quickly understand what's going on in every line of code:
validation, logic or calling external storage. *)
let! cmd = validateProcessPaymentCommand payment |> expectValidationError
let! card = tryGetCard cmd.CardNumber
let today = currentDate.Date |> DateTimeOffset
let tomorrow = currentDate.Date.AddDays 1. |> DateTimeOffset
let! operations = getBalanceOperations (cmd.CardNumber, today, tomorrow)
let spentToday = BalanceOperation.spentAtDate currentDate cmd.CardNumber operations
let! (card, op) =
CardActions.processPayment currentDate spentToday card cmd.PaymentAmount
|> expectOperationNotAllowedError
do! saveBalanceOperation op |> expectDataRelatedErrorProgram
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let setDailyLimit (currentDate: DateTimeOffset, setDailyLimitCommand) =
program {
let! cmd = validateSetDailyLimitCommand setDailyLimitCommand |> expectValidationError
let! card = tryGetCard cmd.CardNumber
let! card = CardActions.setDailyLimit currentDate cmd.DailyLimit card |> expectOperationNotAllowedError
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let topUp (currentDate: DateTimeOffset, topUpCmd) =
program {
let! cmd = validateTopUpCommand topUpCmd |> expectValidationError
let! card = tryGetCard cmd.CardNumber
let! (card, op) = CardActions.topUp currentDate card cmd.TopUpAmount |> expectOperationNotAllowedError
do! saveBalanceOperation op |> expectDataRelatedErrorProgram
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let activateCard activateCmd =
program {
let! cmd = validateActivateCardCommand activateCmd |> expectValidationError
let! result = getCardWithAccountInfo cmd.CardNumber
let! (card, accInfo) = noneToError result cmd.CardNumber.Value |> expectDataRelatedError
let card = CardActions.activate accInfo card
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let deactivateCard deactivateCmd =
program {
let! cmd = validateDeactivateCardCommand deactivateCmd |> expectValidationError
let! card = tryGetCard cmd.CardNumber
let card = CardActions.deactivate card
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let createUser (userId, createUserCommand) =
program {
let! userInfo = validateCreateUserCommand userId createUserCommand |> expectValidationError
do! createNewUser userInfo |> expectDataRelatedErrorProgram
return
{ UserInfo = userInfo
Cards = [] } |> toUserModel |> Ok
}
let createCard cardCommand =
program {
let! card = validateCreateCardCommand cardCommand |> expectValidationError
let accountInfo = AccountInfo.Default cardCommand.UserId
do! createNewCard (card, accountInfo) |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
let getCard cardNumber =
program {
let! cardNumber = CardNumber.create "cardNumber" cardNumber |> expectValidationError
let! card = getCardByNumber cardNumber
return card |> Option.map toCardInfoModel |> Ok
}
let getUser userId =
simpleProgram {
let! maybeUser = getUserById userId
return maybeUser |> Option.map toUserModel
}
================================================
FILE: CardManagement.Api/CardManagement.Api/CardManagement.Api.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<UserSecretsId>c149e81d-fb35-4886-9174-bf495870ef54</UserSecretsId>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="OptionConverter.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<None Include="Dockerfile" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Giraffe" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.4.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CardManagement.Common\CardManagement.Common.fsproj" />
<ProjectReference Include="..\..\CardManagement.Data\CardManagement.Data.fsproj" />
<ProjectReference Include="..\..\CardManagement.Infrastructure\CardManagement.Infrastructure.fsproj" />
<ProjectReference Include="..\..\CardManagement\CardManagement.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement.Api/CardManagement.Api/Dockerfile
================================================
#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed.
#For more information, please see https://aka.ms/containercompat
FROM microsoft/dotnet:2.2-aspnetcore-runtime-nanoserver-1803 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM microsoft/dotnet:2.2-sdk-nanoserver-1803 AS build
WORKDIR /src
COPY ["CardManagement.Api/CardManagement.Api/CardManagement.Api.fsproj", "CardManagement.Api/CardManagement.Api/"]
RUN dotnet restore "CardManagement.Api/CardManagement.Api/CardManagement.Api.fsproj"
COPY . .
WORKDIR "/src/CardManagement.Api/CardManagement.Api"
RUN dotnet build "CardManagement.Api.fsproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "CardManagement.Api.fsproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "CardManagement.Api.dll"]
================================================
FILE: CardManagement.Api/CardManagement.Api/OptionConverter.fs
================================================
namespace CardManagement.Api
open Newtonsoft.Json
open Microsoft.FSharp.Reflection
open System
open System.Collections.Concurrent
[<AutoOpen>]
module CustomConverters =
let private unionCaseCache = ConcurrentDictionary<Type, UnionCaseInfo[]>()
let getUnionCasesFromCache typ =
match unionCaseCache.TryGetValue typ with
| (true, cases) -> cases
| _ ->
let cases = FSharpType.GetUnionCases typ
unionCaseCache.TryAdd(typ, cases) |> ignore
cases
type OptionConverter() =
inherit JsonConverter()
override x.CanConvert(typ) = typ.IsGenericType && typ.GetGenericTypeDefinition() = typedefof<option<_>>
override x.WriteJson(writer, value, serializer) =
let value =
if isNull value then null
else
let _,fields = FSharpValue.GetUnionFields(value, value.GetType())
fields.[0]
serializer.Serialize(writer, value)
override x.ReadJson(reader, typ, existingValue, serializer) =
let innerType =
let innerType = typ.GetGenericArguments().[0]
if innerType.IsValueType then typedefof<Nullable<_>>.MakeGenericType([|innerType|])
else innerType
let cases = getUnionCasesFromCache typ
if reader.TokenType = JsonToken.Null then FSharpValue.MakeUnion(cases.[0], Array.empty)
else
let value = serializer.Deserialize(reader, innerType)
if isNull value then FSharpValue.MakeUnion(cases.[0], Array.empty)
else FSharpValue.MakeUnion(cases.[1], [|value|])
================================================
FILE: CardManagement.Api/CardManagement.Api/Program.fs
================================================
namespace CardManagement.Api
open System
open System.Collections.Generic
open System.IO
open System.Linq
open System.Threading.Tasks
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Giraffe
open CardManagement.Infrastructure
open CardManagement
open Common
open Errors
open ErrorMessages
open Serilog
module Program =
open FSharp.Control.Tasks.V2
open CardManagement.CardDomainCommandModels
open Giraffe.Serialization
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
type [<CLIMutable>] ErrorModel = { Error: string }
let toErrorModel str = { Error = str }
let notFound f = json { Error = "Not found."} f
let errorHandler (ex: Exception) (logger: Microsoft.Extensions.Logging.ILogger) =
match ex with
| :? Newtonsoft.Json.JsonReaderException ->
clearResponse
>=> RequestErrors.BAD_REQUEST ex.Message
| _ ->
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
ex.GetType().FullName |> printf "%s"
clearResponse
>=> ServerErrors.INTERNAL_ERROR ex.Message
let errorToResponse e =
let message = errorMessage e |> toErrorModel |> json
match e with
| Bug exn ->
Log.Error(exn.ToString())
let err = toErrorModel "Oops. Something went wrong" |> json
ServerErrors.internalError err
| OperationNotAllowed _
| ValidationError _ -> RequestErrors.unprocessableEntity message
| DataError e ->
match e with
| EntityNotFound _ -> RequestErrors.notFound message
| _ -> RequestErrors.unprocessableEntity message
let resultToHttpResponseAsync asyncWorkflow : HttpHandler =
fun next ctx ->
task {
let! result = asyncWorkflow |> Async.StartAsTask
let responseFn =
match result with
| Ok ok -> json ok |> Successful.ok
| Error e -> errorToResponse e
return! responseFn next ctx
}
let optionToHttpResponseAsync asyncWorkflow : HttpHandler =
fun next ctx ->
task {
let! result = asyncWorkflow |> Async.StartAsTask
let responseFn =
match result with
| Ok (Some ok) -> json ok |> Successful.ok
| Ok None -> RequestErrors.notFound notFound
| Error e -> errorToResponse e
return! responseFn next ctx
}
let bindJsonForRoute<'a> r f = routeCi r >=> bindJson<'a> f
let webApp =
choose [
GET >=>
choose [
routeCif "/users/%O" (fun userId -> CardApi.getUser userId |> optionToHttpResponseAsync)
routeCif "/cards/%s" (fun cardNumber -> CardApi.getCard cardNumber |> optionToHttpResponseAsync)
]
PATCH >=>
choose [
bindJsonForRoute "/cards/deactivate"
(fun cmd -> CardApi.deactivateCard cmd |> resultToHttpResponseAsync)
bindJsonForRoute "/cards/activate"
(fun cmd -> CardApi.activateCard cmd |> resultToHttpResponseAsync)
bindJsonForRoute "/cards/setDailyLimit"
(fun cmd -> CardApi.setDailyLimit (DateTimeOffset.UtcNow,cmd) |> resultToHttpResponseAsync)
bindJsonForRoute "/cards/processPayment"
(fun cmd -> CardApi.processPayment (DateTimeOffset.UtcNow,cmd) |> resultToHttpResponseAsync)
bindJsonForRoute "/cards/topUp"
(fun cmd -> CardApi.topUp (DateTimeOffset.UtcNow,cmd) |> resultToHttpResponseAsync)
]
POST >=>
choose [
bindJsonForRoute "/users"
(fun cmd -> CardApi.createUser (Guid.NewGuid(),cmd) |> resultToHttpResponseAsync)
bindJsonForRoute "/cards"
(fun cmd -> CardApi.createCard cmd |> resultToHttpResponseAsync)
]
RequestErrors.notFound notFound
]
let configureApp (app : IApplicationBuilder) =
// Add Giraffe to the ASP.NET Core pipeline
app.UseGiraffeErrorHandler(errorHandler)
.UseGiraffe webApp
let configureServices (services : IServiceCollection) =
// Add Giraffe dependencies
services.AddGiraffe() |> ignore
let customSettings = JsonSerializerSettings()
customSettings.Converters.Add(OptionConverter())
let contractResolver = CamelCasePropertyNamesContractResolver()
customSettings.ContractResolver <- contractResolver
services.AddSingleton<IJsonSerializer>(NewtonsoftJsonSerializer(customSettings)) |> ignore
[<EntryPoint>]
let main args =
AppConfiguration.configureLog()
WebHostBuilder()
.UseKestrel()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.Build()
.Run()
0
================================================
FILE: CardManagement.Api/CardManagement.Api/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4364",
"sslPort": 44318
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"CardManagement.Api": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}"
}
}
}
================================================
FILE: CardManagement.Api/CardManagement.Api/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
================================================
FILE: CardManagement.Api/CardManagement.Api/appsettings.json
================================================
{
"MongoDB": {
"Database": "CardDb",
"Host": "localhost",
"Port": 27017,
"User": "root",
"Password": "example"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
================================================
FILE: CardManagement.Common/CardManagement.Common.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="Common.fs" />
<Compile Include="Errors.fs" />
<Compile Include="ErrorMessages.fs" />
<Compile Include="Country.fs" />
<Compile Include="CommonTypes.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement.Common/Common.fs
================================================
namespace CardManagement.Common
[<AutoOpen>]
module Common =
let inline (|HasLength|) x =
fun () -> (^a: (member Length: int) x)
let inline (|HasCount|) x =
fun () -> (^a: (member Count: int) x)
let inline length (HasLength f) = f()
let inline isNullOrEmpty arg =
if arg = null || (length arg) = 0 then true
else false
let bindAsync f a =
async {
let! a = a
return! f a
}
================================================
FILE: CardManagement.Common/CommonTypes.fs
================================================
namespace CardManagement.Common
[<AutoOpen>]
module CommonTypes =
open System.Text.RegularExpressions
open CardManagement.Common.Errors
let cardNumberRegex = new Regex("^[0-9]{16}$", RegexOptions.Compiled)
let lettersRegex = new Regex("^[\w]+[\w ]+[\w]+$", RegexOptions.Compiled)
let postalCodeRegex = new Regex("^[0-9]{5,6}$", RegexOptions.Compiled)
type Month =
| January | February | March | April | May | June | July | August | September | October | November | December
with
member this.ToNumber() =
match this with
| January -> 1us
| February -> 2us
| March -> 3us
| April -> 4us
| May -> 5us
| June -> 6us
| July -> 7us
| August -> 8us
| September -> 9us
| October -> 10us
| November -> 11us
| December -> 12us
static member create field n =
match n with
| 1us -> January |> Ok
| 2us -> February |> Ok
| 3us -> March |> Ok
| 4us -> April |> Ok
| 5us -> May |> Ok
| 6us -> June |> Ok
| 7us -> July |> Ok
| 8us -> August |> Ok
| 9us -> September |> Ok
| 10us -> October |> Ok
| 11us -> November |> Ok
| 12us -> December |> Ok
| _ -> validationError field "Number must be from 1 to 12"
[<Struct>]
type Year = private Year of uint16
with
member this.Value = match this with Year year -> year
static member create field year =
if year >= 2019us && year <= 2050us then Year year |> Ok
else validationError field "Year must be between 2019 and 2050"
type LetterString = private LetterString of string
with
member this.Value = match this with LetterString s -> s
static member create field str =
match str with
| (""|null) -> validationError field "string must contain letters"
| str ->
if lettersRegex.IsMatch(str) then LetterString str |> Ok
else validationError field "string must contain only letters"
[<Struct>]
type MoneyTransaction = private MoneyTransaction of decimal
with
member this.Value = let (MoneyTransaction v) = this in v
static member create amount =
if amount > 0M then MoneyTransaction amount |> Ok
else validationError "transaction" "Transaction amount must be positive"
[<Struct>]
type Money = Money of decimal
with
member this.Value = match this with Money money -> money
static member (+) (Money left, Money right) = left + right |> Money
static member (-) (Money left, Money right) = left - right |> Money
static member (+) (Money money, MoneyTransaction tran) = money + tran |> Money
static member (-) (Money money, MoneyTransaction tran) = money - tran |> Money
type PostalCode = private PostalCode of string
with
member this.Value = match this with PostalCode code -> code
static member create field str =
match str with
| (""|null) -> validationError field "Postal code can't be empty"
| str ->
if postalCodeRegex.IsMatch(str) |> not
then validationError field "postal code must contain 5 or 6 digits and nothing else"
else PostalCode str |> Ok
type Address =
{ Country: Country
City: LetterString
PostalCode: PostalCode
AddressLine1: string
AddressLine2: string }
type nil<'a when 'a: struct and 'a: (new: unit-> 'a) and 'a:> System.ValueType> = System.Nullable<'a>
================================================
FILE: CardManagement.Common/Country.fs
================================================
namespace CardManagement.Common
[<AutoOpen>]
module CountryModule =
open Microsoft.FSharp.Reflection
open Errors
type Country =
| Afghanistan
| Albania
| Algeria
| Andorra
| Angola
| ``Antigua and Barbuda``
| Argentina
| Armenia
| Australia
| Austria
| Azerbaijan
| ``The Bahamas``
| Bahrain
| Bangladesh
| Barbados
| Belarus
| Belgium
| Belize
| Benin
| Bhutan
| Bolivia
| ``Bosnia and Herzegovina``
| Botswana
| Brazil
| Brunei
| Bulgaria
| ``Burkina Faso``
| Burundi
| ``Cabo Verde``
| Cambodia
| Cameroon
| Canada
| ``Central African Republic``
| Chad
| Chile
| China
| Colombia
| Comoros
| ``Congo, Democratic Republic of the``
| ``Congo, Republic of the``
| ``Costa Rica``
| ``Côte d’Ivoire``
| Croatia
| Cuba
| Cyprus
| ``Czech Republic``
| Denmark
| Djibouti
| Dominica
| ``Dominican Republic``
| ``East Timor (Timor-Leste)``
| Ecuador
| Egypt
| ``El Salvador``
| ``Equatorial Guinea``
| Eritrea
| Estonia
| Ethiopia
| Fiji
| Finland
| France
| Gabon
| ``The Gambia``
| Georgia
| Germany
| Ghana
| Greece
| Grenada
| Guatemala
| Guinea
| ``Guinea-Bissau``
| Guyana
| Haiti
| Honduras
| Hungary
| Iceland
| India
| Indonesia
| Iran
| Iraq
| Ireland
| Israel
| Italy
| Jamaica
| Japan
| Jordan
| Kazakhstan
| Kenya
| Kiribati
| ``Korea, North``
| ``Korea, South``
| Kosovo
| Kuwait
| Kyrgyzstan
| Laos
| Latvia
| Lebanon
| Lesotho
| Liberia
| Libya
| Liechtenstein
| Lithuania
| Luxembourg
| Macedonia
| Madagascar
| Malawi
| Malaysia
| Maldives
| Mali
| Malta
| ``Marshall Islands``
| Mauritania
| Mauritius
| Mexico
| ``Micronesia, Federated States of``
| Moldova
| Monaco
| Mongolia
| Montenegro
| Morocco
| Mozambique
| ``Myanmar (Burma)``
| Namibia
| Nauru
| Nepal
| Netherlands
| ``New Zealand``
| Nicaragua
| Niger
| Nigeria
| Norway
| Oman
| Pakistan
| Palau
| Panama
| ``Papua New Guinea``
| Paraguay
| Peru
| Philippines
| Poland
| Portugal
| Qatar
| Romania
| Russia
| Rwanda
| ``Saint Kitts and Nevis``
| ``Saint Lucia``
| ``Saint Vincent and the Grenadines``
| Samoa
| ``San Marino``
| ``Sao Tome and Principe``
| ``Saudi Arabia``
| Senegal
| Serbia
| Seychelles
| ``Sierra Leone``
| Singapore
| Slovakia
| Slovenia
| ``Solomon Islands``
| Somalia
| ``South Africa``
| Spain
| ``Sri Lanka``
| Sudan
| ``Sudan, South``
| Suriname
| Swaziland
| Sweden
| Switzerland
| Syria
| Taiwan
| Tajikistan
| Tanzania
| Thailand
| Togo
| Tonga
| ``Trinidad and Tobago``
| Tunisia
| Turkey
| Turkmenistan
| Tuvalu
| Uganda
| Ukraine
| ``United Arab Emirates``
| ``United Kingdom``
| ``United States``
| Uruguay
| Uzbekistan
| Vanuatu
| ``Vatican City``
| Venezuela
| Vietnam
| Yemen
| Zambia
| Zimbabwe
let (=~) str1 str2 = System.String.Equals(str1, str2, System.StringComparison.InvariantCultureIgnoreCase)
let tryParseEmptyDUCase<'DU> str =
if FSharpType.IsUnion typeof<'DU> |> not then None
else
match str with
| null | "" -> None
| str ->
FSharpType.GetUnionCases typeof<'DU>
|> Array.tryFind (fun c -> c.Name =~ str && (c.GetFields() |> Array.isEmpty))
|> Option.map (fun case -> FSharpValue.MakeUnion(case, [||]) :?> 'DU)
let parseCountry country =
match tryParseEmptyDUCase<Country> country with
| Some country -> Ok country
| None -> Error { FieldPath = "country"; Message = sprintf "Country %s is unknown" country}
================================================
FILE: CardManagement.Common/ErrorMessages.fs
================================================
namespace CardManagement.Common
module ErrorMessages =
open Errors
let private entityDescription = sprintf "[%s] entity with id [%s]"
let dataRelatedErrorMessage =
function
| EntityAlreadyExists (name, id) -> entityDescription name id |> sprintf "%s already exists."
| EntityNotFound (name, id) -> entityDescription name id |> sprintf "%s was not found."
| EntityIsInUse (name, id) -> entityDescription name id |> sprintf "%s is in use."
| UpdateError (name, id, message) ->
message |> (entityDescription name id |> sprintf "%s failed to update. Details:\n%s")
let validationMessage { FieldPath = path; Message = message } =
sprintf "Field [%s] is invalid. Message: %s" path message
let operationNotAllowedMessage { Operation = op; Reason = reason } =
sprintf "Operation [%s] is not allowed. Reason: %s" op reason
let errorMessage error =
match error with
| ValidationError v -> validationMessage v
| OperationNotAllowed o -> operationNotAllowedMessage o
| DataError d -> dataRelatedErrorMessage d
| Bug b -> sprintf "Oops, something went wrong.\n%A" b
================================================
FILE: CardManagement.Common/Errors.fs
================================================
namespace CardManagement.Common
(*
This module is about error handling. One of the problems with exceptions is
they don't appear on function/method signature, so we don't know what to expect
when calling particular method unless we read the code/docs.
So one of the goals here is to make function signatures as descriptive as possible.
That's why we introduce here different types of errors:
- ValidationError: for functions that do only validation and nothing else.
- OperationNotAllowedError: for business logic functions. Sometimes user provides valid data,
but he wants to do something that can't be done, e.g. paying with credit card with no money on it.
- DataRelatedError: for functions that communicate with data storages and 3rd party APIs.
Some things can only be checked when you have enough data, you can't validate them just from your code.
- Panic: for something unexpected. This means that something is broken, so it's most likely a bug.
- Error: finally this is a type to gather all possible errors.
-------------------------------------------------------------------------------------------------------
Having different types of errors and exposing them in function signatures gives us a lot of information
about functions purpose: e.g. when function may return you `DataRelatedError`, you know it's about
data access layer and nothing else.
Same thing goes for `OperationNonAllowedError`: this function operates with valid input and checks only
for business rules violations.
And finally, if function returns just `Error`, it must be a composition of the whole pipeline:
some validation, then probably business rules checking, then some calls to data base or something and so on.
*)
module Errors =
open System
type ValidationError =
{ FieldPath: string
Message: string }
type OperationNotAllowedError =
{ Operation: string
Reason: string }
type DataRelatedError =
| EntityAlreadyExists of entityName: string * id: string
| EntityNotFound of entityName: string * id: string
| EntityIsInUse of entityName: string * id: string
| UpdateError of entityName:string * id: string * message:string
type Error =
| ValidationError of ValidationError
| OperationNotAllowed of OperationNotAllowedError
| DataError of DataRelatedError
| Bug of exn
let validationError fieldPath message = { FieldPath = fieldPath; Message = message } |> Error
let bug exc = Bug exc |> Error
let operationNotAllowed operation reason = { Operation = operation; Reason = reason } |> Error
let notFound name id = EntityNotFound (name, id) |> Error
let entityInUse name = EntityIsInUse name |> Error
let expectValidationError result = Result.mapError ValidationError result
let expectOperationNotAllowedError result = Result.mapError OperationNotAllowed result
let expectDataRelatedError result =
Result.mapError DataError result
let expectDataRelatedErrorAsync asyncResult =
async {
let! result = asyncResult
return expectDataRelatedError result
}
(*
Some type aliases for making code more readable and for preventing
typo-kind of mistakes: so you don't devlare a validation function with
plain `Error` type, for example.
*)
type AsyncResult<'a, 'error> = Async<Result<'a, 'error>>
type ValidationResult<'a> = Result<'a, ValidationError>
type IoResult<'a> = AsyncResult<'a, DataRelatedError>
type PipelineResult<'a> = AsyncResult<'a, Error>
type IoQueryResult<'a> = Async<'a option>
[<RequireQualifiedAccess>]
module Result =
let combine results =
let rec loop acc results =
match results with
| [] -> acc
| result :: tail ->
match result with
| Error e -> Error e
| Ok ok ->
let acc = Result.map (fun oks -> ok :: oks) acc
loop acc tail
loop (Ok []) results
let ofOption err opt =
match opt with
| Some v -> Ok v
| None -> Error err
================================================
FILE: CardManagement.Console/CardManagement.Console.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Content Include="appsettings.Development.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<ProjectReference Include="..\CardManagement.Common\CardManagement.Common.fsproj" />
<ProjectReference Include="..\CardManagement.Data\CardManagement.Data.fsproj" />
<ProjectReference Include="..\CardManagement.Infrastructure\CardManagement.Infrastructure.fsproj" />
<ProjectReference Include="..\CardManagement\CardManagement.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement.Console/Program.fs
================================================
// Learn more about F# at http://fsharp.org
open System
open CardManagement.CardDomainCommandModels
open CardManagement.Infrastructure
open CardManagement
open CardManagement.CardWorkflow
open CardManagement.CardDomainQueryModels
open CardProgramBuilder
[<EntryPoint>]
let main argv =
AppConfiguration.configureLog()
let userId = Guid.Parse "b3f0a6f4-ee04-48ab-b838-9b3330c6bca9"
let cardNumber = "1234123412341234"
//let setDailyLimitModel =
// { SetDailyLimitCardCommandModel.UserId = userId
// Number = cardNumber
// Limit = 500M}
let createUser =
{ Name = "Daario Naharis"
Address =
{ Country = "Russia"
City = "The Great City Of Meereen"
PostalCode = "12345"
AddressLine1 = "Putrid Grove"
AddressLine2 = ""} }
let createCard =
{ CreateCardCommandModel.CardNumber = cardNumber
ExpirationMonth = 11us
ExpirationYear = 2023us
Name = "Daario Naharis"
UserId = userId }
let topUpModel =
{ TopUpCommandModel.CardNumber = cardNumber
TopUpAmount = 10000m }
let paymentModel =
{ ProcessPaymentCommandModel.CardNumber = cardNumber
PaymentAmount = 400M}
let runWholeThingAsync =
async {
let! user = CardApi.createUser (userId, createUser)
let! card = CardApi.createCard createCard
let! card = CardApi.topUp (DateTimeOffset.UtcNow, topUpModel)
let! card = CardApi.processPayment (DateTimeOffset.UtcNow, paymentModel)
return ()
}
runWholeThingAsync |> Async.RunSynchronously
Console.ReadLine() |> ignore
0 // return an integer exit code
================================================
FILE: CardManagement.Console/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
================================================
FILE: CardManagement.Console/appsettings.json
================================================
{
"MongoDB": {
"Database": "CardDb",
"Host": "localhost",
"Port": 27017,
"User": "root",
"Password": "example"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
================================================
FILE: CardManagement.Data/CardDataPipeline.fs
================================================
namespace CardManagement.Data
(*
This is a composition root for data access layer. It combines model mapping and DB interaction,
So it provides nice API for business logic layer: now they you don't have to do mapping in there,
BL layer doesn't even know about entities existence, there's even no reference to DAL project from BL.
And since we are dealing with functions, you don't even have to create interfaces to decouple this layers:
every function has it's signature as an interface.
*)
module CardDataPipeline =
open CardManagement.Common.Errors
open CardManagement.CardDomain
open CardManagement.Data.CardMongoConfiguration
open FsToolkit.ErrorHandling
open CardManagement.Common
open System
type CreateCardAsync = Card*AccountInfo -> IoResult<unit>
type CreateUserAsync = UserInfo -> IoResult<unit>
type ReplaceCardAsync = Card -> IoResult<unit>
type ReplaceUserAsync = UserInfo -> IoResult<unit>
type GetUserInfoAsync = UserId -> IoQueryResult<UserInfo>
type GetUserWithCardsAsync = UserId -> IoQueryResult<User>
type GetCardAsync = CardNumber -> IoQueryResult<Card>
type GetCardWithAccinfoAsync = CardNumber -> IoQueryResult<(Card*AccountInfo)>
type GetBalanceOperationsAsync = CardNumber * DateTimeOffset * DateTimeOffset -> Async<BalanceOperation list>
type CreateBalanceOperationAsync = BalanceOperation -> IoResult<unit>
let createCardAsync (mongoDb: MongoDb) : CreateCardAsync =
fun (card, accountInfo) ->
let cardEntity, _ = card |> DomainToEntityMapping.mapCardToEntity
let accountInfoEntity = (card.CardNumber, accountInfo) |> DomainToEntityMapping.mapAccountInfoToEntity
(cardEntity, accountInfoEntity) |> CommandRepository.createCardAsync mongoDb
let createUserAsync (mongoDb: MongoDb) : CreateUserAsync =
fun user ->
user |> DomainToEntityMapping.mapUserToEntity
|> CommandRepository.createUserAsync mongoDb
let replaceCardAsync (mongoDb: MongoDb) : ReplaceCardAsync =
fun card ->
let cardEntity, maybeAccInfo = card |> DomainToEntityMapping.mapCardToEntity
asyncResult {
do! cardEntity |> CommandRepository.replaceCardAsync mongoDb
match maybeAccInfo with
| None -> return ()
| Some accInfo -> return! accInfo |> CommandRepository.replaceCardAccountInfoAsync mongoDb
}
let replaceUserAsync (mongoDb: MongoDb) : ReplaceUserAsync =
fun user ->
user |> DomainToEntityMapping.mapUserToEntity
|> CommandRepository.replaceUserAsync mongoDb
let getUserInfoAsync (mongoDb: MongoDb) : GetUserInfoAsync =
fun userId ->
async {
let! userInfo = QueryRepository.getUserInfoAsync mongoDb userId
return userInfo |> Option.map EntityToDomainMapping.mapUserInfoEntity
}
let getUserWithCards (mongoDb: MongoDb) : GetUserWithCardsAsync =
fun userId ->
async {
let! userInfo = getUserInfoAsync mongoDb userId
return!
match userInfo with
| None -> None |> async.Return
| Some userInfo ->
async {
let! cardList = QueryRepository.getUserCardsAsync mongoDb userId
let cards = List.map EntityToDomainMapping.mapCardEntity cardList
let user =
{ UserInfo = userInfo
Cards = cards }
return Some user
}
}
let getCardAsync (mongoDb: MongoDb) : GetCardAsync =
fun cardNumber ->
async {
let! card = QueryRepository.getCardAsync mongoDb cardNumber.Value
return card |> Option.map EntityToDomainMapping.mapCardEntity
}
let getCardWithAccountInfoAsync (mongoDb: MongoDb) : GetCardWithAccinfoAsync =
fun cardNumber ->
async {
let! card = QueryRepository.getCardAsync mongoDb cardNumber.Value
return card |> Option.map EntityToDomainMapping.mapCardEntityWithAccountInfo
}
let getBalanceOperationsAsync (mongoDb: MongoDb) : GetBalanceOperationsAsync =
fun (cardNumber, fromDate, toDate) ->
async {
let! operations = QueryRepository.getBalanceOperationsAsync mongoDb (cardNumber.Value, fromDate, toDate)
return List.map EntityToDomainMapping.mapBalanceOperationEntity operations
}
let createBalanceOperationAsync (mongoDb: MongoDb) : CreateBalanceOperationAsync =
fun balanceOperation ->
balanceOperation |> DomainToEntityMapping.mapBalanceOperationToEntity
|> CommandRepository.createBalanceOperationAsync mongoDb
================================================
FILE: CardManagement.Data/CardDomainEntities.fs
================================================
namespace CardManagement.Data
module CardDomainEntities =
open System
open MongoDB.Bson.Serialization.Attributes
open System.Linq.Expressions
open Microsoft.FSharp.Linq.RuntimeHelpers
type UserId = Guid
(*
Over here we have entities for storing our stuff to DB.
We use simple structures so they can be represented via JSON.
Every entity has a different identifier, for User it's Guid `UserId` where for the card it's card number itself.
However we still need some standard way for error messages, e.g. when we want to inform user when
entity with specified Id wasn't found. So we use string `EntityId` property for representing that.
*)
[<CLIMutable>]
type AddressEntity =
{ Country: string
City: string
PostalCode: string
AddressLine1: string
AddressLine2: string }
with
member this.EntityId = sprintf "%A" this
[<CLIMutable>]
type CardEntity =
{ [<BsonId>]
CardNumber: string
Name: string
IsActive: bool
ExpirationMonth: uint16
ExpirationYear: uint16
UserId: UserId }
with
member this.EntityId = this.CardNumber.ToString()
// we use this Id comparer quotation (F# alternative to C# Expression) for updating entity by id,
// since for different entities identifier has different name and type
member this.IdComparer = <@ System.Func<_,_> (fun c -> c.CardNumber = this.CardNumber) @>
[<CLIMutable>]
type CardAccountInfoEntity =
{ [<BsonId>]
CardNumber: string
Balance: decimal
DailyLimit: decimal }
with
member this.EntityId = this.CardNumber.ToString()
member this.IdComparer = <@ System.Func<_,_> (fun c -> c.CardNumber = this.CardNumber) @>
[<CLIMutable>]
type UserEntity =
{ [<BsonId>]
UserId: UserId
Name: string
Address: AddressEntity }
with
member this.EntityId = this.UserId.ToString()
member this.IdComparer = <@ System.Func<_,_> (fun c -> c.UserId = this.UserId) @>
// MongoDb allowes you to use objects as identifiers, so I used this instead of generating some GUID
// which wouldn't mean anything other than something purely DB specific
[<CLIMutable>]
type BalanceOperationId =
{ Timestamp: DateTimeOffset
CardNumber: string }
[<CLIMutable>]
type BalanceOperationEntity =
{ [<BsonId>]
Id: BalanceOperationId
BalanceChange: decimal
NewBalance: decimal }
with
member this.EntityId = sprintf "%A" this.Id
(*
Now here's a little trick: by default F# doesn't allow to use nulls for records and discriminated unions.
So you can't even use construct `if myRecord = null then ...`. Therefore your F# code is null safe.
However we are living in .NET and right now we are using C# library to interact with MongoDB.
This library will return nulls when there's nothing in DB, so we use this `Unchecked.defaultof<>`,
which for reference types returns null.
*)
let isNullUnsafe (arg: 'a when 'a: not struct) =
arg = Unchecked.defaultof<'a>
// then we have this function to convert nulls to option, therefore we limited this
// toxic null thing in here.
let unsafeNullToOption a =
if isNullUnsafe a then None else Some a
(*
Here's another cool feature of F#: we can do structural typing.
Every entity now has `EntityId`, but instead of creating some dull interface and implementing it
in every entity, we can define this 2 functions for retrieving `string EntityId` from every type
that has it.
Note that it works in COMPILE time, it's not reflection magic or something.
For more examples of this thing take a look at https://gist.github.com/atsapura/fd9d7aa26e337eaa2f7f04d6cbb58ef6
*)
let inline (|HasEntityId|) x =
fun () -> (^a : (member EntityId: string) x)
let inline entityId (HasEntityId f) = f()
let inline (|HasIdComparer|) x =
fun () -> (^a : (member IdComparer: Quotations.Expr<Func< ^a, bool>>) x)
// We need to convert F# quotations to C# expressions which C# mongo db driver understands.
let inline idComparer (HasIdComparer id) =
id()
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<_,_>>>
================================================
FILE: CardManagement.Data/CardManagement.Data.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="CardDomainEntities.fs" />
<Compile Include="EntityToDomainMapping.fs" />
<Compile Include="DomainToEntityMapping.fs" />
<Compile Include="CardMongoConfiguration.fs" />
<Compile Include="QueryRepository.fs" />
<Compile Include="CommandRepository.fs" />
<Compile Include="CardDataPipeline.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.8.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CardManagement.Common\CardManagement.Common.fsproj" />
<ProjectReference Include="..\CardManagement\CardManagement.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement.Data/CardMongoConfiguration.fs
================================================
namespace CardManagement.Data
module CardMongoConfiguration =
open CardManagement.Common
open MongoDB.Driver
type MongoSettings =
{ Database: string
Host: string
Port: int
User: string
Password: string }
with
member this.ConnectionString =
if this.User |> isNullOrEmpty then sprintf "mongodb://%s:%i" this.Host this.Port
else sprintf "mongodb://%s:%s@%s:%i" this.User this.Password this.Host this.Port
let private createClient (connectionString:string) = MongoClient(connectionString)
let getDatabase (config: MongoSettings) =
let client = createClient config.ConnectionString
client.GetDatabase(config.Database)
let getSession (config: MongoSettings) =
let client = createClient config.ConnectionString
client.StartSession()
type MongoDb = IMongoDatabase
let [<Literal>] internal cardCollection = "Card"
let [<Literal>] internal userCollection = "User"
let [<Literal>] internal cardAccountInfoCollection = "cardAccountInfo"
let [<Literal>] internal balanceOperationCollection = "BalanceOperation"
type CardNumberString = string
================================================
FILE: CardManagement.Data/CommandRepository.fs
================================================
namespace CardManagement.Data
module CommandRepository =
open CardManagement.Common.Errors
open CardMongoConfiguration
open CardDomainEntities
open System
open MongoDB.Driver
open System.Threading.Tasks
open FsToolkit.ErrorHandling
open System.Linq.Expressions
type CreateUserAsync = UserEntity -> IoResult<unit>
type CreateCardAsync = CardEntity * CardAccountInfoEntity -> IoResult<unit>
type ReplaceUserAsync = UserEntity -> IoResult<unit>
type ReplaceCardAsync = CardEntity -> IoResult<unit>
type ReplaceCardAccountInfoAsync = CardAccountInfoEntity -> IoResult<unit>
type CreateBalanceOperationAsync = BalanceOperationEntity -> IoResult<unit>
let updateOptions =
let opt = UpdateOptions()
opt.IsUpsert <- false
opt
let private isDuplicateKeyException (ex: Exception) =
ex :? MongoWriteException && (ex :?> MongoWriteException).WriteError.Category = ServerErrorCategory.DuplicateKey
let rec private (|DuplicateKey|_|) (ex: Exception) =
match ex with
| :? MongoWriteException as ex when isDuplicateKeyException ex ->
Some ex
| :? MongoBulkWriteException as bex when bex.InnerException |> isDuplicateKeyException ->
Some (bex.InnerException :?> MongoWriteException)
| :? AggregateException as aex when aex.InnerException |> isDuplicateKeyException ->
Some (aex.InnerException :?> MongoWriteException)
| _ -> None
let inline private executeInsertAsync (func: 'a -> Async<unit>) arg =
async {
try
do! func(arg)
return Ok ()
with
| DuplicateKey ex ->
return EntityAlreadyExists (arg.GetType().Name, (entityId arg)) |> Error
}
let inline private executeReplaceAsync (update: _ -> Task<ReplaceOneResult>) arg =
async {
let! updateResult =
update(idComparer arg, arg, updateOptions) |> Async.AwaitTask
if not updateResult.IsAcknowledged then
return sprintf "Update was not acknowledged for %A" arg |> failwith
elif updateResult.MatchedCount = 0L then
return EntityNotFound (arg.GetType().Name, entityId arg) |> Error
else return Ok()
}
let createUserAsync (mongoDb : MongoDb) : CreateUserAsync =
fun userEntity ->
let insertUser = mongoDb.GetCollection(userCollection).InsertOneAsync >> Async.AwaitTask
userEntity |> executeInsertAsync insertUser
let createCardAsync (mongoDb: MongoDb) : CreateCardAsync =
fun (card, accountInfo) ->
let insertCardCommand = mongoDb.GetCollection(cardCollection).InsertOneAsync >> Async.AwaitTask
let insertAccInfoCommand =
mongoDb.GetCollection(cardAccountInfoCollection).InsertOneAsync >> Async.AwaitTask
asyncResult {
do! card |> executeInsertAsync insertCardCommand
do! accountInfo |> executeInsertAsync insertAccInfoCommand
}
let replaceUserAsync (mongoDb: MongoDb) : ReplaceUserAsync =
fun user ->
let replaceCommand (selector: Expression<_>, user, options) =
mongoDb.GetCollection(userCollection).ReplaceOneAsync(selector, user, options)
user |> executeReplaceAsync replaceCommand
let replaceCardAsync (mongoDb: MongoDb) : ReplaceCardAsync =
fun card ->
let replaceCommand (selector: Expression<_>, card, options) =
mongoDb.GetCollection(cardCollection).ReplaceOneAsync(selector, card, options)
card |> executeReplaceAsync replaceCommand
let replaceCardAccountInfoAsync (mongoDb: MongoDb) : ReplaceCardAccountInfoAsync =
fun accInfo ->
let replaceCommand (selector: Expression<_>, accInfo, options) =
mongoDb.GetCollection(cardAccountInfoCollection).ReplaceOneAsync(selector, accInfo, options)
accInfo |> executeReplaceAsync replaceCommand
let createBalanceOperationAsync (mongoDb: MongoDb) : CreateBalanceOperationAsync =
fun balanceOperation ->
let insert = mongoDb.GetCollection(balanceOperationCollection).InsertOneAsync >> Async.AwaitTask
balanceOperation |> executeInsertAsync insert
================================================
FILE: CardManagement.Data/DomainToEntityMapping.fs
================================================
namespace CardManagement.Data
(*
Here however when we map domain types to entities we don't expect any kind of error,
because domain types are valid by their definition.
*)
module DomainToEntityMapping =
open CardManagement
open CardDomain
open CardDomainEntities
open CardManagement.Common
type MapCardAccountInfo = CardNumber * AccountInfo -> CardAccountInfoEntity
type MapCard = Card -> CardEntity * CardAccountInfoEntity option
type MapAddress = Address -> AddressEntity
type MapUser = UserInfo -> UserEntity
type MapBalanceOperation = BalanceOperation -> BalanceOperationEntity
let mapAccountInfoToEntity : MapCardAccountInfo =
fun (cardNumber, accountInfo) ->
let limit =
match accountInfo.DailyLimit with
| Unlimited -> 0m
| Limit limit -> limit.Value
{ Balance = accountInfo.Balance.Value
DailyLimit = limit
CardNumber = cardNumber.Value }
let mapCardToEntity : MapCard =
fun card ->
let isActive =
match card.AccountDetails with
| Deactivated -> false
| Active _ -> true
let details =
match card.AccountDetails with
| Deactivated -> None
| Active accountInfo ->
mapAccountInfoToEntity (card.CardNumber, accountInfo)
|> Some
let card =
{ CardEntity.UserId = card.HolderId
CardNumber = card.CardNumber.Value
Name = card.Name.Value
IsActive = isActive
ExpirationMonth = (fst card.Expiration).ToNumber()
ExpirationYear = (snd card.Expiration).Value }
(card, details)
let mapAddressToEntity : MapAddress =
fun address ->
{ AddressEntity.Country = address.Country.ToString()
City = address.City.Value
PostalCode = address.PostalCode.Value
AddressLine1 = address.AddressLine1
AddressLine2 = address.AddressLine2 }
let mapUserToEntity : MapUser =
fun user ->
{ UserId = user.Id
Address = user.Address |> mapAddressToEntity
Name = user.Name.Value }
let mapBalanceOperationToEntity : MapBalanceOperation =
fun operation ->
{ Id = { Timestamp = operation.Timestamp; CardNumber = operation.CardNumber.Value}
NewBalance = operation.NewBalance.Value
BalanceChange = operation.BalanceChange.ToDecimal() }
================================================
FILE: CardManagement.Data/EntityToDomainMapping.fs
================================================
namespace CardManagement.Data
(*
In our domain types we use types like LetterString, CardNumber etc. with built-in validation.
Those types enforce us to go through validation process, so now we have to validate our
entities during mapping. Normally we shouldn't get any error during this.
We might get it if someone changes data in DB to something invalid directly or if we change
validation rules. In any case we should know about such errors.
*)
module EntityToDomainMapping =
open CardManagement
open CardManagement.CardDomain
open CardDomainEntities
open Common.Errors
open FsToolkit.ErrorHandling
open CardManagement.Common.CommonTypes
open CardManagement.Common
// In here validation error means that invalid data was not provided by user, but instead
// it was in our system. So if we have this error we throw exception
let private throwOnValidationError entityName (err: ValidationError) =
sprintf "Could not deserialize entity [%s]. Field [%s]. Message: %s." entityName err.FieldPath err.Message
|> failwith
let valueOrException (result: Result< 'a, ValidationError>) : 'a =
match result with
| Ok v -> v
| Error e -> throwOnValidationError typeof<'a>.Name e
let private validateCardEntityWithAccInfo (cardEntity: CardEntity, cardAccountEntity)
: Result<Card * AccountInfo, ValidationError> =
result {
let! cardNumber = CardNumber.create "cardNumber" cardEntity.CardNumber
let! name = LetterString.create "name" cardEntity.Name
let! month = Month.create "expirationMonth" cardEntity.ExpirationMonth
let! year = Year.create "expirationYear" cardEntity.ExpirationYear
let accountInfo =
{ Balance = Money cardAccountEntity.Balance
DailyLimit = DailyLimit.ofDecimal cardAccountEntity.DailyLimit
HolderId = cardEntity.UserId }
let cardAccountInfo =
if cardEntity.IsActive then
accountInfo
|> Active
else Deactivated
return
({ CardNumber = cardNumber
Name = name
HolderId = cardEntity.UserId
Expiration = (month, year)
AccountDetails = cardAccountInfo }, accountInfo)
}
let private validateCardEntity (cardEntity: CardEntity, cardAccountEntity) : Result<Card, ValidationError> =
validateCardEntityWithAccInfo (cardEntity, cardAccountEntity)
|> Result.map fst
let mapCardEntity (cardEntity, cardAccountEntity) =
validateCardEntity (cardEntity, cardAccountEntity)
|> valueOrException
let mapCardEntityWithAccountInfo (cardEntity, cardAccountEntity) =
validateCardEntityWithAccInfo (cardEntity, cardAccountEntity)
|> valueOrException
let private validateAddressEntity (entity: AddressEntity) : Result<Address, ValidationError> =
result {
let! country = parseCountry entity.Country
let! city = LetterString.create "city" entity.City
let! postalCode = PostalCode.create "postalCode" entity.PostalCode
return
{ Country = country
City = city
PostalCode = postalCode
AddressLine1 = entity.AddressLine1
AddressLine2 = entity.AddressLine2 }
}
let mapAddressEntity entity = validateAddressEntity entity |> valueOrException
let private validateUserInfoEntity (entity: UserEntity) : Result<UserInfo, ValidationError> =
result {
let! name = LetterString.create "name" entity.Name
let! address = validateAddressEntity entity.Address
return
{ Id = entity.UserId
Name = name
Address = address}
}
let mapUserInfoEntity (entity: UserEntity) =
validateUserInfoEntity entity
|> valueOrException
let mapUserEntity (entity: UserEntity) (cardEntities: (CardEntity * CardAccountInfoEntity) list) =
result {
let! userInfo = validateUserInfoEntity entity
let! cards = List.map validateCardEntity cardEntities |> Result.combine
return
{ UserInfo = userInfo
Cards = cards }
} |> valueOrException
let mapBalanceOperationEntity (entity: BalanceOperationEntity) =
result {
let! cardNumber = entity.Id.CardNumber |> CardNumber.create "id.cardNumber"
let! balanceChange =
if entity.BalanceChange < 0M then
-entity.BalanceChange |> MoneyTransaction.create |> Result.map Decrease
else entity.BalanceChange |> MoneyTransaction.create |> Result.map Increase
return
{ CardNumber = cardNumber
NewBalance = Money entity.NewBalance
Timestamp = entity.Id.Timestamp
BalanceChange = balanceChange }
} |> valueOrException
================================================
FILE: CardManagement.Data/QueryRepository.fs
================================================
namespace CardManagement.Data
module QueryRepository =
open System.Linq
open CardDomainEntities
open MongoDB.Driver
open CardMongoConfiguration
open System
type IoQueryResult<'a> = Async<'a option>
type GetCardAsync = MongoDb -> CardNumberString -> IoQueryResult<(CardEntity * CardAccountInfoEntity)>
type GetUserAsync = MongoDb -> UserId -> IoQueryResult<UserEntity>
type GetUserCardsAsync = MongoDb -> UserId -> Async<(CardEntity * CardAccountInfoEntity) list>
type GetBalanceOperationsAsync = MongoDb -> (CardNumberString * DateTimeOffset * DateTimeOffset) -> Async<BalanceOperationEntity list>
let private runSingleQuery dbQuery id =
async {
let! result = dbQuery id |> Async.AwaitTask
return unsafeNullToOption result
}
let private getCardQuery (mongoDb: MongoDb) cardnumber =
mongoDb.GetCollection<CardEntity>(cardCollection)
.Find(fun c -> c.CardNumber = cardnumber)
.FirstOrDefaultAsync()
let private getAccountInfoQuery (mongoDb: MongoDb) cardnumber =
mongoDb.GetCollection<CardAccountInfoEntity>(cardAccountInfoCollection)
.Find(fun c -> c.CardNumber = cardnumber)
.FirstOrDefaultAsync()
let getCardAsync : GetCardAsync =
fun mongoDb cardNumber ->
let cardQuery = getCardQuery mongoDb
let accInfoQuery = getAccountInfoQuery mongoDb
async {
let! card = runSingleQuery cardQuery cardNumber
let! accInfo = runSingleQuery accInfoQuery cardNumber
return
match card, accInfo with
| Some card, Some accInfo -> Some (card, accInfo)
| _ -> None
}
let private getUserQuery (mongoDb: MongoDb) userId =
mongoDb.GetCollection<UserEntity>(userCollection)
.Find(fun u -> u.UserId = userId)
.FirstOrDefaultAsync()
let getUserInfoAsync : GetUserAsync =
fun mongoDb userId ->
let query = getUserQuery mongoDb
runSingleQuery query userId
let private getUserCardsQuery (mongoDb: MongoDb) userId =
mongoDb.GetCollection<CardEntity>(cardCollection)
.Find(fun c -> c.UserId = userId)
.ToListAsync()
let private getUserAccountInfosQuery (mongoDb: MongoDb) (cardNumbers: #seq<_>) =
mongoDb.GetCollection<CardAccountInfoEntity>(cardAccountInfoCollection)
.Find(fun a -> cardNumbers.Contains a.CardNumber)
.ToListAsync()
let getUserCardsAsync : GetUserCardsAsync =
fun mongoDb userId ->
let cardsCall = getUserCardsQuery mongoDb >> Async.AwaitTask
let getUserAccountInfosCall = getUserAccountInfosQuery mongoDb >> Async.AwaitTask
async {
let! cards = cardsCall userId
let! accountInfos = Seq.map (fun (c: CardEntity) -> c.CardNumber) cards |> getUserAccountInfosCall
// I didn't manage to make `Join` work, so here's some ugly hack
let accountInfos = accountInfos.ToDictionary(fun a -> a.CardNumber)
return [
for card in cards do
yield (card, accountInfos.[card.CardNumber])
]
}
let private getBalanceOperationsQuery (mongoDb: MongoDb) (cardNumber, fromDate, toDate) =
mongoDb.GetCollection<BalanceOperationEntity>(balanceOperationCollection)
.Find(fun bo -> bo.Id.CardNumber = cardNumber && bo.Id.Timestamp >= fromDate && bo.Id.Timestamp < toDate)
.ToListAsync()
let getBalanceOperationsAsync : GetBalanceOperationsAsync =
fun mongoDb (cardNumber, fromDate, toDate) ->
let operationsCall = getBalanceOperationsQuery mongoDb >> Async.AwaitTask
async {
let! result = operationsCall (cardNumber, fromDate, toDate)
return result |> List.ofSeq
}
================================================
FILE: CardManagement.Infrastructure/AppConfiguration.fs
================================================
namespace CardManagement.Infrastructure
module AppConfiguration =
open Microsoft.Extensions.Configuration
open System.IO
open System
open CardManagement.Data.CardMongoConfiguration
open Serilog
open Serilog.Sinks.SystemConsole.Themes
let stringSetting defaultValue str = Option.ofObj str |> Option.defaultValue defaultValue
let intSetting defaultValue (str: string) =
match Int32.TryParse str with
| (false, _) -> defaultValue
| (true, setting) -> setting
let buildConfig() =
ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.Build()
let [<Literal>] logFormat = "[{Timestamp:dd/MM/yy HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}"
let configureLog() =
let logger = LoggerConfiguration().WriteTo.Console(outputTemplate = logFormat, theme = AnsiConsoleTheme.Code).CreateLogger()
Log.Logger <- logger
let getMongoSettings (config: IConfigurationRoot) =
let setting = sprintf "MongoDB:%s"
let database = config.[setting "Database"] |> stringSetting "CardsDb"
let port = config.[setting "Port"] |> intSetting 27017
let host = config.[setting "Host"] |> stringSetting "localhost"
let user = config.[setting "User"] |> stringSetting "root"
let password = config.[setting "Password"] |> stringSetting "example"
{ Database = database
Host = host
Port = port
User = user
Password = password }
================================================
FILE: CardManagement.Infrastructure/CardApi.fs
================================================
namespace CardManagement.Infrastructure
module CardApi =
open CardManagement
open Logging
let createUser arg =
arg |> (CardWorkflow.createUser >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createUser")
let createCard arg =
arg |> (CardWorkflow.createCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createCard")
let activateCard arg =
arg |> (CardWorkflow.activateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.activateCard")
let deactivateCard arg =
arg |> (CardWorkflow.deactivateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.deactivateCard")
let processPayment arg =
arg |> (CardWorkflow.processPayment >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.processPayment")
let topUp arg =
arg |> (CardWorkflow.topUp >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.topUp")
let setDailyLimit arg =
arg |> (CardWorkflow.setDailyLimit >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.setDailyLimit")
let getCard arg =
arg |> (CardWorkflow.getCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.getCard")
let getUser arg =
arg |> (CardWorkflow.getUser >> CardProgramInterpreter.interpretSimple |> logifyResultAsync "CardApi.getUser")
================================================
FILE: CardManagement.Infrastructure/CardManagement.Infrastructure.fsproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="Logging.fs" />
<Compile Include="AppConfiguration.fs" />
<Compile Include="CardProgramInterpreter.fs" />
<Compile Include="CardApi.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Serilog" Version="2.8.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CardManagement.Common\CardManagement.Common.fsproj" />
<ProjectReference Include="..\CardManagement.Data\CardManagement.Data.fsproj" />
<ProjectReference Include="..\CardManagement\CardManagement.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.6.2" />
</ItemGroup>
</Project>
================================================
FILE: CardManagement.Infrastructure/CardProgramInterpreter.fs
================================================
namespace CardManagement.Infrastructure
module CardProgramInterpreter =
open CardManagement
open CardManagement.Common
open Logging
open CardProgramBuilder
open CardManagement.Data
open Errors
let private mongoSettings() = AppConfiguration.buildConfig() |> AppConfiguration.getMongoSettings
let private getMongoDb() = mongoSettings() |> CardMongoConfiguration.getDatabase
let private getCardAsync mongoDb =
CardDataPipeline.getCardAsync mongoDb |> logifyPlainAsync "CardDataPipeline.getCardAsync"
let private getUserAsync mongoDb =
CardDataPipeline.getUserWithCards mongoDb |> logifyPlainAsync "CardDataPipeline.getUserWithCardsAsync"
let private getCardWithAccInfoAsync mongoDb =
CardDataPipeline.getCardWithAccountInfoAsync mongoDb |> logifyPlainAsync "CardDataPipeline.getCardWithAccountInfoAsync"
let private replaceCardAsync mongoDb =
CardDataPipeline.replaceCardAsync mongoDb |> logifyResultAsync "CardDataPipeline.replaceCardAsync"
let private getBalanceOperationsAsync mongoDb =
CardDataPipeline.getBalanceOperationsAsync mongoDb |> logifyPlainAsync "CardDataPipeline.getBalanceOperationsAsync"
let private saveBalanceOperationAsync mongoDb =
CardDataPipeline.createBalanceOperationAsync mongoDb |> logifyResultAsync "CardDataPipeline.createBalanceOperationAsync"
let private createCardAsync mongoDb =
CardDataPipeline.createCardAsync mongoDb |> logifyResultAsync "CardPipeline.createCardAsync"
let private createUserAsync mongoDb =
CardDataPipeline.createUserAsync mongoDb |> logifyResultAsync "CardDataPipeline.createUserAsync"
(* Here is where we inject dependencies. Unlike classic IoC container
it checks that you have all the dependencies in compile time. *)
let rec private interpretCardProgram mongoDb prog =
match prog with
| GetCard (cardNumber, next) ->
cardNumber |> getCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetCardWithAccountInfo (number, next) ->
number |> getCardWithAccInfoAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| CreateCard ((card,acc), next) ->
(card, acc) |> createCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| ReplaceCard (card, next) ->
card |> replaceCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetUser (id, next) ->
getUserAsync mongoDb id |> bindAsync (next >> interpretCardProgram mongoDb)
| CreateUser (user, next) ->
user |> createUserAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetBalanceOperations (request, next) ->
getBalanceOperationsAsync mongoDb request |> bindAsync (next >> interpretCardProgram mongoDb)
| SaveBalanceOperation (op, next) ->
saveBalanceOperationAsync mongoDb op |> bindAsync (next >> interpretCardProgram mongoDb)
| Stop a -> async.Return a
let interpret prog =
try
let interpret = interpretCardProgram (getMongoDb())
interpret prog
with
| failure -> Bug failure |> Error |> async.Return
let interpretSimple prog =
try
let interpret = interpretCardProgram (getMongoDb())
async {
let! result = interpret prog
return Ok result
}
with
| failure -> Bug failure |> Error |> async.Return
================================================
FILE: CardManagement.Infrastructure/Logging.fs
================================================
namespace CardManagement.Infrastructure
(*
All the logging is defined in here, except for configuration.
The idea is simple: you use functions `logify` and `logifyAsync` to wrap functions you want to log.
It will print start of function execution and execution result upon finishing.
*)
module Logging =
open CardManagement.Common
open Serilog
open Errors
open ErrorMessages
let private funcFinishedWithError funcName = sprintf "%s finished with error: %s" funcName
let logDataError funcName e = dataRelatedErrorMessage e |> funcFinishedWithError funcName |> Log.Warning
let logValidationError funcName e = validationMessage e |> funcFinishedWithError funcName |> Log.Information
let logOperationNotAllowed funcName e = operationNotAllowedMessage e |> funcFinishedWithError funcName |> Log.Warning
let logError funcName e =
match e with
| DataError e -> logDataError funcName e
| ValidationError e -> logValidationError funcName e
| OperationNotAllowed e -> logOperationNotAllowed funcName e
| Bug _ ->
let errorMessage = errorMessage e
Log.Error(errorMessage)
let private logResult funcName result =
match result with
| Ok ok -> sprintf "%s finished with result\n%A" funcName ok |> Log.Information
| Error e ->
match box e with
| :? DataRelatedError as er -> logDataError funcName er
| :? Error as er -> logError funcName er
| :? ValidationError as er -> logValidationError funcName er
| :? OperationNotAllowedError as er -> logOperationNotAllowed funcName er
| e -> sprintf "%A" e |> Log.Error
let logifyResult funcName func x =
sprintf "start %s with arg\n%A" funcName x |> Log.Information
let result = func x
logResult funcName result
result
let logifyResultAsync funcName funcAsync x =
async {
sprintf "start %s with arg\n%A" funcName x |> Log.Information
let! result = funcAsync x
logResult funcName result
return result
}
let logifyPlainAsync funcName funcAsync x =
async {
sprintf "start %s with arg\n%A" funcName x |> Log.Information
let! result = funcAsync x
sprintf "%s finished with result\n%A" funcName result |> Log.Information
return result
}
================================================
FILE: CardManagement.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.572
MinimumVisualStudioVersion = 10.0.40219.1
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CardManagement", "CardManagement\CardManagement.fsproj", "{5438E2FD-4002-4BDF-859D-47F2E1B536B6}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CardManagement.Common", "CardManagement.Common\CardManagement.Common.fsproj", "{5F07D8E7-80FE-4C45-9F06-280EF75A51DF}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CardManagement.Api", "CardManagement.Api\CardManagement.Api\CardManagement.Api.fsproj", "{83F3B3AF-970D-4D14-ADED-93E34820EC03}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CardManagement.Data", "CardManagement.Data\CardManagement.Data.fsproj", "{0DB9F3A2-1FB0-42F5-BFDB-528367FFC44E}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CardManagement.Infrastructure", "CardManagement.Infrastructure\CardManagement.Infrastructure.fsproj", "{69C44B43-EC3A-461D-9587-1D7D860394A1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CardManagement.Console", "CardManagement.Console\CardManagement.Console.fsproj", "{E3DB1DF2-673E-4627-927B-95E094BCCC2F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5438E2FD-4002-4BDF-859D-47F2E1B536B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5438E2FD-4002-4BDF-859D-47F2E1B536B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5438E2FD-4002-4BDF-859D-47F2E1B536B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5438E2FD-4002-4BDF-859D-47F2E1B536B6}.Release|Any CPU.Build.0 = Release|Any CPU
{5F07D8E7-80FE-4C45-9F06-280EF75A51DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F07D8E7-80FE-4C45-9F06-280EF75A51DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F07D8E7-80FE-4C45-9F06-280EF75A51DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F07D8E7-80FE-4C45-9F06-280EF75A51DF}.Release|Any CPU.Build.0 = Release|Any CPU
{83F3B3AF-970D-4D14-ADED-93E34820EC03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83F3B3AF-970D-4D14-ADED-93E34820EC03}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83F3B3AF-970D-4D14-ADED-93E34820EC03}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83F3B3AF-970D-4D14-ADED-93E34820EC03}.Release|Any CPU.Build.0 = Release|Any CPU
{0DB9F3A2-1FB0-42F5-BFDB-528367FFC44E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DB9F3A2-1FB0-42F5-BFDB-528367FFC44E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DB9F3A2-1FB0-42F5-BFDB-528367FFC44E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DB9F3A2-1FB0-42F5-BFDB-528367FFC44E}.Release|Any CPU.Build.0 = Release|Any CPU
{69C44B43-EC3A-461D-9587-1D7D860394A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69C44B43-EC3A-461D-9587-1D7D860394A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69C44B43-EC3A-461D-9587-1D7D860394A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69C44B43-EC3A-461D-9587-1D7D860394A1}.Release|Any CPU.Build.0 = Release|Any CPU
{E3DB1DF2-673E-4627-927B-95E094BCCC2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3DB1DF2-673E-4627-927B-95E094BCCC2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3DB1DF2-673E-4627-927B-95E094BCCC2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3DB1DF2-673E-4627-927B-95E094BCCC2F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73984EBC-C77E-4D1B-BB0C-A1E8714711C1}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
# Card Management
## Why?
This is a "real world" example application, written entirely in F#.
The goal is to create a best practice for building applications or at least give a reasonable manual to design one.
## Summary
It's a very simple bank application.
Here you can
- Create/Read users
- Create/Read cards for those users
- Set daily limits for cards
- Top up balance
- Process payments (according to your current balance, daily limit and today's spendings)
## Tech
To run this thing you'll need:
- .NET Core 2.2+
- Docker
- Visual Studio 2017+ or VS Code with Ionide plugin or Rider with F# plugin
Database here is MongoDb, hosted in docker container.
So you just have to navigate to `docker` folder and run `docker-compose up`. That's it.
For web api `Giraffe` framework is used. You can also play with it using `CardManagement.Console` project.
## Project overview
There are several projects in this solution, in order of referencing:
- CardManagement.Common. Self explanatory, I guess.
- CardManagement. This is a business logic project, a core of this application. Domain types, actions and composition root for this layer
- CardManagement.Data. Data access layer. Contains entities, db interaction functions and composition root for this layer.
- CardManagement.Infrastructure. In here you'll find a global composition root, logging, app configuration functions.
- CardManagement.Console/CardManagement.Api. Entry point for using global composition root from infrastructure.
## Detailed description
[Here's](https://github.com/atsapura/CardManagement/blob/master/article/Fighting.Complexity.md) long and boring explanation of why anyone would even bother to use F# for web api and tips on how to do it in such a way so you don't blow your head off.
================================================
FILE: SampleCalls.http
================================================
### Create user
POST https://localhost:5001/users
Content-Type: application/json
Content-Length: 136
{"name": "Uncle Vesemir", "address":{"country":"Finland", "city": "Helsinki", "postalCode": "12345", "addressLine1":"Redenia st. 156"}}
### Create card
POST https://localhost:5001/cards
Content-Type: application/json
Content-Length: 162
{"cardNumber": "4111111111111111", "name": "Uncle Vesemir", "expirationMonth": "8", "expirationYear":"2024", "userId":"69f79c5b-d31b-4a03-a038-062a10b59ad1"}
### top up
PATCH https://localhost:5001/cards/topUp
Content-Type: application/json
Content-Length: 55
{"cardNumber": "4111111111111111", "topUpAmount":8900}
### pay
PATCH https://localhost:5001/cards/processPayment
Content-Type: application/json
Content-Length: 55
{"cardNumber": "1234123412341234","paymentAmount":137}
### set daily limit
PATCH https://localhost:5001/cards/setDailyLimit
Content-Type: application/json
Content-Length: 52
{"cardNumber": "1234123412341234","limit":900}
### deactivate
PATCH https://localhost:5001/cards/deactivate
Content-Type: application/json
Content-Length: 35
{"cardNumber": "1234123412341234"}
### activate
PATCH https://localhost:5001/cards/activate
Content-Type: application/json
Content-Length: 35
{"cardNumber": "1234123412341234"}
================================================
FILE: article/Fighting.Complexity.md
================================================
# Fighting complexity in software development
## What's this about
After working on different projects, I've noticed that every one of them had some common problems, regardless of domain, architecture, code convention and so on. Those problems weren't challenging, just a tedious routine: making sure you didn't miss anything stupid and obvious. Instead of doing this routine on a daily basis I became obsessed with seeking solution: some development approach or code convention or whatever that will help me to design a project in a way that will prevent those problems from happening, so I can focus on interesting stuff. That's the goal of this article: to describe those problems and show you that mix of tools and approaches that I found to solve them.
## Problems we face
While developing software we face a lot of difficulties along the way: unclear requirements, miscommunication, poor development process and so on.
We also face some technical difficulties: legacy code slows us down, scaling is tricky, some bad decisions of the past kick us in the teeth today.
All of them can be if not eliminated then significantly reduced, but there's one fundamental problem you can do nothing about: the complexity of your system.
The idea of a system you are developing itself is always complex, whether you understand it or not.
Even when you're making _yet another CRUD application_, there're always some edge cases, some tricky things, and from time to time someone asks "Hey, what's gonna happen if I do this and this under these circumstances?" and you say "Hm, that's a very good question.".
Those tricky cases, shady logic, validation and access managing - all that adds up to your big idea.
Quite often that idea is so big that it doesn't fit in one head, and that fact alone brings problems like miscommunication.
But let's be generous and assume that this team of domain experts and business analysts communicates clearly and produces fine consistent requirements.
Now we have to implement them, to express that complex idea in our code. Now that code is another system, way more complicated than original idea we had in mind(s).
How so? It faces reality: technical limitations force you to deal with highload, data consistency and availability on top of implementing actual business logic.
As you can see the task is pretty challenging, and now we need proper tools to deal with it.
A programming language is just another tool, and like with every other tool, it's not just about the quality of it, it's probably even more about the tool fitting the job. You might have the best screwdriver there is, but if you need to put some nails into wood, a crappy hammer would be better, right?
## Technical aspects
Most popular languages today are object oriented. When someone makes an introduction to OOP they usually use examples:
Consider a car, which is an object from the real world. It has various properties like brand, weight, color, max speed, current speed and so on.
To reflect this object in our program we gather those properties in one class. Properties can be permanent or mutable, which together form both current state of this object and some boundaries in which it may vary. However combining those properties isn't enough, since we have to check that current state makes sense, e.g. current speed doesn't exceed max speed. To make sure of that we attach some logic to this class, mark properties as private to prevent anyone from creating illegal state.
As you can see objects are about their internal state and life cycle.
So those three pillars of OOP make perfect sense in this context: we use inheritance to reuse certain state manipulations, encapsulation for state protection and polymorphism for treating similar objects the same way. Mutability as a default also makes sense, since in this context immutable object can't have a life cycle and has always one state, which isn't the most common case.
Thing is when you look at a typical web application of these days, it doesn't deal with objects. Almost everything in our code has either eternal lifetime or no proper lifetime at all. Two most common kinds of "objects" are some sort of services like `UserService`, `EmployeeRepository` or some models/entities/DTOs or whatever you call them. Services have no logical state inside them, they die and born again exactly the same, we just recreate the dependency graph with a new database connection.
Entities and models don't have any behavior attached to them, they are merely bundles of data, their mutability doesn't help but quite the opposite.
Therefore key features of OOP aren't really useful for developing this kind of applications.
What happens in a typical web app is data flowing: validation, transformation, evaluation and so on. And there's a paradigm that fits perfectly for that kind of job: functional programming. And there's a proof for that: all the modern features in popular languages today come from there: `async/await`, lambdas and delegates, reactive programming, discriminated unions (enums in swift or rust, not to be confused with enums in java or .net), tuples - all that is from FP.
However those are just crumbles, it's very nice to have them, but there's more, way more.
Before I go any deeper, there's a point to be made. Switching to a new language, especially a new paradigm, is an investment for developers and therefore for business. Doing foolish investments won't give you anything but troubles, but reasonable investments may be the very thing that'll keep you afloat.
## Tools we have and what they give us
A lot of us prefer languages with static typing. The reason for that is simple: compiler takes care of tedious checks like passing proper parameters to functions, constructing our entities correctly and so on. These checks come for free. Now, as for the stuff that compiler can't check, we have a choice: hope for the best or make some tests. Writing tests means money, and you don't pay just once per test, you have to maintain them. Besides, people get sloppy, so every once in a while we get false positive and false negative results. The more tests you have to write the lower is the average quality of those tests. There's another problem: in order to test something, you have to know and remember that that thing should be tested, but the bigger your system is the easier it is to miss something.
However compiler is only as good as the type system of the language. If it doesn't allow you to express something in static ways, you have to do that in runtime. Which means tests, yes. It's not only about type system though, syntax and small sugar features are very important too, because at the end of the day we want to write as little code as possible, so if some approach requires you to write ten times more lines, well, no one is gonna use it. That's why it's important that language you choose has the fitting set of features and tricks - well, right focus overall. If it doesn't - instead of using its features to fight original challenges like complexity of your system and changing requirements, you gonna be fighting the language as well. And it all comes down to money, since you pay developers for their time. The more problem they have to solve, the more time they gonna need and the more developers you are gonna need.
Finally we are about to see some code to prove all that. I'm happen to be a .NET developer, so code samples are gonna be in C# and F#, but the general picture would look more or less the same in other popular OOP and FP languages.
## Let the coding begin
We are gonna build a web application for managing credit cards.
Basic requirements:
- Create/Read users
- Create/Read credit cards
- Activate/Deactivate credit cards
- Set daily limit for cards
- Top up balance
- Process payments (considering balance, card expiration date, active/deactivated state and daily limit)
For the sake of simplicity we are gonna use one card per account and we will skip authorization. But for the rest we're gonna build capable application with validation, error handling, database and web api. So let's get down to our first task: design credit cards.
First, let's see what it would look like in C#
```csharp
public class Card
{
public string CardNumber {get;set;}
public string Name {get;set;}
public int ExpirationMonth {get;set;}
public int ExpirationYear {get;set;}
public bool IsActive {get;set;}
public AccountInfo AccountInfo {get;set;}
}
public class AccountInfo
{
public decimal Balance {get;set;}
public string CardNumber {get;set;}
public decimal DailyLimit {get;set;}
}
```
But that's not enough, we have to add validation, and commonly it's being done in some `Validator`, like the one from `FluentValidation`.
The rules are simple:
- Card number is required and must be a 16-digit string.
- Name is required and must contain only letters and can contain spaces in the middle.
- Month and year have to satisfy boundaries.
- Account info must be present when the card is active and absent when the card is deactivated. If you are wondering why, it's simple: when card is deactivated, it shouldn't be possible to change balance or daily limit.
```csharp
public class CardValidator : IValidator
{
internal static CardNumberRegex = new Regex("^[0-9]{16}$");
internal static NameRegex = new Regex("^[\w]+[\w ]+[\w]+$");
public CardValidator()
{
RuleFor(x => x.CardNumber)
.Must(c => !string.IsNullOrEmpty(c) && CardNumberRegex.IsMatch(c))
.WithMessage("oh my");
RuleFor(x => x.Name)
.Must(c => !string.IsNullOrEmpty(c) && NameRegex.IsMatch(c))
.WithMessage("oh no");
RuleFor(x => x.ExpirationMonth)
.Must(x => x >= 1 && x <= 12)
.WithMessage("oh boy");
RuleFor(x => x.ExpirationYear)
.Must(x => x >= 2019 && x <= 2023)
.WithMessage("oh boy");
RuleFor(x => x.AccountInfo)
.Null()
.When(x => !x.IsActive)
.WithMessage("oh boy");
RuleFor(x => x.AccountInfo)
.NotNull()
.When(x => x.IsActive)
.WithMessage("oh boy");
}
}
```
Now there're several problems with this approach:
- Validation is separated from type declaration, which means to see the full picture of _what card really is_ we have to navigate through code and recreate this image in our head. It's not a big problem when it happens only once, but when we have to do that for every single entity in a big project, well, it's very time consuming.
- This validation isn't forced, we have to keep in mind to use it everywhere. We can ensure this with tests, but then again, you have to remember about it when you write tests.
- When we want to validate card number in other places, we have to do same thing all over again. Sure, we can keep regex in a common place, but still we have to call it in every validator.
In F# we can do it in a different way:
```fsharp
// First we define a type for CardNumber with private constructor
// and public factory which receives string and returns `Result<CardNumber, string>`.
// Normally we would use `ValidationError` instead, but string is good enough for example
type CardNumber = private CardNumber of string
with
member this.Value = match this with CardNumber s -> s
static member create str =
match str with
| (null|"") -> Error "card number can't be empty"
| str ->
if cardNumberRegex.IsMatch(str) then CardNumber str |> Ok
else Error "Card number must be a 16 digits string"
// Then in here we express this logic "when card is deactivated, balance and daily limit manipulations aren't available`.
// Note that this is way easier to grasp that reading `RuleFor()` in validators.
type CardAccountInfo =
| Active of AccountInfo
| Deactivated
// And then that's it. The whole set of rules is here, and it's described in a static way.
// We don't need tests for that, the compiler is our test. And we can't accidentally miss this validation.
type Card =
{ CardNumber: CardNumber
Name: LetterString // LetterString is another type with built-in validation
HolderId: UserId
Expiration: (Month * Year)
AccountDetails: CardAccountInfo }
```
Of course some things from here we can do in C#. We can create `CardNumber` class which will throw `ValidationException` in there too. But that trick with `CardAccountInfo` can't be done in C# in easy way.
Another thing - C# heavily relies on exceptions. There are several problems with that:
- Exceptions have "go to" semantics. One moment you're here in this method, another - you ended up in some global handler.
- They don't appear in method signature. Exceptions like `ValidationException` or `InvalidUserOperationException` are part of the contract, but you don't know that until you read _implementation_. And it's a major problem, because quite often you have to use code written by someone else, and instead of reading just signature, you have to navigate all the way to the bottom of the call stack, which takes a lot of time.
And this is what bothers me: whenever I implement some new feature, implementation process itself doesn't take much time, the majority of it goes to two things:
- Reading other people's code and figuring out business logic rules.
- Making sure nothing is broken.
It may sound like a symptom of a bad code design, but same thing what happens even on decently written projects.
Okay, but we can try use same `Result` thing in C#. The most obvious implementation would look like this:
```csharp
public class Result<TOk, TError>
{
public TOk Ok {get;set;}
public TError Error {get;set;}
}
```
and it's a pure garbage, it doesn't prevent us from setting both `Ok` and `Error` and allows error to be completely ignored. The proper version would be something like this:
```csharp
public abstract class Result<TOk, TError>
{
public abstract bool IsOk { get; }
private sealed class OkResult : Result<TOk, TError>
{
public readonly TOk _ok;
public OkResult(TOk ok) { _ok = ok; }
public override bool IsOk => true;
}
private sealed class ErrorResult : Result<TOk, TError>
{
public readonly TError _error;
public ErrorResult(TError error) { _error = error; }
public override bool IsOk => false;
}
public static Result<TOk, TError> Ok(TOk ok) => new OkResult(ok);
public static Result<TOk, TError> Error(TError error) => new ErrorResult(error);
public Result<T, TError> Map<T>(Func<TOk, T> map)
{
if (this.IsOk)
{
var value = ((OkResult)this)._ok;
return Result<T, TError>.Ok(map(value));
}
else
{
var value = ((ErrorResult)this)._error;
return Result<T, TError>.Error(value);
}
}
public Result<TOk, T> MapError<T>(Func<TError, T> mapError)
{
if (this.IsOk)
{
var value = ((OkResult)this)._ok;
return Result<TOk, T>.Ok(value);
}
else
{
var value = ((ErrorResult)this)._error;
return Result<TOk, T>.Error(mapError(value));
}
}
}
```
Pretty cumbersome, right? And I didn't even implement the `void` versions for `Map` and `MapError`. The usage would look like this:
```csharp
void Test(Result<int, string> result)
{
var squareResult = result.Map(x => x * x);
}
```
Not so bad, uh? Well, now imagine you have three results and you want to do something with them when all of them are `Ok`. Nasty. So that's hardly an option.
F# version:
```fsharp
// this type is in standard library, but declaration looks like this:
type Result<'ok, 'error> =
| Ok of 'ok
| Error of 'error
// and usage:
let test res1 res2 res3 =
match res1, res2, res3 with
| Ok ok1, Ok ok2, Ok ok3 -> printfn "1: %A 2: %A 3: %A" ok1 ok2 ok3
| _ -> printfn "fail"
```
Basically, you have to choose whether you write reasonable amount of code, but the code is obscure, relies on exceptions, reflection, expressions and other "magic", or you write much more code, which is hard to read, but it's more durable and straight forward. When such a project gets big you just can't fight it, not in languages with C#-like type systems. Let's consider a simple scenario: you have some entity in your codebase for a while. Today you want to add a new required field. Naturally you need to initialize this field everywhere this entity is created, but compiler doesn't help you at all, since class is mutable and `null` is a valid value. And libraries like `AutoMapper` make it even harder. This mutability allows us to partially initialize objects in one place, then push it somewhere else and continue initialization there. That's another source of bugs.
Meanwhile language feature comparison is nice, however it's not what this article about. If you're interested in it, I covered that topic in my [previous article](https://medium.com/@liman.rom/f-spoiled-me-or-why-i-dont-enjoy-c-anymore-39e025035a98). But language features themselves shouldn't be a reason to switch technology.
So that brings us to these questions:
1. Why do we really need to switch from modern OOP?
2. Why should we switch to FP?
Answer to first question is using common OOP languages for modern applications gives you a lot of troubles, because they were designed for a different purposes. It results in time and money you spend to fight their design along with fighting complexity of your application.
And the second answer is FP languages give you an easy way to design your features so they work like a clock, and if a new feature breaks existing logic, it breaks the code, hence you know that immediately.
***
However those answers aren't enough. As my friend pointed out during one of our discussions, switching to FP would be useless when you don't know best practices. Our big industry produced tons of articles, books and tutorials about designing OOP applications, and we have production experience with OOP, so we know what to expect from different approaches. Unfortunately, it's not the case for functional programming, so even if you switch to FP, your first attempts most likely would be awkward and certainly wouldn't bring you the desired result: fast and painless developing of complex systems.
Well, that's precisely what this article is about. As I said, we're gonna build production-like application to see the difference.
## How do we design application?
A lot of this ideas I used in design process I borrowed from the great book [Domain Modeling Made Functional](https://www.amazon.com/Domain-Modeling-Made-Functional-Domain-Driven/dp/1680502549), so I strongly encourage you to read it.
Full source code with comments is [here](https://github.com/atsapura/CardManagement). Naturally, I'm not going to put all of it in here, so I'll just walk through key points.
We'll have 4 main projects: business layer, data access layer, infrastructure and, of course, common. Every solution has it, right?
We begin with modeling our domain. At this point we don't know and don't care about database. It's done on purpose, because having specific database in mind we tend to design our domain according to it, we bring this entity-table relation in business layer, which later brings problems. You only need implement mapping `domain -> DAL` once, while wrong design will trouble us constantly until the point we fix it. So here's what we do: we create a project named `CardManagement` (very creative, I know), and immediately turn on the setting `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` in project file. Why do we need this? Well, we're gonna use discriminated unions heavily, and when you do pattern matching, compiler gives us a warning, if we didn't cover all the possible cases:
```fsharp
let fail result =
match result with
| Ok v -> printfn "%A" v
// warning: Incomplete pattern matches on this expression. For example, the value 'Error' may indicate a case not covered by the pattern(s).
```
With this setting on, this code just won't compile, which is exactly what we need, when we extend existing functionality and want it to be adjusted everywhere. Next thing we do is creating module (it compiles in a static class) `CardDomain`. In this file we describe domain types and nothing more. Keep in mind that in F#, code and file order matters: by default you can use only what you declared earlier.
### Domain types
We begin defining our types with `CardNumber` I showed before, although we're gonna need more practical `Error` than just a string, so we'll use `ValidationError`.
```fsharp
type ValidationError =
{ FieldPath: string
Message: string }
let validationError field message = { FieldPath = field; Message = message }
// Actually we should use here Luhn's algorithm, but I leave it to you as an exercise,
// so you can see for yourself how easy is updating code to new requirements.
let private cardNumberRegex = new Regex("^[0-9]{16}$", RegexOptions.Compiled)
type CardNumber = private CardNumber of string
with
member this.Value = match this with CardNumber s -> s
static member create fieldName str =
match str with
| (null|"") -> validationError fieldName "card number can't be empty"
| str ->
if cardNumberRegex.IsMatch(str) then CardNumber str |> Ok
else validationError fieldName "Card number must be a 16 digits string"
```
Then we of course define `Card` which is the heart of our domain. We know that card has some permanent attributes like number, expiration date and name on card, and some changeable information like balance and daily limit, so we encapsulate that changeable info in other type:
```fsharp
type AccountInfo =
{ HolderId: UserId
Balance: Money
DailyLimit: DailyLimit }
type Card =
{ CardNumber: CardNumber
Name: LetterString
HolderId: UserId
Expiration: (Month * Year)
AccountDetails: CardAccountInfo }
```
Now, there're several types here, which we haven't declared yet:
1. **Money**
We could use `decimal` (and we will, but no directly), but `decimal` is less descriptive. Besides, it can be used for representation of other things than money, and we don't want it to be mixed up. So we use custom type `type [<Struct>] Money = Money of decimal `.
2. **DailyLimit**
Daily limit can be either set to a specific amount or to be absent at all. If it's present, it must be positive. Instead of using `decimal` or `Money` we define this type:
```fsharp
[<Struct>]
type DailyLimit =
private // private constructor so it can't be created directly outside of module
| Limit of Money
| Unlimited
with
static member ofDecimal dec =
if dec > 0m then Money dec |> Limit
else Unlimited
member this.ToDecimalOption() =
match this with
| Unlimited -> None
| Limit limit -> Some limit.Value
```
It is more descriptive than just implying that `0M` means that there's no limit, since it also could mean that you can't spend money on this card. The only problem is since we've hidden the constructor, we can't do pattern matching. But no worries, we can use [Active Patterns](https://fsharpforfunandprofit.com/posts/convenience-active-patterns/):
```fsharp
let (|Limit|Unlimited|) limit =
match limit with
| Limit dec -> Limit dec
| Unlimited -> Unlimited
```
Now we can pattern match `DailyLimit` everywhere as a regular DU.
3. **LetterString**
That one is simple. We use same technique as in `CardNumber`. One little thing though: `LetterString` is hardly about credit cards, it's a rather thing and we should move it in `Common` project in `CommonTypes` module. Time comes we move `ValidationError` into separate place as well.
4. **UserId**
That one is just an alias `type UserId = System.Guid`. We use it for descriptiveness only.
5. **Month and Year**
Those have to go to `Common` too. `Month` is gonna be a discriminated union with methods to convert it to and from `unsigned int16`, `Year` is going to be like `CardNumber` but for `uint16` instead of string.
Now let's finish our domain types declaration. We need `User` with some user information and card collection, we need balance operations for top-ups and payments.
```fsharp
type UserInfo =
{ Name: LetterString
Id: UserId
Address: Address }
type User =
{ UserInfo : UserInfo
Cards: Card list }
[<Struct>]
type BalanceChange =
| Increase of increase: MoneyTransaction // another common type with validation for positive amount
| Decrease of decrease: MoneyTransaction
with
member this.ToDecimal() =
match this with
| Increase i -> i.Value
| Decrease d -> -d.Value
[<Struct>]
type BalanceOperation =
{ CardNumber: CardNumber
Timestamp: DateTimeOffset
BalanceChange: BalanceChange
NewBalance: Money }
```
Good, we designed our types in a way that invalid state is unrepresentable. Now whenever we deal with instance of any of these types we are sure that data in there is valid and we don't have to validate it again. Now we can proceed to business logic!
### Business logic
We'll have an unbreakable rule here: all business logic is gonna be coded in **pure functions**. A pure function is a function which satisfies following criteria:
- The only thing it does is computes output value. It has no side effects at all.
- It always produces same output for the same input.
Hence pure functions don't throw exceptions, don't produce random values, don't interact with outside world at any form, be it database or a simple `DateTime.Now`. Of course interacting with impure function automatically renders calling function impure. So what shall we implement?
Here's a list of requirements we have:
- **Activate/deactivate card**
- **Process payments**
We can process payment if:
1. Card isn't expired
2. Card is active
3. There's enough money for the payment
4. Spendings for today haven't exceeded daily limit.
- **Top up balance**
We can top up balance for active and not expired card.
- **Set daily limit**
User can set daily limit if card isn't expired and is active.
When operation can't be completed we have to return an error, so we need to define `OperationNotAllowedError`:
```fsharp
type OperationNotAllowedError =
{ Operation: string
Reason: string }
// and a helper function to wrap it in `Error` which is a case for `Result<'ok,'error> type
let operationNotAllowed operation reason = { Operation = operation; Reason = reason } |> Error
```
In this module with business logic that would be _the only_ type of error we return. We don't do validation in here, don't interact with database - just executing operations if we can otherwise return `OperationNotAllowedError`.
Full module can be found [here](https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardActions.fs). I'll list here the trickiest case here: `processPayment`. We have to check for expiration, active/deactivated status, money spent today and current balance. Since we can't interact with outer world, we have to pass all the necessary information as parameters. That way this _logic_ would be very easy to test, and allows you to do [property based testing](https://github.com/fscheck/FsCheck).
```fsharp
let processPayment (currentDate: DateTimeOffset) (spentToday: Money) card (paymentAmount: MoneyTransaction) =
// first check for expiration
if isCardExpired currentDate card then
cardExpiredMessage card.CardNumber |> processPaymentNotAllowed
else
// then active/deactivated
match card.AccountDetails with
| Deactivated -> cardDeactivatedMessage card.CardNumber |> processPaymentNotAllowed
| Active accInfo ->
// if active then check balance
if paymentAmount.Value > accInfo.Balance.Value then
sprintf "Insufficent funds on card %s" card.CardNumber.Value
|> processPaymentNotAllowed
else
// if balance is ok check limit and money spent today
match accInfo.DailyLimit with
| Limit limit when limit < spentToday + paymentAmount ->
sprintf "Daily limit is exceeded for card %s with daily limit %M. Today was spent %M"
card.CardNumber.Value limit.Value spentToday.Value
|> processPaymentNotAllowed
(*
We could use here the ultimate wild card case like this:
| _ ->
but it's dangerous because if a new case appears in `DailyLimit` type,
we won't get a compile error here, which would remind us to process this
new case in here. So this is a safe way to do the same thing.
*)
| Limit _ | Unlimited ->
let newBalance = accInfo.Balance - paymentAmount
let updatedCard = { card with AccountDetails = Active { accInfo with Balance = newBalance } }
// note that we have to return balance operation, so it can be stored to DB later.
let balanceOperation =
{ Timestamp = currentDate
CardNumber = card.CardNumber
NewBalance = newBalance
BalanceChange = Decrease paymentAmount }
Ok (updatedCard, balanceOperation)
```
This `spentToday` - we'll have to calculate it from `BalanceOperation` collection we'll keep in database. So we'll need module for that, which will basically have 1 public function:
```fsharp
let private isDecrease change =
match change with
| Increase _ -> false
| Decrease _ -> true
let spentAtDate (date: DateTimeOffset) cardNumber operations =
let date = date.Date
let operationFilter { CardNumber = number; BalanceChange = change; Timestamp = timestamp } =
isDecrease change && number = cardNumber && timestamp.Date = date
let spendings = List.filter operationFilter operations
List.sumBy (fun s -> -s.BalanceChange.ToDecimal()) spendings |> Money
```
Good. Now that we're done with all the business logic implementation, time to think about mapping. A lot of our types use discriminated unions, some of our types have no public constructor, so we can't expose them as is to the outside world. We'll need to deal with (de)serialization. Besides that, right now we have only one bounded context in our application, but later on in real life you would want to build a bigger system with multiple bounded contexts, and they have to interact with each other through public contracts, which should be comprehensible for everyone, including other programming languages.
We have to do both way mapping: from public models to domain and vise versa. While mapping from domain to models is pretty straight forward, the other direction has a bit of a pickle: models can have invalid data, after all we use plain types that can be serialized to json. Don't worry, we'll have to build our validation in that mapping. The very fact that we use different types for possibly invalid data and data, that's **always** valid means, that compiler won't let us forget to execute validation.
Here's what it looks like:
```fsharp
// You can use type aliases to annotate your functions. This is just an example, but sometimes it makes code more readable
type ValidateCreateCardCommand = CreateCardCommandModel -> ValidationResult<Card>
let validateCreateCardCommand : ValidateCreateCardCommand =
fun cmd ->
// that's a computation expression for `Result<>` type.
// Thanks to this we don't have to chose between short code and straight forward one,
// like we have to do in C#
result {
let! name = LetterString.create "name" cmd.Name
let! number = CardNumber.create "cardNumber" cmd.CardNumber
let! month = Month.create "expirationMonth" cmd.ExpirationMonth
let! year = Year.create "expirationYear" cmd.ExpirationYear
return
{ Card.CardNumber = number
Name = name
HolderId = cmd.UserId
Expiration = month,year
AccountDetails =
AccountInfo.Default cmd.UserId
|> Active }
}
```
Full module for mappings and validations is [here](https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardDomainCommandModels.fs) and module for mapping to models is [here](https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardDomainQueryModels.fs).
At this point we have implementation for all the business logic, mappings, validation and so on, and so far all of this is completely isolated from real world: it's written in pure functions entirely. Now you're maybe wondering, how exactly are we gonna make use of this? Because we do have to interact with outside world. More than that, during a workflow execution we have to make some decisions based on outcome of those real-world interactions. So the question is how do we assemble all of this? In OOP they use IoC containers to take care of that, but here we can't do that, since we don't even have objects, we have static functions.
We are gonna use `Interpreter pattern` for that! It's a bit tricky, mostly because it's unfamiliar, but I'll do my best to explain this pattern. First, let's talk about function composition. For instance we have a function `int -> string`. This means that function expects `int` as a parameter and returns string. Now let's say we have another function `string -> char`. At this point we can chain them, i.e. execute first one, take it's output and feed it to the second function, and there's even an operator for that: `>>`. Here's how it works:
```fsharp
let intToString (i: int) = i.ToString()
let firstCharOrSpace (s: string) =
match s with
| (null| "") -> ' '
| s -> s.[0]
let firstDigitAsChar = intToString >> firstCharOrSpace
// And you can chain as many functions as you like
let alwaysTrue = intToString >> firstCharOrSpace >> Char.IsDigit
```
However we can't use simple chaining in some scenarios, e.g. activating card. Here's a sequence of actions:
- validate input card number. If it's valid, then
- try to get card by this number. If there's one
- activate it.
- save results. If it's ok then
- map to model and return.
The first two steps have that `If it's ok then...`. That's the reason why direct chaining is not working.
We could simply inject as parameters those functions, like this:
```fsharp
let activateCard getCardAsync saveCardAsync cardNumber = ...
```
But there're certain problems with that. First, number of dependencies can grow big and function signature will look ugly. Second, we are tied to specific effects in here: we have to choose if it's a `Task` or `Async` or just plain sync calls. Third, it's easy to mess things up when you have that many functions to pass: e.g. `createUserAsync` and `replaceUserAsync` have same signature but different effects, so when you have to pass them hundreds of times you can make a mistake with really weird symptoms. Because of those reasons we go for interpreter.
The idea is that we divide our composition code in 2 parts: execution tree and interpreter for that tree. Every node in this tree is a place for a function with effect we want to inject, like `getUserFromDatabase`. Those nodes are defined by name, e.g. `getCard`, input parameter type, e.g. `CardNumber` and return type, e.g. `Card option`. We don't specify here `Task` or `Async`, that's not the part of the tree, _it's a part of interpreter_. Every edge of this tree is some series of pure transformations, like validation or business logic function execution. The edges also have some input, e.g. raw string card number, then there's validation, which can give us an error or a valid card number. If there's an error, we are gonna interrupt that edge, if not, it leads us to the next node: `getCard`. If this node will return `Some card`, we can continue to the next edge, which would be activation, and so on.
For every scenario like `activateCard` or `processPayment` or `topUp` we are gonna build a separate tree. When those trees are built, their nodes are kinda blank, they don't have real functions in them, _they have a place_ for those functions. The goal of interpreter is to fill up those nodes, simple as that. Interpreter knows effects we use, e.g. `Task`, and it knows which real function to put in a given node. When it visits a node, it executes corresponding real function, awaits it in case of `Task` or `Async`, and passes the result to the next edge. That edge may lead to another node, and then it's a work for interpreter again, until this interpreter reaches the stop node, the bottom of our recursion, where we just return the result of the whole execution of our tree.
The whole tree would be represented with discriminated union, and a node would look like this:
```fsharp
type Program<'a> =
| GetCard of CardNumber * (Card option -> Program<'a>) // <- THE NODE
| ... // ANOTHER NODE
```
It's always gonna be a tuple, where the first element is an input for your dependency, and the last element is a _function_, which receives the result of that dependency. That "space" between those elements of tuple is where your dependency will fit in, like in those composition examples, where you have function `'a -> 'b`, `'c -> 'd` and you need to put another one `'b -> 'c` in between to connect them.
Since we are inside of our bounded context, we shouldn't have too many dependencies, and if we do - it's probably a time to split our context into smaller ones.
Here's what it looks like, full source is [here](https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardProgramBuilder.fs):
```fsharp
type Program<'a> =
| GetCard of CardNumber * (Card option -> Program<'a>)
| GetCardWithAccountInfo of CardNumber * ((Card*AccountInfo) option -> Program<'a>)
| CreateCard of (Card*AccountInfo) * (Result<unit, DataRelatedError> -> Program<'a>)
| ReplaceCard of Card * (Result<unit, DataRelatedError> -> Program<'a>)
| GetUser of UserId * (User option -> Program<'a>)
| CreateUser of UserInfo * (Result<unit, DataRelatedError> -> Program<'a>)
| GetBalanceOperations of (CardNumber * DateTimeOffset * DateTimeOffset) * (BalanceOperation list -> Program<'a>)
| SaveBalanceOperation of BalanceOperation * (Result<unit, DataRelatedError> -> Program<'a>)
| Stop of 'a
// This bind function allows you to pass a continuation for current node of your expression tree
// the code is basically a boiler plate, as you can see.
let rec bind f instruction =
match instruction with
| GetCard (x, next) -> GetCard (x, (next >> bind f))
| GetCardWithAccountInfo (x, next) -> GetCardWithAccountInfo (x, (next >> bind f))
| CreateCard (x, next) -> CreateCard (x, (next >> bind f))
| ReplaceCard (x, next) -> ReplaceCard (x, (next >> bind f))
| GetUser (x, next) -> GetUser (x,(next >> bind f))
| CreateUser (x, next) -> CreateUser (x,(next >> bind f))
| GetBalanceOperations (x, next) -> GetBalanceOperations (x,(next >> bind f))
| SaveBalanceOperation (x, next) -> SaveBalanceOperation (x,(next >> bind f))
| Stop x -> f x
// this is a set of basic functions. Use them in your expression tree builder to represent dependency call
let stop x = Stop x
let getCardByNumber number = GetCard (number, stop)
let getCardWithAccountInfo number = GetCardWithAccountInfo (number, stop)
let createNewCard (card, acc) = CreateCard ((card, acc), stop)
let replaceCard card = ReplaceCard (card, stop)
let getUserById id = GetUser (id, stop)
let createNewUser user = CreateUser (user, stop)
let getBalanceOperations (number, fromDate, toDate) = GetBalanceOperations ((number, fromDate, toDate), stop)
let saveBalanceOperation op = SaveBalanceOperation (op, stop)
```
With a help of [computation expressions](https://fsharpforfunandprofit.com/series/computation-expressions.html), we now have a very easy way to build our workflows without having to care about implementation of real-world interactions. We do that in [CardWorkflow module](https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardWorkflow.fs):
```fsharp
// `program` is the name of our computation expression.
// In every `let!` binding we unwrap the result of operation, which can be
// either `Program<'a>` or `Program<Result<'a, Error>>`. What we unwrap would be of type 'a.
// If, however, an operation returns `Error`, we stop the execution at this very step and return it.
// The only thing we have to take care of is making sure that type of error is the same in every operation we call
let processPayment (currentDate: DateTimeOffset, payment) =
program {
(* You can see these `expectValidationError` and `expectDataRelatedErrors` functions here.
What they do is map different errors into `Error` type, since every execution branch
must return the same type, in this case `Result<'a, Error>`.
They also help you quickly understand what's going on in every line of code:
validation, logic or calling external storage. *)
let! cmd = validateProcessPaymentCommand payment |> expectValidationError
let! card = tryGetCard cmd.CardNumber
let today = currentDate.Date |> DateTimeOffset
let tomorrow = currentDate.Date.AddDays 1. |> DateTimeOffset
let! operations = getBalanceOperations (cmd.CardNumber, today, tomorrow)
let spentToday = BalanceOperation.spentAtDate currentDate cmd.CardNumber operations
let! (card, op) =
CardActions.processPayment currentDate spentToday card cmd.PaymentAmount
|> expectOperationNotAllowedError
do! saveBalanceOperation op |> expectDataRelatedErrorProgram
do! replaceCard card |> expectDataRelatedErrorProgram
return card |> toCardInfoModel |> Ok
}
```
This module is the last thing we need to implement in business layer. Also, I've done some refactoring: I moved errors and common types to [Common project](https://github.com/atsapura/CardManagement/tree/master/CardManagement.Common). About time we moved on to implementing data access layer.
### Data access layer
The design of entities in this layer may depend on our database or framework we use to interact with it. Therefore domain layer doesn't know anything about these entities, which means we have to take care of mapping to and from domain models in here. Which is quite convenient for consumers of our DAL API. For this application I've chosen MongoDB, not because it's a best choice for this kind of task, but because there're many examples of using SQL DBs already and I wanted to add something different. We are gonna use C# driver.
For the most part it's gonna be pretty straight forward, the only tricky moment is with `Card`. When it's active it has an `AccountInfo` inside, when it's not it doesn't. So we have to split it in two documents: `CardEntity` and `CardAccountInfoEntity`, so that deactivating card doesn't erase information about balance and daily limit.
Other than that we just gonna use primitive types instead of discriminated unions and types with built-in validation.
There're also few things we need to take care of, since we are using C# library:
- Convert `null`s to `Option<'a>`
- Catch expected exceptions and convert them to our errors and wrap it in `Result<_,_>`
We start with [CardDomainEntities module](https://github.com/atsapura/CardManagement/blob/master/CardManagement.Data/CardDomainEntities.fs), where we define our entities:
```fsharp
[<CLIMutable>]
type CardEntity =
{ [<BsonId>]
CardNumber: string
Name: string
IsActive: bool
ExpirationMonth: uint16
ExpirationYear: uint16
UserId: UserId }
with
// we're gonna need this in every entity for error messages
member this.EntityId = this.CardNumber.ToString()
// we use this Id comparer quotation (F# alternative to C# Expression) for updating entity by id,
// since for different entities identifier has different name and type
member this.IdComparer = <@ System.Func<_,_> (fun c -> c.CardNumber = this.CardNumber) @>
```
Those fields `EntityId` and `IdComparer` we are gonna use with a help of [SRTP](https://gist.github.com/atsapura/fd9d7aa26e337eaa2f7f04d6cbb58ef6). We'll define functions that will retrieve them from any type that has those fields define, without forcing every entity to implement some interface:
```fsharp
let inline (|HasEntityId|) x =
fun () -> (^a : (member EntityId: string) x)
let inline entityId (HasEntityId f) = f()
let inline (|HasIdComparer|) x =
fun () -> (^a : (member IdComparer: Quotations.Expr<Func< ^a, bool>>) x)
// We need to convert F# quotations to C# expressions which C# mongo db driver understands.
let inline idComparer (HasIdComparer id) =
id()
|> LeafExpressionConverter.QuotationToExpression
|> unbox<Expression<Func<_,_>>>
```
As for `null` and `Option` thing, since we use record types, F# compiler doesn't allow using `null` value, neither for assigning nor for comparison. At the same time record types are just another CLR types, so technically we can and will get a `null` value, thanks to C# and design of this library. We can solve this in 2 ways: use `AllowNullLiteral` attribute, or use `Unchecked.defaultof<'a>`. I went for the second choice since this `null` situation should be localized as much as possible:
```fsharp
let isNullUnsafe (arg: 'a when 'a: not struct) =
arg = Unchecked.defaultof<'a>
// then we have this function to convert nulls to option, therefore we limited this
// toxic null thing in here.
let unsafeNullToOption a =
if isNullUnsafe a then None else Some a
```
In order to deal with expected exception for duplicate key, we use Active Patterns again:
```fsharp
// First we define a function which checks, whether exception is about duplicate key
let private isDuplicateKeyException (ex: Exception) =
ex :? MongoWriteException && (ex :?> MongoWriteException).WriteError.Category = ServerErrorCategory.DuplicateKey
// Then we have to check wrapping exceptions for this
let rec private (|DuplicateKey|_|) (ex: Exception) =
match ex with
| :? MongoWriteException as ex when isDuplicateKeyException ex ->
Some ex
| :? MongoBulkWriteException as bex when bex.InnerException |> isDuplicateKeyException ->
Some (bex.InnerException :?> MongoWriteException)
| :? AggregateException as aex when aex.InnerException |> isDuplicateKeyException ->
Some (aex.InnerException :?> MongoWriteException)
| _ -> None
// And here's the usage:
let inline private executeInsertAsync (func: 'a -> Async<unit>) arg =
async {
try
do! func(arg)
return Ok ()
with
| DuplicateKey ex ->
return EntityAlreadyExists (arg.GetType().Name, (entityId arg)) |> Error
}
```
After mapping is implemented we have everything we need to assemble [API for our data access layer](https://github.com/atsapura/CardManagement/blob/master/CardManagement.Data/CardDataPipeline.fs), which looks like this:
```fsharp
// `MongoDb` is a type alias for `IMongoDatabase`
let replaceUserAsync (mongoDb: MongoDb) : ReplaceUserAsync =
fun user ->
user |> DomainToEntityMapping.mapUserToEntity
|> CommandRepository.replaceUserAsync mongoDb
let getUserInfoAsync (mongoDb: MongoDb) : GetUserInfoAsync =
fun userId ->
async {
let! userInfo = QueryRepository.getUserInfoAsync mongoDb userId
return userInfo |> Option.map EntityToDomainMapping.mapUserInfoEntity
}
```
The last moment I mention is when we do mapping `Entity -> Domain`, we have to instantiate types with built-in validation, so there can be validation errors. In this case we won't use `Result<_,_>` because if we've got invalid data in DB, it's a bug, not something we expect. So we just throw an exception. Other than that nothing really interesting is happening in here. The full source code of data access layer you'll find [here](https://github.com/atsapura/CardManagement/tree/master/CardManagement.Data).
### Composition, logging and all the rest
As you remember, we're not gonna use DI framework, we went for interpreter pattern. If you want to know why, here's some reasons:
- IoC container operates in runtime. So until you run your program you can't know that all the dependencies are satisfied.
- It's a powerful tool which is very easy to abuse: you can do property injection, use lazy dependencies, and sometimes even some business logic can find it's way in dependency registering/resolving (yeah, I've witnessed it). All of that makes code maintaining extremely hard.
That means we need a place for that functionality. We could place it on a top level in our Web Api, but in my opinion it's not a best choice: right now we are dealing with only 1 bounded context, but if there's more, this global place with all the interpreters for each context will become cumbersome. Besides, there's single responsibility rule, and web api project should be responsible for web, right? So we create [CardManagement.Infrastructure project](https://github.com/atsapura/CardManagement/tree/master/CardManagement.Infrastructure).
Here we will do several things:
- Composing our functionality
- App configuration
- Logging
If we had more than 1 context, app configuration and log configuration should be moved to global infrastructure project, and the only thing happening in this project would be assembling API for our bounded context, but in our case this separation is not necessary.
Let's get down to composition. We've built execution trees in our domain layer, now we have to interpret them. Every node in that tree represents some dependency call, in our case a call to database. If we had a need to interact with 3rd party api, that would be in here also. So our interpreter has to know how to handle every node in that tree, which is verified in compile time, thanks to `<TreatWarningsAsErrors>` setting. Here's what it looks like:
```fsharp
// Those `bindAsync (next >> interpretCardProgram mongoDb)` work pretty simple:
// we execute async function to the left of this expression, await that operation
// and pass the result to the next node, after which we interpret that node as well,
// until we reach the bottom of this recursion: `Stop a` node.
let rec private interpretCardProgram mongoDb prog =
match prog with
| GetCard (cardNumber, next) ->
cardNumber |> getCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetCardWithAccountInfo (number, next) ->
number |> getCardWithAccInfoAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| CreateCard ((card,acc), next) ->
(card, acc) |> createCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| ReplaceCard (card, next) ->
card |> replaceCardAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetUser (id, next) ->
getUserAsync mongoDb id |> bindAsync (next >> interpretCardProgram mongoDb)
| CreateUser (user, next) ->
user |> createUserAsync mongoDb |> bindAsync (next >> interpretCardProgram mongoDb)
| GetBalanceOperations (request, next) ->
getBalanceOperationsAsync mongoDb request |> bindAsync (next >> interpretCardProgram mongoDb)
| SaveBalanceOperation (op, next) ->
saveBalanceOperationAsync mongoDb op |> bindAsync (next >> interpretCardProgram mongoDb)
| Stop a -> async.Return a
let interpret prog =
try
let interpret = interpretCardProgram (getMongoDb())
interpret prog
with
| failure -> Bug failure |> Error |> async.Return
```
Note that this interpreter is the place where we have this `async` thing. We can do another interpreter with `Task` or just a plain sync version of it. Now you're probably wondering, how we can cover this with unit-test, since familiar mock libraries ain't gonna help us. Well, it's easy: you have to make another interpreter. Here's what it can look like:
```fsharp
type SaveResult = Result<unit, DataRelatedError>
type TestInterpreterConfig =
{ GetCard: Card option
GetCardWithAccountInfo: (Card*AccountInfo) option
CreateCard: SaveResult
ReplaceCard: SaveResult
GetUser: User option
CreateUser: SaveResult
GetBalanceOperations: BalanceOperation list
SaveBalanceOperation: SaveResult }
let defaultConfig =
{ GetCard = Some card
GetUser = Some user
GetCardWithAccountInfo = (card, accountInfo) |> Some
CreateCard = Ok()
GetBalanceOperations = balanceOperations
SaveBalanceOperation = Ok()
ReplaceCard = Ok()
CreateUser = Ok() }
let testInject a = fun _ -> a
let rec interpretCardProgram config (prog: Program<'a>) =
match prog with
| GetCard (cardNumber, next) ->
cardNumber |> testInject config.GetCard |> (next >> interpretCardProgram config)
| GetCardWithAccountInfo (number, next) ->
number |> testInject config.GetCardWithAccountInfo |> (next >> interpretCardProgram config)
| CreateCard ((card,acc), next) ->
(card, acc) |> testInject config.CreateCard |> (next >> interpretCardProgram config)
| ReplaceCard (card, next) ->
card |> testInject config.ReplaceCard |> (next >> interpretCardProgram config)
| GetUser (id, next) ->
id |> testInject config.GetUser |> (next >> interpretCardProgram config)
| CreateUser (user, next) ->
user |> testInject config.CreateUser |> (next >> interpretCardProgram config)
| GetBalanceOperations (request, next) ->
testInject config.GetBalanceOperations request |> (next >> interpretCardProgram config)
| SaveBalanceOperation (op, next) ->
testInject config.SaveBalanceOperation op |> (next >> interpretCardProgram config)
| Stop a -> a
```
We've created `TestInterpreterConfig` which holds desired results of every operation we want to inject. You can easily change that config for every given test and then just run interpreter. This interpreter is sync, since there's no reason to bother with `Task` or `Async`.
There's nothing really tricky about the logging, but you can find it in [this module](https://github.com/atsapura/CardManagement/blob/master/CardManagement.Infrastructure/Logging.fs). The approach is that we wrap the function in logging: we log function name, parameters and log result. If result is ok, it's info, if error it's a warning and if it's a `Bug` then it's an error. That's pretty much it.
One last thing is to make a facade, since we don't want to expose raw interpreter calls. Here's the whole thing:
```fsharp
let createUser arg =
arg |> (CardWorkflow.createUser >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createUser")
let createCard arg =
arg |> (CardWorkflow.createCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createCard")
let activateCard arg =
arg |> (CardWorkflow.activateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.activateCard")
let deactivateCard arg =
arg |> (CardWorkflow.deactivateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.deactivateCard")
let processPayment arg =
arg |> (CardWorkflow.processPayment >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.processPayment")
let topUp arg =
arg |> (CardWorkflow.topUp >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.topUp")
let setDailyLimit arg =
arg |> (CardWorkflow.setDailyLimit >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.setDailyLimit")
let getCard arg =
arg |> (CardWorkflow.getCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.getCard")
let getUser arg =
arg |> (CardWorkflow.getUser >> CardProgramInterpreter.interpretSimple |> logifyResultAsync "CardApi.getUser")
```
All the dependencies here are injected, logging is taken care of, no exceptions is thrown - that's it. For web api I used [Giraffe](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md) framework. Web project is [here](https://github.com/atsapura/CardManagement/tree/master/CardManagement.Api/CardManagement.Api).
## Conclusion
We have built an application with validation, error handling, logging, business logic - all those things you usually have in your application. The difference is this code is way more durable and easy to refactor. Note that we haven't used reflection or code generation, no exceptions, but still our code isn't verbose. It's easy to read, easy to understand and hard to break. As soon as you add another field in your model, or another case in one of our union types, the code won't compile until you update every usage. Sure it doesn't mean you're totally safe or that you don't need any kind of testing at all, it just means that you're gonna have fewer problems when you develope new features or do some refactoring. The development process will be both cheaper and more interesting, because this tool allows you to focus on your domain and business tasks, instead of drugging focus on keeping an eye out that nothing is broken.
Another thing: I don't claim that OOP is completely useless and we don't need it, that's not true. I'm saying that we don't need it for solving _every single task_ we have, and that a big portion of our tasks can be better solved with FP. And truth is, as always, in balance: we can't solve everything efficiently with only one tool, so a good programming language should have a decent support of both FP and OOP. And, unfortunately, a lot of most popular languages today have only lambdas and async programming from functional world.
================================================
FILE: docker/docker-compose.yml
================================================
version: '2.1'
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- 27017:27017
mem_limit: 200m
mongo-express:
image: mongo-express
restart: always
ports:
- 27000:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example
depends_on:
- mongo
mem_limit: 200m
gitextract_g_zz3ke9/
├── .dockerignore
├── .gitignore
├── CardManagement/
│ ├── BalanceOperation.fs
│ ├── CardActions.fs
│ ├── CardDomain.fs
│ ├── CardDomainCommandModels.fs
│ ├── CardDomainQueryModels.fs
│ ├── CardManagement.fsproj
│ ├── CardProgramBuilder.fs
│ └── CardWorkflow.fs
├── CardManagement.Api/
│ └── CardManagement.Api/
│ ├── CardManagement.Api.fsproj
│ ├── Dockerfile
│ ├── OptionConverter.fs
│ ├── Program.fs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── appsettings.Development.json
│ └── appsettings.json
├── CardManagement.Common/
│ ├── CardManagement.Common.fsproj
│ ├── Common.fs
│ ├── CommonTypes.fs
│ ├── Country.fs
│ ├── ErrorMessages.fs
│ └── Errors.fs
├── CardManagement.Console/
│ ├── CardManagement.Console.fsproj
│ ├── Program.fs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── CardManagement.Data/
│ ├── CardDataPipeline.fs
│ ├── CardDomainEntities.fs
│ ├── CardManagement.Data.fsproj
│ ├── CardMongoConfiguration.fs
│ ├── CommandRepository.fs
│ ├── DomainToEntityMapping.fs
│ ├── EntityToDomainMapping.fs
│ └── QueryRepository.fs
├── CardManagement.Infrastructure/
│ ├── AppConfiguration.fs
│ ├── CardApi.fs
│ ├── CardManagement.Infrastructure.fsproj
│ ├── CardProgramInterpreter.fs
│ └── Logging.fs
├── CardManagement.sln
├── README.md
├── SampleCalls.http
├── article/
│ └── Fighting.Complexity.md
└── docker/
└── docker-compose.yml
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (173K chars).
[
{
"path": ".dockerignore",
"chars": 74,
"preview": ".dockerignore\n.env\n.git\n.gitignore\n.vs\n.vscode\n*/bin\n*/obj\n**/.toolstarget"
},
{
"path": ".gitignore",
"chars": 71,
"preview": "/.vs\n[Bb]in\n[Oo]bj\n[Dd]ebug\n[Rr]elease\n*.user\n*.suo\n/.vscode\n/.ionide\n"
},
{
"path": "CardManagement/BalanceOperation.fs",
"chars": 726,
"preview": "namespace CardManagement\n\n[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]\nmodule BalanceOper"
},
{
"path": "CardManagement/CardActions.fs",
"chars": 5062,
"preview": "namespace CardManagement\n\n(*\nThis module contains business logic only. It doesn't know anything about data access layer"
},
{
"path": "CardManagement/CardDomain.fs",
"chars": 5090,
"preview": "namespace CardManagement\n\n(*\nThis file contains our domain types.\nThere are several important goals to pursue when you "
},
{
"path": "CardManagement/CardDomainCommandModels.fs",
"chars": 6514,
"preview": "namespace CardManagement\n\n(*\nThis module contains command models, validated commands and validation functions.\nIn C# co"
},
{
"path": "CardManagement/CardDomainQueryModels.fs",
"chars": 2606,
"preview": "namespace CardManagement\n\n(*\nThis module contains mappings of our domain types to something that user/client will see.\n"
},
{
"path": "CardManagement/CardManagement.fsproj",
"chars": 862,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.0</TargetFramework>\n <TreatWa"
},
{
"path": "CardManagement/CardProgramBuilder.fs",
"chars": 5933,
"preview": "namespace CardManagement\n\nmodule CardProgramBuilder =\n open CardDomain\n open System\n open CardManagement.Commo"
},
{
"path": "CardManagement/CardWorkflow.fs",
"chars": 5178,
"preview": "namespace CardManagement\n\n(* Finally this is our composition of domain functions. In here we build those execution tree"
},
{
"path": "CardManagement.Api/CardManagement.Api/CardManagement.Api.fsproj",
"chars": 1309,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp2.2</TargetFramework>\n <AspN"
},
{
"path": "CardManagement.Api/CardManagement.Api/Dockerfile",
"chars": 924,
"preview": "#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in"
},
{
"path": "CardManagement.Api/CardManagement.Api/OptionConverter.fs",
"chars": 1699,
"preview": "namespace CardManagement.Api\nopen Newtonsoft.Json\nopen Microsoft.FSharp.Reflection\nopen System\nopen System.Collections."
},
{
"path": "CardManagement.Api/CardManagement.Api/Program.fs",
"chars": 5330,
"preview": "namespace CardManagement.Api\n\nopen System\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Th"
},
{
"path": "CardManagement.Api/CardManagement.Api/Properties/launchSettings.json",
"chars": 796,
"preview": "{\n \"iisSettings\": {\n \"windowsAuthentication\": false,\n \"anonymousAuthentication\": true,\n \"iisExpress\": {\n "
},
{
"path": "CardManagement.Api/CardManagement.Api/appsettings.Development.json",
"chars": 137,
"preview": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Debug\",\n \"System\": \"Information\",\n \"Microsoft\": \"Informat"
},
{
"path": "CardManagement.Api/CardManagement.Api/appsettings.json",
"chars": 233,
"preview": "{\n \"MongoDB\": {\n \"Database\": \"CardDb\",\n \"Host\": \"localhost\",\n \"Port\": 27017,\n \"User\": \"root\",\n \"Password"
},
{
"path": "CardManagement.Common/CardManagement.Common.fsproj",
"chars": 510,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.0</TargetFramework>\n <TreatWa"
},
{
"path": "CardManagement.Common/Common.fs",
"chars": 468,
"preview": "namespace CardManagement.Common\n\n[<AutoOpen>]\nmodule Common =\n let inline (|HasLength|) x = \n fun () -> (^a: (m"
},
{
"path": "CardManagement.Common/CommonTypes.fs",
"chars": 3826,
"preview": "namespace CardManagement.Common\n\n[<AutoOpen>]\nmodule CommonTypes =\n\n open System.Text.RegularExpressions\n open Ca"
},
{
"path": "CardManagement.Common/Country.fs",
"chars": 4928,
"preview": "namespace CardManagement.Common\n\n[<AutoOpen>]\nmodule CountryModule =\n open Microsoft.FSharp.Reflection\n open Erro"
},
{
"path": "CardManagement.Common/ErrorMessages.fs",
"chars": 1193,
"preview": "namespace CardManagement.Common\n\nmodule ErrorMessages =\n\n open Errors\n\n let private entityDescription = sprintf \""
},
{
"path": "CardManagement.Common/Errors.fs",
"chars": 4158,
"preview": "namespace CardManagement.Common\n\n(*\nThis module is about error handling. One of the problems with exceptions is\nthey do"
},
{
"path": "CardManagement.Console/CardManagement.Console.fsproj",
"chars": 1171,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>netcoreapp2."
},
{
"path": "CardManagement.Console/Program.fs",
"chars": 1779,
"preview": "// Learn more about F# at http://fsharp.org\n\nopen System\n\n open CardManagement.CardDomainCommandModels\n open Card"
},
{
"path": "CardManagement.Console/appsettings.Development.json",
"chars": 137,
"preview": "{\n \"Logging\": {\n \"LogLevel\": {\n \"Default\": \"Debug\",\n \"System\": \"Information\",\n \"Microsoft\": \"Informat"
},
{
"path": "CardManagement.Console/appsettings.json",
"chars": 233,
"preview": "{\n \"MongoDB\": {\n \"Database\": \"CardDb\",\n \"Host\": \"localhost\",\n \"Port\": 27017,\n \"User\": \"root\",\n \"Password"
},
{
"path": "CardManagement.Data/CardDataPipeline.fs",
"chars": 4779,
"preview": "namespace CardManagement.Data\n(*\nThis is a composition root for data access layer. It combines model mapping and DB int"
},
{
"path": "CardManagement.Data/CardDomainEntities.fs",
"chars": 4470,
"preview": "namespace CardManagement.Data\n\nmodule CardDomainEntities =\n open System\n open MongoDB.Bson.Serialization.Attribut"
},
{
"path": "CardManagement.Data/CardManagement.Data.fsproj",
"chars": 946,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp2.2</TargetFramework>\n <TreatWar"
},
{
"path": "CardManagement.Data/CardMongoConfiguration.fs",
"chars": 1207,
"preview": "namespace CardManagement.Data\n\nmodule CardMongoConfiguration =\n\n open CardManagement.Common\n open MongoDB.Driver\n"
},
{
"path": "CardManagement.Data/CommandRepository.fs",
"chars": 4337,
"preview": "namespace CardManagement.Data\nmodule CommandRepository =\n open CardManagement.Common.Errors\n open CardMongoConfig"
},
{
"path": "CardManagement.Data/DomainToEntityMapping.fs",
"chars": 2496,
"preview": "namespace CardManagement.Data\n(*\nHere however when we map domain types to entities we don't expect any kind of error,\nb"
},
{
"path": "CardManagement.Data/EntityToDomainMapping.fs",
"chars": 5123,
"preview": "namespace CardManagement.Data\n\n(*\nIn our domain types we use types like LetterString, CardNumber etc. with built-in val"
},
{
"path": "CardManagement.Data/QueryRepository.fs",
"chars": 4024,
"preview": "namespace CardManagement.Data\n\nmodule QueryRepository =\n open System.Linq\n open CardDomainEntities\n open Mongo"
},
{
"path": "CardManagement.Infrastructure/AppConfiguration.fs",
"chars": 1598,
"preview": "namespace CardManagement.Infrastructure\n\nmodule AppConfiguration =\n open Microsoft.Extensions.Configuration\n open"
},
{
"path": "CardManagement.Infrastructure/CardApi.fs",
"chars": 1409,
"preview": "namespace CardManagement.Infrastructure\n\nmodule CardApi =\n open CardManagement\n open Logging\n\n let createUser "
},
{
"path": "CardManagement.Infrastructure/CardManagement.Infrastructure.fsproj",
"chars": 1176,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netcoreapp2.2</TargetFramework>\n <TreatWar"
},
{
"path": "CardManagement.Infrastructure/CardProgramInterpreter.fs",
"chars": 3563,
"preview": "namespace CardManagement.Infrastructure\n\nmodule CardProgramInterpreter =\n open CardManagement\n open CardManagemen"
},
{
"path": "CardManagement.Infrastructure/Logging.fs",
"chars": 2440,
"preview": "namespace CardManagement.Infrastructure\n(*\nAll the logging is defined in here, except for configuration.\nThe idea is si"
},
{
"path": "CardManagement.sln",
"chars": 3699,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio 15\nVisualStudioVersion = 15.0.28307.572\nMi"
},
{
"path": "README.md",
"chars": 1769,
"preview": "# Card Management\n## Why?\nThis is a \"real world\" example application, written entirely in F#.\nThe goal is to create a be"
},
{
"path": "SampleCalls.http",
"chars": 1289,
"preview": "### Create user\n\nPOST https://localhost:5001/users\nContent-Type: application/json\nContent-Length: 136\n\n{\"name\": \"Uncle V"
},
{
"path": "article/Fighting.Complexity.md",
"chars": 59321,
"preview": "# Fighting complexity in software development\n## What's this about\nAfter working on different projects, I've noticed tha"
},
{
"path": "docker/docker-compose.yml",
"chars": 508,
"preview": "version: '2.1'\n\nservices:\n\n mongo:\n image: mongo\n restart: always\n environment:\n MONGO_INITDB_ROO"
}
]
About this extraction
This page contains the full source code of the atsapura/CardManagement GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (161.2 KB), approximately 38.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.