Skip to content

Commit

Permalink
Complete porting of Todo Service
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Jan 4, 2019
1 parent 310f565 commit 15a2d27
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 71 deletions.
2 changes: 1 addition & 1 deletion equinox-web-csharp/Domain/Aggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public Handler(ILogger log, IStream<IEvent, State> stream)

/// Execute `command`, syncing any events decided upon
public Task<Unit> Execute(ICommand c) =>
_inner.Decide(ctx =>
_inner.Execute(ctx =>
ctx.Execute(s => Commands.Interpret(s, c)));

/// Establish the present state of the Stream, project from that as specified by `projection`
Expand Down
2 changes: 1 addition & 1 deletion equinox-web-csharp/Domain/Infrastructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public EquinoxHandler
}

// Run the decision method, letting it decide whether or not the Command's intent should manifest as Events
public async Task<Unit> Decide(Action<Context<TEvent, TState>> decide) =>
public async Task<Unit> Execute(Action<Context<TEvent, TState>> decide) =>
await FSharpAsync.StartAsTask(Decide(FuncConvert.ToFSharpFunc(decide)), null, null);

// Execute a command, as Decide(Action) does, but also yield an outcome from the decision
Expand Down
131 changes: 63 additions & 68 deletions equinox-web-csharp/Domain/Todo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,29 @@ public abstract class ItemData
public class Added : ItemData, IEvent
{
}

public class Updated : ItemData, IEvent
{
}

public class Deleted: IEvent
public class Deleted : IEvent
{
public int Id { get; set; }
}

public class Cleared: IEvent
public class Cleared : IEvent
{
public int NextId { get; set; }
}
public class Compacted: IEvent

public class Compacted : IEvent
{
public int NextId { get; set; }
public ItemData[] Items { get; set; }
}

private static readonly JsonNetUtf8Codec Codec = new JsonNetUtf8Codec(new JsonSerializerSettings());

public static IEvent TryDecode(string et, byte[] json)
{
switch (et)
Expand All @@ -69,8 +69,8 @@ public static IEvent TryDecode(string et, byte[] json)
default: return null;
}
}
public static Tuple<string,byte[]> Encode(IEvent x)

public static Tuple<string, byte[]> Encode(IEvent x)
{
switch (x)
{
Expand Down Expand Up @@ -110,7 +110,7 @@ public static State Fold(State origin, IEnumerable<IEvent> xs)
{
var nextId = origin.NextId;
var items = origin.Items.ToList();
foreach (var x in xs)
foreach (var x in xs)
switch (x)
{
case Events.Added e:
Expand Down Expand Up @@ -147,7 +147,7 @@ public class Props
public string Title { get; set; }
public bool Completed { get; set; }
}

/// Defines the operations a caller can perform on a Todo List
public interface ICommand
{
Expand Down Expand Up @@ -189,9 +189,11 @@ public static IEnumerable<IEvent> Interpret(State s, ICommand x)
break;
case Update c:
var proposed = Tuple.Create(c.Props.Order, c.Props.Title, c.Props.Completed);

bool IsEquivalent(Events.ItemData i) =>
i.Id == c.Id
&& Tuple.Create(i.Order, i.Title, i.Completed).Equals(proposed);

if (!s.Items.Any(IsEquivalent))
yield return Make<Events.Updated>(c.Id, c.Props);
break;
Expand Down Expand Up @@ -224,7 +226,7 @@ public Handler(ILogger log, IStream<IEvent, State> stream)

/// Execute `command`; does not emit the post state
public Task<Unit> Execute(ICommand c) =>
_inner.Decide(ctx =>
_inner.Execute(ctx =>
ctx.Execute(s => Commands.Interpret(s, c)));

/// Handle `command`, return the items after the command's intent has been applied to the stream
Expand All @@ -234,85 +236,78 @@ public Task<Unit> Execute(ICommand c) =>
ctx.Execute(s => Commands.Interpret(s, c));
return ctx.State.Items;
});

/// Establish the present state of the Stream, project from that as specified by `projection`
public Task<T> Query<T>(Func<State, T> projection) =>
_inner.Query(projection);
}

/// A single Item in the Todo List
public class View
{
public bool Sorted { get; set; }
public int Id { get; set; }
public int Order { get; set; }
public string Title { get; set; }
public bool Completed { get; set; }
}

/// Defines operations that a Controller can perform on a Todo List
public class Service
{
/// Maps a ClientId to Handler for the relevant stream
readonly Func<string, Handler> _stream;
readonly Func<ClientId, Handler> _stream;

public Service(ILogger handlerLog, Func<Target, IStream<IEvent, State>> resolve) =>
_stream = id => new Handler(handlerLog, resolve(CategoryId(id)));

static Target CategoryId(string id) => Target.NewCatId("Todo", id);

static View Render(State s) => new View() {Sorted = s.Happened};

/// Read the present state
// TOCONSIDER: you should probably be separating this out per CQRS and reading from a denormalized/cached set of projections
public Task<View> Read(string id) => _stream(id).Query(Render);

/// Execute the specified command
public Task<Unit> Execute(string id, ICommand command) =>
_stream(id).Execute(command);
}
}
}


