StateLens StateLens

Documentation Try it with sample data

Try StateLens with sample data

On this page

StateLens shows you a store through your own domain types. To try it before you have either — or to see what a store with something interesting in it looks like — two things ship beside the tool:

  • The seeder, statelens.azurecr.io/statelens-samples: an image that fills a store with an ecommerce domain modelled once in each consistency model, written through the framework itself. It is in the same private registry as the tool, and the same pull token pulls it — see The container image for the docker login
  • The sample archives, published with each release on the public Memoria repository: one zip per model, each holding the domain assembly the seeder wrote through and the statelens.json that declares it, ready to upload

The types the tool displays are the types the seeder exercises, so nothing reaches the tool that was never written through Memoria first.

Four steps, and about five minutes:

  1. Fill a store
  2. Run the tool over it
  3. Download the archives
  4. Upload them and look around

1. Fill a store

The seeder writes to whichever store ConnectionStrings__Memoria names. Any of the four engines works, and the string itself says which one — the resolution is the tool's own, compiled into both, so a string that reaches one reaches the other; see which engine it is for the keywords and for Database__Provider. Unlike the tool, the seeder creates what is missing before it writes: the database and each store's tables on a relational engine, the database and the container on Cosmos.

The quickest store is a SQLite file on a volume, which the tool's container mounts too in the next step:

docker run -i --rm -v statelens-store:/store \
  -e ConnectionStrings__Memoria="Data Source=/store/samples.db" \
  statelens.azurecr.io/statelens-samples:1.0.0

For a store that is already running, name it instead. The name is resolved from inside the container, so a database on the machine itself is host.docker.internal rather than localhost:

Engine ConnectionStrings__Memoria
PostgreSQL Host=host.docker.internal;Port=5432;Database=memoria_samples;Username=postgres;Password=…
SQL Server Server=host.docker.internal,1433;Initial Catalog=memoria_samples;User Id=sa;Password=…;TrustServerCertificate=True
SQLite Data Source=/store/samples.db, on a volume mounted at /store
Cosmos DB AccountEndpoint=https://host.docker.internal:8081/;AccountKey=<emulator key>

For Cosmos, set the database and container as well, Database__Cosmos__DatabaseName and Database__Cosmos__ContainerName, and start the emulator first — the run stops with "Could not reach the database" before asking anything otherwise. The tool then reads what the seeder wrote through a manifest of kind cosmos — see Limits.

The run says what it found and what it bound — here against a PostgreSQL database:

StateLens.Samples
  store    : Npgsql
  writing  : memoria_samples
  bound    : 30 events, 5+5 aggregates, 4+6 projections (streamed+dcb)
  schema   : installed

It then asks what to do, answered by number on standard input — which is what the -i above is for:

Answer Action
1 Add — writes alongside whatever is already there
2 Replace — empties the store, then writes
3 Delete — empties the store and writes nothing
Esc Quit; the run ends without touching another row

Then, only when the store holds both models, which data to write: 1 streamed, 2 DCB, 3 both. A Cosmos store holds the streamed model only, so it is never asked there.

Then, for anything that writes, how many of each item: a number, or enter for a random handful. One number covers every kind the run writes — customers and reviewed products on the streamed side, products, orders and suppliers on the DCB side. What hangs off each of them is still as many as the seeding feels like.

The menu comes back when the operation has finished, so one run can add, look at the store, and replace or delete without being started again. Esc at any of the questions ends it.

End of input counts as Esc — it means nobody is there to answer — so a piped run works and an unattended one stops rather than hanging:

printf '1\n3\n25\n' | docker run -i --rm -v statelens-store:/store \
  -e ConnectionStrings__Memoria="Data Source=/store/samples.db" \
  statelens.azurecr.io/statelens-samples:1.0.0

Exit code 0 means the run finished, including a run where nothing was chosen; 1 means the store could not be reached and nothing was written.

What it writes

Streamed — customers, the orders they place, and the reviews products collect. Every order is driven through the Order aggregate's own methods, so the log holds only sequences the domain would have allowed. Some reviews go through ProductReviewV1 rather than ProductReview, so the store ends up holding snapshots of two shapes of one model side by side.

DCB — a catalogue, stock for it, orders holding some of that stock, and the purchase orders that restock it. Every append follows the read-decide-append cycle on condition that the boundary has not moved, the way an application would write it.

