首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >带有验证的异步IBAN

带有验证的异步IBAN
EN

Code Review用户
提问于 2019-07-26 20:42:39
回答 2查看 282关注 0票数 6

我正在开发一个iban api,它是关于在数据库(iban验证)之后保存一些成员的iban。代码分为域(实体、iban验证行为)、数据库及其访问(新成员、检索等)。

我正在寻找反馈关于async代码和一般的编码风格。

域实体:

代码语言:javascript
复制
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 }

域行为:

代码语言:javascript
复制
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

实体框架核心DbContext

代码语言:javascript
复制
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

创建的代码

代码语言:javascript
复制
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
EN

回答 2

Code Review用户

回答已采纳

发布于 2019-07-28 06:14:06

我对EF不是很有经验,在我看来,您的代码看起来很好,但是有一些细节您可以改进,如另一个答案所述,当然还有其他的写作方法,我将向您展示,只是为了说明,并不是说这是正确的方法:

代码语言:javascript
复制
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) // consider one liners

    let checkCharacters iban =
        if illegalCharacters.IsMatch iban // parens not needed
        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 = limit 2 iban // since you're using F#+ you can use this generic function, next version will ship with String.truncate
        match Dict.tryGetValue country lengthPerCountry with // also this function is available in F#+
        | Some length when length = iban.Length -> Ok iban
        | None -> Error UnknownCountry
        | _    -> Error IncorrectLength
        // Reorganizing the cases like this makes it easier to visualize the rules.
        // My advice is try not to mix match with if, as far as practical.


    let checkRemainder (iban: string) =

        let digitalIban =
            let rearrangedIban = iban.[4..] + iban.[..3] // You can use F# slicing syntax
            let replaceBase36LetterWithBase10String (s: string) (c: char) =
                String.replace (string c) (string (int c - int 'A' + 10)) s // (int)c looks like a C# cast, but this is not a cast, int is a function.
            List.fold replaceBase36LetterWithBase10String rearrangedIban [ 'A' .. 'Z' ]
            // You can also use String.replace from F#+
            // Note that using string function is shorter and looks more functional than ToString, and most importantly it's culture neutral.
            // ToString without additional parameters depends on current thread culture.

        let remainder =
            let inline reduceOnce r n = Int32.Parse (string r + string n) % 97
            Regex.Matches (digitalIban.[2..], @"\d{1,7}")
            |> fold reduceOnce (reduceOnce 0 (digitalIban.[..1]))
        // This is a bit F#+ advanced stuff: Matches are part of the Foldable abstraction, so you can fold them directly with the generic fold operation.
        // then by using string and making the function online, your reduceOnce becomes polymorphic on 'r'.

        if remainder = 1 then Ok iban
        else Error TypingError

    let format iban = Regex.Replace (iban, ".{4}", "$0 ") |> Ok

let validate =
    checkCharacters
    >=> cleanup
    >=> checkLength
    >=> checkRemainder
    >=> format
// Is not that I am a big fun of point free functions, but I've seen many F# validation examples written in this style, by using composition with the monadic >=> fish operator.

创建的代码

代码语言:javascript
复制
module Rm.Iban.App.IbanRetrieval

open System
open System.Linq
open Microsoft.FSharp.Data
open 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 =
        let requested = memberIbansWith context memberId IbanState.Requested
        requested.Select(fun iban -> Some iban)
                             .SingleOrDefaultAsync()
                    |> Async.AwaitTask
    // The async workflow is not really needed here.

    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)) // here you can use directly async.Return instead of the whole workflow.

let memberIbans (context: DbContext.IbanDbContext) memberId =
    memberIbansWith context memberId IbanState.Ok

就像我说过的,没有什么大的改变,只是一些建议和其他同样的写作方式,这并不意味着它比你已经拥有的更好。

另一个答案是对设计进行一些有趣的更改,对于IbanString建议,您还可以使用一种UoMs技术来区分原始字符串和已验证的ibans,这可能会稍微提高一些效率,因为UoMs在运行时被删除了。

最后要注意的是,您的验证函数所做的不仅仅是验证,您可以这样保留它,它看起来不错,但也许您可以更改名称来反映这一点,比如格式和验证。

票数 3
EN

Code Review用户

发布于 2019-07-27 14:01:31

关于您的解决方案的Async行为,我不能说太多,因为我不习惯于在F#中处理异步编程,但乍一看,它看起来不错。相反,我将集中精力于其他方面。

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' ]

在我看来,这是相当无效的,因为它遍历整个字母表,并查询字母表中每个字符的整个iban字符串,而不只是遍历iban序列:

代码语言:javascript
复制
    let digitalIban =
        let replacer str ch =
            match ch with
            | d when Char.IsDigit ch -> sprintf "%s%c" str ch
            | _ -> sprintf "%s%d" str ((int)ch - (int)'A' + 10)

        iban.Substring(4) + iban.Substring(0,4) |> Seq.fold replacer ""

您可以通过定义一个IbanString来加强验证和数据库检索模块之间的关系:

代码语言:javascript
复制
type IbanString = 
    | IbanString of string

module IbanString =
    let toString = function IbanString(str) -> str

然后,IbanValidation.validate的返回值可以是:

代码语言:javascript
复制
Result<IbanString, ValidationError>

然后updateRequestWith应该只接受一个IbanString,而不是一个任意字符串:

代码语言:javascript
复制
let updateRequestWith (context: DbContext.IbanDbContext) memberId (ibanValue: IbanString) = ...

在其他情况下,当使用IbanValidation.validate时,您知道返回的字符串是有效的,并且是封装的,因此可以区别于所有其他字符串。

类型ValidationError =\x{e76f}\x{e76f}{##**$$}#.=\x{e76f}\x{e76f}{##**$$}\x{e76f}\x{e76f}{##**$$}#.

您可以使用一些有用的信息来增强这些值:

代码语言:javascript
复制
type ValidationError =
    | IllegalCharacters of char list
    | IncorrectLength of Actual:int * Required:int
    | UnknownCountry of string
    | TypingError of Message:string * Remainder:int

例如,可以将checkCharacters更改为:

代码语言:javascript
复制
let private checkCharacters iban =
    match illegalCharacters.Matches(iban) with
    | col when col.Count > 0 -> Error (IllegalCharacters(col |> Seq.map (fun m -> m.Value.[0]) |> Seq.toList))
    | _ -> Ok iban

..。提供一些有关发现的无效字符的信息。

票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/224990

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档