FsCodec
F# Event-Union Contract Encoding with versioning tolerant converters supporting System.Text.Json and Newtonsoft.Json
Install / Use
/learn @jet/FsCodecREADME
FsCodec

Defines a minimal interface for serialization and deserialization of events for event-sourcing systems on .NET. Provides implementation packages for writing simple yet versionable Event Contract definitions in F# using ubiquitous serializers.
Typically used in applications leveraging Equinox and/or Propulsion, but also applicable to defining DTOs for other purposes such as Web APIs.
Components
The components within this repository are delivered as multi-targeted Nuget packages supporting netstandard2.1 (F# 4.5+) profiles.
FsCodecDefines interfaces with trivial implementation helpers.- No dependencies.
FsCodec.IEventCodec: defines a base interface for serializers.FsCodec.Codec: enables plugging in custom serialization (a trivial implementation of the interface that simply delegates to a pair ofencodeanddecodefunctions you supply)FsCodec.StreamName: strongly-typed wrapper for a Stream Name, together with factory functions and active patterns for parsing sameFsCodec.StreamId: strongly-typed wrapper for a Stream Id, together with factory functions and active patterns for parsing same
FsCodec.Box: SeeFsCodec.Box.Codec;IEventCodec<obj>implementation that provides a null encode/decode step in order to enable decoupling of serialization/deserialization concerns from the encoding aspect, typically used together withEquinox.MemoryStore- depends on
FsCodec,TypeShape >= 10
- depends on
FsCodec.NewtonsoftJson: As described in a scheme for the serializing Events modelled as an F# Discriminated Union, enabled tagging of F# Discriminated Union cases in a versionable manner with low-dependencies using TypeShape'sUnionContractEncoder- Uses the ubiquitous
Newtonsoft.Jsonlibrary to serialize the event bodies. - Provides relevant Converters for common non-primitive types prevalent in F#
- depends on
FsCodec.Box,Newtonsoft.Json >= 13.0.3,Microsoft.IO.RecyclableMemoryStream >= 3.0.0,System.Buffers >= 4.5.1
- Uses the ubiquitous
FsCodec.SystemTextJson: See #38: drop in replacement that allows one to retarget fromNewtonsoft.Jsonto the .NET Core >= v 3.0 default serializer:System.Text.Json, solely by changing the referenced namespace.- depends on
FsCodec.Box,System.Text.Json >= 6.0.1,
- depends on
Features: FsCodec
The purpose of the FsCodec package is to provide a minimal interface on which libraries such as Equinox and Propulsion can depend on in order that they can avoid forcing a specific serialization mechanism.
FsCodec.IEventDatarepresents a single event and/or related metadata in raw form (i.e. still as a UTF8 string etc, not yet bound to a specific Event Type)FsCodec.ITimelineEventrepresents a single stored event and/or related metadata in raw form (i.e. still as a UTF8 string etc, not yet bound to a specific Event Type). InheritsIEventData, addingIndexandIsUnfoldin order to represent the position on the timeline that the event logically occupies.FsCodec.IEventCodecpresentsEncode: 'Context option * 'Event -> IEventDataandDecode: ITimelineEvent -> 'Event voptionmethods that can be used in low level application code to generateIEventDatas or decodeITimelineEvents based on a contract defined by'UnionFsCodec.Codec.CreateimplementsIEventCodecin terms of suppliedencode: 'Event -> string * byte[]anddecode: string * byte[] -> 'Event voptionfunctions (other overloads are available for advanced cases)FsCodec.Core.EventData.Createis a low level helper to create anIEventDatadirectly for purposes such as tests etc.FsCodec.Core.TimelineEvent.Createis a low level helper to create anITimelineEventdirectly for purposes such as tests etc.
Features: FsCodec.(Newtonsoft|SystemText)Json
Common API
The concrete implementations implement common type/member/function signatures and behavior that offer consistent behavior using either Newtonsoft.Json or System.Text.Json, emphasizing the following qualities:
- avoid non-straightforward encodings:
- tuples don't magically become arrays
- union bodies don't become arrays of mixed types like they do OOTB in JSON.NET (they become JSON Objects with named fields via
UnionEncoder, orstringvalues viaTypeSafeEnumConverter)
- don't surprise .NET developers used to
JSON.NETorSystem.Text.Json - having an opinionated core set of behaviors, but don't conflict with the standard extensibility mechanisms afforded by the underlying serializer (one should be able to search up and apply answers from StackOverflow to questions regarding corner cases)
- maintain a minimal but well formed set of built in converters that are implemented per supported serializer - e.g., choices like not supporting F#
listtypes (althoughSystem.Text.Jsonv>= 6does now provide such support)
Codec
FsCodec.NewtonsoftJson/SystemTextJson.Codec provides an implementation of IEventCodec as described in a scheme for the serializing Events modelled as an F# Discriminated Union. This yields a clean yet versionable way of managing the roundtripping events based on a contract inferred from an F# Discriminated Union Type using Newtonsoft.Json >= 13.0.3 / System.Text.Json to serialize the bodies.
Converters: Newtonsoft.Json.Converters / System.Text.Json.Serialization.JsonConverters
Explicit vs Implicit
While it's alluded to in the recommendations, it's worth calling out that the converters in FsCodec (aside from obvious exceptions like the Option and Record ones) are intended to be used by tagging the type with a JsonConverterAttribute rather than by inclusion in the global converters list of the underlying serializer.
The key effect of this is that any non-trivial mapping will manifest as the application of the relevant attribute on the type or property in question. This also aligns well with the notion of cordoning off a module Events as described in Equinox's module Aggregate documentation: types that participate in an Event union are defined and namespaced together (including any snapshot serialization contracts).
This set might be all you need ...
While this may not seem like a sufficiently large set of converters for a large app, it should be mentioned that the serializer-neutral escape hatch represented by JsonIsomorphism has resulted in this set alone proving sufficient for two major subsystems of a large e-commerce software suite. See recommendations for further expansion on this (TL;DR it does mean ruling out using some type constructs directly in event and/or binding contracts and using Anti Corruption Layer and/or event versioning techniques.
... but don't forget FSharp.SystemTextJson
The role and intention of the converters in the box in FsCodec.SystemTextJson and/or FsCodec.NewtonsoftJson has always been to be minimal but provide escape hatches; short lived shims absolutely fit within this remit. For example, with regard to System.Text.Json, over time the shimming provided has been adjusted in alignment with the STJ implementation:
System.Text.Jsonv4 did not even support F# recor
