{"id":34837103,"url":"https://github.com/modelingevolution/micro-plumberd","last_synced_at":"2025-12-25T16:08:59.753Z","repository":{"id":226969042,"uuid":"770070486","full_name":"modelingevolution/micro-plumberd","owner":"modelingevolution","description":"Micro library for EventStore, CQRS and EventSourcing","archived":false,"fork":false,"pushed_at":"2025-12-10T13:30:24.000Z","size":21968,"stargazers_count":7,"open_issues_count":11,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-10T20:25:44.420Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/modelingevolution.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-03-10T20:41:01.000Z","updated_at":"2025-12-10T13:30:11.000Z","dependencies_parsed_at":"2025-09-05T10:10:14.470Z","dependency_job_id":"68d8327e-c430-4743-80c5-f2522f89c8cd","html_url":"https://github.com/modelingevolution/micro-plumberd","commit_stats":null,"previous_names":["modelingevolution/micro-plumberd"],"tags_count":138,"template":false,"template_full_name":null,"purl":"pkg:github/modelingevolution/micro-plumberd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modelingevolution%2Fmicro-plumberd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modelingevolution%2Fmicro-plumberd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modelingevolution%2Fmicro-plumberd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modelingevolution%2Fmicro-plumberd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/modelingevolution","download_url":"https://codeload.github.com/modelingevolution/micro-plumberd/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modelingevolution%2Fmicro-plumberd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28032374,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-25T02:00:05.988Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-25T16:07:10.263Z","updated_at":"2025-12-25T16:08:59.743Z","avatar_url":"https://github.com/modelingevolution.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# micro-plumberd\nMicro library for EventStore, CQRS and EventSourcing.\nJust eXtreamly simple.\n\n## NuGet Packages\n\n### Core Packages\n[![MicroPlumberd](https://img.shields.io/nuget/v/MicroPlumberd.svg)](https://www.nuget.org/packages/MicroPlumberd/)\n[![MicroPlumberd.SourceGenerators](https://img.shields.io/nuget/v/MicroPlumberd.SourceGenerators.svg)](https://www.nuget.org/packages/MicroPlumberd.SourceGenerators/)\n[![MicroPlumberd.Testing](https://img.shields.io/nuget/v/MicroPlumberd.Testing.svg)](https://www.nuget.org/packages/MicroPlumberd.Testing/)\n\n### Service Packages\n[![MicroPlumberd.Services](https://img.shields.io/nuget/v/MicroPlumberd.Services.svg)](https://www.nuget.org/packages/MicroPlumberd.Services/)\n[![MicroPlumberd.CommandBus.Abstractions](https://img.shields.io/nuget/v/MicroPlumberd.CommandBus.Abstractions.svg)](https://www.nuget.org/packages/MicroPlumberd.CommandBus.Abstractions/)\n[![MicroPlumberd.Services.Cron](https://img.shields.io/nuget/v/MicroPlumberd.Services.Cron.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.Cron/)\n[![MicroPlumberd.Services.Cron.Ui](https://img.shields.io/nuget/v/MicroPlumberd.Services.Cron.Ui.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.Cron.Ui/)\n\n### Process Manager\n[![MicroPlumberd.ProcessManager.Abstractions](https://img.shields.io/nuget/v/MicroPlumberd.ProcessManager.Abstractions.svg)](https://www.nuget.org/packages/MicroPlumberd.ProcessManager.Abstractions/)\n[![MicroPlumberd.Services.ProcessManagers](https://img.shields.io/nuget/v/MicroPlumberd.Services.ProcessManagers.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.ProcessManagers/)\n\n### Additional Services\n[![MicroPlumberd.Encryption](https://img.shields.io/nuget/v/MicroPlumberd.Encryption.svg)](https://www.nuget.org/packages/MicroPlumberd.Encryption/)\n[![MicroPlumberd.Protobuf](https://img.shields.io/nuget/v/MicroPlumberd.Protobuf.svg)](https://www.nuget.org/packages/MicroPlumberd.Protobuf/)\n[![MicroPlumberd.Services.Uniqueness](https://img.shields.io/nuget/v/MicroPlumberd.Services.Uniqueness.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.Uniqueness/)\n[![MicroPlumberd.Services.Grpc.DirectConnect](https://img.shields.io/nuget/v/MicroPlumberd.Services.Grpc.DirectConnect.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.Grpc.DirectConnect/)\n[![MicroPlumberd.Services.Identity](https://img.shields.io/nuget/v/MicroPlumberd.Services.Identity.svg)](https://www.nuget.org/packages/MicroPlumberd.Services.Identity/)\n\n---\n\nQuick \"how to\" section is [here](#quick-how-to-section)\nDocumentation can be found here:\n[MicroPlumberd Documentation](https://modelingevolution.github.io/micro-plumberd/)\n\n## Getting started\n\n### Install nugets: \n\n```powershell\ndotnet add package MicroPlumberd                      # For your domain\ndotnet add package MicroPlumberd.Services             # For IoC integration and CommandBus\ndotnet add package MicroPlumberd.SourceGenerators     # Code generators for Aggregates, EventHandlers and more.\n```\n\n### Configure plumber\n\n```csharp\n// Vanilla\nstring connectionString = $\"esdb://admin:changeit@localhost:2113?tls=false\u0026tlsVerifyCert=false\";\nvar settings = EventStoreClientSettings.Create(connectionString);\nvar plumber = Plumber.Create(settings);\n```\n\nHowever, typicly you would add plumberd to your app:\n```csharp\nservices.AddPlumberd();\n```\n\n## Features\n\n### State\n\nSuppose you want to save some small \"state\" to your EventStoreDB. For example. current configuration of your Raspherry PI Camera. You can expect that state would be dependend only on previous state.\n\n```csharp\nrecord class CameraConfiguration : IVersionAware: {\n    public int Shutter {get;set;}\n    public float Contrast {get;set;}\n    // ...\n    public Guid Id { get; set; } = Guid.NewGuid();\n    public long Version { get; set; } = -1;\n}\n\n// To save the state:\nvar state = new CameraConfiguration { /* ... */ };\nplumber.AppendState(state); // because CameraConfiguration implements IVersionAware, \n                            // optimistic concurrency check will be performed.\n\n// To retrive latest state:\nvar id = state.Id; // We need to have Id from somewhere...\nvar actual = plumber.GetState\u003cCameraConfiguration\u003e(id);\n```\n\n### Aggregates\n\nEvent-sourced aggregates are the guardians of transaction. They encapsulate object(s) that we want to threat in isolation to \nthe rest of the world because we want its data to be consistent. \n\nEvent-sourced aggregates are \"rehydrated\" from history (its stream) every time we need them. This means that theirs streams should be relatively short ~1K events max, to accomplish this usually you would you \"close-the-books\" pattern. \n\nFor performance reasons, sometimes you would want to have \"snapshots\". Snapshots are saved in related snapshot stream. When you want to retrive an aggregate:\n\n1) plumber would try to read latest event from shaphost stream.\n2) retrive and apply all the events that were from latest snahshot till now.\n\n```csharp\n[Aggregate(SnapshotEvery = 50)]\npublic partial class FooAggregate(Guid id) : AggregateBase\u003cGuid,FooAggregate.FooState\u003e(id)\n{\n    public record FooState { public string Name { get; set; } };\n    private static FooState Given(FooState state, FooCreated ev) =\u003e state with { Name = ev.Name };\n    private static FooState Given(FooState state, FooRefined ev) =\u003e state with { Name =ev.Name };\n    public void Open(string msg) =\u003e AppendPendingChange(new FooCreated() { Name = msg });\n    public void Change(string msg) =\u003e AppendPendingChange(new FooRefined() { Name = msg });\n}\n// And events:\npublic record FooCreated { public string? Name { get; set; } }\npublic record FooRefined { public string? Name { get; set; } }\n```\nComments:\n\n- State is encapsulated in nested class FooState. \n- Given methods, that are used when loading aggregate from the EventStoreDB are private and static. State is encouraged to be immutable.\n- [Aggregate] attribute is used by **SourceGenerator** that will generate dispatching code and handy metadata.\n\n2) Consume an aggregate.\n\nIf you want to create a new aggregate and save it to EventStoreDB:\n```csharp\n\nFooAggregate aggregate = FooAggregate.New(Guid.NewGuid());\naggregate.Open(\"Hello\");\n\nawait plumber.SaveNew(aggregate);\n\n```\n\nIf you want to load aggregate from EventStoreDB, change it and save back to EventStoreDB\n\n```csharp\nvar aggregate = await plumber.Get\u003cFooAggregate\u003e(\"YOUR_ID\");\naggregate.Change(\"World\");\nawait plumber.SaveChanges(aggregate);\n```\n\n### Write a read-model/processor\n\n1) Read-Models\n```csharp\n[EventHandler]\npublic partial class FooModel\n{\n    private async Task Given(Metadata m, FooCreated ev)\n    {\n        // your code\n    }\n    private async Task Given(Metadata m, FooRefined ev)\n    {\n         // your code\n    }\n}\n```\n\nComments:\n\n- ReadModels have private async Given methods. Since they are async, you can invoke SQL here, or othere APIs to store your model.\n- Metadata contains standard stuff (Created, CorrelationId, CausationId), but can be reconfigured.\n\n```csharp\nvar fooModel = new FooModel();\nvar sub= await plumber.SubscribeEventHandler(fooModel);\n\n// or if you want to persist progress of your subscription\nvar sub2= await plumber.SubscribeEventHandlerPersistently(fooModel);\n```\n\nWith **SubscribeModel** you can subscribe from start, from certain moment or from the end of the stream. If you want to use DI and have your model as a scoped one, you can configure plumber at the startup and don't need to invoke SubscribeEventHandler manually.\nHere you have an example with EF Core.\n\n```csharp\n// Program.cs\nservices\n    .AddPlumberd()\n    .AddEventHandler\u003cFooModel\u003e();\n\n// FooModel.cs\n[EventHandler]\npublic partial class FooModel : DbContext\n{\n    private async Task Given(Metadata m, FooCreated ev)\n    {\n        // your code\n    }\n    private async Task Given(Metadata m, FooRefined ev)\n    {\n         // your code\n    }\n    // other stuff, DbSet... etc...\n}\n```\n\n2) Processors\n\n```csharp\n[EventHandler]\npublic partial class FooProcessor(IPlumber plumber)\n{\n    private async Task Given(Metadata m, FooRefined ev)\n    {\n        var agg = FooAggregate.New(Guid.NewGuid());\n        agg.Open(ev.Name + \" new\");\n        await plumber.SaveNew(agg);\n    }\n}\n```\n\nImplementing a processor is technically the same as implementing a read-model, but inside the Given method you would typically invoke a command or execute an aggregate. A process would subscribe persistently. \n\n### Read-Model with EF (EntityFramework)\n\nLet's analyse this example:\n\n1. You create a read-model that subscribes persistently.\n2. You subscribe it with plumber.\n3. You changed something in the event and want to see the new model.\n4. Instead of re-creating old read-model, you can easily create new one. Just change MODEL_VER to reflect new version.\n\n*Please note that Sql schema create/drop auto-generation script will be covered in a different article. (For now we leave it for developers.)*\n\nComments:\n- By creating a new read-model you can always compare the differences with the previous one.\n- You can leverage canary-deployment strategy and have 2 versions of your system running in parallel.\n\n```csharp\n[OutputStream(FooModel.MODEL_NAME)]\n[EventHandler]\npublic partial class FooModel : DbContext\n{\n    internal const string MODEL_VER = \"_v1\";\n    internal const string MODEL_NAME = $\"FooModel{MODEL_VER}\";\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        modelBuilder\n           .Entity\u003cFooEntity\u003e()\n           .ToTable($\"FooEntities{MODEL_VER}\");\n    }\n    private async Task Given(Metadata m, FooCreated ev)\n    {\n        // your code\n    }\n    private async Task Given(Metadata m, FooRefined ev)\n    {\n        // your code\n    }\n}\n```\n\n### Read-Model with LiteDB\n\nWith LiteDB you can easily achive the same without a hassle of schema recreation.\n\n\n```csharp\n[OutputStream(DbReservationModel.MODEL_NAME)]\n[EventHandler]\npublic partial class DbReservationModel(LiteDatabase db)\n{\n    internal const string MODEL_VER = \"_v2\";\n    internal const string MODEL_NAME = $\"Reservations{MODEL_VER}\";\n    public ILiteCollection\u003cReservation\u003e Reservations { get; } = db.Reservations();\n\n    private async Task Given(Metadata m, TicketReserved ev)\n    {\n        Reservations.Insert(new Reservation() { RoomName = ev.RoomName, MovieName = ev.MovieName });\n        \n    }\n}\n\npublic static class DbExtensions\n{\n    public static ILiteCollection\u003cReservation\u003e Reservations(this LiteDatabase db) =\u003e db.GetCollection\u003cReservation\u003e(DbReservationModel.MODEL_NAME);\n}\npublic record Reservation\n{\n    public ObjectId ReservationId { get; set; }\n    public string RoomName { get; set; }\n    public string MovieName { get; set; }\n}\n\n/// Configure your app:\nservices.AddEventHandler\u003cDbReservationModel\u003e(true)\n\n```\n\n### Command-Handlers \u0026 Message Bus\n\nIf you want to start as quickly as possible, you can start with EventStoreDB as command-message-bus.\n```csharp\n\nservices.AddPlumberd()\n        .AddCommandHandler\u003cFooCommandHandler\u003e()\n\n// on the client side:\nICommandBus bus; // from DI\nbus.SendAsync(Guid.NewGuid(), new CreateFoo() { Name = \"Hello\" });\n```\n\n#### Scaling considerations\nIf you are running many replicas of your service, you need to switch command-execution to persistent mode:\n\n```csharp\n\nservices.AddPlumberd(configure: c =\u003e c.Conventions.ServicesConventions().AreHandlersExecutedPersistently = () =\u003e true)\n        .AddCommandHandler\u003cFooCommandHandler\u003e()\n\n```\nThis means, that once your microservice subscribes to commands, it will execute all. So if your service is down, and commands are saved, once your service is up, they will be executed.\nTo skip old commands, you can configure a filter.\n\n```csharp\nservices.AddPlumberd(configure: c =\u003e {\n    c.Conventions.ServicesConventions().AreHandlersExecutedPersistently = () =\u003e true;\n    c.Conventions.ServicesConventions().CommandHandlerSkipFilter = (m,ev) =\u003e DateTimeOffset.Now.Substract(m.Created()) \u003e TimeSpan.FromSeconds(60);\n    })\n    .AddCommandHandler\u003cFooCommandHandler\u003e()\n```\n    \n### Conventions\n  - SteamNameConvention - from aggregate type, and aggregate id\n  - EventNameConvention - from aggregate? instance and event instance\n  - MetadataConvention - to enrich event with metadata based on aggregate instance and event instance\n  - EventIdConvention - from aggregate instance and event instance\n  - OutputStreamModelConvention - for output stream name from model-type\n  - GroupNameModelConvention - for group name from model-type\n\n\n### Subscription Sets\n  - You can easily create a stream that joins events together by event-type, and subscribe many read-models at once. Here it is named 'MasterStream', which is created out of events used to create DimentionLookupModel and MasterModel.\n  - In this way, you can easily manage the composition and decoupling of read-models. You can nicely composite your read-models. And if you don't wish to decouple read-models, you can reuse your existing one. \n\n\n/// Given simple models, where master-model has foreign-key used to obtain value from dimentionLookupModel\n\n```csharp\nvar dimentionTable = new DimentionLookupModel();\nvar factTable = new MasterModel(dimentionTable);\n\nawait plumber.SubscribeSet()\n    .With(dimentionTable)\n    .With(factTable)\n    .SubscribeAsync(\"MasterStream\", FromStream.Start);\n```\n\n### Integration tests support\n\n### Specflow/Ghierkin step-files generation\n\nGiven you have written your domain, you can generate step files that would populate Ghierkin API to your domain. \n\n\n### EXPERIMENTAL GRPC Direct communication\n\nIf you'd like to use direct dotnet-dotnet communication to execute command-handlers install MicroPlumberd.DirectConnect\n\n```powershell\n\ndotnet add package MicroPlumberd.Services.Grpc.DirectConnect\n```\n\n\nIf you prefer direct communication (like REST-API, but without the hassle for contract generation/etc.) you can use direct communication where client invokes command handle using grpc.\nCommand is not stored in EventStore.\n\n```csharp\n/// Let's configure server:\nservices.AddCommandHandler\u003cFooCommandHandler\u003e().AddServerDirectConnect();\n\n/// Add mapping to direct-connect service\napp.MapDirectConnect();\n```\n\nHere is an example of a command handler code:\n\n```csharp\n[CommandHandler]\npublic partial class FooCommandHandler(IPlumber plumber)\n{\n\n    [ThrowsFaultException\u003cBusinessFault\u003e]\n    public async Task Handle(Guid id, CreateFoo cmd)\n    {\n        if (cmd.Name == \"error\")\n            throw new BusinessFaultException(\"Foo\");\n\n        var agg = FooAggregate.New(id);\n        agg.Open(cmd.Name);\n\n        await plumber.SaveNew(agg);\n    }\n\n    [ThrowsFaultException\u003cBusinessFault\u003e]\n    public async Task\u003cHandlerOperationStatus\u003e Handle(Guid id, ChangeFoo cmd)\n    {\n        if (cmd.Name == \"error\")\n            throw new BusinessFaultException(\"Foo\");\n\n        var agg = await plumber.Get\u003cFooAggregate\u003e(id);\n        agg.Change(cmd.Name);\n\n        await plumber.SaveChanges(agg);\n        return HandlerOperationStatus.Ok();\n    }\n}\n```\n\nAnd how on the client side:\n```csharp\nservice.AddClientDirectConnect().AddCommandInvokers();\n\n// And invocation\n var clientPool = sp.GetRequiredService\u003cIRequestInvokerPool\u003e();\n var invoker = clientPool.Get(\"YOUR_GRPC_URL\");\n await invoker.Execute(Guid.NewId(), new CreateFoo(){});\n```\n\n### EXPERIMENTAL Process-Manager\n\nGiven diagram:\n![Saga](./pm.png)\n\n```powershell\n# Add required packages:\ndotnet add package MicroPlumberd.Services.ProcessManagers\n```\n\nThe code of Order Process Manager looks like this:\n\n```csharp\n// Let's configure stuff beforehand\nservices.AddPlumberd(eventStoreConfig)\n    .AddCommandHandler\u003cOrderCommandHandler\u003e() // handles PlaceOrder command.\n    .AddProcessManager\u003cOrderProcessManager\u003e();\n\n// And process manager.\n[ProcessManager]\npublic class OrderProcessManager(IPlumberd plumberd)\n{\n    public async Task\u003cICommandRequest\u003cMakeReservation\u003e\u003e StartWhen(Metadata m, OrderCreated e) \n    {\n        return CommandRequest.Create(Guid.NewId(), new MakeReservation());\n    }\n    public async Task\u003cICommandRequest\u003cMakePayment\u003e\u003e When(Metadata m, SeatsReserved e)\n    {\n        return CommandRequest.Create(Guid.NewId(), new MakePayment());\n    }\n    public async Task When(Metadata m, PaymentAccepted e)\n    {\n        var order = await plumberd.Get\u003cOrder\u003e(this.Id);\n        order.Confirm();\n        await plumberd.SaveChanges(order);\n    }\n    // Optional\n    private async Task Given(Metadata m, OrderCreated v){\n        // this will be used to rehydrate state of process-manager\n        // So that when(SeatsReserved) you can adjust the response.\n    }\n    // Optional 2\n    private async Task Given(Metadata m, CommandEnqueued\u003cMakeReservation\u003e e){\n        // same here.\n    }\n}\n\n```\n\n### EXPERIMENTAL Uniqueness support\n\nUniqueness support in EventSourcing is not out-of-the-box, especially in regards to EventStoreDB. You can use some \"hacks\" but at the end of the day, you want uniqueness to be enforced by some kind of database. EventStoreDB is not designed for that purpose. \n\nHowever, you can leverage typical reservation patterns. At the moment the library supports only the first option:\n\n- At domain-layer, a domain-service usually would enforce uniqueness. This commonly requires a round-trip to a database. So just before actual event(s) are saved in a stream, a check against uniqueness constraints should be evaluated - thus reservation is made. When the event is appended to the stream, a confirmation is done automatically (on db).\n\n- At a app-layer, command-handler would typically reserve a name. And when aggregate, which is being executed by the handler, saves its events successfully, then the reservation is confirmed. If the handler fails, then the reservation is deleted. Seems simple? Under the hood, it is not that simple, because what if the process is terminated while the command-handler is executing? We need to make sure, that we can recover successfully from this situation.\n\nLet's see the API proposal:\n\n```csharp\n// Let's define unique-category name\nrecord FooCategory;\n\n\npublic class FooCreated \n    // and apply it to one fo the columns.\n    [Unique\u003cFooCategory\u003e]\n    public string? Name { get; set; }\n    \n    // other stuff   \n}\n```\n\nFor complex types, we need more flexibility.\n\n```csharp\n// Let's define unique-category name, this will be mapped to columns in db\n// If you'd opt for domain-layer enforcment, you need to change commands to events.\nrecord BooCategory(string Name, string OtherName) : IUniqueFrom\u003cBooCategory, BooCreated\u003e, IUniqueFrom\u003cBooCategory, BooRefined\u003e\n{\n    public static BooCategory From(BooCreated x) =\u003e new(x.InitialName, x.OtherName);\n    public static BooCategory From(BooRefined x) =\u003e new(x.NewName, x.OtherName);\n}\n\n[Unique\u003cBooCategory\u003e]\npublic record BooCreated(string InitialName, string OtherName);\n\n[Unique\u003cBooCategory\u003e]\npublic record BooRefined(string NewName, string OtherName);\n```\n\n# How-to\n\nAll \"How to's\" are in [MicroPlumber.Tests](https://github.com/modelingevolution/micro-plumberd/tree/master/src/MicroPlumberd.Tests/) projects, so you can easily run it!\n\n## How to append event to its default stream\n\nExample event:\n```csharp\n[OutputStream(\"ReservationStream\")]\npublic record TicketReserved { \n    public string? MovieName { get; set; } \n    public string? RoomName { get; set; }\n}\n```\nCode:\n```csharp\npublic async Task HowToAppendEventToHisDefaultStream(IPlumber plumber)\n{\n var ourLovelyEvent = new TicketReserved();\n var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();\n await plumber.AppendEvent(ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);\n}\n```\n## How to append event to specific stream\n\nCode:\n```csharp\npublic async Task HowToAppendEventToSpecificStream(IPlumber plumber)\n{\n  var streamIdentifier = Guid.NewGuid();\n  var ourLovelyEvent = new TicketReserved();\n\n  await plumber.AppendEventToStream($\"VIPReservationStream-{streamIdentifier}\", ourLovelyEvent);\n}\n```\n\n## How to subscribe a model\n\nModel:\n```csharp\n[OutputStream(\"ReservationModel_v1\")]\n[EventHandler]\npublic partial class ReservationModel(InMemoryModelStore assertionModelStore)\n{\n    public InMemoryModelStore ModelStore =\u003e assertionModelStore;\n    public bool EventHandled{ get; set; } = false;\n    private async Task Given(Metadata m, TicketReserved ev)\n    {\n        EventHandled = true;\n        assertionModelStore.Given(m, ev);\n        await Task.Delay(0);\n    }\n   \n\n}\n```\n\nCode:\n```csharp\npublic async Task HowToMakeAModelSubscribe(IPlumber plumber)\n{\n    var fromWhenShouldWeSubscribeOurStream = FromRelativeStreamPosition.Start;\n    var modelThatWantToSubscribeToStream = new ReservationModel(new InMemoryModelStore());\n          \n    await plumber.SubscribeEventHandler(modelThatWantToSubscribeToStream, start: fromWhenShouldWeSubscribeOurStream);\n\n    var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();\n    var ourLovelyEvent = new TicketReserved();\n\n    await plumber.AppendEvent(ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);\n    await Task.Delay(1000);\n\n    modelThatWantToSubscribeToStream.EventHandled.Should().BeTrue();\n}\n```\n\n## How to make a model subscribe but from last event\n\nCode:\n```csharp \npublic async Task HowToMakeAModelSubscribeButFromLastEvent(IPlumber plumber)\n{\n    var modelThatWantToSubscribeToStream = new ReservationModel(new InMemoryModelStore());\n    var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();\n    var someVeryOldEvent = new TicketReserved();\n\n    await plumber.AppendEvent(someVeryOldEvent, suffixOfStreamWhereOurEventWillBeAppend);\n    await Task.Delay(1000);\n\n    await plumber.SubscribeEventHandler(modelThatWantToSubscribeToStream, start: FromRelativeStreamPosition.End-1);\n    modelThatWantToSubscribeToStream.EventHandled.Should().BeFalse();\n\n    var ourLovelyEvent = new TicketReserved();\n\n    await plumber.AppendEvent(ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);\n    await Task.Delay(1000);\n    modelThatWantToSubscribeToStream.EventHandled.Should().BeTrue();\n}\n```\n## How to link events to another stream\n\nProjection:\n```csharp\n[EventHandler]\npublic partial class TicketProjection(IPlumber plumber)\n{\n    private async Task Given(Metadata m, TicketReserved ev)\n    {\n        await plumber.AppendLink($\"RoomOccupancy-{ev.RoomName}\", m);\n    }\n}\n```\nModel:\n\n```csharp\n[EventHandler]\npublic partial class RoomOccupancy\n{\n    public int HowManySeatsAreReserved { get; set; }\n    private async Task Given(Metadata m, TicketReserved ev)\n    {\n        HowManySeatsAreReserved++;\n    }\n}\n```\nCode:\n```csharp\npublic async Task HowToLinkEventsToAnotherStream(IPlumber plumber)\n{\nvar ourLovelyEvent = new TicketReserved()\n{\n    MovieName = \"Scream\",\n    RoomName = \"Venus\"\n};\nawait plumber.SubscribeEventHandlerPersistently(new TicketProjection(plumber));\nawait plumber.AppendEvent(ourLovelyEvent);\n\nawait plumber.Subscribe($\"RoomOccupancy-{ourLovelyEvent.RoomName}\", FromRelativeStreamPosition.Start)\n    .WithHandler(new RoomOccupancy());\n}\n```\n\n## How to rehydrate model (run all events again)\n\nModel:\n```csharp\n[OutputStream(\"ReservationModel_v1\")]\n[EventHandler]\npublic partial class ReservationModel(InMemoryModelStore assertionModelStore)\n{\n    public InMemoryModelStore ModelStore =\u003e assertionModelStore;\n    public bool EventHandled{ get; set; } = false;\n    private async Task Given(Metadata m, TicketReserved ev)\n    {\n        EventHandled = true;\n        assertionModelStore.Given(m, ev);\n        await Task.Delay(0);\n    }\n   \n\n}\n```\n\nCode:\n```csharp\npublic async Task HowToRehydrateModel(IPlumber plumber)\n{\n   var modelThatWantToSubscribeToStream = new ReservationModel(new InMemoryModelStore());\n   var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();\n   var ourLovelyEvent = new TicketReserved();\n\n   await plumber.AppendEvent( ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);\n   await Task.Delay(1000);\n\n   await plumber.SubscribeEventHandler(modelThatWantToSubscribeToStream, start: FromRelativeStreamPosition.Start);\n\n   await Task.Delay(1000);\n\n   modelThatWantToSubscribeToStream.EventHandled.Should().BeTrue();\n   modelThatWantToSubscribeToStream.EventHandled = false;\n   modelThatWantToSubscribeToStream.EventHandled.Should().BeFalse();\n\n   await plumber.Rehydrate(modelThatWantToSubscribeToStream,\n       $\"ReservationStream-{suffixOfStreamWhereOurEventWillBeAppend}\");\n\n   modelThatWantToSubscribeToStream.EventHandled.Should().BeTrue();\n}\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmodelingevolution%2Fmicro-plumberd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmodelingevolution%2Fmicro-plumberd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmodelingevolution%2Fmicro-plumberd/lists"}