Marten: F#: Discriminated Unions are not supported for aggregates

Created on 7 Jun 2019  路  17Comments  路  Source: JasperFx/marten

it seems that F# discriminated unions are not supported for aggregation / projection, example of code that does not work:

open System
open Marten
open Marten.Schema.Identity

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 Account() =
    member val Id = Unchecked.defaultof<Guid> with get,set
    member val Owner = Unchecked.defaultof<string> with get,set
    member val Balance = Unchecked.defaultof<decimal> with get,set
    member val CreatedAt = Unchecked.defaultof<DateTimeOffset> with get,set
    member val UpdatedAt = Unchecked.defaultof<DateTimeOffset> with get,set

    member this.Apply(accountEvent: AccountEvent) =
        printfn "I've been called %A" accountEvent

[<EntryPoint>]
let main argv =
    use store = DocumentStore.For(fun options ->
            let connectionString = sprintf "host=%s;database=%s;username=%s;password=%s"
                                       "localhost"
                                       "postgres"
                                       "root"
                                       "root"
            options.Connection(connectionString)
            options.Events.AddEventType(typeof<AccountEvent>)
            options.Events.InlineProjections.AggregateStreamsWith<Account>() |> ignore
        )

    use session = store.LightweightSession()

    let khalidId = CombGuidIdGeneration.NewGuid()
    let billId = CombGuidIdGeneration.NewGuid()

    let khalid = AccountEvent.AccountCreated({
        Owner = "Khalid Abuhakmeh"
        AccountId = khalidId
        StartingBalance = 1000m
        CreatedAt = DateTimeOffset.UtcNow
    })

    let bill = AccountEvent.AccountCreated({
        Owner = "Bill Boga"
        AccountId = billId
        StartingBalance = 0m
        CreatedAt = DateTimeOffset.UtcNow
    })

    let transaction = AccountEvent.AccountCredited({
        From = khalidId
        To = billId
        Amount = 100m
        Time = DateTimeOffset.UtcNow
        Description = "transfer to bill"
    })

    session.Events.Append(khalidId, khalid) |> ignore
    session.Events.Append(billId, bill) |> ignore
    session.Events.Append(khalidId, transaction) |> ignore

    session.SaveChangesAsync()
    |> Async.AwaitTask
    |> Async.RunSynchronously

    let account = session.LoadAsync<Account>(khalidId)
                    |> Async.AwaitTask
                    |> Async.RunSynchronously

    let stream = session.Events.FetchStream(khalidId)

    printfn "%A" account
    printfn "%A" stream

    0

Long story short:

  • At the end, account is equal to null
  • The method Apply(accountEvent: AccountEvent) is never called.

However if I am doing something more classic like:

open System
open Marten
open Marten.Schema.Identity

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 Account() =
    member val Id = Unchecked.defaultof<Guid> with get,set
    member val Owner = Unchecked.defaultof<string> with get,set
    member val Balance = Unchecked.defaultof<decimal> with get,set
    member val CreatedAt = Unchecked.defaultof<DateTimeOffset> with get,set
    member val UpdatedAt = Unchecked.defaultof<DateTimeOffset> with get,set

    member this.Apply(accountCreation: AccountCreation) =
        printfn "I've been called %A" accountCreation
        this.Id <- accountCreation.AccountId
        this.Owner <- accountCreation.Owner
        this.Balance <- accountCreation.StartingBalance
        this.CreatedAt <- accountCreation.CreatedAt
        this.UpdatedAt <- accountCreation.CreatedAt