Snapshots are deliberately left in three states, because a store where everything is current has nothing to demonstrate:

State How it is reached
Up to date Snapshotted after the last event on its stream
Behind Snapshotted, then more events appended without snapshotting again
No snapshot Events appended and never snapshotted

The run ends by listing every model it wrote and where its snapshot stands, so you know which rows the tool will say are behind their stream:

store    kind       model                 identifier            values     snapshot
streamed aggregate  Order                 OrderId               o-o5lwx2   v4, up to date
streamed aggregate  CustomerAccount       CustomerAccountId     c-o5lwx1   v2 of 6 — 4 behind
streamed projection OrderSummary          OrderSummaryId        o-o5lwxc   no snapshot — 2 events waiting
Back to top

2. Run the tool over it

The tool's image, open, over the same store: the same volume mounted at the same path, or the same connection string. The string is given under the name Memoria, which is the name the sample archives' manifests read it by.

docker run -p 8080:8080 -v statelens-data:/data -v statelens-store:/store \
  -e Authentication__Disabled=true \
  -e ConnectionStrings__Memoria="Data Source=/store/samples.db" \
  statelens.azurecr.io/statelens:1.0.0

Then open http://localhost:8080. See Run it locally for the shape of this, and Running it open for why it is for localhost alone.

Back to top

3. Download the archives

The two zips are attached to the release named after the tool's version on the public Memoria repository, statelens-v1.0.0 for 1.0.0:

Each holds one .dll and its statelens.json at the root, and nothing else: a zip carrying a Memoria* core assembly would be refused — see what to put in a zip. Each declares one service — Samples Streamed, browsed at /samples-streamed, for the streamed model, and Samples DCB, at /samples-dcb, for dynamic consistency boundaries — over the Memoria connection string. Two archives rather than one, each declaring a service of its own, so that you can upload one model alone; the home page lists whichever are installed, and each service's page is laid out for the model it registered types under.

Take them from the release matching the images you run. The assemblies are compiled against the framework version the tool reads through; one built against another version still loads, and then contributes no types at all.

Back to top

4. Upload them and look around

Go to Settings → Installed → Upload, choose a zip, and upload it — then the other, if you want both models. The page reports what registered; Settings → Types counts it per model. Nothing restarts. Each row of the installed table opens a sheet over its zip: the service the manifest declares, Samples Streamed or Samples DCB, the address it is browsed at, the Memoria connection string it reads over, and the types registered from the assembly. Home then lists the services installed; open one, and its page leads into its events, aggregates and projections — at /samples-streamed/streamed/... and /samples-dcb/dcb/....

Things worth opening first:

  • Streamed → Aggregates → Data. Two versions of ProductReview sit here under one name, because some reviews were written through ProductReviewV1. The version column is what tells them apart.
  • A row the seeder left behind. Open it: the Info tab says how far behind its stream the snapshot is, the Events tab lists the events it has not applied yet, and Compare folds the stream on the page to show what the model would be — without writing anything back.
  • A row with no snapshot at all. The compare tab folds one from scratch, and says beside each version that no snapshot is stored.
  • DCB → Events → Data. Filter by a tag rather than by a stream: a DCB event belongs to no stream, so its tags are the handle a boundary finds it through.
  • DCB → Aggregates → Data. Each row is a snapshot keyed by the boundary that produced it. The tags column is that boundary, whole.
Back to top

Limits

Cosmos holds the streamed samples only. The seeder writes the streamed model to a Cosmos account and never the DCB one, since there is no Cosmos boundary store; and the tool reads it through a manifest of kind cosmos — the sample zip's statelens.json declares tables, so over Cosmos swap its store block for the one in cosmos-memoria.json, with the database and container the seeder was given. The DCB pages are not offered under such a service.

In-memory SQLite is refused. Data Source=:memory: lives only as long as the connection that opened it, and both the seeder and the tool open a connection per unit of work — they would find an empty store rather than the one that was seeded. Point them at a file.

Do not upload the DCB samples alongside Memoria.Examples.Ecommerce.Dcb. Both claim the event types ProductCreated, ProductDeleted and ProductDetailsChanged at version 1, and the DCB aggregate Product at version 1. Whichever loses the name loses its bindings, and its pages then report no events inside the boundary. Remove one archive before uploading the other.

Back to top
Back to top