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

Port EquinoxWeb template to C# #5

Merged
merged 17 commits into from
Jan 17, 2019
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Jet `dotnet new` Templates

This repo hosts the source for Jet's [`dotnet new`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new) templates. While there's presently just one, for [Equinox](https://github.com/jet/equinox), over time the intention is to add templates for other systems where relevant.
This repo hosts the source for Jet's [`dotnet new`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new) templates. While that's presently just for [Equinox](https://github.com/jet/equinox), over time the intention is to add templates for other systems where relevant.

## Available Templates

- [`equinox-web`](equinox-web/readme.md) - Boilerplate for an ASP .NET Core Web App, with an associated storage-independent Backend project. At present, only F# has been implemented.
- [`eqxweb`](equinox-web/readme.md) - Boilerplate for an ASP .NET Core Web App, with an associated storage-independent Domain project.
- [`eqxwebcs`](equinox-web-csharp/readme.md) - Boilerplate for an ASP .NET Core Web App, with an associated storage-independent Domain project _ported to C#_.

## How to use

Expand All @@ -14,13 +15,16 @@ To use from the command line, the outline is:
1. Install a template locally (use `dotnet new --list` to view your current list)
2. Use `dotnet new` to expand the template in a given directory

# install the templates into `dotnet new`s list of avaiable templates so it can be picked up by
# install the templates into `dotnet new`s list of available templates so it can be picked up by
# `dotnet new`, Rider, Visual Studio etc.
dotnet new -i Equinox.Templates

# --help shows the options including wiring for storage subsystems,
# -t includes an example Controller and Service to test the storage subsystem)
dotnet new equinoxweb -t --help
# -t includes an example Domain, Handler, Service and Controller to test from app to storage subsystem
dotnet new eqxweb -t --help

# if you want to see a C# equivalent:
dotnet new eqxwebcs -t

# see readme.md in the generated code for further instructions regarding the TodoBackend the above -t switch above triggers the inclusion of
start readme.md
Expand All @@ -33,14 +37,14 @@ Please don't hesitate to [create a GitHub issue](https://github.com/jet/dotnet-t

See [the Equinox repo's CONTRIBUTING section](https://github.com/jet/equinox/blob/master/README.md#contributing) for general guidelines wrt how contributions are considered specifically wrt Equinox.

The following sorts of things are top of the list for the `equinox-web` template at the present time:
The following sorts of things are top of the list for the `eqxweb*` templates at the present time:

- Fixes for typos, adding of info to the readme or comments in the emitted code etc
- Small-scale cleanup or clarifications of the emitted code
- support for additional .NET languages in the templates
- support for additional languages in the templates
- further straightforward starter projects

While there is no rigid or defined limit to what makes sense to add, it should be borne in mind that `dotnet new equinoxweb` is often going to be a new user's first interaction with Equinox. Hence there's a delicate (and intrinsically subjective) balance to be struck between:
While there is no rigid or defined limit to what makes sense to add, it should be borne in mind that `dotnet new eqxweb` is often going to be a new user's first interaction with Equinox and/or [asp]dotnetcore. Hence there's a delicate (and intrinsically subjective) balance to be struck between:

1. simplicity of programming techniques used / beginner friendliness
2. brevity of the generated code
Expand Down
91 changes: 91 additions & 0 deletions equinox-web-csharp/.template.config/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "@jet @bartelink",
"classifications": [
"Web",
"Equinox",
"Event Sourcing"
],
"tags": {
"language": "C#"
},
"identity": "Equinox.Template.CSharp",
"name": "Equinox Web App",
"shortName": "eqxwebcs",
"sourceName": "TodoBackendTemplate",
"preferNameDirectory": true,

"symbols": {
"aggregate": {
"type": "parameter",
"datatype": "bool",
"isRequired": false,
"defaultValue": "true",
"description": "Generate an example Aggregate."
},
"todos": {
"type": "parameter",
"datatype": "bool",
"isRequired": false,
"defaultValue": "false",
"description": "Generate an example TodosController in your project, together with associated Domain logic."
},
"memoryStore": {
"type": "parameter",
"dataType": "bool",
"defaultValue": "true",
"description": "'Store' Events in an In-Memory volatile store (for test purposes only.)"
},
"eventStore": {
"type": "parameter",
"dataType": "bool",
"defaultValue": "false",
"description": "Store Events in an EventStore Cluster (see https://eventstore.org)"
},
"cosmos": {
"type": "parameter",
"dataType": "bool",
"defaultValue": "false",
"description": "Store Events in an Azure CosmosDb Account"
}
},
"sources": [
{
"modifiers": [
{
"condition": "(!todos)",
"exclude": [
"*/Controllers/**/*",
"**/Todo.cs",
"**/ClientId.cs",
"README.md"
]
},
{
"condition": "(!aggregate)",
"exclude": [
"**/Aggregate.cs"
]
},
{
"condition": "(!memoryStore)",
"exclude": [
"**/MemoryStoreContext.cs"
]
},
{
"condition": "(!eventStore)",
"exclude": [
"**/EventStoreContext.cs"
]
},
{
"condition": "(!cosmos)",
"exclude": [
"**/CosmosContext.cs"
]
}
]
}
]
}
138 changes: 138 additions & 0 deletions equinox-web-csharp/Domain/Aggregate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Equinox;
using Equinox.Store;
using Microsoft.FSharp.Core;
using Newtonsoft.Json;
using Serilog;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

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 class Happened : Event
{
}

public class Compacted : Event
{
public new bool Happened { get; set; }
}

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

public static Event TryDecode(string et, byte[] json)
{
switch (et)
{
case nameof(Happened): return Codec.Decode<Happened>(json);
case nameof(Compacted): return Codec.Decode<Compacted>(json);
default: return null;
}
}

public static (string, byte[]) Encode(Event e) => (e.GetType().Name, Codec.Encode(e));
}
public class State
{
public bool Happened { get; set; }

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");
}
}