[<EntryPoint>]
let main argv =
    use store = DocumentStore.For(fun options ->
            let connectionString = sprintf "host=%s;database=%s;username=%s;password=%s"
                                       "localhost"
                                       "postgres"
                                       "root"
                                       "root"
            options.Connection(connectionString)
            options.Events.AddEventType(typeof<AccountEvent>)
            options.Events.InlineProjections.AggregateStreamsWith<Account>() |> ignore
        )

    use session = store.LightweightSession()

    let khalidId = CombGuidIdGeneration.NewGuid()
    let billId = CombGuidIdGeneration.NewGuid()

    let khalid = {
        Owner = "Khalid Abuhakmeh"
        AccountId = khalidId
        StartingBalance = 1000m
        CreatedAt = DateTimeOffset.UtcNow
    }

    let bill = {
        Owner = "Bill Boga"
        AccountId = billId
        StartingBalance = 0m
        CreatedAt = DateTimeOffset.UtcNow
    }

    let transaction = {
        From = khalidId
        To = billId
        Amount = 100m
        Time = DateTimeOffset.UtcNow
        Description = "transfer to bill"
    }

    session.Events.Append(khalidId, khalid) |> ignore
    session.Events.Append(billId, bill) |> ignore
    session.Events.Append(khalidId, transaction) |> ignore

    session.SaveChangesAsync()
    |> Async.AwaitTask
    |> Async.RunSynchronously

    let account = session.LoadAsync<Account>(khalidId)
                    |> Async.AwaitTask
                    |> Async.RunSynchronously

    let stream = session.Events.FetchStream(khalidId)

    printfn "%A" account
    printfn "%A" stream

    0

account is properly loaded

What did I change between the two?
I basically removed discriminated unions, in the events that are append to the stream:

From (AccountEvent.AccountCreated):

let khalid = AccountEvent.AccountCreated({
    Owner = "Khalid Abuhakmeh"
    AccountId = khalidId
    StartingBalance = 1000m
    CreatedAt = DateTimeOffset.UtcNow
})

To (AccountCreation type):

let khalid = {
    Owner = "Khalid Abuhakmeh"
    AccountId = khalidId
    StartingBalance = 1000m
    CreatedAt = DateTimeOffset.UtcNow
}

and change the parameter passed to Apply:

member this.Apply(accountEvent: AccountEvent) =

to

member this.Apply(accountCreation: AccountCreation) =

I think this is really frustrating in F# to not be able to use Discriminated Unions because of the possibility it offers in terms of pattern matching. It forces to aggregate from the whole stream without persisting the aggregation / projection, which can be an issue in terms of performances for queries.

AFAIK, this is not due Newtonsoft.Json cause it does support both the serialization and deserialization with discriminated unions:

[<EntryPoint>]
let main argv =
    let accountCreated = AccountEvent.AccountCreated({
        Owner = "Khalid Abuhakmeh"
        AccountId = Guid.NewGuid()
        StartingBalance = 1000m
        CreatedAt = DateTimeOffset.UtcNow
    })
    let serialized = JsonConvert.SerializeObject(accountCreated)
    let deserialized = JsonConvert.DeserializeObject<AccountEvent>(serialized)

    printfn "%A" (accountCreated = deserialized)

    0

It most likely resides in some reflection tasks performed by marten upon event appending.

I am not sure if someone could have a hint about a decent workaround or maybe a hint about where to lookup in the source code.

F# bug

Most helpful comment

Thank you @ehouarn-perret.

I'm just finishing Domain Modeling Made Functional by @swlaschin, last pages left - so I think that now I have the good basis to tackle this issue and make Marten more F# friendly.

After I finish my work on https://github.com/JasperFx/marten/issues/1302 I'll try to tackle that.

All 17 comments

Side note about types:
In the code I previously posted:

[<EntryPoint>]
let main argv =
    let accountCreated = AccountEvent.AccountCreated({
        Owner = "Khalid Abuhakmeh"
        AccountId = Guid.NewGuid()
        StartingBalance = 1000m
        CreatedAt = DateTimeOffset.UtcNow
    })
    let serialized = JsonConvert.SerializeObject(accountCreated)
    let deserialized = JsonConvert.DeserializeObject<AccountEvent>(serialized)

    printfn "%A" (accountCreated = deserialized)

    0

serialized is equal to:

{Owner = "Khalid Abuhakmeh";
 AccountId = 2a81eb7c-2907-462f-bf15-222343f8582a;
 CreatedAt = 2019-06-06 11:12:16 PM +00:00;
 StartingBalance = 1000M;}

