Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: nested DU support? #33

Closed
TerenceHinrichsen opened this issue Feb 3, 2020 · 5 comments
Closed

Question: nested DU support? #33

TerenceHinrichsen opened this issue Feb 3, 2020 · 5 comments

Comments

@TerenceHinrichsen
Copy link

This might just be my ignorance, but I am having great difficulty in using the UnionEncoder, with Equinox to create events.

I have this:

type WeirdThing = {value: string; active: bool}
type WeirdThing2 = {value: string; active: bool; location: string }

type Something = 
  | WeirdThing of WeirdThing
  | WeirdThing2 of WeirdThing2

type Event =
  | [<DataMember(Name = "Created")>]          Created of New
  | [<DataMember(Name = "Something")>]       Something of Something
  with interface IUnionContract

I get no error during BUILD - so syntax is good, however, during runtime, when I create an event Something I get a exception:

Union case 'xxxx' contains field that is not an F# record

So now I am wondering whether a DU nested inside a DU is possible using the TypeShape UnionEncoder? What am I missing?

@bartelink
Copy link
Collaborator

@CumpsD ran into this - will reply in more depth later TL;DR use StreamName to route at high level but UnionEncoder only works on top level unions

There is a UnionConverter in the box which lets you manage this (UnionConverter) but a) TypeShape UnionEncoder does not get involved b) its questionable to apply unions within event contract

Need to be AFK for a bit - perhaps @CumpsD can explain in mode depth if this is not clear

@CumpsD
Copy link
Contributor

CumpsD commented Feb 3, 2020

I ran into the same, yes: https://github.com/exira/FsCodec/blob/creating-events/tests/FsCodec.NewtonsoftJson.Tests/DomainExample.fsx#L227-L236

I'll let @bartelink reply more on it, I'm currently taking another approach by playing with Chiron to serialize DUs in DUs :)

@bartelink
Copy link
Collaborator

@TerenceHinrichsen Finally have time to expand on this

The general pattern being catered for is

  1. uniting all the events that happen in a given kind of stream (category), discriminated by EventType
  2. trying to do KISS JSON - no tricks like DUs in the json

For that reason, DU hierarchies are definitely not on the table as they rarely represent the cleanest way to model as directly as possible the domain's events.

type WeirdThing = {value: string; active: bool}
type WeirdThing2 = {value: string; active: bool; location: string }

type Something = 
  | WeirdThing of WeirdThing
  | WeirdThing2 of WeirdThing2

type Event =
  | [<DataMember(Name = "Created")>]          Created of New
  | [<DataMember(Name = "SomethingWierd")>]       Something of WierdThing
  | [<DataMember(Name = "SomethingWierd2")>]       Something2 of WierdThing2
  with interface IUnionContract

But, I have to declare I've done it, and don't completely regret it, so I'll invest no further time dissuading you from it!


In the box, the UnionConverter enables this. You'd take your code and tag as follows:

type WeirdThing = {value: string; active: bool}
type WeirdThing2 = {value: string; active: bool; location: string }

[<Newtonsoft.Json.JsonConverter(typeof<FsCodec.NewtonsoftJson.UnionConverter>)>]
type Something = 
  | WeirdThing of WeirdThing
  | WeirdThing2 of WeirdThing2

type Event =
  | [<DataMember(Name = "Created")>]          Created of New
  | [<DataMember(Name = "Something")>]       Something of Something
  with interface IUnionContract

There's a small chance that that'll fall foul of the "ensure all bodies are records" restriction - that can be inhibited by passing requireRecordFields=false or similar to Create()

Perhaps you'd care to expand more on your real use case - there may be a cleaner solution; feel free to DM in DDD-CQRS-ES slack if you can't share it here

@TerenceHinrichsen
Copy link
Author

Thanks @bartelink & @CumpsD
At least I am not going crazy. Will have a look at the UnionConverter. The main reason we have this is for a Snapshotted event, where the state is determined by a flag in the event. If I get stuck I will contact you on slack.

@bartelink
Copy link
Collaborator

Interesting - I've serialized lots of snapshots over time (with plenty Fold.State top level values (and embedded values) being DUs ) but have always for some reason side-stepped representing them as DUs. The UnionConverter deals with adding / reordering of fields well so it should be a decent fit.

I'd agree that for a Snapshotted event it's more valid than it is for an organic event. Also, as part of versioning, having a top level record (as the exception forces you) also gives you an extensibility point (aside from stuffing it into the Meta) for adding other top-level fields without having to add it to every union case.

(The fact UnionEncoder has this restriction by default, in addition to requiring the marker interface is that experience showed him that the relatively unobtrusive nature of how you define the types can leave random devs making tweaks pretty unaware that these things are versioned union contracts, so over time you'll come to appreciate these nudges, even if they seem very arbitrary from the off)

Let us know if you run into any inconsistencies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants