Business Rule DSL for Values in Domain-Driven Design
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
2
down vote
favorite
Following the patterns from the book Domain Modeling Made Functional, I am implementing a single-case union for the simple values in my domain model instead of using primitives. The union cases have private constructors, and each union type has a module with a create
function that validates the business rules around the type and returns a Result
. To simplify the validation of the business rules, I have created a DSL (in the form of an F# Computation Expression) that applies the specified rules (which can be either required or suggested) and then either returns a successful Result
or a result with the relevant errors/warnings as domain events.
The BusinessRule
DSL is implemented as follows:
open System
// Functions representing the evaluation of a business rule and
// the creation of an error based on the given value
type BusinessRuleExpr<'value, 'error> = ('value -> bool) * ('value -> 'error)
// Business Rules define the restrictions around the creation of a specific type.
// These may be either required (which cause an error when not satisfied) or suggested (which only cause a warning)
type BusinessRule<'value, 'error> =
| Required of BusinessRuleExpr<'value,'error>
| Suggested of BusinessRuleExpr<'value,'error>
module BusinessRule =
let inline require eval error =
[Required (eval, error)]
let inline suggest eval warn =
[Suggested (eval, warn)]
let inline also eval error rules =
rules @ (require eval error)
let inline should eval warn rules =
rules @ (suggest eval warn)
let inline isRequired rule =
match rule with
| Required _ -> true
| _ -> false
let inline isSuggested rule =
match rule with
| Suggested _ -> true
| _ -> false
let inline applies value rule =
match rule with
| Required (eval,_) -> eval value
| Suggested (eval,_) -> eval value
|> not
let inline fail value rule =
match rule with
| Required (_,error) -> error value
| Suggested (_,warn) -> warn value
// Computation Builder for the Business Rule DSL
type BusinessRuleBuilder () =
member inline __.Yield _ = List.empty
[<CustomOperation("require")>]
member inline __.Require (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, error: 'value -> 'error) =
rules |> BusinessRule.also eval error
[<CustomOperation("should")>]
member inline __.Should (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, warn: 'value -> 'error) =
rules |> BusinessRule.should eval warn
[<AutoOpen>]
module Builder =
let businessRules = BusinessRuleBuilder()
The computation builder for BusinessRule
does no implement Bind
or Return
as it would for a monadic computation. Instead, I am simply using the computation builder as a way of creating a simple DSL. I'm not sure what the best practices are around computation builders that don't implement Bind
/Return
, so I'd appreciate any thoughts you may have on that subject.
The business rules themselves are then defined in a separate module:
module Rules =
open System
open System.Text.RegularExpressions
/// Compose function f with functions g and h, and-ing the results
let private (>>&) f (g, h) x =
let r = f x
g r && h r
/// Regular-Expression based business rule
let inline regex pattern =
let completePattern =
if pattern |> Seq.contains '^' || pattern |> Seq.contains '$'
then Regex(pattern, RegexOptions.Compiled)
else Regex(sprintf "^%s$" pattern, RegexOptions.Compiled)
completePattern.IsMatch
// String Rules
let private lengthRule op length = String.length >> op length
let isNotEmpty = String.IsNullOrWhiteSpace >> not
let isAlpha = String.forall Char.IsLetter
let isNumber input = match Decimal.TryParse input with
| true,_ -> true
| _ -> false
let isNumerical = String.forall Char.IsDigit
let isAlphanumeric = String.forall Char.IsLetterOrDigit
let isLength = lengthRule (=)
let maxLength = lengthRule (>=)
let minLength = lengthRule (<=)
let lengthBetween min max = String.length >>& ((>=) max, (<=) min)
// Number rules
// Could we use LanguagePrimitives.GenericZero instead of Unchecked.defaultOf here?
let inline private compareZero<'n when 'n: comparison> = LanguagePrimitives.GenericComparison Unchecked.defaultof<'n>
let isNegative n = n |> (compareZero >> (<) 0)
let isNotNegative n = n|> (compareZero >> (>=) 0)
let isNotPositive n = n|> (compareZero >> (<=) 0)
let isPositive n = n|> (compareZero >> (>) 0)
The rule definitions are generally pretty simple. One thing I'd like to do is replace the Unchecked.defaulof<'n>
with LanguagePrimitives.GenericZero<'n>
in the compareZero
function, but I think I would need a static member on a class to make that work, since the GenericZero
function uses an SRTP for the get_Zero
member.
Finally, I have this little helper module to simplify the validation of values when constructing the domain models. Note, this uses the CurryOn.FSharp.Control package for a type like Result
that includes domain events with successful results as well. This is used to include the warnings with the value for Suggested
rules that aren't satisfied.
[<AutoOpen>]
module private Validation =
open FSharp.Control
open System.Text.RegularExpressions
let inline private validate<'a,'b> (ctor: 'a -> 'b) (validations: BusinessRule<'a, DomainErrors> list) (value: 'a) =
let errors = [for rule in validations |> List.filter BusinessRule.isRequired do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
let warnings = [for rule in validations |> List.filter BusinessRule.isSuggested do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
match errors with
| -> match warnings with
| -> Result.success <| ctor value
| _ -> Result.successWithEvents (ctor value) warnings
| _ -> Result.failure (errors @ warnings)
let validateString<'b> (ctor: string -> 'b) (validations: BusinessRule<string, DomainErrors> list) (value: string) =
if value |> isNull
then Result.failure [ValueWasNull]
else validate ctor validations value
let validateNumber<'a,'b when 'a : comparison> ctor validations =
validate<'a,'b> ctor validations
With the BusinessRule
DSL and the rules themselves defined, I can then build my simple values for the domain model as follows:
[<Struct>] type CostPrice = private CostPrice of decimal
[<Struct>] type FundType = private FundType of string
[<Struct>] type OrderLineNumber = private OrderLineNumber of int
type ValidationError =
| CostPriceIsNegative of decimal
| FundTypeIsEmpty
| InvalidFundType of string
| OrderLineNumberIsNegative of int
| ValueWasNull
module CostPrice =
let create =
validateNumber CostPrice <|
businessRules require Rules.isNotNegative CostPriceIsNegative
let value (CostPrice price) = price
let Default = CostPrice 0M
module FundType =
let create =
validateString FundType <|
businessRules
require Rules.isNotEmpty (fun _ -> FundTypeIsEmpty)
require (Rules.regex "w2d2") InvalidFundType
let value (FundType fundType) = fundType
module OrderLineNumber =
let create =
validateNumber OrderLineNumber <|
businessRules require Rules.isNotNegative OrderLineNumberIsNegative
let value (OrderLineNumber line) = line
let Default = OrderLineNumber 0
And with all that in place, I can make a Value Object that uses the simple values. This uses the operation
computation builder from CurryOn.FSharp.Control to compose the results of each validation.
type Rebate =
LineNumber: OrderLineNumber
FundType: FundType
Amount: CostPrice
static member create lineNumber fundType amount =
operation
let! validLineNumber = OrderLineNumber.create lineNumber
let! validFundType = FundType.create fundType
let! validAmount = CostPrice.create amount
return LineNumber = validLineNumber; FundType = validFundType; Amount = validAmount
functional-programming f# ddd dsl
add a comment |Â
up vote
2
down vote
favorite
Following the patterns from the book Domain Modeling Made Functional, I am implementing a single-case union for the simple values in my domain model instead of using primitives. The union cases have private constructors, and each union type has a module with a create
function that validates the business rules around the type and returns a Result
. To simplify the validation of the business rules, I have created a DSL (in the form of an F# Computation Expression) that applies the specified rules (which can be either required or suggested) and then either returns a successful Result
or a result with the relevant errors/warnings as domain events.
The BusinessRule
DSL is implemented as follows:
open System
// Functions representing the evaluation of a business rule and
// the creation of an error based on the given value
type BusinessRuleExpr<'value, 'error> = ('value -> bool) * ('value -> 'error)
// Business Rules define the restrictions around the creation of a specific type.
// These may be either required (which cause an error when not satisfied) or suggested (which only cause a warning)
type BusinessRule<'value, 'error> =
| Required of BusinessRuleExpr<'value,'error>
| Suggested of BusinessRuleExpr<'value,'error>
module BusinessRule =
let inline require eval error =
[Required (eval, error)]
let inline suggest eval warn =
[Suggested (eval, warn)]
let inline also eval error rules =
rules @ (require eval error)
let inline should eval warn rules =
rules @ (suggest eval warn)
let inline isRequired rule =
match rule with
| Required _ -> true
| _ -> false
let inline isSuggested rule =
match rule with
| Suggested _ -> true
| _ -> false
let inline applies value rule =
match rule with
| Required (eval,_) -> eval value
| Suggested (eval,_) -> eval value
|> not
let inline fail value rule =
match rule with
| Required (_,error) -> error value
| Suggested (_,warn) -> warn value
// Computation Builder for the Business Rule DSL
type BusinessRuleBuilder () =
member inline __.Yield _ = List.empty
[<CustomOperation("require")>]
member inline __.Require (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, error: 'value -> 'error) =
rules |> BusinessRule.also eval error
[<CustomOperation("should")>]
member inline __.Should (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, warn: 'value -> 'error) =
rules |> BusinessRule.should eval warn
[<AutoOpen>]
module Builder =
let businessRules = BusinessRuleBuilder()
The computation builder for BusinessRule
does no implement Bind
or Return
as it would for a monadic computation. Instead, I am simply using the computation builder as a way of creating a simple DSL. I'm not sure what the best practices are around computation builders that don't implement Bind
/Return
, so I'd appreciate any thoughts you may have on that subject.
The business rules themselves are then defined in a separate module:
module Rules =
open System
open System.Text.RegularExpressions
/// Compose function f with functions g and h, and-ing the results
let private (>>&) f (g, h) x =
let r = f x
g r && h r
/// Regular-Expression based business rule
let inline regex pattern =
let completePattern =
if pattern |> Seq.contains '^' || pattern |> Seq.contains '$'
then Regex(pattern, RegexOptions.Compiled)
else Regex(sprintf "^%s$" pattern, RegexOptions.Compiled)
completePattern.IsMatch
// String Rules
let private lengthRule op length = String.length >> op length
let isNotEmpty = String.IsNullOrWhiteSpace >> not
let isAlpha = String.forall Char.IsLetter
let isNumber input = match Decimal.TryParse input with
| true,_ -> true
| _ -> false
let isNumerical = String.forall Char.IsDigit
let isAlphanumeric = String.forall Char.IsLetterOrDigit
let isLength = lengthRule (=)
let maxLength = lengthRule (>=)
let minLength = lengthRule (<=)
let lengthBetween min max = String.length >>& ((>=) max, (<=) min)
// Number rules
// Could we use LanguagePrimitives.GenericZero instead of Unchecked.defaultOf here?
let inline private compareZero<'n when 'n: comparison> = LanguagePrimitives.GenericComparison Unchecked.defaultof<'n>
let isNegative n = n |> (compareZero >> (<) 0)
let isNotNegative n = n|> (compareZero >> (>=) 0)
let isNotPositive n = n|> (compareZero >> (<=) 0)
let isPositive n = n|> (compareZero >> (>) 0)
The rule definitions are generally pretty simple. One thing I'd like to do is replace the Unchecked.defaulof<'n>
with LanguagePrimitives.GenericZero<'n>
in the compareZero
function, but I think I would need a static member on a class to make that work, since the GenericZero
function uses an SRTP for the get_Zero
member.
Finally, I have this little helper module to simplify the validation of values when constructing the domain models. Note, this uses the CurryOn.FSharp.Control package for a type like Result
that includes domain events with successful results as well. This is used to include the warnings with the value for Suggested
rules that aren't satisfied.
[<AutoOpen>]
module private Validation =
open FSharp.Control
open System.Text.RegularExpressions
let inline private validate<'a,'b> (ctor: 'a -> 'b) (validations: BusinessRule<'a, DomainErrors> list) (value: 'a) =
let errors = [for rule in validations |> List.filter BusinessRule.isRequired do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
let warnings = [for rule in validations |> List.filter BusinessRule.isSuggested do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
match errors with
| -> match warnings with
| -> Result.success <| ctor value
| _ -> Result.successWithEvents (ctor value) warnings
| _ -> Result.failure (errors @ warnings)
let validateString<'b> (ctor: string -> 'b) (validations: BusinessRule<string, DomainErrors> list) (value: string) =
if value |> isNull
then Result.failure [ValueWasNull]
else validate ctor validations value
let validateNumber<'a,'b when 'a : comparison> ctor validations =
validate<'a,'b> ctor validations
With the BusinessRule
DSL and the rules themselves defined, I can then build my simple values for the domain model as follows:
[<Struct>] type CostPrice = private CostPrice of decimal
[<Struct>] type FundType = private FundType of string
[<Struct>] type OrderLineNumber = private OrderLineNumber of int
type ValidationError =
| CostPriceIsNegative of decimal
| FundTypeIsEmpty
| InvalidFundType of string
| OrderLineNumberIsNegative of int
| ValueWasNull
module CostPrice =
let create =
validateNumber CostPrice <|
businessRules require Rules.isNotNegative CostPriceIsNegative
let value (CostPrice price) = price
let Default = CostPrice 0M
module FundType =
let create =
validateString FundType <|
businessRules
require Rules.isNotEmpty (fun _ -> FundTypeIsEmpty)
require (Rules.regex "w2d2") InvalidFundType
let value (FundType fundType) = fundType
module OrderLineNumber =
let create =
validateNumber OrderLineNumber <|
businessRules require Rules.isNotNegative OrderLineNumberIsNegative
let value (OrderLineNumber line) = line
let Default = OrderLineNumber 0
And with all that in place, I can make a Value Object that uses the simple values. This uses the operation
computation builder from CurryOn.FSharp.Control to compose the results of each validation.
type Rebate =
LineNumber: OrderLineNumber
FundType: FundType
Amount: CostPrice
static member create lineNumber fundType amount =
operation
let! validLineNumber = OrderLineNumber.create lineNumber
let! validFundType = FundType.create fundType
let! validAmount = CostPrice.create amount
return LineNumber = validLineNumber; FundType = validFundType; Amount = validAmount
functional-programming f# ddd dsl
add a comment |Â
up vote
2
down vote
favorite
up vote
2
down vote
favorite
Following the patterns from the book Domain Modeling Made Functional, I am implementing a single-case union for the simple values in my domain model instead of using primitives. The union cases have private constructors, and each union type has a module with a create
function that validates the business rules around the type and returns a Result
. To simplify the validation of the business rules, I have created a DSL (in the form of an F# Computation Expression) that applies the specified rules (which can be either required or suggested) and then either returns a successful Result
or a result with the relevant errors/warnings as domain events.
The BusinessRule
DSL is implemented as follows:
open System
// Functions representing the evaluation of a business rule and
// the creation of an error based on the given value
type BusinessRuleExpr<'value, 'error> = ('value -> bool) * ('value -> 'error)
// Business Rules define the restrictions around the creation of a specific type.
// These may be either required (which cause an error when not satisfied) or suggested (which only cause a warning)
type BusinessRule<'value, 'error> =
| Required of BusinessRuleExpr<'value,'error>
| Suggested of BusinessRuleExpr<'value,'error>
module BusinessRule =
let inline require eval error =
[Required (eval, error)]
let inline suggest eval warn =
[Suggested (eval, warn)]
let inline also eval error rules =
rules @ (require eval error)
let inline should eval warn rules =
rules @ (suggest eval warn)
let inline isRequired rule =
match rule with
| Required _ -> true
| _ -> false
let inline isSuggested rule =
match rule with
| Suggested _ -> true
| _ -> false
let inline applies value rule =
match rule with
| Required (eval,_) -> eval value
| Suggested (eval,_) -> eval value
|> not
let inline fail value rule =
match rule with
| Required (_,error) -> error value
| Suggested (_,warn) -> warn value
// Computation Builder for the Business Rule DSL
type BusinessRuleBuilder () =
member inline __.Yield _ = List.empty
[<CustomOperation("require")>]
member inline __.Require (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, error: 'value -> 'error) =
rules |> BusinessRule.also eval error
[<CustomOperation("should")>]
member inline __.Should (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, warn: 'value -> 'error) =
rules |> BusinessRule.should eval warn
[<AutoOpen>]
module Builder =
let businessRules = BusinessRuleBuilder()
The computation builder for BusinessRule
does no implement Bind
or Return
as it would for a monadic computation. Instead, I am simply using the computation builder as a way of creating a simple DSL. I'm not sure what the best practices are around computation builders that don't implement Bind
/Return
, so I'd appreciate any thoughts you may have on that subject.
The business rules themselves are then defined in a separate module:
module Rules =
open System
open System.Text.RegularExpressions
/// Compose function f with functions g and h, and-ing the results
let private (>>&) f (g, h) x =
let r = f x
g r && h r
/// Regular-Expression based business rule
let inline regex pattern =
let completePattern =
if pattern |> Seq.contains '^' || pattern |> Seq.contains '$'
then Regex(pattern, RegexOptions.Compiled)
else Regex(sprintf "^%s$" pattern, RegexOptions.Compiled)
completePattern.IsMatch
// String Rules
let private lengthRule op length = String.length >> op length
let isNotEmpty = String.IsNullOrWhiteSpace >> not
let isAlpha = String.forall Char.IsLetter
let isNumber input = match Decimal.TryParse input with
| true,_ -> true
| _ -> false
let isNumerical = String.forall Char.IsDigit
let isAlphanumeric = String.forall Char.IsLetterOrDigit
let isLength = lengthRule (=)
let maxLength = lengthRule (>=)
let minLength = lengthRule (<=)
let lengthBetween min max = String.length >>& ((>=) max, (<=) min)
// Number rules
// Could we use LanguagePrimitives.GenericZero instead of Unchecked.defaultOf here?
let inline private compareZero<'n when 'n: comparison> = LanguagePrimitives.GenericComparison Unchecked.defaultof<'n>
let isNegative n = n |> (compareZero >> (<) 0)
let isNotNegative n = n|> (compareZero >> (>=) 0)
let isNotPositive n = n|> (compareZero >> (<=) 0)
let isPositive n = n|> (compareZero >> (>) 0)
The rule definitions are generally pretty simple. One thing I'd like to do is replace the Unchecked.defaulof<'n>
with LanguagePrimitives.GenericZero<'n>
in the compareZero
function, but I think I would need a static member on a class to make that work, since the GenericZero
function uses an SRTP for the get_Zero
member.
Finally, I have this little helper module to simplify the validation of values when constructing the domain models. Note, this uses the CurryOn.FSharp.Control package for a type like Result
that includes domain events with successful results as well. This is used to include the warnings with the value for Suggested
rules that aren't satisfied.
[<AutoOpen>]
module private Validation =
open FSharp.Control
open System.Text.RegularExpressions
let inline private validate<'a,'b> (ctor: 'a -> 'b) (validations: BusinessRule<'a, DomainErrors> list) (value: 'a) =
let errors = [for rule in validations |> List.filter BusinessRule.isRequired do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
let warnings = [for rule in validations |> List.filter BusinessRule.isSuggested do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
match errors with
| -> match warnings with
| -> Result.success <| ctor value
| _ -> Result.successWithEvents (ctor value) warnings
| _ -> Result.failure (errors @ warnings)
let validateString<'b> (ctor: string -> 'b) (validations: BusinessRule<string, DomainErrors> list) (value: string) =
if value |> isNull
then Result.failure [ValueWasNull]
else validate ctor validations value
let validateNumber<'a,'b when 'a : comparison> ctor validations =
validate<'a,'b> ctor validations
With the BusinessRule
DSL and the rules themselves defined, I can then build my simple values for the domain model as follows:
[<Struct>] type CostPrice = private CostPrice of decimal
[<Struct>] type FundType = private FundType of string
[<Struct>] type OrderLineNumber = private OrderLineNumber of int
type ValidationError =
| CostPriceIsNegative of decimal
| FundTypeIsEmpty
| InvalidFundType of string
| OrderLineNumberIsNegative of int
| ValueWasNull
module CostPrice =
let create =
validateNumber CostPrice <|
businessRules require Rules.isNotNegative CostPriceIsNegative
let value (CostPrice price) = price
let Default = CostPrice 0M
module FundType =
let create =
validateString FundType <|
businessRules
require Rules.isNotEmpty (fun _ -> FundTypeIsEmpty)
require (Rules.regex "w2d2") InvalidFundType
let value (FundType fundType) = fundType
module OrderLineNumber =
let create =
validateNumber OrderLineNumber <|
businessRules require Rules.isNotNegative OrderLineNumberIsNegative
let value (OrderLineNumber line) = line
let Default = OrderLineNumber 0
And with all that in place, I can make a Value Object that uses the simple values. This uses the operation
computation builder from CurryOn.FSharp.Control to compose the results of each validation.
type Rebate =
LineNumber: OrderLineNumber
FundType: FundType
Amount: CostPrice
static member create lineNumber fundType amount =
operation
let! validLineNumber = OrderLineNumber.create lineNumber
let! validFundType = FundType.create fundType
let! validAmount = CostPrice.create amount
return LineNumber = validLineNumber; FundType = validFundType; Amount = validAmount
functional-programming f# ddd dsl
Following the patterns from the book Domain Modeling Made Functional, I am implementing a single-case union for the simple values in my domain model instead of using primitives. The union cases have private constructors, and each union type has a module with a create
function that validates the business rules around the type and returns a Result
. To simplify the validation of the business rules, I have created a DSL (in the form of an F# Computation Expression) that applies the specified rules (which can be either required or suggested) and then either returns a successful Result
or a result with the relevant errors/warnings as domain events.
The BusinessRule
DSL is implemented as follows:
open System
// Functions representing the evaluation of a business rule and
// the creation of an error based on the given value
type BusinessRuleExpr<'value, 'error> = ('value -> bool) * ('value -> 'error)
// Business Rules define the restrictions around the creation of a specific type.
// These may be either required (which cause an error when not satisfied) or suggested (which only cause a warning)
type BusinessRule<'value, 'error> =
| Required of BusinessRuleExpr<'value,'error>
| Suggested of BusinessRuleExpr<'value,'error>
module BusinessRule =
let inline require eval error =
[Required (eval, error)]
let inline suggest eval warn =
[Suggested (eval, warn)]
let inline also eval error rules =
rules @ (require eval error)
let inline should eval warn rules =
rules @ (suggest eval warn)
let inline isRequired rule =
match rule with
| Required _ -> true
| _ -> false
let inline isSuggested rule =
match rule with
| Suggested _ -> true
| _ -> false
let inline applies value rule =
match rule with
| Required (eval,_) -> eval value
| Suggested (eval,_) -> eval value
|> not
let inline fail value rule =
match rule with
| Required (_,error) -> error value
| Suggested (_,warn) -> warn value
// Computation Builder for the Business Rule DSL
type BusinessRuleBuilder () =
member inline __.Yield _ = List.empty
[<CustomOperation("require")>]
member inline __.Require (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, error: 'value -> 'error) =
rules |> BusinessRule.also eval error
[<CustomOperation("should")>]
member inline __.Should (rules: BusinessRule<'value,'error> list, eval: 'value -> bool, warn: 'value -> 'error) =
rules |> BusinessRule.should eval warn
[<AutoOpen>]
module Builder =
let businessRules = BusinessRuleBuilder()
The computation builder for BusinessRule
does no implement Bind
or Return
as it would for a monadic computation. Instead, I am simply using the computation builder as a way of creating a simple DSL. I'm not sure what the best practices are around computation builders that don't implement Bind
/Return
, so I'd appreciate any thoughts you may have on that subject.
The business rules themselves are then defined in a separate module:
module Rules =
open System
open System.Text.RegularExpressions
/// Compose function f with functions g and h, and-ing the results
let private (>>&) f (g, h) x =
let r = f x
g r && h r
/// Regular-Expression based business rule
let inline regex pattern =
let completePattern =
if pattern |> Seq.contains '^' || pattern |> Seq.contains '$'
then Regex(pattern, RegexOptions.Compiled)
else Regex(sprintf "^%s$" pattern, RegexOptions.Compiled)
completePattern.IsMatch
// String Rules
let private lengthRule op length = String.length >> op length
let isNotEmpty = String.IsNullOrWhiteSpace >> not
let isAlpha = String.forall Char.IsLetter
let isNumber input = match Decimal.TryParse input with
| true,_ -> true
| _ -> false
let isNumerical = String.forall Char.IsDigit
let isAlphanumeric = String.forall Char.IsLetterOrDigit
let isLength = lengthRule (=)
let maxLength = lengthRule (>=)
let minLength = lengthRule (<=)
let lengthBetween min max = String.length >>& ((>=) max, (<=) min)
// Number rules
// Could we use LanguagePrimitives.GenericZero instead of Unchecked.defaultOf here?
let inline private compareZero<'n when 'n: comparison> = LanguagePrimitives.GenericComparison Unchecked.defaultof<'n>
let isNegative n = n |> (compareZero >> (<) 0)
let isNotNegative n = n|> (compareZero >> (>=) 0)
let isNotPositive n = n|> (compareZero >> (<=) 0)
let isPositive n = n|> (compareZero >> (>) 0)
The rule definitions are generally pretty simple. One thing I'd like to do is replace the Unchecked.defaulof<'n>
with LanguagePrimitives.GenericZero<'n>
in the compareZero
function, but I think I would need a static member on a class to make that work, since the GenericZero
function uses an SRTP for the get_Zero
member.
Finally, I have this little helper module to simplify the validation of values when constructing the domain models. Note, this uses the CurryOn.FSharp.Control package for a type like Result
that includes domain events with successful results as well. This is used to include the warnings with the value for Suggested
rules that aren't satisfied.
[<AutoOpen>]
module private Validation =
open FSharp.Control
open System.Text.RegularExpressions
let inline private validate<'a,'b> (ctor: 'a -> 'b) (validations: BusinessRule<'a, DomainErrors> list) (value: 'a) =
let errors = [for rule in validations |> List.filter BusinessRule.isRequired do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
let warnings = [for rule in validations |> List.filter BusinessRule.isSuggested do if rule |> BusinessRule.applies value then yield rule |> BusinessRule.fail value]
match errors with
| -> match warnings with
| -> Result.success <| ctor value
| _ -> Result.successWithEvents (ctor value) warnings
| _ -> Result.failure (errors @ warnings)
let validateString<'b> (ctor: string -> 'b) (validations: BusinessRule<string, DomainErrors> list) (value: string) =
if value |> isNull
then Result.failure [ValueWasNull]
else validate ctor validations value
let validateNumber<'a,'b when 'a : comparison> ctor validations =
validate<'a,'b> ctor validations
With the BusinessRule
DSL and the rules themselves defined, I can then build my simple values for the domain model as follows:
[<Struct>] type CostPrice = private CostPrice of decimal
[<Struct>] type FundType = private FundType of string
[<Struct>] type OrderLineNumber = private OrderLineNumber of int
type ValidationError =
| CostPriceIsNegative of decimal
| FundTypeIsEmpty
| InvalidFundType of string
| OrderLineNumberIsNegative of int
| ValueWasNull
module CostPrice =
let create =
validateNumber CostPrice <|
businessRules require Rules.isNotNegative CostPriceIsNegative
let value (CostPrice price) = price
let Default = CostPrice 0M
module FundType =
let create =
validateString FundType <|
businessRules
require Rules.isNotEmpty (fun _ -> FundTypeIsEmpty)
require (Rules.regex "w2d2") InvalidFundType
let value (FundType fundType) = fundType
module OrderLineNumber =
let create =
validateNumber OrderLineNumber <|
businessRules require Rules.isNotNegative OrderLineNumberIsNegative
let value (OrderLineNumber line) = line
let Default = OrderLineNumber 0
And with all that in place, I can make a Value Object that uses the simple values. This uses the operation
computation builder from CurryOn.FSharp.Control to compose the results of each validation.
type Rebate =
LineNumber: OrderLineNumber
FundType: FundType
Amount: CostPrice
static member create lineNumber fundType amount =
operation
let! validLineNumber = OrderLineNumber.create lineNumber
let! validFundType = FundType.create fundType
let! validAmount = CostPrice.create amount
return LineNumber = validLineNumber; FundType = validFundType; Amount = validAmount
functional-programming f# ddd dsl
edited May 22 at 13:11
asked May 22 at 13:06
Aaron M. Eshbach
3297
3297
add a comment |Â
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f194942%2fbusiness-rule-dsl-for-values-in-domain-driven-design%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password