However it seems that when using discriminated unions, in the mt_events table I have:

1   016b2f00-db9e-4389-a150-7f118eaa4951    016b2f00-d8a1-4140-8bec-88f857a96104    1   {"Case": "AccountCreated", "Fields": [{"Owner": "Khalid Abuhakmeh", "AccountId": "016b2f00-d8a1-4140-8bec-88f857a96104", "CreatedAt": "2019-06-06T22:55:13.0596873+00:00", "StartingBalance": 1000.0}]} account_created 2019-06-06 22:55:13.974592  Program+AccountEvent+AccountCreated, Marten.FSharp  *DEFAULT*
2   016b2f00-dba0-437d-85c8-4433e0eb6723    016b2f00-d8a1-4140-8bec-88f857a96104    2   {"Case": "AccountCredited", "Fields": [{"To": "016b2f00-d8a3-45a4-9bb9-334a4e3e8484", "From": "016b2f00-d8a1-4140-8bec-88f857a96104", "Time": "2019-06-06T22:55:13.060011+00:00", "Amount": 100.0, "Description": "transfer to bill"}]} account_credited    2019-06-06 22:55:13.974592  Program+AccountEvent+AccountCredited, Marten.FSharp *DEFAULT*
3   016b2f00-db9f-486e-ba77-3f795e2c5b1b    016b2f00-d8a3-45a4-9bb9-334a4e3e8484    1   {"Case": "AccountCreated", "Fields": [{"Owner": "Bill Boga", "AccountId": "016b2f00-d8a3-45a4-9bb9-334a4e3e8484", "CreatedAt": "2019-06-06T22:55:13.0600103+00:00", "StartingBalance": 0.0}]}   account_created 2019-06-06 22:55:13.974592  Program+AccountEvent+AccountCreated, Marten.FSharp  *DEFAULT*

I am not sure the issue could be related to that but in the mt_dotnet_type:

  • Program+AccountEvent+AccountCreated
  • Program+AccountEvent+AccountCredited
  • Program+AccountEvent+AccountCreated

I am not too sure but I found it weird that the type register is Program+AccountEvent+AccountCreated
instead of Program+AccountEvent.

Knowing that Program+AccountEvent+AccountCreated is not a type that cannot be passed as a parameter of a function / method...

Now if I am taking the type of accountCreated at runtime in my first snippet:

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

let accountCreatedType = accountCreated.GetType()

I have the following properties for accountCreatedType:

