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

Move Equinox.Codec out to FsCodec #156

Merged
merged 13 commits into from
Aug 30, 2019
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ The `Unreleased` section name is replaced by the expected version of next releas

### Added
### Changed

- Extracted `Equinox.Codec` to external project `FsCodec`, with Json.net support in `FsCodec.NewtonsoftJson` [#156](https://github.com/jet/equinox/pull/156)

### Removed
### Fixed

Expand Down
2 changes: 1 addition & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ The fact that we have a `Cleared` Event stems from the fact that the spec define

The `Compacted` event is used to represent Rolling Snapshots (stored in-stream) and/or Unfolds (stored in Tip document-Item); For a real Todo list, using this facility may well make sense - the State can fit in a reasonable space, and the likely number of Events may reach an interesting enough count to justify applying such a feature
i) it should also be noted that Caching may be a better answer - note `Compacted` is also an `isOrigin` event - there's no need to go back any further if you meet one.
ii) we use an Array in preference to a [F#] `list`; while there are `ListConverter`s out there (notably not in [`Jet.JsonNet.Converters`](https://github.com/jet/Jet.JsonNet.Converters)), in this case an Array is better from a GC and memory-efficiency stance, and does not need any special consideration when using `Newtonsoft.Json` to serialize.
ii) we use an Array in preference to a [F#] `list`; while there are `ListConverter`s out there (notably not in [`FsCodec`](https://github.com/jet/FsCodec)), in this case an Array is better from a GC and memory-efficiency stance, and does not need any special consideration when using `Newtonsoft.Json` to serialize.

#### `State` + `initial`+`fold`

Expand Down
6 changes: 0 additions & 6 deletions Equinox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.MemoryStore", "src\
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.MemoryStore.Integration", "tests\Equinox.MemoryStore.Integration\Equinox.MemoryStore.Integration.fsproj", "{B2C695C3-ED04-4094-B672-926440B5F335}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Codec", "src\Equinox.Codec\Equinox.Codec.fsproj", "{F5491EB1-D072-4262-8184-2A908E1D6175}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Tool", "tools\Equinox.Tool\Equinox.Tool.fsproj", "{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Cosmos", "src\Equinox.Cosmos\Equinox.Cosmos.fsproj", "{54EA6187-9F9F-4D67-B602-163D011E43E6}"
Expand Down Expand Up @@ -110,10 +108,6 @@ Global
{B2C695C3-ED04-4094-B672-926440B5F335}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C695C3-ED04-4094-B672-926440B5F335}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C695C3-ED04-4094-B672-926440B5F335}.Release|Any CPU.Build.0 = Release|Any CPU
{F5491EB1-D072-4262-8184-2A908E1D6175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5491EB1-D072-4262-8184-2A908E1D6175}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5491EB1-D072-4262-8184-2A908E1D6175}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5491EB1-D072-4262-8184-2A908E1D6175}.Release|Any CPU.Build.0 = Release|Any CPU
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The implementations are distilled from [`Jet.com` systems dating all the way bac
- Designed not to invade application code; Domain tests can be written directly against your models without any need to involve or understand Equinox assemblies or constructs as part of writing those tests.
- Extracted from working software; currently used for all data storage within Jet's API gateway and Cart processing.
- Significant test coverage for core facilities, and with baseline and specific tests per Storage system and a comprehensive test and benchmarking story
- Encoding of events via `Equinox.Codec` provides for pluggable encoding of events based on either:
- Encoding of events via `FsCodec.IUnionEncoder` provides for pluggable encoding of events based on either:
- a [versionable convention-based approach](https://eiriktsarpalis.wordpress.com/2018/10/30/a-contract-pattern-for-schemaless-datastores/) (using `Typeshape`'s `UnionContractEncoder` under the covers), providing for serializer-agnostic schema evolution with minimal boilerplate
- an explicitly coded pair of `encode` and `tryDecode` functions for when you need to customize
- Independent of the store used, Equinox provides for caching using the .NET `MemoryCache` to minimize roundtrips, latency and bandwidth / Request Charges by maintaining the folded state, without necessitating making the Domain Model folded state serializable
Expand All @@ -44,12 +44,17 @@ The implementations are distilled from [`Jet.com` systems dating all the way bac

The components within this repository are delivered as a series of multi-targeted Nuget packages targeting `net461` (F# 3.1+) and `netstandard2.0` (F# 4.5+) profiles; each of the constituent elements is designed to be easily swappable as dictated by the task at hand. Each of the components can be inlined or customized easily:-

### Core libraries
### Core library

- `Equinox[.Stream]` [![NuGet](https://img.shields.io/nuget/v/Equinox.svg)](https://www.nuget.org/packages/Equinox/): Store-agnostic decision flow runner that manages the optimistic concurrency protocol. ([depends](https://www.fuget.org/packages/Equinox) on `Serilog` (but no specific Serilog sinks, i.e. you configure to emit to `NLog` etc))
- `Equinox.Codec` [![Codec NuGet](https://img.shields.io/nuget/v/Equinox.Codec.svg)](https://www.nuget.org/packages/Equinox.Codec/): [a scheme for the serializing Events modelled as an F# Discriminated Union](https://eiriktsarpalis.wordpress.com/2018/10/30/a-contract-pattern-for-schemaless-datastores/) ([depends](https://www.fuget.org/packages/Equinox.Codec) on `TypeShape 7.*`, `Microsoft.IO.RecyclableMemoryStream 1.2.2`, `Newtonsoft.Json >= 11.0.2` but can support any serializer) with the following capabilities:
- `Equinox.Codec.NewtonsoftJson.Json`: allows tagging of F# Discriminated Union cases in a versionable manner with low-dependency `DataMember(Name=` tags using [TypeShape](https://github.com/eiriktsarpalis/TypeShape)'s [`UnionContractEncoder`](https://github.com/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs)
- `Equinox.Codec.Custom`: independent of any specific serializer; enables plugging in a serializer and/or Union Encoder of your choice
- `Equinox` [![NuGet](https://img.shields.io/nuget/v/Equinox.svg)](https://www.nuget.org/packages/Equinox/): Store-agnostic decision flow runner that manages the optimistic concurrency protocol. ([depends](https://www.fuget.org/packages/Equinox) on `Serilog` (but no specific Serilog sinks, i.e. you configure to emit to `NLog` etc))

### Serialization support

- `FsCodec` [![Codec NuGet](https://img.shields.io/nuget/v/FsCodec.svg)](https://www.nuget.org/packages/FsCodec/): [a scheme for the serializing Events modelled as an F# Discriminated Union](https://eiriktsarpalis.wordpress.com/2018/10/30/a-contract-pattern-for-schemaless-datastores/) ([depends](https://www.fuget.org/packages/FsCodec) on `TypeShape 7.*`
- `FsCodec.IUnionEncoder`: allows tagging of F# Discriminated Union cases in a versionable manner with low-dependency `DataMember(Name=` tags using [TypeShape](https://github.com/eiriktsarpalis/TypeShape)'s [`UnionContractEncoder`](https://github.com/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs)
- `FsCodec.Codec`: enables plugging in a serializer and/or Union Encoder of your choice (typically this is used to supply a pair of functions:- `encode` and `tryDecode`)
- `FsCodec.NewtonsoftJson` [![Codec NuGet](https://img.shields.io/nuget/v/FsCodec.NewtonsoftJson.svg)](https://www.nuget.org/packages/FsCodec.NewtonsoftJson/): Implementation of `FsCodec.IUnionEncoder` that uses Json.net to serialize the bodies of the union cases. ([depends](https://www.fuget.org/packages/FsCodec.NewtonsoftJson) on `FsCodec`, `Microsoft.IO.RecyclableMemoryStream 1.2.2`, `Newtonsoft.Json >= 11.0.2`
- (planned) `FsCodec.SystemTextJson`: drop in replacement that allows one to target the .NET `System.Text.Json` serializer solely by changing the referenced namespace.

### Store libraries

Expand Down
1 change: 0 additions & 1 deletion build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

<Target Name="Pack">
<Exec Command="dotnet pack src/Equinox $(Cfg) $(PackOptions)" />
<Exec Command="dotnet pack src/Equinox.Codec $(Cfg) $(PackOptions)" />
<Exec Command="dotnet pack src/Equinox.Cosmos $(Cfg) $(PackOptions)" />
<Exec Command="dotnet pack src/Equinox.EventStore $(Cfg) $(PackOptions)" />
<Exec Command="dotnet pack src/Equinox.MemoryStore $(Cfg) $(PackOptions)" />
Expand Down
36 changes: 13 additions & 23 deletions samples/Infrastructure/Services.fs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module Samples.Infrastructure.Services

open Domain
open Microsoft.Extensions.DependencyInjection
open System

type StreamResolver(storage) =
member __.Resolve
( codec : Equinox.Codec.IUnionEncoder<'event,byte[]>,
( codec : FsCodec.IUnionEncoder<'event,byte[]>,
fold: ('state -> 'event seq -> 'state),
initial: 'state,
snapshot: (('event -> bool) * ('state -> 'event))) =
Expand All @@ -20,40 +21,29 @@ type StreamResolver(storage) =
let accessStrategy = if unfolds then Equinox.Cosmos.AccessStrategy.Snapshot snapshot |> Some else None
Equinox.Cosmos.Resolver<'event,'state>(store, codec, fold, initial, caching, ?access = accessStrategy).Resolve

type ICodecGen =
abstract Generate<'Union when 'Union :> TypeShape.UnionContract.IUnionContract> : unit -> Equinox.Codec.IUnionEncoder<'Union,byte[]>

type ServiceBuilder(storageConfig, handlerLog, codecGen : ICodecGen) =
type ServiceBuilder(storageConfig, handlerLog) =
let resolver = StreamResolver(storageConfig)

member __.CreateFavoritesService() =
let codec = codecGen.Generate<Domain.Favorites.Events.Event>()
let fold, initial = Domain.Favorites.Folds.fold, Domain.Favorites.Folds.initial
let snapshot = Domain.Favorites.Folds.isOrigin,Domain.Favorites.Folds.compact
Backend.Favorites.Service(handlerLog, resolver.Resolve(codec,fold,initial,snapshot))
let fold, initial = Favorites.Folds.fold, Favorites.Folds.initial
let snapshot = Favorites.Folds.isOrigin,Favorites.Folds.compact
Backend.Favorites.Service(handlerLog, resolver.Resolve(Favorites.Events.codec,fold,initial,snapshot))

member __.CreateSaveForLaterService() =
let codec = codecGen.Generate<Domain.SavedForLater.Events.Event>()
let fold, initial = Domain.SavedForLater.Folds.fold, Domain.SavedForLater.Folds.initial
let snapshot = Domain.SavedForLater.Folds.isOrigin,Domain.SavedForLater.Folds.compact
Backend.SavedForLater.Service(handlerLog, resolver.Resolve(codec,fold,initial,snapshot), maxSavedItems=50)
let fold, initial = SavedForLater.Folds.fold, SavedForLater.Folds.initial
let snapshot = SavedForLater.Folds.isOrigin,SavedForLater.Folds.compact
Backend.SavedForLater.Service(handlerLog, resolver.Resolve(SavedForLater.Events.codec,fold,initial,snapshot), maxSavedItems=50)

member __.CreateTodosService() =
let codec = codecGen.Generate<TodoBackend.Events.Event>()
let fold, initial = TodoBackend.Folds.fold, TodoBackend.Folds.initial
let snapshot = TodoBackend.Folds.isOrigin, TodoBackend.Folds.compact
TodoBackend.Service(handlerLog, resolver.Resolve(codec,fold,initial,snapshot))
TodoBackend.Service(handlerLog, resolver.Resolve(TodoBackend.Events.codec,fold,initial,snapshot))

let register (services : IServiceCollection, storageConfig, handlerLog, codecGen : ICodecGen) =
let register (services : IServiceCollection, storageConfig, handlerLog) =
let regF (factory : IServiceProvider -> 'T) = services.AddSingleton<'T>(fun (sp: IServiceProvider) -> factory sp) |> ignore

regF <| fun _sp -> ServiceBuilder(storageConfig, handlerLog, codecGen)
regF <| fun _sp -> ServiceBuilder(storageConfig, handlerLog)

regF <| fun sp -> sp.GetService<ServiceBuilder>().CreateFavoritesService()
regF <| fun sp -> sp.GetService<ServiceBuilder>().CreateSaveForLaterService()
regF <| fun sp -> sp.GetService<ServiceBuilder>().CreateTodosService()

let serializationSettings = Newtonsoft.Json.Converters.FSharp.Settings.CreateCorrect()
type NewtonsoftJsonCodecGen() =
interface ICodecGen with
member __.Generate() = Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)
regF <| fun sp -> sp.GetService<ServiceBuilder>().CreateTodosService()
1 change: 1 addition & 0 deletions samples/Store/Domain/Cart.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Events =
| ItemQuantityChanged of ItemQuantityChangeInfo
| ItemWaiveReturnsChanged of ItemWaiveReturnsInfo
interface TypeShape.UnionContract.IUnionContract
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()

module Folds =
type ItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool }
Expand Down
1 change: 1 addition & 0 deletions samples/Store/Domain/ContactPreferences.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Events =
type Event =
| [<System.Runtime.Serialization.DataMember(Name = "contactPreferencesChanged")>]Updated of Value
interface TypeShape.UnionContract.IUnionContract
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()

module Folds =
type State = Events.Preferences
Expand Down
5 changes: 2 additions & 3 deletions samples/Store/Domain/Domain.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
<PackageReference Include="FSharp.Core" Version="3.1.2.5" Condition=" '$(TargetFramework)' == 'net461' " />
<PackageReference Include="FSharp.Core" Version="4.3.4" Condition=" '$(TargetFramework)' == 'netstandard2.0' " />

<PackageReference Include="FsCodec.NewtonsoftJson" Version="1.0.0-rc1" />
<PackageReference Include="FSharp.UMX" Version="1.0.0" />
<PackageReference Include="Jet.JsonNet.Converters" Version="0.2.2" />
<PackageReference Include="TypeShape" Version="7.0.0" />
</ItemGroup>

</Project>
</Project>
1 change: 1 addition & 0 deletions samples/Store/Domain/Favorites.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Events =
| Favorited of Favorited
| Unfavorited of Unfavorited
interface TypeShape.UnionContract.IUnionContract
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()

module Folds =
type State = Events.Favorited []
Expand Down
4 changes: 2 additions & 2 deletions samples/Store/Domain/Infrastructure.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[<AutoOpen>]
module Domain.Infrastructure

open FsCodec.NewtonsoftJson
open FSharp.UMX
open Newtonsoft.Json
open Newtonsoft.Json.Converters.FSharp
open System

#if NET461
Expand Down Expand Up @@ -74,7 +74,7 @@ type CartId = Guid<cartId>
and [<Measure>] cartId
module CartId = let toStringN (value : CartId) : string = Guid.toStringN %value

/// CartId strongly typed id; represented internally as a Guid; not used for storage so rendering is not significant
/// ClientId strongly typed id; represented internally as a Guid; not used for storage so rendering is not significant
type ClientId = Guid<clientId>
and [<Measure>] clientId
module ClientId = let toStringN (value : ClientId) : string = Guid.toStringN %value
Expand Down
1 change: 1 addition & 0 deletions samples/Store/Domain/SavedForLater.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module Events =
/// Addition of a collection of skus to the list
| Added of Added
interface TypeShape.UnionContract.IUnionContract
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()

module Folds =
open Events
Expand Down
2 changes: 1 addition & 1 deletion samples/Store/Integration/CartIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let createMemoryStore () =
let createServiceMemory log store =
Backend.Cart.Service(log, Resolver(store, fold, initial).Resolve)

let codec = Equinox.EventStore.Integration.EventStoreIntegration.genCodec<Domain.Cart.Events.Event>()
let codec = Domain.Cart.Events.codec

let resolveGesStreamWithRollingSnapshots gateway =
EventStore.Resolver(gateway, codec, fold, initial, access = AccessStrategy.RollingSnapshots snapshot).Resolve
Expand Down
16 changes: 4 additions & 12 deletions samples/Store/Integration/CodecIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,13 @@ open Domain
open Swensen.Unquote
open TypeShape.UnionContract

let serializationSettings =
Newtonsoft.Json.Converters.FSharp.Settings.CreateCorrect(converters=
[| // Don't let json.net treat 't option as the DU it is internally
Newtonsoft.Json.Converters.FSharp.OptionConverter() |])

let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() =
Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

type EventWithId = { id : CartId } // where CartId uses FSharp.UMX

type EventWithOption = { age : int option }

type EventWithUnion = { value : Union }
// Using converter to collapse the `fields` of the union into the top level, alongside the `case`
and [<Newtonsoft.Json.JsonConverter(typeof<Newtonsoft.Json.Converters.FSharp.UnionConverter>)>] Union =
and [<Newtonsoft.Json.JsonConverter(typeof<FsCodec.NewtonsoftJson.UnionConverter>)>] Union =
| I of Int
| S of MaybeInt
and Int = { i : int }
Expand All @@ -37,16 +29,16 @@ type SimpleDu =

let render = function
| EventA { id = id } -> sprintf """{"id":"%O"}""" id
| EventB { age = None } -> sprintf "{}"
| EventB { age = None } -> sprintf "{\"age\":null}"
| EventB { age = Some age } -> sprintf """{"age":%d}""" age
| EventC { value = I { i = i } } -> sprintf """{"value":{"case":"I","i":%d}}""" i
| EventC { value = S { maybeI = None } } -> sprintf """{"value":{"case":"S"}}"""
| EventC { value = S { maybeI = None } } -> sprintf """{"value":{"case":"S","maybeI":null}}"""
| EventC { value = S { maybeI = Some i } } -> sprintf """{"value":{"case":"S","maybeI":%d}}""" i
| EventD -> null
//| EventE i -> string i
//| EventF s -> Newtonsoft.Json.JsonConvert.SerializeObject s

let codec = genCodec<SimpleDu>()
let codec = FsCodec.NewtonsoftJson.Codec.Create()

[<AutoData(MaxTest=100)>]
let ``Can roundtrip, rendering correctly`` (x: SimpleDu) =
Expand Down
2 changes: 1 addition & 1 deletion samples/Store/Integration/ContactPreferencesIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let createMemoryStore () =
let createServiceMemory log store =
Backend.ContactPreferences.Service(log, MemoryStore.Resolver(store, fold, initial).Resolve)

let codec = genCodec<Domain.ContactPreferences.Events.Event>()
let codec = Domain.ContactPreferences.Events.codec
let resolveStreamGesWithOptimizedStorageSemantics gateway =
EventStore.Resolver(gateway 1, codec, fold, initial, access = EventStore.AccessStrategy.EventsAreState).Resolve
let resolveStreamGesWithoutAccessStrategy gateway =
Expand Down
3 changes: 0 additions & 3 deletions samples/Store/Integration/EventStoreIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ module Samples.Store.Integration.EventStoreIntegration
open Equinox.EventStore
open System

let serializationSettings = Newtonsoft.Json.Converters.FSharp.Settings.CreateCorrect()
let genCodec<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>() = Equinox.Codec.NewtonsoftJson.Json.Create<'Union>(serializationSettings)

/// Connect with Gossip based cluster discovery using the default Commercial edition Manager port config
/// Such a config can be simulated on a single node with zero config via the EventStore OSS package:-
/// 1. cinst eventstore-oss -y # where cinst is an invocation of the Chocolatey Package Installer on Windows
Expand Down
2 changes: 1 addition & 1 deletion samples/Store/Integration/FavoritesIntegration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let createMemoryStore () =
let createServiceMemory log store =
Backend.Favorites.Service(log, MemoryStore.Resolver(store, fold, initial).Resolve)

let codec = genCodec<Domain.Favorites.Events.Event>()
let codec = Domain.Favorites.Events.codec
let createServiceGes gateway log =
let resolveStream = EventStore.Resolver(gateway, codec, fold, initial, access = EventStore.AccessStrategy.RollingSnapshots snapshot).Resolve
Backend.Favorites.Service(log, resolveStream)
Expand Down
1 change: 1 addition & 0 deletions samples/TodoBackend/Todo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Events =
| Cleared
| Compacted of CompactedInfo
interface TypeShape.UnionContract.IUnionContract
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()

module Folds =
type State = { items : Todo list; nextId : int }
Expand Down
Loading