{"id":27715312,"url":"https://github.com/jardotnet/eventsourcingsample","last_synced_at":"2026-04-28T18:04:30.281Z","repository":{"id":289136449,"uuid":"970071048","full_name":"jarDotNet/EventSourcingSample","owner":"jarDotNet","description":"A sample of Event Sourcing that writes events to a MongoDB Collection with .NET Core 8","archived":false,"fork":false,"pushed_at":"2026-02-02T10:42:17.000Z","size":141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-02T23:43:20.840Z","etag":null,"topics":["csharp","event-sourcing","mongodb","netcore8","railway","railway-oriented-programming","secrets","secretstorage"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jarDotNet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2025-04-21T12:21:12.000Z","updated_at":"2026-02-02T10:42:22.000Z","dependencies_parsed_at":"2025-04-21T18:53:25.388Z","dependency_job_id":null,"html_url":"https://github.com/jarDotNet/EventSourcingSample","commit_stats":null,"previous_names":["jardotnet/eventsourcingsample"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jarDotNet/EventSourcingSample","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarDotNet%2FEventSourcingSample","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarDotNet%2FEventSourcingSample/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarDotNet%2FEventSourcingSample/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarDotNet%2FEventSourcingSample/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jarDotNet","download_url":"https://codeload.github.com/jarDotNet/EventSourcingSample/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jarDotNet%2FEventSourcingSample/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32392315,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T14:34:11.604Z","status":"ssl_error","status_checked_at":"2026-04-28T14:32:37.009Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["csharp","event-sourcing","mongodb","netcore8","railway","railway-oriented-programming","secrets","secretstorage"],"created_at":"2025-04-27T01:01:42.604Z","updated_at":"2026-04-28T18:04:30.275Z","avatar_url":"https://github.com/jarDotNet.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EventSourcingSample\n\nA sample of Event Sourcing that writes events to a MongoDB Collection with .NET Core 8.\n\nIn this sample, the implementation of an Ordering system allows to create an order, mark the order as paid or dispatched, and finally complete the order. The data and status of an order can be consulted at any time. The different events are stored in a MongoDB collection.\n\nThis repo includes:\n\n- API REST with .NET Core 8.\n- Event Sourcing in conjunction with MongoDB to store the events.\n- Vertical Slice mixed with Clean Architecture.\n- CQRS to separate reads and writes.\n- Railway Oriented Programming (ROP).\n- Secret Storage to store the connection string to MongoDB.\n\n## Event Sourcing\n\nEvent Sourcing is a software architectural pattern that stores the state of an application as a sequence of events. Instead of persisting the current state of an object, Event Sourcing captures all changes to the object as a series of events. This allows for a complete history of changes, enabling features like auditing, time travel, and more.\n\n### The Aggregate\n\nIn the context of Event Sourcing, an Aggregate is a cluster of domain objects that can be treated as a single unit. An Aggregate is responsible for enforcing invariants and encapsulating the business logic related to its state. It typically consists of an Aggregate Root and one or more child entities or value objects.\n\nIn our sample, the Aggregate is the type that we are going to use to realize all the logic of our object. This `Aggregate` type contains the list of `events`, the `Id`, and the current �`version`�, i.e. the number of events.\n\nFinally, some logic is included to know when an element is a change coming from the database or is a new change to be stored:\n\n```csharp\npublic abstract class Aggregate\n{\n    private List\u003cAggregateChange\u003e _changes = [];\n\n    public Guid Id { get; internal set; }\n    public int Version { get; set; } = 0;\n\n    /// \u003csummary\u003e\n    /// This flag is used to identify when an event is being loaded from the DB\n    /// or when the event is being created as new\n    /// \u003c/summary\u003e\n    private bool ReadingFromHistory { get; set; } = false;\n\n    protected Aggregate(Guid id)\n    {\n        Id = id;\n    }\n\n    internal void Initialize(Guid id)\n    {\n        Id = id;\n        _changes = [];\n    }\n\n    public IList\u003cAggregateChange\u003e GetUncommittedChanges()\n    {\n        return [.. _changes.Where(a =\u003e a.IsNew)];\n    }\n\n    public void MarkChangesAsCommitted()\n    {\n        _changes.Clear();\n    }\n\n    protected void ApplyChange\u003cT\u003e(T eventObject)\n    {\n        ArgumentNullException.ThrowIfNull(eventObject, nameof(eventObject));\n\n        Version++;\n\n        AggregateChange change = new AggregateChange(\n            eventObject,\n            Id,\n            eventObject.GetType(),\n            $\"{Id}:{Version}\",\n            Version,\n            ReadingFromHistory != true\n        );\n        _changes.Add(change);\n\n    }\n\n    public void LoadFromHistory(IList\u003cAggregateChange\u003e history)\n    {\n        if (!history.Any())\n        {\n            return;\n        }\n\n        ReadingFromHistory = true;\n        foreach (var e in history)\n        {\n            ApplyChanges(e.Content);\n        }\n        ReadingFromHistory = false;\n\n        Version = history.Last().Version;\n\n        void ApplyChanges\u003cTEvent\u003e(TEvent eventObject)\n        {\n            this.AsDynamic()!.Apply(eventObject);\n        }\n    }\n}\n```\n\nWhen a domain object is created in your domain, **you must implement Aggregate** so that it works as an aggregate, and use the `IApply\u003cT\u003e` interface for each event that the object will work with:\n```csharp\npublic class Order : Aggregate, IApply\u003cOrderCreated\u003e, IApply\u003cOrderPaid\u003e, IApply\u003cOrderDispatched\u003e, IApply\u003cOrderCompleted\u003e\n{\n    public Order(Guid id) : base(id)\n    {\n    }\n    public void Apply(OrderCreated @event)\n    {\n        // Logic to apply the event\n    }\n    public void Apply(OrderPaid @event)\n    {\n        // Logic to apply the event\n    }\n    public void Apply(OrderDispatched @event)\n    {\n        // Logic to apply the event\n    }\n    public void Apply(OrderCompleted @event)\n    {\n        // Logic to apply the event\n    }\n}\n```\n\nAs we can see in the example, we have the aggregate `OrderDetails`, and then an `IApply\u003cT\u003e` for each event shown above.\n\n### Save the Aggregate\n\nWhen we store information we do it in a special way, since we do not store only the event, but we store information that makes it possible to identify it or group it together with others.\n\nThat information is what you can see in the `AggregateChange` and `AggregateChangeDto` types, this type contains information such as:\n\n- `Content`: for the contents of the object (includes the event).\n- `AggregateId`: Aggregate ID, in our case, the order ID.\n- `AggregateType`: to know what type it is.\n- `Version`: version of the aggregate, each new event adds 1 to the version number.\n- `TransactionId`: combination between the Id and the version.\n- `Created:` date of creation of the event.\n\nThe `AggregateRepository\u003cTAggregate\u003e` class contains two methods and must be implemented by each repository:\n\n- `GetByIdAsync`: Reads from the database by ID and assembles the Aggregate in order and correctly.\n- `SaveAsync`: Saves the new events in the database.\n\nThis allows to inject `IAggregateRepository\u003cTAggregate\u003e` directly into the services / command handler, or create your own repository and implement `AggregateRepository\u003cTAggregate\u003e` (as a recommended option):\n\n```csharp\npublic interface IOrderRepository\n{\n    Task\u003cOrderDetails?\u003e GetById(Guid id, CancellationToken cancellationToken = default);\n\n    Task Save(OrderDetails orderDetails, CancellationToken cancellationToken = default);\n}\n\npublic class OrderRepository(IEventStore eventStore) : AggregateRepository\u003cOrderDetails\u003e(eventStore), IOrderRepository\n{\n    public async Task\u003cOrderDetails?\u003e GetById(Guid id, CancellationToken cancellationToken = default)\n        =\u003e await GetByIdAsync(id, cancellationToken);\n\n    public async Task Save(OrderDetails orderDetails, CancellationToken cancellationToken = default)\n        =\u003e await SaveAsync(orderDetails, cancellationToken);\n}\n```\n\nA fairly simple implementation.\n\n### Creating an Aggregate\n\nIn our sample, we have the `OrderDetails` aggregate, which is the one that will be used to create the order:\n\n```csharp\npublic class OrderDetails : Aggregate, IApply\u003cOrderCreated\u003e\n{\n    public DeliveryDetails Delivery { get; private set; } = default!;\n    public PaymentInformation PaymentInformation { get; private set; } = default!;\n    public ImmutableArray\u003cProductQuantity\u003e Products { get; private set; } = [];\n    public OrderStatus Status { get; private set; }\n\n    public OrderDetails(Guid id) : base(id)\n    {\n    }\n\n    public void Apply(OrderCreated ev)\n    {\n        Delivery = ev.Delivery;\n        PaymentInformation = ev.PaymentInformation;\n        Products = [.. ev.Products];\n        Status = OrderStatus.Created;\n        ApplyChange(ev);\n    }\n}\n```\n\nWhen implementing the interface, we create a method called `Apply` that receives the event. Inside the method, we modify the object at will and call the `ApplyChange` method, which will store the event as a new event. Finally, when we save the aggregate through `AggregateRepository`, it will detect that it is a new event and save it.\n\nAs we have implemente additional use cases, we can repeat the same process for each even:\n\n```csharp\npublic class OrderDetails : Aggregate, IApply\u003cOrderCreated\u003e, IApply\u003cOrderPaid\u003e, IApply\u003cOrderDispatched\u003e, IApply\u003cOrderCompleted\u003e\n{\n    public DeliveryDetails Delivery { get; private set; } = default!;\n    public PaymentInformation PaymentInformation { get; private set; } = default!;\n    public ImmutableArray\u003cProductQuantity\u003e Products { get; private set; } = [];\n    public OrderStatus Status { get; private set; }\n\n    public OrderDetails(Guid id) : base(id)\n    {\n    }\n\n    public void Apply(OrderCreated ev)\n    {\n        Delivery = ev.Delivery;\n        PaymentInformation = ev.PaymentInformation;\n        Products = [.. ev.Products];\n        Status = OrderStatus.Created;\n        ApplyChange(ev);\n    }\n\n    public void Apply(OrderPaid ev)\n    {\n        Status = OrderStatus.Paid;\n        ApplyChange(ev);\n    }\n\n    public void Apply(OrderDispatched ev)\n    {\n        Status = OrderStatus.Dispatched;\n        ApplyChange(ev);\n    }\n\n    public void Apply(OrderCompleted ev)\n    {\n        Status = OrderStatus.Completed;\n        ApplyChange(ev);\n    }\n}\n```\n\n## Railway Oriented Programming\n\nRailway Oriented Programming (ROP) is a functional programming pattern that facilitates error handling and is often used in languages that support functional programming concepts, like F#, Haskell, and others. The analogy of a railway is used to describe the flow of data through a series of functions, similar to how a train travels along tracks.\n\n\n### Result pattern\n\nThe Result pattern (`Result\u003cT\u003e`) is a key concept in ROP. It typically allows to represent the outcome of a function as either a `Success` or a `Failure`, enabling to chain operations together while handling errors gracefully. This pattern is particularly useful in scenarios where you want to avoid throwing exceptions for control flow and instead use a more functional approach to error handling.\n\n![Simple ROP](./Images/SimpleRop.png)\n\nWith ROP, the Result pattern and functional programming techniques, developing a program is like following the steps of a recipe, where the output of a previous step is the input for the next step.\n\n### ROP benefits\n\nSome of the benefits using ROP pattern are:\n\n- **Improved readability:** The code clearly shows the flow of operations and potential failure points, making it easier to understand and maintain.\n- **Improved error handling:** Error handling is comprehensive and consistent throughout the entire process.\n- **Avoiding exceptions for control flow:** It is slow to \"throw and catch\", it is faster to return as Result.\n- **Maintainability:** New steps can be easily added to the reservation process by extending the railway chain.\n- **Testability:** Each operation is isolated and can be tested independently, while the entire flow can be tested as a unit.\n\n### ROP in the sample\n\nHere we implemented a simple example of how ROP and the Result pattern can be used in C#:\n\n- `EventSourcingSample.ROP`: Implementation of the `Result\u003cT\u003e` structure.\n- `EventSourcingSample.WebAPI`: In the ROP dolder, Api Extensions providing `ToActionResult\u003cT\u003e` that converts `Result\u003cT\u003e` into `IActionResult`.\n\nBelow, we can see how the ROP is implemented in the `CreateOrder` use case. The use of the ROP pattern is centered on the `Handler()` method:\n\n```csharp\npublic sealed class CreateOrder\n{\n    public record CreateOrderRequest(DeliveryDetails DeliveryDetails, PaymentInformation PaymentInformation, IEnumerable\u003cProductQuantity\u003e Products);\n\n    public record CreateOrderResponse(Guid OrderId, string Location);\n\n    public interface ICreateOrderHandler\n    {\n        Task\u003cResult\u003cCreateOrderResponse\u003e\u003e Handle(CreateOrderRequest createOrder, CancellationToken cancellationToken = default);\n    }\n\n    public class CreateOrderHandler : ICreateOrderHandler\n    {\n        private readonly IOrderRepository _orderRepository;\n\n        public CreateOrderHandler(IOrderRepository orderRepository)\n        {\n            _orderRepository = orderRepository;\n        }\n\n        public async Task\u003cResult\u003cCreateOrderResponse\u003e\u003e Handle(CreateOrderRequest createOrder, CancellationToken cancellationToken = default)\n        {\n            return await CreateOrder(createOrder)\n                .Async()\n                .Bind(x =\u003e SaveOrder(x, cancellationToken))\n                .Map(x =\u003e new CreateOrderResponse(x.Id, $\"orders/{x.Id}\"));\n        }\n\n        private static Result\u003cOrderDetails\u003e CreateOrder(CreateOrderRequest createOrder)\n        {\n            Guid createdOrderId = Guid.NewGuid();\n\n            var orderDetails = new OrderDetails(createdOrderId);\n            orderDetails.Apply(new OrderCreated(createOrder.DeliveryDetails, createOrder.PaymentInformation, createOrder.Products));\n\n            return orderDetails;\n        }\n\n        private async Task\u003cResult\u003cOrderDetails\u003e\u003e SaveOrder(OrderDetails orderDetails, CancellationToken cancellationToken)\n        {\n            await _orderRepository.Save(orderDetails, cancellationToken);\n            return orderDetails;\n        }\n    }\n}\n```\n\n## MongoDB\n\nThis sample uses MongoDB as the event store. MongoDB is a NoSQL database that stores data in a flexible, JSON-like format. It is well-suited for Event Sourcing due to its ability to handle large volumes of data and its support for complex queries.\n\nIn the `appsettings.json` file, we have the Database and Collection names used in MongoDB:\n\n```json\n{\n  \"EventSourcing\": {\n    \"DatabaseName\": \"EventSourcingSample\",\n    \"CollectionName\": \"EventsOrders\"\n  }\n}\n```\n\nAll the Event Sourcing logic is in the `EventSourcingSample.EventSourcing` project which is linked to MongoBD.\n\nThe dependency container has registered the `IEventStore` interface, which is the one that will be used in the `Aggregate` and which communicates with MongoDb through the `MongoEventStoreManager` class.\n\nWhen saving the `AggregateChangeDto` information, we create an index in MongoDb with the Id (`AggregateId`), type (`AggregateType`) and version (`Version`) automatically.\n\n## Secret Storage\n\nThe Secret Manager tool was used to store the connection string to MongoDB. This allows to keep sensitive information out of the source code and provides a way to manage secrets in a secure manner during development.\n\n### Enable secret storage\n\nThe Secret Manager tool operates on project-specific configuration settings stored in the user profile.\n\nTo use user secrets in an existing project, you need to enable it. You can do this by running the following command in the project directory:\n\n```bash\ndotnet user-secrets init\n```\n\nThis command adds a `UserSecretsId` element within a `PropertyGroup` of the project file, which is used to identify the secrets for this project:\n\n![User Secrets ID](./Images/usersecretsid.png)\n\n\u003e [!NOTE]\n\u003e The `EventSourcingSample.WebAPI` project already has the `UserSecretsId` element in the project file. If you create a new project, you can add this element manually or use the `dotnet user-secrets init` command to generate it automatically.\n\n### Set a secret\n\nDefine an app secret consisting of a key and its value. The secret is associated with the project's `UserSecretsId` value. So, run the following command from the `EventSourcingSample.WebAPI` directory to add your own connection string to MongoDB:\n\n```bash\ndotnet user-secrets set \"MongoDb:ConnectionString\" \"mongodb+srv://\u003cusername\u003e:\u003cdb_password\u003e@\u003cserver\u003e:\u003cport\u003e\" \n```\n\nThis configuration setting is used in the `MongoDbConnectionProvider` class in order to establish a connection to the MongoDB database. Thus, the connection string is retrieved from the secret storage at runtime.\n\n### List the secrets\n\nRun the following command from the directory in which the project file exists:\n\n```bash\ndotnet user-secrets list\n```\n\n### Remove a single secret\n\nIn case the secret needs to be removed, run the following command from the directory in which the project file exists:\n```bash\ndotnet user-secrets remove \"MongoDb:ConnectionString\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjardotnet%2Feventsourcingsample","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjardotnet%2Feventsourcingsample","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjardotnet%2Feventsourcingsample/lists"}