Home

Awesome

Jet dotnet new Templates Build Status release NuGet license code size <img src="https://img.shields.io/badge/discord-DDD--CQRS--ES%20%23equinox-yellow.svg?logo=discord">

This repo hosts the source for Jet's dotnet new templates.

Equinox only

These templates focus solely on Consistent Processing using Equinox Stores:

Propulsion related

The following templates focus specifically on the usage of Propulsion components:

Producer/Reactor Templates combining usage of Equinox and Propulsion

The bulk of the remaining templates have a consumer aspect, and hence involve usage of Propulsion. The specific behaviors carried out in reaction to incoming events often use `Equinox components

<a name="proReactor"></a>

<a name="eqxShipping"></a>

<a name="proHotel"></a>

Walkthrough

As dictated by the design of dotnet's templating mechanism, consumption is ultimately via the .NET Core SDK's dotnet new CLI facility and/or associated facilities in Visual Studio, Rider etc.

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 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 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

# ... to add an Ingester that reacts to events, as they are written (via EventStore $all or CosmosDB ChangeFeedProcessor) summarising them and feeding them into a secondary stream
# (equivalent to pairing the Projector and Ingester programs we make below)
md -p ../DirectIngester | Set-Location
dotnet new proReactor

# ... to add a Projector
md -p ../Projector | Set-Location
# (-k emits to Kafka and hence implies having a Consumer)
dotnet new proProjector -k
start README.md

# ... to add a Generic Consumer (proProjector -k emits to Kafka and hence implies having a Consumer)
md -p ../Consumer | Set-Location
dotnet new proConsumer
start README.md

# ... to add an Ingester based on the events that Projector sends to kafka
# (equivalent in function to DirectIngester, above)
md -p ../Ingester | Set-Location
dotnet new proReactor --source kafkaEventSpans

# ... to add a Summary Projector
md -p ../SummaryProducer | Set-Location
dotnet new proReactor --kafka 
start README.md

# ... to add a Custom Projector
md -p ../SummaryProducer | Set-Location
dotnet new proReactor --kafka --blank
start README.md

# ... to add a Summary Consumer (ingesting output from `SummaryProducer`)
md -p ../SummaryConsumer | Set-Location
dotnet new summaryConsumer
start README.md

# ... to add a Testbed
md -p ../My.Tools.Testbed | Set-Location
# -e -c # add EventStore and CosmosDb suppport to got with the default support for MemoryStore
dotnet new eqxtestbed -c -e
start README.md
# run for 1 min with 10000 rps against an in-memory store
dotnet run -p Testbed -- run -d 1 -f 10000 memory
# run for 30 mins with 2000 rps against a local EventStore
dotnet run -p Testbed -- run -f 2000 es
# run for two minutes against CosmosDb (see https://github.com/jet/equinox#quickstart) for provisioning instructions
dotnet run -p Testbed -- run -d 2 cosmos

# ... to add a Sync tool
md -p ../My.Tools.Sync | Set-Location
# (-m includes an example of how to upconvert from similar event-sourced representations in an existing store)
dotnet new proSync -m
start README.md

# ... to add a Shipping Domain example containing a Process Manager with a Watchdog Service
md -p ../Shipping | Set-Location
dotnet new eqxShipping

# ... to add a Reactor against a Cosmos container for both listening and writing
md -p ../Indexer | Set-Location
dotnet new proIndexer

# ... to add a Hotel Sample for use with MessageDb or DynamoDb
md -p ../ProHotel | Set-Location
dotnet new proHotel

TESTING

There's integration tests in the repo that check everything compiles before we merge/release

dotnet build build.proj # build Equinox.Templates package, run tests \/
dotnet pack build.proj # build Equinox.Templates package only
dotnet test build.proj -c Release # Test aphabetically newest file in bin/nupkgs only (-c Release to run full tests)

One can also do it manually:

  1. Generate the package (per set of changes you make locally)

    a. ensuring the template's base code compiles (see runnable templates concept in dotnet new docs)

    b. packaging into a local nupkg

     $ cd ~/dotnet-templates
     $ dotnet pack build.proj
     Successfully created package '/Users/me/dotnet-templates/bin/nupkg/Equinox.Templates.3.10.1-alpha.0.1.nupkg'.
    
  2. Test, per variant

    (Best to do this in another command prompt in a scratch area)

    a. installing the templates into the dotnet new local repo

     $ dotnet new -i /Users/me/dotnet-templates/bin/nupkg/Equinox.Templates.3.10.1-alpha.0.1.nupkg
    

    b. get to an empty scratch area

     $ mkdir -p ~/scratch/templs/t1
     $ cd ~/scratch/templs/t1
    

    c. test a variant (i.e. per symbol in the config)

     $ dotnet new proReactor -k # an example - in general you only need to test stuff you're actually changing
     $ dotnet build # test it compiles
     $ # REPEAT N TIMES FOR COMBINATIONS OF SYMBOLS
    
  3. uninstalling the locally built templates from step 2a:

    $ dotnet new -u Equinox.Templates

PATTERNS / GUIDANCE

Use Strongly typed ids

Wherever possible, the samples strongly type identifiers, particularly ones that might naturally be represented as primitives, i.e. string etc.

Managing Projections and Reactions with Equinox, Propulsion and FsKafka

<a name="aggregate-module"></a>

Aggregate module conventions

There are established conventions documented in Equinox's module Aggregate overview

<a name="programfs"></a>

Microservice Program.fs conventions

All the templates herein attempt to adhere to a consistent structure for the composition root module (the one containing an Application’s main), consisting of the following common elements:

type Configuration

Responsible for: Loading secrets and custom configuration, supplying defaults when environment variables are not set

Wiring up retrieval of configuration values is the most environment-dependent aspect of the wiring up of an application's interaction with its environment and/or data storage mechanisms. This is particularly relevant where there is variance between local (development time), testing and production deployments. For this reason, the retrieval of values from configuration stores or key vaults is not managed directly within the module Args section

The Configuration type is responsible for encapsulating all bindings to Configuration or Secret stores (Vaults) in order that this does not have to be complected with the argument parsing or defaulting in module Args

module Args

Responsible for: mapping Environment Variables and the Command Line argv to an Arguments model

module Args fulfils three roles:

  1. uses Argu to map the inputs passed via argv to values per argument, providing good error and/or help messages in the case of invalid inputs
  2. responsible for managing all defaulting of input values including echoing them to console such that an operator can infer the arguments in force without having to go look up defaults in a source control repo
  3. expose an object model that the build or start functions can use to succinctly wire up the dependencies without needing to touch Argu, Configuration, or any concrete Configuration or Secrets storage mechanisms

NOTE: there's a medium term plan to submit a PR to Argu extending it to be able to fall back to environment variables where a value is not supplied, by means of declarative attributes on the Argument specification in the DU, including having the --help message automatically include a reference to the name of the environment variable that one can supply the value through

type Logging

Responsible for applying logging config and setting up loggers for the application

example

type Logging() =

    [<Extension>]
    static member Configure(configuration : LoggingConfiguration, ?verbose) =
        configuration
            .Enrich.FromLogContext()
        |> fun c -> if verbose = Some true then c.MinimumLevel.Debug() else c
        // etc.

start function

The start function contains the specific wireup relevant to the infrastructure requirements of the microservice - it's the sole aspect that is not expected to adhere to a standard layout as prescribed in this section.

example

let start (args : Args.Arguments) =
    …
    (yields a started application loop)

run, main function

The run function formalizes the overall pattern. It is responsible for:

  1. Managing the correct sequencing of the startup procedure, weaving together the above elements
  2. managing the emission of startup or abnormal termination messages to the console

example

let run args = async {
    use consumer = start args
    return! consumer.AwaitWithStopOnCancellation()
}

[<EntryPoint>]
let main argv =
    try let args = Args.parse EnvVar.tryGet argv
        try Log.Logger <- LoggerConfiguration().Configure(verbose=args.Verbose).CreateLogger()
            try run args |> Async.RunSynchronously; 0
            with e when not (e :? System.Threading.Tasks.TaskCanceledException) -> Log.Fatal(e, "Exiting"); 2
        finally Log.CloseAndFlush()
    with :? Argu.ArguParseException as e -> eprintfn "%s" e.Message; 1
        | e -> eprintf "Exception %s" e.Message; 1

CONTRIBUTING

Please don't hesitate to create a GitHub issue for any questions, so others can benefit from the discussion. For any significant planned changes or additions, please err on the side of reaching out early so we can align expectations - there's nothing more frustrating than having your hard work not yielding a mutually agreeable result ;)

See the Equinox repo's CONTRIBUTING section for general guidelines wrt how contributions are considered specifically wrt Equinox.

The following sorts of things are top of the list for the templates:

While there is no rigid or defined limit to what makes sense to add, it should be borne in mind that dotnet new eqx/pro* is sometimes 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
  3. encouraging good design practices

In other words, there's lots of subtlety to what should and shouldn't go into a template - so discussing changes before investing time is encouraged; agreed changes will generally be rolled out across the repo.