public static State Fold(State origin, IEnumerable<Event> xs)
{
// NB Fold must not mutate the origin
var s = new State(origin.Happened);
foreach (var x in xs)
Evolve(s, x);
return s;
}

public static bool IsOrigin(Event e) => e is Event.Compacted;

public static Event Compact(State s) => new Event.Compacted {Happened = s.Happened};
}

/// 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 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");
}
}
}

class Handler
{
readonly EquinoxHandler<Event, State> _inner;

public Handler(ILogger log, IStream<Event, State> stream) =>
_inner = new EquinoxHandler<Event, State>(State.Fold, log, stream);

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

/// 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);
}

public class View
{
public bool Sorted { get; set; }
}

public class Service
{
/// Maps a ClientId to Handler for the relevant stream
readonly Func<string, Handler> _stream;

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

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

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

/// 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);

static View Render(State s) => new View() {Sorted = s.Happened};
}
}
}
35 changes: 35 additions & 0 deletions equinox-web-csharp/Domain/ClientId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;

namespace TodoBackendTemplate
{
/// ClientId strongly typed id
// To support model binding using aspnetcore 2 FromHeader
[TypeConverter(typeof(ClientIdStringConverter))]
public class ClientId
{
private ClientId(Guid value) => Value = value;

[IgnoreDataMember] // Prevent Swashbuckle inferring there is a Value property
public Guid Value { get; }

// TOCONSIDER - happy for this to become a ctor and ClientIdStringConverter to be removed if it just works correctly as-is when a header is supplied
public static ClientId Parse(string input) => new ClientId(Guid.Parse(input));

public override string ToString() => Value.ToString("N");
}

class ClientIdStringConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) =>
value is string s ? ClientId.Parse(s) : base.ConvertFrom(context, culture, value);

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) =>
base.ConvertTo(context, culture, value, destinationType);
}
}
14 changes: 14 additions & 0 deletions equinox-web-csharp/Domain/Domain.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Equinox" Version="1.0.3" />
<PackageReference Include="FSharp.Core" Version="4.5.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup>

</Project>
Loading