Skip to content

Commit

Permalink
Add TodoBackend initial impl
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Dec 10, 2018
1 parent 8bfd61e commit 4edc86c
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 18 deletions.
37 changes: 23 additions & 14 deletions Equinox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Codec", "src\Equino
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Equinox.Cli", "cli\Equinox.Cli\Equinox.Cli.fsproj", "{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TodoBackend", "samples\TodoBackend\TodoBackend.fsproj", "{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples.Todo", "Samples.Todo", "{CEFE6686-FD35-445D-975A-D622BF009B0B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -60,18 +64,26 @@ Global
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37B4A45F-039E-4515-8A84-D242DDE12D22}.Release|Any CPU.Build.0 = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.Build.0 = Release|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Debug|Any CPU.Build.0 = Debug|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Release|Any CPU.ActiveCfg = Release|Any CPU
{406A280E-0708-4B12-8443-8FD5660CD271}.Release|Any CPU.Build.0 = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D98B603-2EBB-40C2-AFE5-083C92EFB822}.Release|Any CPU.Build.0 = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.Build.0 = Release|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92}.Release|Any CPU.Build.0 = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.Build.0 = Release|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92B4ACC9-7F30-4727-A4B6-0B6903D0AA08}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -96,25 +108,22 @@ Global
{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
{C8992C1C-6DC5-42CD-A3D7-1C5663433FED}.Release|Any CPU.Build.0 = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B0D4568-96FD-4083-8520-CD537C0B2FF0}.Release|Any CPU.Build.0 = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACE52D04-2FE3-4FD6-A066-9C81429C3997}.Release|Any CPU.Build.0 = Release|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{37B4A45F-039E-4515-8A84-D242DDE12D22} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{0D98B603-2EBB-40C2-AFE5-083C92EFB822} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{406A280E-0708-4B12-8443-8FD5660CD271} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{0D98B603-2EBB-40C2-AFE5-083C92EFB822} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{ACE52D04-2FE3-4FD6-A066-9C81429C3997} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{0B2D5815-D6A5-4AAC-9B75-D57B165E2A92} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{1B0D4568-96FD-4083-8520-CD537C0B2FF0} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{ACE52D04-2FE3-4FD6-A066-9C81429C3997} = {D67D5A5F-2E59-4514-A997-FEBDC467AAF6}
{EC2EC658-3D85-44F3-AD2F-52AFCAFF8871} = {CEFE6686-FD35-445D-975A-D622BF009B0B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {177E1E7B-E275-4FC6-AE3C-2C651ECCF71E}
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,38 @@ Run, including running the tests that assume you've got a local EventStore and p

./build -se

# SAMPLES

The `samples/` folder contains various examples, with the complementary goals of:

- being a starting point for users to see how one might consume the libraries.
- acting as [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) to validate and pin API designs.
- providing rough, but not official guidance as to things that are valid to do in an application consuming Equinox components.
- to validate that each specific Storage implementation can fulfill the needs of each of the example Services/Aggregates/Applications. (_unfortunately this concern makes a lot of the DI wiring more complex than a real application should be; it's definitely not a goal for every Equinox app to be able to switch between backends, even though that's very much possible to achieve._)

## [TODOBACKEND, see samples/TodoBackend](/samples/TodoBackend)

The repo contains a vanilla ASP.NET Core 2.1 implemention of [the well-known TodoBackend Spec](https://www.todobackend.com). **NB the implementation is largely dictated by spec; no architectural guidance expressed or implied ;)**. It can be run via:

& dotnet run -f netcoreapp2.1 -p samples/TodoBackend -S es # run against eventstore, omit `es` to use in-memory store, or see PROVISIONING EVENTSTORE, below
start https://www.todobackend.com/specs/index.html?https://localhost:5001/todos # for low-level debugging / validation of hosting arrangements
start https://www.todobackend.com/client/index.html?https://localhost:5001/todos # Actual UI
start http://localhost:5341/#/events # see logs triggered by `-S` above in https://getseq.net

## [STORE, see /samples/Store](/samples/Store)

The core sample in this repo is the `Store` sample, which contains code and tests extracted from real implementations (with minor simplifications in some cases).

These facts mean that:

- some of the code may be less than approachable for a beginner (e.g. some of the code is written is in its present form for reasons of efficiency)
- some of the code may not represent official best practice guidance that the authors would necessarily stand over (e.g., the CQRS pattern is not strictly adhered to in all circumstances; some command designs are not completely correct from the point of view of making sense from an idempotency perspective)

While these things can of course be perfected through PRs, this is definitely not top of the TODO list for the purposes of this repo. (We'd be happy to link to other samples, including cleanups / rewrites of these samples written with different testing platforms, web platforms, or DDD/CQRS/ES design flavors).

# BENCHMARKS

A key facility of this repo is beoing able to run load tests, either in process against a nominated store, or via HTTP to a nominated Web app. The following tests are implemented at present:
A key facility of this repo is being able to run load tests, either in process against a nominated store, or via HTTP to a nominated Web app. The following tests are implemented at present:

- `Favorite` - Simulate a very enthusiastic user that favorites things once per Second - triggering an ever-growing state which can only work efficiently if you:
- apply a snapshotting scheme (although being unbounded, it will eventually hit the store's limits - 4MB/event for EventStore, 3MB/document for CosmosDb)
Expand Down
3 changes: 0 additions & 3 deletions samples/Store/Web/Web.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
<PackageReference Include="Argu" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
59 changes: 59 additions & 0 deletions samples/TodoBackend/Controllers/TodosController.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace TodoBackend.Controllers

open Microsoft.AspNetCore.Mvc
open TodoBackend

type TodoView =
{ id: int
url: string
order: int; title: string; completed: bool }

type GetByIdArgsTemplate = { id: int }

// Fulfuls contract dictated by https://www.todobackend.com/specs/index.html?https://localhost:5001/todos
// To run, start:
// & dotnet run -f netcoreapp2.1 -p samples/TodoBackend
// https://www.todobackend.com/client/index.html?https://localhost:5001/todos
// Stolen from https://github.com/ChristianAlexander/dotnetcore-todo-webapi/blob/master/src/TodoWebApi/Controllers/TodosController.cs
// even more stolen from https://github.com/joeaudette/playground/blob/master/spa-stack/src/FSharp.WebLib/Controllers.fs
[<Route "[controller]">]
type TodosController(service: Service) =
inherit Controller()

let toModel (value : TodoView) : Todo = { id = value.id; order = value.order; title = value.title; completed = value.completed }

member private __.WithUri(x : Todo) : TodoView =
let url = __.Url.RouteUrl("GetTodo", { id=x.id }, __.Request.Scheme) // Supplying scheme is secret sauce for making it absolute as required by client
{ id = x.id; url = url; order = x.order; title = x.title; completed = x.completed }

[<HttpGet>]
member __.Get() : Async<TodoView seq> = async {
let! xs = service.List
return seq { for x in xs -> __.WithUri(x) }
}

[<HttpGet("{id}", Name="GetTodo")>]
member __.Get id : Async<IActionResult> = async {
let! x = service.TryGet id
return match x with None -> __.NotFound() :> _ | Some x -> ObjectResult(__.WithUri x) :> _
}

[<HttpPost>]
member __.Post([<FromBody>]value : TodoView) : Async<TodoView> = async {
let! created = service.Create(toModel value)
return __.WithUri created
}

[<HttpPatch "{id}">]
member __.Patch( id, [<FromBody>]value : TodoView) : Async<TodoView> = async {
let! updated = service.Patch { toModel value with id = id }
return __.WithUri updated
}

[<HttpDelete "{id}">]
member __.Delete id : Async<unit> =
service.Execute <| Delete id

[<HttpDelete>]
member __.DeleteAll(): Async<unit> =
service.Execute <| Clear
39 changes: 39 additions & 0 deletions samples/TodoBackend/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace TodoBackend

open Argu
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.DependencyInjection
open Serilog

module Program =
let createWebHostBuilder (args,parsed) : IWebHostBuilder =
WebHost
.CreateDefaultBuilder(args)
.ConfigureServices(fun services -> Startup.ConfigureServices(services, parsed))
.Configure(fun app -> Startup.Configure(app, app.ApplicationServices.GetService<IHostingEnvironment>()))
.UseSerilog()

[<EntryPoint>]
let main argv =
try
let programName = System.Reflection.Assembly.GetEntryAssembly().GetName().Name
let args = ArgumentParser.Create<Arguments>(programName = programName).ParseCommandLine(argv)
// Replace logger chain with https://github.com/serilog/serilog-aspnetcore
let c =
LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console()
let c =
let maybeSeq = if args.Contains LocalSeq then Some "http://localhost:5341" else None
match maybeSeq with None -> c | Some endpoint -> c.WriteTo.Seq(endpoint)
let log : ILogger = c.CreateLogger() :> _
Log.Logger <- log
log.Information("Running with Process Id: {pid}", System.Diagnostics.Process.GetCurrentProcess().Id)
createWebHostBuilder(argv, args).Build().Run()
0
with e ->
eprintfn "%s" e.Message
1
78 changes: 78 additions & 0 deletions samples/TodoBackend/Startup.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace TodoBackend

open Argu
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.DependencyInjection
open Samples.Infrastructure.Services
open Samples.Infrastructure.Storage
open Serilog
open Serilog.Events
open System

[<NoComparison>]
type Arguments =
| [<AltCommandLine("-vc")>] VerboseConsole
| [<AltCommandLine("-S")>] LocalSeq
| [<AltCommandLine("-C")>] Cached
| [<AltCommandLine("-U")>] Unfolds
| [<CliPrefix(CliPrefix.None); Last; Unique>] Memory of ParseResults<MemArguments>
| [<CliPrefix(CliPrefix.None); Last; Unique>] Es of ParseResults<EsArguments>
interface IArgParserTemplate with
member a.Usage = a |> function
| VerboseConsole -> "Include low level Domain and Store logging in screen output."
| LocalSeq -> "configures writing to a local Seq endpoint at http://localhost:5341, see https://getseq.net"
| Cached -> "employ a 50MB cache."
| Unfolds -> "employ a store-appropriate Rolling Snapshots and/or Unfolding strategy."
| Memory _ -> "specify In-Memory Volatile Store (Default store)."
| Es _ -> "specify storage in EventStore (--help for options)."

type App = class end

// :shame: This should be a class used via UseStartup, but I couldnt figure out how to pass the parsed args in as MS have changed stuff around too much to make it googleable within my boredom threshold
type Startup() =
// This method gets called by the runtime. Use this method to add services to the container.
static member ConfigureServices(services: IServiceCollection, args: ParseResults<Arguments>) : unit =
services.AddCors().AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1) |> ignore

let verboseConsole = args.Contains VerboseConsole
let maybeSeq = if args.Contains LocalSeq then Some "http://localhost:5341" else None
let createStoreLog verboseStore =
let c = LoggerConfiguration().Destructure.FSharpTypes()
let c = if verboseStore then c.MinimumLevel.Debug() else c
let c = c.WriteTo.Console((if verboseStore && verboseConsole then LogEventLevel.Debug else LogEventLevel.Warning), theme = Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code)
let c = match maybeSeq with None -> c | Some endpoint -> c.WriteTo.Seq(endpoint)
c.CreateLogger() :> ILogger

let storeConfig, storeLog : StorageConfig * ILogger =
let options = args.GetResults Cached @ args.GetResults Unfolds
let cache, unfolds = options |> List.contains Cached, options |> List.contains Unfolds
let log = Log.ForContext<App>()

match args.TryGetSubCommand() with
| Some (Es sargs) ->
let storeLog = createStoreLog <| sargs.Contains EsArguments.VerboseStore
log.Information("EventStore Storage options: {options:l}", options)
EventStore.config (log,storeLog) (cache, unfolds) sargs, storeLog
| _ | Some (Memory _) ->
log.Fatal("Web App is using Volatile Store; Storage options: {options:l}", options)
MemoryStore.config (), log

let regF (factory : IServiceProvider -> 'T) = services.AddSingleton<'T>(fun (sp: IServiceProvider) -> factory sp) |> ignore

let resolver = StreamResolver(storeConfig)

regF <| fun _sp ->
let codec = genCodec<Events.Event>()
let fold, initial, snapshot = Folds.fold, Folds.initial, Folds.snapshot
Service(storeLog, resolver.Resolve(codec,fold,initial,snapshot))

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
static member Configure(app: IApplicationBuilder, env: IHostingEnvironment) : unit =
if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
else app.UseHsts() |> ignore

app.UseHttpsRedirection()
.UseCors(fun x -> x.WithOrigins([|"https://www.todobackend.com"|]).AllowAnyHeader().AllowAnyMethod() |> ignore)
.UseMvc() |> ignore
25 changes: 25 additions & 0 deletions samples/TodoBackend/TodoBackend.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="TodosService.fs" />
<Compile Include="Startup.fs" />
<Compile Include="Controllers\TodosController.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Store\Infrastructure\Infrastructure.fsproj" />
<ProjectReference Include="..\..\src\Equinox.MemoryStore\Equinox.MemoryStore.fsproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Argu" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
</ItemGroup>

</Project>

0 comments on commit 4edc86c

Please sign in to comment.