Skip to content

Commit

Permalink
Id Generation Updated, Now Includes Snowflake (#32)
Browse files Browse the repository at this point in the history
* introduced a Twitter Snowflake-like ID generator dependency

* updated existing (core) ID mechanisms to allow longs and strings

* bug fix; inverted conditional

* allowing for Product ID to be provided or not provided (temp? demoing?)

* network names shortened; was repetitive

* fix; projection wasn't providing name of enum (status)

* updated; draft and draft-with-id changes

* fix, added; configs for Prometheus and Grafana were missing 🙃

* added; Grafana and Prometheus added to docker-compose

* added; health checks for subscriptions

* cleanup; carriage return

* feat; moved logging to its own Infrastructure extension method (Seq)

* enhance; emojis on headlines, because 😎

* 🗺️

* chore; streamlined logging of non-legacy apps

* chore; legacy appsettings tweaks; constr moved to dev
  • Loading branch information
erikshafer authored May 14, 2024
1 parent 767dabf commit 47ad16c
Show file tree
Hide file tree
Showing 31 changed files with 350 additions and 106 deletions.
50 changes: 21 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<samp>TL;DR: A collection of event sourcing use cases in the ecommerce domain that leverage EventStoreDB</samp>


## Table of Contents
## 🗺️ Table of Contents
- [1.0 What is this repository?](#what-is-this-repository)
- [2.0 Technologies, frameworks, and libraries, oh my!](#technologies-frameworks-and-libraries-oh-my)
- [2.1 Polyglot](#polyglot)
Expand Down Expand Up @@ -41,24 +41,24 @@
- [10.0 Maintainer](#maintainer)
- [11.0 License](#license)

## What is this repository?
## 🤔 What is this repository?

This repository's objective to demonstrate how an ecommerce backend can be built using the data storage technique known as event sourcing, along with related concepts frequently employed such as [event-driven architecture (EDA)](https://en.wikipedia.org/wiki/Event-driven_architecture), [Command and Query Responsibility Segregation (CQRS)](https://martinfowler.com/bliki/CQRS.html), and more.

The aim is to provide an assortment of use cases of varying complexity across different technologies. That is to say, examples that are beyond the `Hello World` level that showcase different methodologies and technologies.


## Technologies, frameworks, and libraries, oh my!
## 🧑‍💻 Technologies, frameworks, and libraries, oh my!

As mentioned, moderns tools are leverage to to demonstrate different ways to interact with [EventStoreDB](https://www.eventstore.com/eventstoredb), the event-native database. While it was written from the ground up for [Event Sourcing](https://www.eventstore.com/event-sourcing), there are other interesting uses the database can be used for that this repository may explore in the future.

### Polyglot
### 🔤 Polyglot

An exciting yet perhaps lofty idea is to have this single code repository be the home for different runtimes and programming languages that work in tandem. Where one module (service) is written in C# running in .NET, while another service it communicates with is written in TypeScript running Node.js.

If this proves to be too ambitious or if the community finds it confusing, changes can be made. Such as making different versions of this repository with each featuring a different language and runtime.

### Suggestions
### 📬 Suggestions

Is there a library, framework, or other piece of tech you would like to see here? Simply open an issue, pull request, or contact me directly (see above). I would love to hear more about what you think should be highlighted here.

Expand All @@ -83,31 +83,23 @@ Is there a library, framework, or other piece of tech you would like to see here
- [SQL Server](https://www.microsoft.com/en-us/sql-server/)
- [PostgreSQL](https://www.postgresql.org/)
- [Elasticsearch](https://www.elastic.co/)
- [MongoDB](https://www.mongodb.com/)

### Messaging
- TBD. Current candidates:
- TBD. Current candidates:
- [Kafka](https://kafka.apache.org/)
- Demonstrate how Kafka and ESDB can be great friends!
- [RabbitMQ](https://www.rabbitmq.com/)
- Classic. Stable. Easiest option to get up and running with.

### Other notable dependencies
- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json)
- [FluentValidation](https://github.com/FluentValidation/FluentValidation)
## 📝 Documentation

### Testing
- [xUnit](https://github.com/xunit/xunit)
- [FluentAssertions](https://github.com/fluentassertions/fluentassertions)
- [Shouldly](https://github.com/shouldly/shouldly)
A companion guide is currently in development.

## Documentation

Coming soon.
## 🛣️ Roadmap


## Roadmap

More details coming soon.
Details are being worked out and will be shared soon.

In the meantime, check out how the modules of code are broken up:

Expand Down Expand Up @@ -163,7 +155,7 @@ This early on in development, this is effectively a loose roadmap of what techno
Names, structure, and hierarchy are based on personal experiences and opinions derived from time spent in the ecommerce industry. They do not reflect the inner workings of any specific singular system, team, or organization.


## Compatibility
## 🔨 Compatibility

At this time it is preferred you build the projects on your machine directly, the traditional way.

Expand All @@ -173,7 +165,7 @@ As this time the background services, such as the databases, are ran inside of D

[<img src="https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white">](https://www.docker.com/)

## Installation Requirements
## 🛠️ Installation Requirements

1. Install [.NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. Install [Docker](https://www.docker.com/products/docker-desktop/)
Expand All @@ -185,7 +177,7 @@ As this time the background services, such as the databases, are ran inside of D
</tr>
</table>

## How To Run
## 🚀 How To Run

### Clone the repo

Expand Down Expand Up @@ -235,25 +227,25 @@ Check the [Docker Compose documentation](https://docs.docker.com/compose/intro/f

### Running the API projects

🚧 Work In Progress 👷
Work In Progress 🚧

As more vertical slices and implemented and projects are more fleshed out as a whole.

**TL;DR:** execute `dotnet run` where applicable. If you're a dotnet developer you likely know what to do!


## The Story
## 📖 The Story

An ecommerce company has grown out of its startup phase. It is needing to scale not just the amount of requests and responses it's capable of per second, but make itself capable to adapt to changing trends and shifts in the industry.

Enter event sourcing with EventStoreDB!

⚠️ To be continued ⚠️
To be continued ⚠️


## Resources
## 🏫 Resources

🚧 More to come 👷
More to come 🚧

- Event Store blog and webinars
- [A Beginner's Guide to Event Sourcing](https://www.eventstore.com/event-sourcing)
Expand All @@ -280,13 +272,13 @@ I've been a large fan of [JetBrains](https://www.jetbrains.com/)' suite of Integ
<img src="https://img.shields.io/badge/Rider-000000?style=for-the-badge&logo=Rider&logoColor=white" alt="jetbrains rider">


## Maintainer
## 👷‍♂️ Maintainer

Erik "Faelor" Shafer

blog: www.event-sourcing.dev


## License
## ⚖️ License

[MIT license](./LICENSE).
33 changes: 25 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,31 @@ services:
# "--start-standard-projections"
]
networks:
- ecomm_esdb_network
- esdb_network

zipkin:
image: openzipkin/zipkin
container_name: ecomm_zipkin
ports:
- "9411:9411"

prometheus:
container_name: ecomm_prometheus
image: prom/prometheus:v2.17.1
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

grafana:
container_name: ecomm_grafana
image: grafana/grafana:6.7.2
ports:
- "3000:3000"
volumes:
- ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/prometheus.yaml
- ./grafana/dashboards:/dashboards

seq:
image: datalust/seq:latest
container_name: ecomm_seq
Expand All @@ -48,7 +65,7 @@ services:
- MSSQL_SA_PASSWORD=myStrong_Password123#
- MSSQL_PID=Developer
networks:
- ecomm_sql_network
- sql_network

postgres:
image: postgres:latest
Expand All @@ -58,7 +75,7 @@ services:
environment:
- POSTGRES_PASSWORD=Password123!
networks:
- ecomm_pg_network
- pg_network

pgadmin:
image: dpage/pgadmin4
Expand All @@ -69,7 +86,7 @@ services:
ports:
- "${PGADMIN_PORT:-5050}:80"
networks:
- ecomm_pg_network
- pg_network

mongo:
container_name: ecomm_mongo
Expand Down Expand Up @@ -127,10 +144,10 @@ services:

networks:
default:
name: ecomm_network
ecomm_esdb_network:
name: network
esdb_network:
driver: bridge
ecomm_sql_network:
sql_network:
driver: bridge
ecomm_pg_network:
pg_network:
driver: bridge
12 changes: 12 additions & 0 deletions grafana/__inputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"__inputs": [
{
"name": "ECOMM_PROMETHEUS",
"label": "prometheus",
"description": "Default data source",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
]
}
11 changes: 11 additions & 0 deletions grafana/datasources.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1

datasources:
- name: prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
isDefault: true
version: 1
editable: false
11 changes: 11 additions & 0 deletions prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
global:
scrape_interval: 5s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'products'
static_configs:
- targets:
- 'host.docker.internal:5000'
27 changes: 24 additions & 3 deletions src/Catalog/Catalog.Api/Catalog.Api.http
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
GET {{Inventory.Api_HostAddress}}/swagger/
Accept: application/json

###

# curl -X 'POST'
# 'http://localhost:5252/product/draft-with-id'
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "productId": "36606-001",
# "sku": "36606",
# "name": "Bubbletron",
# "description": "A magical machine that blows out bubbles! Woo!",
# "createdBy": "Erik"
#}'
POST http://localhost:5252/product/draft-with-id
accept: text/plain
Content-Type: application/json

{
"productId": "36606-001",
"sku": "36606",
"name": "Bubbletron",
"description": "A magical machine that blows out bubbles! Woo!",
"createdBy": "Erik"
}

###

Expand All @@ -13,7 +37,6 @@ Accept: application/json
# -H 'accept: text/plain'
# -H 'Content-Type: application/json'
# -d '{
# "productId": "36606-001",
# "sku": "36606",
# "name": "Bubbletron",
# "description": "A magical machine that blows out bubbles! Woo!",
Expand All @@ -24,7 +47,6 @@ accept: text/plain
Content-Type: application/json

{
"productId": "36606-001",
"sku": "36606",
"name": "Bubbletron",
"description": "A magical machine that blows out bubbles! Woo!",
Expand Down Expand Up @@ -143,4 +165,3 @@ GET http://localhost:5252/products/36606-001
accept: text/plain

###

18 changes: 16 additions & 2 deletions src/Catalog/Catalog.Api/Commands/ProductCommandService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Catalog.Products;
using Ecommerce.Core.Identities;
using Eventuous;
using static Catalog.Api.Commands.ProductCommands;

Expand All @@ -10,12 +11,13 @@ public class ProductCommandService : CommandService<Product, ProductState, Produ
public ProductCommandService(
IAggregateStore store,
Services.IsSkuAvailable isSkuAvailable,
Services.IsUserAuthorized isUserAuthorized)
Services.IsUserAuthorized isUserAuthorized,
ISnowflakeIdGenerator idGenerator)
: base(store)
{
// On<InitializeProduct>(); // TODO use new API instead of obsolete versions

OnNewAsync<Draft>(cmd => new ProductId(cmd.ProductId),
OnNewAsync<DraftWithProvidedId>(cmd => new ProductId(cmd.ProductId),
((product, cmd, _) => product.Draft(
cmd.ProductId,
cmd.Sku,
Expand All @@ -26,6 +28,18 @@ public ProductCommandService(
isSkuAvailable,
isUserAuthorized)));

var generatedId = idGenerator.New();
OnNewAsync<Draft>(cmd => new ProductId(generatedId),
((product, cmd, _) => product.Draft(
generatedId,
cmd.Sku,
cmd.Name,
cmd.Description,
DateTimeOffset.Now,
cmd.CreatedBy,
isSkuAvailable,
isUserAuthorized)));

OnExisting<Activate>(cmd => new ProductId(cmd.ProductId),
((product, cmd) => product.Activate(
DateTimeOffset.Now,
Expand Down
9 changes: 8 additions & 1 deletion src/Catalog/Catalog.Api/Commands/ProductCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ namespace Catalog.Api.Commands;

public static class ProductCommands
{
public record Draft(
public record DraftWithProvidedId(
string ProductId,
string Sku,
string Name,
string Description,
string CreatedBy
);

public record Draft(
string Sku,
string Name,
string Description,
string CreatedBy
);

public record Activate(
string ProductId,
string ActivatedBy);
Expand Down
5 changes: 5 additions & 0 deletions src/Catalog/Catalog.Api/HttpApi/CommandApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ namespace Catalog.Api.HttpApi;
[Route("/product")]
public class CommandApi(ICommandService<Product> service) : CommandHttpApiBase<Product>(service)
{
[HttpPost]
[Route("draft-with-id")]
public Task<ActionResult<Result>> Draft([FromBody] DraftWithProvidedId cmd, CancellationToken ct)
=> Handle(cmd, ct);

[HttpPost]
[Route("draft")]
public Task<ActionResult<Result>> Draft([FromBody] Draft cmd, CancellationToken ct)
Expand Down
Loading

0 comments on commit 47ad16c

Please sign in to comment.