//
// READ
//

/// List all open items
public Task<IEnumerable<View>> List(ClientId clientId) =>
_stream(clientId).Query(s => s.Items.Select(Render));

/// A single Item in the Todo List
type View = { id: int; order: int; title: string; completed: bool }

/// Defines operations that a Controller can perform on a Todo List
type Service(handlerLog, resolve) =

/// Maps a ClientId to the CatId that specifies the Stream in whehch the data for that client will be held
let (|CategoryId|) (clientId: ClientId) = Equinox.CatId("Todos", if obj.ReferenceEquals(clientId,null) then "1" else clientId.Value)

/// Maps a ClientId to Handler for the relevant stream
let (|Stream|) (CategoryId catId) = Handler(handlerLog, resolve catId)

let render (item: Events.ItemData) : View =
{ id = item.id
order = item.order
title = item.title
completed = item.completed }

(* READ *)
/// Load details for a single specific item
public Task<View> TryGet(ClientId clientId, int id) =>
_stream(clientId).Query(s =>
{
var i = s.Items.SingleOrDefault(x => x.Id == id);
return i == null ? null : Render(i);
});

/// List all open items
member __.List(Stream stream) : Async<View seq> =
stream.Query (fun x -> seq { for x in x.items -> render x })
//
// WRITE
//

/// Load details for a single specific item
member __.TryGet(Stream stream, id) : Async<View option> =
stream.Query (fun x -> x.items |> List.tryFind (fun x -> x.id = id) |> Option.map render)
/// Execute the specified (blind write) command
public Task<Unit> Execute(ClientId clientId, ICommand command) =>
_stream(clientId).Execute(command);

(* WRITE *)
//
// WRITE-READ
//

/// Execute the specified (blind write) command
member __.Execute(Stream stream, command) : Async<unit> =
stream.Execute command
/// Create a new ToDo List item; response contains the generated `id`
public async Task<View> Create(ClientId clientId, Props template)
{
var state = await _stream(clientId).Decide(new Commands.Add {Props = template});
return Render(state.First());
}

(* WRITE-READ *)
/// Update the specified item as referenced by the `item.id`
public async Task<View> Patch(ClientId clientId, int id, Props value)
{
var state = await _stream(clientId).Decide(new Commands.Update {Id = id, Props = value});
return Render(state.Single(x => x.Id == id));
}

/// Create a new ToDo List item; response contains the generated `id`
member __.Create(Stream stream, template: Props) : Async<View> = async {
let! state' = stream.Handle(Commands.Add template)
return List.head state' |> render }
/// Maps a ClientId to the CatId that specifies the Stream in which the data for that client will be held
static Target CategoryId(ClientId id) =>
Target.NewCatId("Todos", id?.ToString() ?? "1");

/// Update the specified item as referenced by the `item.id`
member __.Patch(Stream stream, id: int, value: Props) : Async<View> = async {
let! state' = stream.Handle(Commands.Update (id, value))
return state' |> List.find (fun x -> x.id = id) |> render}
static View Render(Events.ItemData i) =>
new View {Id = i.Id, Order = i.Order, Title = i.Title, Completed = i.Completed};
}
}
}
2 changes: 1 addition & 1 deletion equinox-web/Domain/TodosService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ type View = { id: int; order: int; title: string; completed: bool }
/// Defines operations that a Controller can perform on a Todo List
type Service(handlerLog, resolve) =

/// Maps a ClientId to the CatId that specifies the Stream in whehch the data for that client will be held
/// Maps a ClientId to the CatId that specifies the Stream in which the data for that client will be held
let (|CategoryId|) (clientId: ClientId) = Equinox.CatId("Todos", if obj.ReferenceEquals(clientId,null) then "1" else clientId.Value)

/// Maps a ClientId to Handler for the relevant stream
Expand Down

0 comments on commit 15a2d27

Please sign in to comment.