accountCreatedType = {RuntimeType} "Program+AccountEvent+AccountCreated"
 Assembly = {RuntimeAssembly} "ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
 AssemblyQualifiedName = {string} "Program+AccountEvent+AccountCreated, ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
 Attributes = {TypeAttributes} NestedPublic | SpecialName | Serializable | BeforeFieldInit
 BaseType = {RuntimeType} "Program+AccountEvent"
 Cache = {RuntimeTypeCache} {System.RuntimeType.RuntimeTypeCache}
 ContainsGenericParameters (RuntimeType) = {bool} false
 ContainsGenericParameters (Type) = {bool} false
 CustomAttributes = {ReadOnlyCollection<CustomAttributeData>} Count = 3
 DeclaredConstructors = {ConstructorInfo[]} Count = 1
 DeclaredEvents = {EventInfo[]} Count = 0
 DeclaredFields = {FieldInfo[]} Count = 1
 DeclaredMembers = {MemberInfo[]} Count = 4
 DeclaredMethods = {MethodInfo[]} Count = 1
 DeclaredNestedTypes = {<get_DeclaredNestedTypes>d__22} {System.Reflection.TypeInfo.<get_DeclaredNestedTypes>d__22}
 DeclaredProperties = {PropertyInfo[]} Count = 1
 DeclaringMethod (RuntimeType) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 DeclaringMethod (Type) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 DeclaringType (RuntimeType) = {RuntimeType} "Program+AccountEvent"
 DeclaringType (Type) = {RuntimeType} "Program+AccountEvent"
 DomainInitialized = {bool} false
 FullName = {string} "Program+AccountEvent+AccountCreated"
 GUID = {Guid} "d394093c-17e9-394e-bcd7-1c9ae1e379fb"
 GenericCache = {object} null
 GenericParameterAttributes (RuntimeType) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 GenericParameterAttributes (Type) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 GenericParameterPosition (RuntimeType) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 GenericParameterPosition (Type) = {System.InvalidOperationException} Exception of type 'System.InvalidOperationException' was thrown
 GenericTypeArguments = {Type[]} Count = 0
 GenericTypeParameters = {Type[]} Count = 0
 HasElementType = {bool} false
 ImplementedInterfaces = {Type[]} Count = 5
 IsAbstract = {bool} false
 IsAnsiClass = {bool} true
 IsArray = {bool} false
 IsAutoClass = {bool} false
 IsAutoLayout = {bool} true
 IsByRef = {bool} false
 IsByRefLike (RuntimeType) = {bool} false
 IsByRefLike (Type) = {bool} false
 IsCOMObject = {bool} false
 IsClass = {bool} true
 IsCollectible (RuntimeType) = {bool} false
 IsCollectible (Type) = {bool} false
 IsConstructedGenericType (RuntimeType) = {bool} false
 IsConstructedGenericType (Type) = {bool} false
 IsContextful = {bool} false
 IsEnum = {bool} false
 IsExplicitLayout = {bool} false
 IsExportedToWindowsRuntime = {bool} false
 IsGenericMethodParameter = {bool} false
 IsGenericParameter (RuntimeType) = {bool} false
 IsGenericParameter (Type) = {bool} false
 IsGenericType (RuntimeType) = {bool} false
 IsGenericType (Type) = {bool} false
 IsGenericTypeDefinition (RuntimeType) = {bool} false
 IsGenericTypeDefinition (Type) = {bool} false
 IsGenericTypeParameter = {bool} false
 IsImport = {bool} false
 IsInterface = {bool} false
 IsLayoutSequential = {bool} false
 IsMarshalByRef = {bool} false
 IsNested = {bool} true
 IsNestedAssembly = {bool} false
 IsNestedFamANDAssem = {bool} false
 IsNestedFamORAssem = {bool} false
 IsNestedFamily = {bool} false
 IsNestedPrivate = {bool} false
 IsNestedPublic = {bool} true
 IsNotPublic = {bool} false
 IsPointer = {bool} false
 IsPrimitive = {bool} false
 IsPublic = {bool} false
 IsSZArray (RuntimeType) = {bool} false
 IsSZArray (Type) = {bool} false
 IsSealed = {bool} false
 IsSecurityCritical (RuntimeType) = {bool} true
 IsSecurityCritical (Type) = {bool} true
 IsSecuritySafeCritical (RuntimeType) = {bool} false
 IsSecuritySafeCritical (Type) = {bool} false
 IsSecurityTransparent (RuntimeType) = {bool} false
 IsSecurityTransparent (Type) = {bool} false
 IsSerializable = {bool} true
 IsSignatureType = {bool} false
 IsSpecialName = {bool} true
 IsTypeDefinition (RuntimeType) = {bool} true
 IsTypeDefinition (Type) = {bool} true
 IsUnicodeClass = {bool} false
 IsValueType = {bool} false
 IsVariableBoundArray = {bool} false
 IsVisible = {bool} true
 IsWindowsRuntimeObject = {bool} false
 MemberType (RuntimeType) = {MemberTypes} NestedType
 MemberType (Type) = {MemberTypes} NestedType
 MetadataToken (RuntimeType) = {int} 33554439
 MetadataToken (MemberInfo) = {int} 33554439
 Module (RuntimeType) = {RuntimeModule} "ConsoleApp1.exe"
 Module (MemberInfo) = {RuntimeModule} "ConsoleApp1.exe"
 Name = {string} "AccountCreated"
 Namespace = {string} null
 ReflectedType (RuntimeType) = {RuntimeType} "Program+AccountEvent"
 ReflectedType (Type) = {RuntimeType} "Program+AccountEvent"
 StructLayoutAttribute (RuntimeType) = {StructLayoutAttribute} {System.Runtime.InteropServices.StructLayoutAttribute}
 StructLayoutAttribute (Type) = {StructLayoutAttribute} {System.Runtime.InteropServices.StructLayoutAttribute}
 TypeHandle (RuntimeType) = {RuntimeTypeHandle} "System.RuntimeTypeHandle"
 TypeHandle (Type) = {RuntimeTypeHandle} "System.RuntimeTypeHandle"
 TypeInitializer = {ConstructorInfo} null
 UnderlyingSystemType = {RuntimeType} "Program+AccountEvent+AccountCreated"

