I am working on an iban api which is about saving the iban of some members after (iban validation) in a database. The code is split between Domain (entities, behaviour for iban validation), Database and its access (new member, retrieval, etc.).
I am looking for feedbacks about the async
code and the general coding style.
The Domain entities:
module Rm.Iban.Domain
type IbanState =
| Unknown = 0
| Ok = 1
| Requested = 2
[<CLIMutable>]
type Iban = {
Id: Guid
MemberId: int
Iban: string option
State: IbanState
CreatedOn: DateTimeOffset
UpdatedOn: DateTimeOffset option }
Domain behaviour:
module Rm.Iban.Domain.IbanValidation
open System
open System.Text.RegularExpressions
open FSharpPlus
type ValidationError =
| IllegalCharacters
| IncorrectLength
| UnknownCountry
| TypingError
[<AutoOpen>]
module private Impl =
let illegalCharacters =
Regex(@"[^0-9A-Za-z ]", RegexOptions.Compiled)
let checkCharacters iban =
if illegalCharacters.IsMatch(iban)
then Error IllegalCharacters
else Ok iban
let cleanup =
String.toUpper
>> String.replace " " ""
>> Ok
let lengthPerCountry = dict [
("AL", 28); ("AD", 24); ("AT", 20); ("AZ", 28); ("BE", 16); ("BH", 22); ("BA", 20); ("BR", 29);
("BG", 22); ("CR", 21); ("HR", 21); ("CY", 28); ("CZ", 24); ("DK", 18); ("DO", 28); ("EE", 20);
("FO", 18); ("FI", 18); ("FR", 27); ("GE", 22); ("DE", 22); ("GI", 23); ("GR", 27); ("GL", 18);
("GT", 28); ("HU", 28); ("IS", 26); ("IE", 22); ("IL", 23); ("IT", 27); ("KZ", 20); ("KW", 30);
("LV", 21); ("LB", 28); ("LI", 21); ("LT", 20); ("LU", 20); ("MK", 19); ("MT", 31); ("MR", 27);
("MU", 30); ("MC", 27); ("MD", 24); ("ME", 22); ("NL", 18); ("NO", 15); ("PK", 24); ("PS", 29);
("PL", 28); ("PT", 25); ("RO", 24); ("SM", 27); ("SA", 24); ("RS", 22); ("SK", 24); ("SI", 19);
("ES", 24); ("SE", 24); ("CH", 21); ("TN", 24); ("TR", 26); ("AE", 23); ("GB", 22); ("VG", 24); ]
let checkLength (iban: string) =
let country = iban.Substring(0, Math.Min(2, iban.Length))
match lengthPerCountry.TryGetValue(country) with
| true, length ->
if length = iban.Length
then Ok iban
else Error IncorrectLength
| _ -> Error UnknownCountry
let checkRemainder (iban: string) =
let digitalIban =
let rearrangedIban = iban.Substring(4) + iban.Substring(0,4)
let replaceBase36LetterWithBase10String (s: string) (c: char) =
s.Replace(c.ToString(), ((int)c - (int)'A' + 10).ToString())
List.fold replaceBase36LetterWithBase10String rearrangedIban [ 'A' .. 'Z' ]
let remainder =
let reduceOnce r n = Int32.Parse(r.ToString() + n) % 97
Regex.Matches(digitalIban.Substring(2), @"\d{1,7}")
|> Seq.cast
|> Seq.map (fun x -> x.ToString())
|> Seq.fold reduceOnce (reduceOnce 0 (digitalIban.Substring(0, 2)))
if remainder = 1
then Ok iban
else Error TypingError
let format iban =
Regex.Replace(iban, ".{4}", "$0 ") |> Ok
let validate (iban: string) =
iban
|> checkCharacters
>>= cleanup
>>= checkLength
>>= checkRemainder
>>= format
The Entity Framework Core DbContext
:
module Rm.Iban.Data.DbContext
type IbanDbContext (options: DbContextOptions<IbanDbContext>) =
inherit DbContext(options)
[<DefaultValue>]
val mutable private ibans: DbSet<Iban>
member x.Ibans with get() = x.ibans and set v = x.ibans <- v
The code that create
module Rm.Iban.App.IbanRetrieval
open System
open System.Linq
open Rm.Iban.Data
open Rm.Iban.Domain
open Microsoft.EntityFrameworkCore
type RequestError =
| AlreadyRequested
type MeetRequestError =
| RequestNotFound
| IbanInvalid of IbanValidation.ValidationError
[<AutoOpen>]
module private Impl =
let memberIbansWith (context: DbContext.IbanDbContext) memberId ibanState = query {
for iban in context.Ibans do
where (
iban.MemberId = memberId &&
iban.State = ibanState) }
let requested (context: DbContext.IbanDbContext) memberId =
async {
let requested = memberIbansWith context memberId IbanState.Requested
return! requested.Select(fun iban -> Some iban)
.SingleOrDefaultAsync()
|> Async.AwaitTask
}
let avoidDuplicateRequest (context: DbContext.IbanDbContext) memberId =
async {
let! exists = context.Ibans.AnyAsync(fun iban ->
iban.MemberId = memberId &&
iban.State = IbanState.Requested)
|> Async.AwaitTask
if exists
then return Error AlreadyRequested
else return Ok (context, memberId)
}
let newRequest ((context: DbContext.IbanDbContext), memberId) =
async {
let iban: Iban = {
Id = Guid.Empty
MemberId = memberId
Iban = None
State = IbanState.Requested
CreatedOn = DateTimeOffset.UtcNow
UpdatedOn = None }
let iban = context.Ibans.Add iban
do! context.SaveChangesAsync true
|> Async.AwaitTask
|> Async.Ignore
return Ok iban.Entity
}
let updateRequestWith (context: DbContext.IbanDbContext) memberId ibanValue =
async {
match! requested context memberId with
| Some iban ->
context.UpdateWith iban
{ iban with
Iban = Some ibanValue
State = IbanState.Ok
UpdatedOn = Some DateTimeOffset.UtcNow }
do! context.SaveChangesAsync true
|> Async.AwaitIAsyncResult
|> Async.Ignore
return Ok iban
| _ ->
return Error RequestNotFound
}
let request (context: DbContext.IbanDbContext) memberId =
async {
match! avoidDuplicateRequest context memberId with
| Ok value -> return! newRequest value
| Error error -> return Error error
}
let requested (context: DbContext.IbanDbContext) memberId =
requested context memberId
let meetRequest (context: DbContext.IbanDbContext) memberId ibanValue =
match IbanValidation.validate ibanValue with
| Ok ibanValue -> updateRequestWith context memberId ibanValue
| Error error -> async { return Error (IbanInvalid error) }
let memberIbans (context: DbContext.IbanDbContext) memberId =
memberIbansWith context memberId IbanState.Ok