From 4b2dbc63370a8f45bfe15ce0bbe412e788440973 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 18 Feb 2020 23:59:57 +0000 Subject: [PATCH 1/3] Add EventId to IEventData --- CHANGELOG.md | 3 +++ src/FsCodec.NewtonsoftJson/BoxCodec.fs | 18 +++++++++--------- src/FsCodec.NewtonsoftJson/Codec.fs | 16 ++++++++-------- src/FsCodec/Codec.fs | 14 +++++++------- src/FsCodec/FsCodec.fs | 20 +++++++++++++------- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a14dc9..a1516a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas ## [Unreleased] ### Added + +- Add `EventId` to `IEventData` [#36](https://github.com/jet/FsCodec/pull/36) + ### Changed - Permit embedded dashes in `FsCodec.StreamName`'s `{aggregateId}` segment [#34](https://github.com/jet/FsCodec/pull/34) diff --git a/src/FsCodec.NewtonsoftJson/BoxCodec.fs b/src/FsCodec.NewtonsoftJson/BoxCodec.fs index 005f0d8a..bbe5a01d 100755 --- a/src/FsCodec.NewtonsoftJson/BoxCodec.fs +++ b/src/FsCodec.NewtonsoftJson/BoxCodec.fs @@ -24,13 +24,13 @@ type Codec private () = up : FsCodec.ITimelineEvent * 'Contract -> 'Event, /// Maps a fresh Event resulting from a Decision in the Domain representation type down to the TypeShape UnionContract 'Contract /// The function is also expected to derive a meta object that will be held alongside the data (if it's not None) - /// together with its correlationId, causationId and an event creation timestamp (defaults to UtcNow). - down : 'Context option * 'Event -> 'Contract * 'Meta option * string * string * DateTimeOffset option, + /// together with its eventId, correlationId, causationId and an event creation timestamp (defaults to UtcNow). + down : 'Context option * 'Event -> 'Contract * 'Meta option * Guid * string * string * DateTimeOffset option, /// Enables one to fail encoder generation if 'Contract contains nullary cases. Defaults to false, i.e. permitting them [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, obj, 'Context> = - let boxEncoder : TypeShape.UnionContract.IEncoder = new TypeShape.UnionContract.BoxEncoder() :> _ + let boxEncoder : TypeShape.UnionContract.IEncoder = TypeShape.UnionContract.BoxEncoder() :> _ let dataCodec = TypeShape.UnionContract.UnionContractEncoder.Create<'Contract, obj>( boxEncoder, @@ -39,10 +39,10 @@ type Codec private () = { new FsCodec.IEventCodec<'Event, obj, 'Context> with member __.Encode(context, event) = - let (c, meta : 'Meta option, correlationId, causationId, timestamp : DateTimeOffset option) = down (context, event) + let (c, meta : 'Meta option, eventId, correlationId, causationId, timestamp : DateTimeOffset option) = down (context, event) let enc = dataCodec.Encode c let meta = meta |> Option.map boxEncoder.Encode<'Meta> - FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, defaultArg meta null, correlationId, causationId, ?timestamp = timestamp) + FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, defaultArg meta null, eventId, correlationId, causationId, ?timestamp = timestamp) member __.TryDecode encoded = let cOption = dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } @@ -63,15 +63,15 @@ type Codec private () = /// and an Event Creation timestamp. down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option, /// Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the correlationId and c) the correlationId - mapCausation : 'Context option * 'Meta option -> 'Meta option * string * string, + mapCausation : 'Context option * 'Meta option -> 'Meta option * Guid * string * string, /// Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, obj, 'Context> = let down (context, event) = let c, m, t = down event - let m', correlationId, causationId = mapCausation (context, m) - c, m', correlationId, causationId, t + let m', eventId, correlationId, causationId = mapCausation (context, m) + c, m', eventId, correlationId, causationId, t Codec.Create(up = up, down = down, ?rejectNullaryCases = rejectNullaryCases) /// Generate an IEventCodec that roundtrips events by holding the boxed form of the Event body. @@ -92,7 +92,7 @@ type Codec private () = [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, obj, obj> = - let mapCausation (_context : obj, m : 'Meta option) = m, null, null + let mapCausation (_context : obj, m : 'Meta option) = m, Guid.NewGuid(), null, null Codec.Create(up = up, down = down, mapCausation = mapCausation, ?rejectNullaryCases = rejectNullaryCases) /// Generate an IEventCodec that roundtrips events by holding the boxed form of the Event body. diff --git a/src/FsCodec.NewtonsoftJson/Codec.fs b/src/FsCodec.NewtonsoftJson/Codec.fs index 4d6420a0..31cbf88d 100755 --- a/src/FsCodec.NewtonsoftJson/Codec.fs +++ b/src/FsCodec.NewtonsoftJson/Codec.fs @@ -27,7 +27,7 @@ module private Utf8BytesEncoder = let private utf8NoBom = new System.Text.UTF8Encoding(false, true) let makeJsonWriter ms = // We need to `leaveOpen` in order to allow .Dispose of the `.rentStream`'d to return it - let sw = new StreamWriter(ms, utf8NoBom, 1024, leaveOpen = true) // same middle args as StreamWriter default ctor + let sw = new StreamWriter(ms, utf8NoBom, 1024, leaveOpen = true) // same middle args as StreamWriter default ctor new JsonTextWriter(sw, ArrayPool = CharBuffersPool.instance) module Core = @@ -69,7 +69,7 @@ type Codec private () = /// The function is also expected to derive /// a meta object that will be serialized with the same settings (if it's not None) /// and an Event Creation timestamp. - down : 'Context option * 'Event -> 'Contract * 'Meta option * string * string * DateTimeOffset option, + down : 'Context option * 'Event -> 'Contract * 'Meta option * Guid * string * string * DateTimeOffset option, /// Configuration to be used by the underlying Newtonsoft.Json Serializer when encoding/decoding. Defaults to same as Settings.Create() [] ?settings, /// Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them @@ -86,10 +86,10 @@ type Codec private () = { new FsCodec.IEventCodec<'Event, byte[], 'Context> with member __.Encode(context, event) = - let (c, meta : 'Meta option, correlationId, causationId, timestamp : DateTimeOffset option) = down (context, event) + let (c, meta : 'Meta option, eventId, correlationId, causationId, timestamp : DateTimeOffset option) = down (context, event) let enc = dataCodec.Encode c let metaUtf8 = meta |> Option.map bytesEncoder.Encode<'Meta> - FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, defaultArg metaUtf8 null, correlationId, causationId, ?timestamp = timestamp) + FsCodec.Core.EventData.Create(enc.CaseName, enc.Payload, defaultArg metaUtf8 null, eventId, correlationId, causationId, ?timestamp = timestamp) member __.TryDecode encoded = match dataCodec.TryDecode { CaseName = encoded.EventType; Payload = encoded.Data } with @@ -111,7 +111,7 @@ type Codec private () = /// and an Event Creation timestamp. down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option, /// Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the correlationId and c) the correlationId - mapCausation : 'Context option * 'Meta option -> 'Meta option * string * string, + mapCausation : 'Context option * 'Meta option -> 'Meta option * Guid * string * string, /// Configuration to be used by the underlying Newtonsoft.Json Serializer when encoding/decoding. Defaults to same as Settings.Create() [] ?settings, /// Enables one to fail encoder generation if union contains nullary cases. Defaults to false, i.e. permitting them @@ -120,8 +120,8 @@ type Codec private () = let down (context, union) = let c, m, t = down union - let m', correlationId, causationId = mapCausation (context, m) - c, m', correlationId, causationId, t + let m', eventId, correlationId, causationId = mapCausation (context, m) + c, m', eventId, correlationId, causationId, t Codec.Create(up = up, down = down, ?settings = settings, ?rejectNullaryCases = rejectNullaryCases) /// Generate an IEventCodec using the supplied Newtonsoft.Json settings. @@ -144,7 +144,7 @@ type Codec private () = [] ?rejectNullaryCases) : FsCodec.IEventCodec<'Event, byte[], obj> = - let mapCausation (_context : obj, m : 'Meta option) = m, null, null + let mapCausation (_context : obj, m : 'Meta option) = m, Guid.NewGuid(), null, null Codec.Create(up = up, down = down, mapCausation = mapCausation, ?settings = settings, ?rejectNullaryCases = rejectNullaryCases) /// Generate an IEventCodec using the supplied Newtonsoft.Json settings. diff --git a/src/FsCodec/Codec.fs b/src/FsCodec/Codec.fs index 038a767d..1c3d9294 100755 --- a/src/FsCodec/Codec.fs +++ b/src/FsCodec/Codec.fs @@ -12,15 +12,15 @@ type Codec = // (IME, while many systems have some code touching the metadata, it's not something one typically wants to encourage) static member private Create<'Event, 'Format, 'Context> ( /// Maps an 'Event to: an Event Type Name, a pair of <>'Format's representing the Data and Meta together with the correlationId, causationId and timestamp. - encode : 'Context option * 'Event -> string * 'Format * 'Format * string * string * System.DateTimeOffset option, + encode : 'Context option * 'Event -> string * 'Format * 'Format * Guid * string * string * System.DateTimeOffset option, /// Attempts to map from an Event's stored data to Some 'Event, or None if not mappable. tryDecode : ITimelineEvent<'Format> -> 'Event option) : IEventCodec<'Event, 'Format, 'Context> = { new IEventCodec<'Event, 'Format, 'Context> with member __.Encode(context, event) = - let eventType, data, metadata, correlationId, causationId, timestamp = encode (context, event) - Core.EventData.Create(eventType, data, metadata, correlationId, causationId, ?timestamp = timestamp) + let eventType, data, metadata, eventId, correlationId, causationId, timestamp = encode (context, event) + Core.EventData.Create(eventType, data, metadata, eventId, correlationId, causationId, ?timestamp = timestamp) member __.TryDecode encoded = tryDecode encoded } @@ -37,13 +37,13 @@ type Codec = /// to the 'Event representation (typically a Discriminated Union) that is to be presented to the programming model. tryDecode : FsCodec.ITimelineEvent<'Format> -> 'Event option, /// Uses the 'Context passed to the Encode call and the 'Meta emitted by down to a) the final metadata b) the correlationId and c) the correlationId - mapCausation : 'Context option * 'Event -> 'Format * string * string) + mapCausation : 'Context option * 'Event -> 'Format * Guid * string * string) : FsCodec.IEventCodec<'Event, 'Format, 'Context> = let encode (context, event) = let et, d, t = encode event - let m, correlationId, causationId = mapCausation (context, event) - et, d, m, correlationId, causationId, t + let m, eventId, correlationId, causationId = mapCausation (context, event) + et, d, m, eventId, correlationId, causationId, t Codec.Create(encode, tryDecode) /// Generate an IEventCodec using the supplied pair of encode and tryDecode functions. @@ -54,6 +54,6 @@ type Codec = tryDecode : string * 'Format -> 'Event option) : IEventCodec<'Event, 'Format, obj> = - let encode' (_context : obj, event) = let (et, d : 'Format) = encode event in et, d, null, null, null, None + let encode' (_context : obj, event) = let (et, d : 'Format) = encode event in et, d, null, Guid.NewGuid(), null, null, None let tryDecode' (encoded : FsCodec.ITimelineEvent<'Format>) = tryDecode (encoded.EventType, encoded.Data) Codec.Create(encode', tryDecode') diff --git a/src/FsCodec/FsCodec.fs b/src/FsCodec/FsCodec.fs index 5d769e3b..db59796d 100755 --- a/src/FsCodec/FsCodec.fs +++ b/src/FsCodec/FsCodec.fs @@ -8,6 +8,8 @@ type IEventData<'Format> = abstract member Data : 'Format /// Optional metadata (null, or same as Data, not written if missing) abstract member Meta : 'Format + /// Application-generated identifier used to drive idempotent writes based on deterministic Ids and/or Request Id + abstract member EventId : System.Guid /// The Event's Creation Time (as defined by the writer, i.e. in a mirror, this is intended to reflect the original time) /// - For EventStore, this value is not honored when writing; the server applies an authoritative timestamp when accepting the write. abstract member Timestamp : System.DateTimeOffset @@ -40,27 +42,30 @@ open System /// An Event about to be written, see IEventData for further information [] -type EventData<'Format> private (eventType, data, meta, correlationId, causationId, timestamp) = - static member Create(eventType, data, ?meta, ?correlationId, ?causationId, ?timestamp) : IEventData<'Format> = +type EventData<'Format> private (eventType, data, meta, eventId, correlationId, causationId, timestamp) = + static member Create(eventType, data, ?meta, ?eventId, ?correlationId, ?causationId, ?timestamp) : IEventData<'Format> = let meta, correlationId, causationId = defaultArg meta Unchecked.defaultof<_>, defaultArg correlationId null, defaultArg causationId null - EventData(eventType, data, meta, correlationId, causationId, match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow) :> _ + let eventId = match eventId with Some id -> id | None -> Guid.NewGuid() + EventData(eventType, data, meta, eventId, correlationId, causationId, match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow) :> _ interface FsCodec.IEventData<'Format> with member __.EventType = eventType member __.Data = data member __.Meta = meta + member __.EventId = eventId member __.Timestamp = timestamp member __.CorrelationId = correlationId member __.CausationId = causationId /// An Event or Unfold that's been read from a Store and hence has a defined Index on the Event Timeline [] -type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, correlationId, causationId, timestamp, context) = - static member Create(index, eventType, data, ?meta, ?correlationId, ?causationId, ?timestamp, ?isUnfold, ?context) : ITimelineEvent<'Format> = - let meta, timestamp = defaultArg meta Unchecked.defaultof<_>, match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow +type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, eventId, correlationId, causationId, timestamp, context) = + static member Create(index, eventType, data, ?meta, ?eventId, ?correlationId, ?causationId, ?timestamp, ?isUnfold, ?context) : ITimelineEvent<'Format> = let isUnfold, context = defaultArg isUnfold false, defaultArg context null + let meta, eventId = defaultArg meta Unchecked.defaultof<_>, match eventId with Some x -> x | None -> Guid.Empty + let timestamp = match timestamp with Some ts -> ts | None -> DateTimeOffset.UtcNow let correlationId, causationId = defaultArg correlationId null, defaultArg causationId null - TimelineEvent(index, isUnfold, eventType, data, meta, correlationId, causationId, timestamp, context) :> _ + TimelineEvent(index, isUnfold, eventType, data, meta, eventId, correlationId, causationId, timestamp, context) :> _ interface FsCodec.ITimelineEvent<'Format> with member __.Index = index @@ -69,6 +74,7 @@ type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, cor member __.EventType = eventType member __.Data = data member __.Meta = meta + member __.EventId = eventId member __.Timestamp = timestamp member __.CorrelationId = correlationId member __.CausationId = causationId From 338b52bd959f310f044fadedf4294a3ce7e752cb Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 19 Feb 2020 00:09:44 +0000 Subject: [PATCH 2/3] Tidy; Add Readme --- README.md | 8 +++++--- src/FsCodec/FsCodec.fs | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5ff126b7..9046fa97 100644 --- a/README.md +++ b/README.md @@ -251,13 +251,15 @@ type IEventData<'Format> = abstract member Data : 'Format /// Optional metadata (null, or same as Data, not written if missing) abstract member Meta : 'Format - /// The Event's Creation Time (as defined by the writer, i.e. in a mirror, this is intended to reflect the original time) - /// - For EventStore, this value is not honored when writing; the server applies an authoritative timestamp when accepting the write. - abstract member Timestamp : System.DateTimeOffset + /// Application-generated identifier used to drive idempotent writes based on deterministic Ids and/or Request Id + abstract member EventId : System.Guid /// The Correlation Id associated with the flow that generated this event. Can be `null` abstract member CorrelationId : string /// The Causation Id associated with the flow that generated this event. Can be `null` abstract member CausationId : string + /// The Event's Creation Time (as defined by the writer, i.e. in a mirror, this is intended to reflect the original time) + /// - For EventStore, this value is not honored when writing; the server applies an authoritative timestamp when accepting the write. + abstract member Timestamp : System.DateTimeOffset ``` diff --git a/src/FsCodec/FsCodec.fs b/src/FsCodec/FsCodec.fs index db59796d..11c0a48a 100755 --- a/src/FsCodec/FsCodec.fs +++ b/src/FsCodec/FsCodec.fs @@ -10,13 +10,13 @@ type IEventData<'Format> = abstract member Meta : 'Format /// Application-generated identifier used to drive idempotent writes based on deterministic Ids and/or Request Id abstract member EventId : System.Guid - /// The Event's Creation Time (as defined by the writer, i.e. in a mirror, this is intended to reflect the original time) - /// - For EventStore, this value is not honored when writing; the server applies an authoritative timestamp when accepting the write. - abstract member Timestamp : System.DateTimeOffset /// The Correlation Id associated with the flow that generated this event. Can be `null` abstract member CorrelationId : string /// The Causation Id associated with the flow that generated this event. Can be `null` abstract member CausationId : string + /// The Event's Creation Time (as defined by the writer, i.e. in a mirror, this is intended to reflect the original time) + /// - For EventStore, this value is not honored when writing; the server applies an authoritative timestamp when accepting the write. + abstract member Timestamp : System.DateTimeOffset /// Represents a Domain Event or Unfold, together with it's 0-based Index in the event sequence type ITimelineEvent<'Format> = @@ -53,9 +53,9 @@ type EventData<'Format> private (eventType, data, meta, eventId, correlationId, member __.Data = data member __.Meta = meta member __.EventId = eventId - member __.Timestamp = timestamp member __.CorrelationId = correlationId member __.CausationId = causationId + member __.Timestamp = timestamp /// An Event or Unfold that's been read from a Store and hence has a defined Index on the Event Timeline [] @@ -75,6 +75,6 @@ type TimelineEvent<'Format> private (index, isUnfold, eventType, data, meta, eve member __.Data = data member __.Meta = meta member __.EventId = eventId - member __.Timestamp = timestamp member __.CorrelationId = correlationId member __.CausationId = causationId + member __.Timestamp = timestamp From 742b0e339429ec11151980662fa099a545559305 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 19 Feb 2020 00:15:43 +0000 Subject: [PATCH 3/3] Add eventId to Comment SKIPCI --- src/FsCodec/Codec.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FsCodec/Codec.fs b/src/FsCodec/Codec.fs index 1c3d9294..efc2d26a 100755 --- a/src/FsCodec/Codec.fs +++ b/src/FsCodec/Codec.fs @@ -11,7 +11,8 @@ type Codec = // employed in the convention-based Codec // (IME, while many systems have some code touching the metadata, it's not something one typically wants to encourage) static member private Create<'Event, 'Format, 'Context> - ( /// Maps an 'Event to: an Event Type Name, a pair of <>'Format's representing the Data and Meta together with the correlationId, causationId and timestamp. + ( /// Maps an 'Event to: an Event Type Name, a pair of <>'Format's representing the Data and Meta together with the + /// eventId, correlationId, causationId and timestamp. encode : 'Context option * 'Event -> string * 'Format * 'Format * Guid * string * string * System.DateTimeOffset option, /// Attempts to map from an Event's stored data to Some 'Event, or None if not mappable. tryDecode : ITimelineEvent<'Format> -> 'Event option)