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

Experiment: Polish eqxweb C# template by using OneOf library to pattern match #6

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 10 additions & 27 deletions equinox-web-csharp/Domain/Aggregate.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Equinox.Store;
using Microsoft.FSharp.Core;
using Newtonsoft.Json;
using OneOf;
using Serilog;
using System;
using System.Collections.Generic;
Expand All @@ -12,7 +13,7 @@ namespace TodoBackendTemplate
public static class Aggregate
{
/// NB - these types and names reflect the actual storage formats and hence need to be versioned with care
public abstract class Event
public abstract class Event : OneOfBase<Event.Happened, Event.Compacted>
{
public class Happened : Event
{
Expand Down Expand Up @@ -43,21 +44,10 @@ public class State

internal State(bool happened) { Happened = happened; }

public static readonly State Initial = new State(false);

static void Evolve(State s, Event x)
{
switch (x)
{
case Event.Happened e:
s.Happened = true;
break;
case Event.Compacted e:
s.Happened = e.Happened;
break;
default: throw new ArgumentOutOfRangeException(nameof(x), x, "invalid");
}
}
static void Evolve(State s, Event x) =>
x.Match(
(Event.Happened _) => s.Happened = true,
(Event.Compacted e) => s.Happened = e.Happened);

public static State Fold(State origin, IEnumerable<Event> xs)
{
Expand All @@ -74,22 +64,15 @@ public static State Fold(State origin, IEnumerable<Event> xs)
}

/// Defines the decision process which maps from the intent of the `Command` to the `Event`s that represent that decision in the Stream
public abstract class Command
public abstract class Command : OneOfBase<Command.MakeItSo>
{
public class MakeItSo : Command
{
}

public static IEnumerable<Event> Interpret(State s, Command x)
{
switch (x)
{
case MakeItSo c:
if (!s.Happened) yield return new Event.Happened();
break;
default: throw new ArgumentOutOfRangeException(nameof(x), x, "invalid");
}
}
public static Event[] Interpret(State s, Command x) =>
x.Match((MakeItSo _) =>
s.Happened ? new Event[0] : new Event [] { new Event.Happened()});
}

class Handler
Expand Down
1 change: 1 addition & 0 deletions equinox-web-csharp/Domain/Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageReference Include="Equinox" Version="1.0.3" />
<PackageReference Include="FSharp.Core" Version="4.5.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="OneOf" Version="2.1.127" />
</ItemGroup>

</Project>
81 changes: 29 additions & 52 deletions equinox-web-csharp/Domain/Todo.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Equinox.Store;
using Microsoft.FSharp.Core;
using Newtonsoft.Json;
using OneOf;
using Serilog;
using System;
using System.Collections.Generic;
Expand All @@ -14,6 +15,7 @@ public static class Todo
{
/// NB - these types and names reflect the actual storage formats and hence need to be versioned with care
public abstract class Event
: OneOfBase<Event.Added, Event.Updated, Event.Deleted, Event.Cleared, Event.Compacted>
{
/// Information we retain per Todo List entry
public abstract class ItemData
Expand Down Expand Up @@ -95,37 +97,23 @@ public static State Fold(State origin, IEnumerable<Event> xs)
var nextId = origin.NextId;
var items = origin.Items.ToList();
foreach (var x in xs)
switch (x)
{
case Event.Added e:
nextId++;
items.Insert(0, e.Data);
break;
case Event.Updated e:
x.Switch(
(Event.Added e) => { nextId++; items.Insert(0, e.Data); },
(Event.Updated e) =>
{
var i = items.FindIndex(item => item.Id == e.Data.Id);
if (i != -1)
items[i] = e.Data;
break;
case Event.Deleted e:
items.RemoveAll(item => item.Id == e.Id);
break;
case Event.Cleared e:
nextId = e.NextId;
items.Clear();
break;
case Event.Compacted e:
nextId = e.NextId;
items = e.Items.ToList();
break;
default:
throw new ArgumentOutOfRangeException(nameof(x), x, "invalid");
}
},
(Event.Deleted e) => items.RemoveAll(item => item.Id == e.Id),
(Event.Cleared e) => { nextId = e.NextId; items.Clear(); },
(Event.Compacted e) => { nextId = e.NextId; items = e.Items.ToList(); });
return new State(nextId, items.ToArray());
}

/// Determines whether a given event represents a checkpoint that implies we don't need to see any preceding events
public static bool IsOrigin(Event e) => e is Event.Cleared || e is Event.Compacted;

/// Prepares an Event that encodes all relevant aspects of a State such that `evolve` can rehydrate a complete State from it
public static Event Compact(State state) => new Event.Compacted { NextId = state.NextId, Items = state.Items };
}
Expand All @@ -140,6 +128,7 @@ public class Props

/// Defines the operations a caller can perform on a Todo List
public abstract class Command
: OneOfBase<Command.Add, Command.Update, Command.Delete, Command.Clear>
{
/// Create a single item
public class Add : Command
Expand All @@ -166,38 +155,26 @@ public class Clear : Command
}

/// Defines the decision process which maps from the intent of the `Command` to the `Event`s that represent that decision in the Stream
public static IEnumerable<Event> Interpret(State s, Command x)
{
switch (x)
{
case Add c:
yield return Make<Event.Added>(s.NextId, c.Props);
break;
case Update c:
var proposed = new {c.Props.Order, c.Props.Title, c.Props.Completed};
public static IEnumerable<Event> Interpret(State s, Command x) =>
x.Match(
(Add c) => new Event[] { Make<Event.Added>(s.NextId, c.Props) },
(Update c) =>
{
var proposed = new { c.Props.Order, c.Props.Title, c.Props.Completed };

bool IsEquivalent(Event.ItemData i) =>
i.Id == c.Id
&& new {i.Order, i.Title, i.Completed} == proposed;

if (!s.Items.Any(IsEquivalent))
yield return Make<Event.Updated>(c.Id, c.Props);
break;
case Delete c:
if (s.Items.Any(i => i.Id == c.Id))
yield return new Event.Deleted {Id = c.Id};
break;
case Clear _:
if (s.Items.Any()) yield return new Event.Cleared {NextId = s.NextId};
break;

default:
throw new ArgumentOutOfRangeException(nameof(x), x, "invalid");
}
&& new { i.Order, i.Title, i.Completed } == proposed;

T Make<T>(int id, Props value) where T : Event.ItemEvent, new() =>
new T {Data = {Id = id, Order = value.Order, Title = value.Title, Completed = value.Completed}};
}
if (s.Items.Any(IsEquivalent))
return Enumerable.Empty<Event>();
return new[] { Make<Event.Updated>(c.Id, c.Props) };
},
(Delete c) => s.Items.Any(i => i.Id == c.Id) ? new Event[] { new Event.Deleted { Id = c.Id } } : new Event[0],
(Clear _) => s.Items.Any() ? new Event[] { new Event.Cleared { NextId = s.NextId } } : new Event[0]);

static T Make<T>(int id, Props value) where T : Event.ItemEvent, new() =>
new T { Data = { Id = id, Order = value.Order, Title = value.Title, Completed = value.Completed } };
}

/// Defines low level stream operations relevant to the Todo Stream in terms of Command and Events
Expand Down
Empty file modified equinox-web-csharp/Web/Startup.cs
100755 → 100644
Empty file.