2

Let's say, I have a discriminated union type AccountEvent and a class Aggregate that carries two methods:

  • Apply1(event : AccountEvent)
  • Apply2(event : Event<AccountEvent>)

Event<'TEvent> being just a dummy class for sake of having a generic type.

I am trying to create an Expression that represents the call to Apply1 and Apply2 supporting for the parameter type the Discriminated union case type. That is allowing:

  • AccountEvent.AccountCreated type for Apply1
  • Event<AccountEvent.AccountCreated> type for Apply2

I want to achieve that without changing the signature of Apply1, Apply2 and the definition the discriminated union.

The code

type AccountCreation = {
    Owner: string
    AccountId: Guid
    CreatedAt: DateTimeOffset
    StartingBalance: decimal
}

type Transaction = {
    To: Guid
    From: Guid
    Description: string
    Time: DateTimeOffset
    Amount: decimal
}

type AccountEvent =
    | AccountCreated of AccountCreation
    | AccountCredited of Transaction
    | AccountDebited of Transaction

type Event<'TEvent>(event : 'TEvent)=
    member val Event = event with get

type Aggregate()=
    member this.Apply1(event : AccountEvent)=
        ()

    member this.Apply2(event : Event<AccountEvent>)=
        ()

let createExpression (aggregateType: Type)(eventType: Type)(method: MethodInfo) =
    let instance = Expression.Parameter(aggregateType, "a")
    let eventParameter = Expression.Parameter(eventType, "e")
    let body = Expression.Call(instance, method, eventParameter)
    ()

[<EntryPoint>]
let main argv =

    let accountCreated = AccountEvent.AccountCreated({
        Owner = "Khalid Abuhakmeh"
        AccountId = Guid.NewGuid()
        StartingBalance = 1000m
        CreatedAt = DateTimeOffset.UtcNow
    })
    let accountCreatedType = accountCreated.GetType()

    let method1 = typeof<Aggregate>.GetMethods().Single(fun x -> x.Name = "Apply1")
    createExpression typeof<Aggregate> typeof<AccountEvent> method1
    createExpression typeof<Aggregate> accountCreatedType method1

    let method2 = typeof<Aggregate>.GetMethods().Single(fun x -> x.Name = "Apply2")
    let eventAccountCreatedType = typedefof<Event<_>>.MakeGenericType(accountCreatedType)
    createExpression typeof<Aggregate> typeof<Event<AccountEvent>> method2
    createExpression typeof<Aggregate> eventAccountCreatedType method2

    0

With my current solution it does not work to generate an expression for Apply2:

System.ArgumentException: Expression of type 'Program+Event`1[Program+AccountEvent+AccountCreated]' cannot be used for parameter of type 'Program+Event`1[Program+AccountEvent]' of method 'Void Apply2(Event`1)'
Parameter name: arg0
  at at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
  at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0)
  at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
  at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression[] arguments)
  at Program.doingStuff(Type aggregateType, Type eventType, MethodInfo method) in C:\Users\eperret\Desktop\ConsoleApp1\ConsoleApp1\Program.fs:40
  at Program.main(String[] argv) in C:\Users\eperret\Desktop\ConsoleApp1\ConsoleApp1\Program.fs:61

I am wondering how I can adjust the creation of my expression to accept the Event<AccountEvent.AccountCreated>?

I am thinking that maybe there is a need to have an intermediate layer to have a conversion layer from AccountEvent.AccountCreated to its base classAccountEvent (this is how discriminated unions are compiled), or more precisely considering the generic, from Event<AccountEvent.AccountCreated to Event<AccountEvent>.

Natalie Perret
  • 8,013
  • 12
  • 66
  • 129

1 Answers1

1

hard to say if this answers your question.

open System
open System
type AccountCreation = {
    Owner: string
    AccountId: Guid
    CreatedAt: DateTimeOffset
    StartingBalance: decimal
}

type Transaction = {
    To: Guid
    From: Guid
    Description: string
    Time: DateTimeOffset
    Amount: decimal
}

type AccountEvent =
    | AccountCreated of AccountCreation
    | AccountCredited of Transaction
    | AccountDebited of Transaction
type CheckinEvent =
    | CheckedIn
    | CheckedOut
type Event<'T> = AccountEvent of AccountEvent | OtherEvent of 'T
let ev : Event<CheckinEvent> = AccountEvent (AccountCreated {
      Owner= "string"
      AccountId= Guid.NewGuid()
      CreatedAt=  DateTimeOffset()
      StartingBalance=0m
    })
let ev2 : Event<CheckinEvent> = OtherEvent CheckedOut
let f ev =
  match ev with
      | AccountEvent e -> Some e
      | OtherEvent (CheckedOut) -> None 
      | OtherEvent (CheckedIn) -> None 

let x = f ev
let y = f ev2

afterwards, a match statement like this might simplify all that. Honestly it's a little complicated for me to follow what precisely what you're doing there, but using a function instead of a method and using a match statement appears to accomplish the same goal. Ideally you should probably fully spell out the types in a DU instead of using a generic so that you'll get compile time checks instead of run time errors and can know for certain that your code is fully covered by the compiler.

VoronoiPotato
  • 3,113
  • 20
  • 30
  • 2
    I have rewritten my question. It's mostly about the `Expression` generation and to allow `GenericType` for `GenericType`. – Natalie Perret Jun 08 '19 at 15:23
  • You may be interested in https://stackoverflow.com/questions/28773518/f-type-matching-on-du-cases-make-this-slightly-more-generic however I must warn you, the further you stray into the darkness the more alone you will be. – VoronoiPotato Jun 10 '19 at 19:57
  • what darkness are you talking about? – Natalie Perret Jun 10 '19 at 19:58
  • Sorry for being so poetic. You're traveling away from compile time checks, and away from the common way of doing things. It's likely that there exists a way to achieve your goal without losing compile time checking. In F# we often try to put as much on the compiler as we can because it reduces bugs. – VoronoiPotato Jun 11 '19 at 14:03
  • ah that sort of poetry, well it was not really I wanted to do that initially. I am trying to add some support for DUs in a C# library, the pipeline is made of reflections. The common way of doing things would require to most likely rewrite something from scratch. – Natalie Perret Jun 11 '19 at 14:07
  • Are you in the F#.org slack? You would be surprised how often things can be managed with a thin wrapper. – VoronoiPotato Jun 11 '19 at 14:15
  • 1
    was not even aware of those :x https://fsharp.org/guides/slack/, need to check this out. – Natalie Perret Jun 11 '19 at 14:34