It's interesting to notice that the type that can actually be passed as the parameter of Apply for the aggregate is considered as the parent type: BaseType = {RuntimeType} "Program+AccountEvent"
and

  • DeclaringType (RuntimeType) = {RuntimeType} "Program+AccountEvent"
  • DeclaringType (Type) = {RuntimeType} "Program+AccountEvent"

Depending on which type marten is using to lookup the aggregate method / Apply pattern it may explain why nothing is triggered with discriminated unions.

It also worth mentioning that:

let serialized = JsonConvert.SerializeObject(accountCreated)
let deserialized = JsonConvert.DeserializeObject<AccountEvent>(serialized)
let rawDeserialized = JsonConvert.DeserializeObject(serialized, accountCreatedType)
let castedDeserialized = castAs<AccountEvent>(rawDeserialized).Value

printfn "%A" (accountCreated = deserialized)
printfn "%A" (deserialized = castedDeserialized)

shows that deserialization works either way, be it with AccountEvent or the runtime type Program+AccountEvent+AccountCreated

Also that question on SO: https://stackoverflow.com/questions/17832203/is-f-aware-of-its-discriminated-unions-compiled-forms

Helped me to realize that:

A discriminated union in F# is compiled to an abstract class and its options become nested concrete classes.

type DU = A | B`

DU is abstract while DU.A and DU.B are concrete.

Which echoes what I was writing just above about the base type.

@ehouarn-perret You know that you don't have to use the built in aggregator, right? You can use your own projection and/or aggregator if you just implement the interfaces. That might be easier than trying to force fit the F#isms into C#-centric reflection

@ehouarn-perret You know that you don't have to use the built in aggregator, right? You can use your own projection and/or aggregator if you just implement the interfaces. That might be easier than trying to force fit the F#isms into C#-centric reflection

Yea hence my comment about which source files to go through in order to know how to properly implement those interfaces and maybe provide a generic type / helper for discrimimated unions

Out of curiosity I translated the C# version Aggregator<T> to F#:

type Aggregator<'T when 'T : (new : unit -> 'T) and 'T : not struct> (overrideMethodLookup : IEnumerable<MethodInfo>)=
    let aggregations : IDictionary<Type, obj> = (new Dictionary<Type, obj>() :> IDictionary<Type, obj>) 
    let aggregateType = typeof<'T>
    let mutable alias = Unchecked.defaultof<string>
    do
        alias <-  typeof<'T>.Name.ToTableAlias();
        overrideMethodLookup.Each(fun (method : MethodInfo) ->
            let mutable step = Unchecked.defaultof<obj>
            let mutable eventType = method.GetParameters().Single<ParameterInfo>().ParameterType;
            if eventType.Closes(typedefof<Event<_>>) then
                eventType <- eventType.GetGenericArguments().Single();
                step <- typedefof<EventAggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| typeof<'T>; eventType |]);
            else
                step <- typedefof<AggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| typeof<'T>; eventType |]);
            aggregations.Add(eventType, step)
        ) |> ignore

    static let ApplyMethod = "Apply"

    new() = new Aggregator<'T>(typeof<'T>.GetMethods()
                               |> Seq.where (fun x -> x.Name = ApplyMethod &&
                                                      x.GetParameters().Length = 1))

    member this.Add<'TEvent>(aggregation: IAggregation<'T, 'TEvent>) =
        if aggregations.ContainsKey(typeof<'TEvent>) then
            aggregations.[typeof<'TEvent>] <- aggregation
        else
            aggregations.Add(typeof<'TEvent>, aggregation)
        this

    member this.Add<'TEvent>(application: Action<'T, 'TEvent>) =
        this.Add(new AggregationStep<'T, 'TEvent>(application));

    interface IAggregator<'T> with

        member this.AggregatorFor<'TEvent>() =
            if aggregations.ContainsKey(typeof<'TEvent>) then
                aggregations.[typeof<'TEvent>].As<IAggregation<'T, 'TEvent>>()
            else
                null

        member this.Build(events, session, state) =
            events.Each(fun (x : IEvent) -> x.Apply(state, this)) |> ignore
            state

        member this.Build(events, session) =
            (this :> IAggregator<'T>).Build(events, session, new 'T());

        member this.EventTypes =
            aggregations.Keys.ToArray();

        member this.AggregateType =
            aggregateType

        member this.Alias =
            alias

        member this.AppliesTo(stream) =
            stream.Events.Any(fun x -> aggregations.ContainsKey(x.Data.GetType()));

Update with a partial solution:

type MyEventAggregationStep<'T, 'TEvent>(apply: Action<'T, Event<'TEvent>>)=

    new(method: MethodInfo) =
        let parameters = method.GetParameters()
        let eventType = typeof<Event<'TEvent>>
        let parameter = parameters.SingleOrDefault()
        let parameterGenericType = parameter.ParameterType.GetGenericArguments().SingleOrDefault()
        let eventGenericType = eventType.GetGenericArguments().SingleOrDefault()
        if parameters.Length <> 1 ||
            not ((Reflection.FSharpType.IsUnion parameterGenericType
                 && eventGenericType.BaseType = parameterGenericType)
                <>
                (parameter.ParameterType = eventType)) ||
            (method.DeclaringType <> typeof<'T>) then
            let message = String.Format("Method {0} on {1} cannot be used as an aggregation method", method.Name, method.DeclaringType)
            raise(new ArgumentOutOfRangeException(message));
        let aggregateParameter = Expression.Parameter(typeof<'T>, "a");
        let eventParameter = Expression.Parameter(typeof<Event<'TEvent>>, "e");
        let body = Expression.Call(aggregateParameter, method, eventParameter);
        let lambda = Expression.Lambda<Action<'T, Event<'TEvent>>>(body, aggregateParameter, eventParameter);
        let compilation = lambda.Compile()
        // ExpressionCompiler is internal =/
        // let expressionCompiler = Type.GetType("Marten.Util.ExpressionCompiler, Marten")
        new MyEventAggregationStep<'T, 'TEvent>(compilation)

    interface IAggregationWithMetadata<'T, 'TEvent> with
        member this.Apply(aggregate, event) =
            apply.Invoke(aggregate, event)

    interface IAggregation<'T, 'TEvent> with
        member this.Apply(aggregate, event) =
            raise(new NotSupportedException("Should never be called"))


type MyAggregationStep<'T, 'TEvent>(apply: Action<'T, 'TEvent>)=

    new(method: MethodInfo) =
        let parameters = method.GetParameters()
        let eventType = typeof<'TEvent>
        let parameter = parameters.SingleOrDefault()
        if parameters.Length <> 1 ||
            not ((Reflection.FSharpType.IsUnion parameter.ParameterType
                 && eventType.BaseType = parameter.ParameterType)
                <>
                (parameter.ParameterType = eventType)) ||
            not (method.DeclaringType.IsAssignableFrom(typeof<'T>)) then
            let message = String.Format("Method {0} on {1} cannot be used as an aggregation method", method.Name, method.DeclaringType)
            raise(new ArgumentOutOfRangeException(message));
        let aggregateParameter = Expression.Parameter(typeof<'T>, "a");
        let eventParameter = Expression.Parameter(typeof<'TEvent>, "e");
        let body = Expression.Call(aggregateParameter, method, eventParameter);
        let lambda = Expression.Lambda<Action<'T, 'TEvent>>(body, aggregateParameter, eventParameter);
        let compilation = lambda.Compile()
        // ExpressionCompiler is internal =/
        // let expressionCompiler = Type.GetType("Marten.Util.ExpressionCompiler, Marten")
        new MyAggregationStep<'T, 'TEvent>(compilation)

    interface IAggregation<'T, 'TEvent> with

        member this.Apply(aggregate, event) =
            apply.Invoke(aggregate, event)


type MyAggregator<'T when 'T : (new : unit -> 'T) and 'T : not struct> (overrideMethodLookup : IEnumerable<MethodInfo>)=
    let aggregations : IDictionary<Type, obj> = (new Dictionary<Type, obj>() :> IDictionary<Type, obj>) 
    let aggregateType = typeof<'T>
    let mutable alias = Unchecked.defaultof<string>
    do
        alias <-  typeof<'T>.Name.ToTableAlias();
        overrideMethodLookup.Each(fun (method : MethodInfo) ->
            let mutable step = Unchecked.defaultof<obj>
            let mutable eventType = method.GetParameters().Single<ParameterInfo>().ParameterType;

            if eventType.Closes(typedefof<Event<_>>) then
                eventType <- eventType.GetGenericArguments().Single();
                if Reflection.FSharpType.IsUnion eventType then
                    Reflection.FSharpType.GetUnionCases(eventType)
                    |> Seq.map (fun x -> x.GetFields().[0].DeclaringType)
                    |> Seq.iter (fun x ->
                        step <- typedefof<MyEventAggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| aggregateType; x |]);
                        aggregations.Add(x, step))
                else 
                    step <- typedefof<MyEventAggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| aggregateType; eventType |]);
            else
                if Reflection.FSharpType.IsUnion eventType then
                    Reflection.FSharpType.GetUnionCases(eventType)
                    |> Seq.map (fun x -> x.GetFields().[0].DeclaringType)
                    |> Seq.iter (fun x ->
                        step <- typedefof<MyAggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| aggregateType; x |]);
                        aggregations.Add(x, step))
                else
                    step <- typedefof<MyAggregationStep<_,_>>.CloseAndBuildAs<obj>(method, [| aggregateType; eventType |]);
                    aggregations.Add(eventType, step)
        ) |> ignore

    static let ApplyMethod = "Apply"

    new() =
        new MyAggregator<'T>(typeof<'T>.GetMethods()
                             |> Seq.where (fun x ->
                                 x.Name = ApplyMethod &&
                                 x.GetParameters().Length = 1))

    member this.Add<'TEvent>(aggregation: IAggregation<'T, 'TEvent>) =
        if aggregations.ContainsKey(typeof<'TEvent>) then
            aggregations.[typeof<'TEvent>] <- aggregation
        else
            aggregations.Add(typeof<'TEvent>, aggregation)
        this

    member this.Add<'TEvent>(application: Action<'T, 'TEvent>) =
        this.Add(new AggregationStep<'T, 'TEvent>(application));

    interface IAggregator<'T> with

        member this.AggregatorFor<'TEvent>() =
            // Need to investigate here
            let typeEvent = typeof<'TEvent>
            if aggregations.ContainsKey(typeEvent) then
                let aggregation = aggregations.[typeEvent]
                aggregation.As<IAggregation<'T, 'TEvent>>()
            else
                null

        member this.Build(events, session, state) =
            events.Each(fun (x : IEvent) -> x.Apply(state, this)) |> ignore
            state

        member this.Build(events, session) =
            (this :> IAggregator<'T>).Build(events, session, new 'T());

        member this.EventTypes =
            aggregations.Keys.ToArray();

        member this.AggregateType =
            aggregateType

        member this.Alias =
            alias

        member this.AppliesTo(stream) =
            stream.Events.Any(fun x ->

                aggregations.ContainsKey(x.Data.GetType()));

type MyAggregatorLookup (factory: Func<Type, IAggregator>)=

    new() =
        new MyAggregatorLookup(Unchecked.defaultof<Func<Type, IAggregator>>)

    interface IAggregatorLookup with

        member this.Lookup<'T when 'T : (new : unit -> 'T) and 'T : not struct>() =
            let thisFactory = factory
            let mutable aggregator = Unchecked.defaultof<IAggregator<'T>>
            if thisFactory <> null then
                aggregator <- thisFactory.Invoke(typeof<'T>) :?> IAggregator<'T>
            else
                aggregator <- null
            if aggregator = null then
                aggregator <- new MyAggregator<'T>()
            aggregator

        member this.Lookup(aggregateType) =
            let thisFactory = factory
            let mutable aggregator = Unchecked.defaultof<IAggregator>
            if thisFactory <> null then 
                aggregator <- thisFactory.Invoke(aggregateType)
            else
                aggregator <- null
            typedefof<MyAggregator<_>>.CloseAndBuildAs<IAggregator>([| aggregateType |])

However there is an issue about Event<TDiscriminatedUnion>

@ehouarn-perret I鈥檒l try to check what we can do on that.

@oskardudycz thanks!
Newtonsoft has this class, might worth to have a look at it: https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DiscriminatedUnionConverter.cs

Thanks for sharing this finding 馃憤

Note: a potential workaround is to create a wrapper type that embeds the DU type.
Sorry to have ghosted for a while: been busy working

Depending on how well this can be addressed in Marten, think it (workarounds) could at least be documented e.g. under scenarios (https://jasperfx.github.io/marten/documentation/scenarios/), mentioning any possible caveats. Though no F# code in Marten.sln, so documenting them with unit tests (as opposed to just MD) would introduce new build requirements :(

I have a plan to add new sample projects to our solution: one written with C# and other with F#, based on what @ehouarn-perret provided. It would be easier to check and verify if it's working also for F# and might be good starting point for our users. Thoughts?
@jokokko I like the idea of updating also scenarios 馃憤

@oskardudycz I absolutely prefer having the docs as code (so our docs won't go stale). Just a new build prerequisite (F#) for anybody wanting to build Marten. Unless they are not made part of the default build or build is otherwise tweaked.

I have a plan to add new sample projects to our solution: one written with C# and other with F#, based on what @ehouarn-perret provided. It would be easier to check and verify if it's working also for F# and might be good starting point for our users. Thoughts?
@jokokko I like the idea of updating also scenarios 馃憤

Might need a review, if @wastaz could have a look at it 馃憤

For sure I'll add you and @wastaz as reviewers. I have plan to work on that during the weekend or at worst in the next week if my non-marten life won't mess with that ;)

I'll have also some learning curve, as I wasn't coding in F# much.

A work-colleague showed me something the other day, some food for thoughts: https://github.com/Dzoukr/CosmoStore

Does not provide much (nothing from my understanding) for projections, but well this is something that may worth looking at.

Also about this issue, the main problem behind it is a reflection issue: https://github.com/JasperFx/marten/issues/1283#issuecomment-500151729

I asked something a while ago on SO: https://stackoverflow.com/questions/56502392/f-how-to-call-expression-call-for-a-method-with-discriminated-union-in-a-gener

Didn't get any proper answer, I may dig into that when I will have more time.

Thank you @ehouarn-perret.

I'm just finishing Domain Modeling Made Functional by @swlaschin, last pages left - so I think that now I have the good basis to tackle this issue and make Marten more F# friendly.

After I finish my work on https://github.com/JasperFx/marten/issues/1302 I'll try to tackle that.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kasparurban picture kasparurban  路  7Comments

mhov picture mhov  路  5Comments

jokokko picture jokokko  路  9Comments

ppumkin picture ppumkin  路  4Comments

kamranayub picture kamranayub  路  7Comments