{"id":28523120,"url":"https://github.com/leiratech/event-sourcing-core","last_synced_at":"2025-09-06T14:35:58.753Z","repository":{"id":144113855,"uuid":"261034923","full_name":"leiratech/Event-Sourcing-Core","owner":"leiratech","description":"Strongly typed event sourcing framework that uses CosmosDb as a datastore with strong consistency and resiliency.","archived":false,"fork":false,"pushed_at":"2020-05-25T20:26:34.000Z","size":70,"stargazers_count":8,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-09T10:07:19.714Z","etag":null,"topics":["cosmosdb","cqrs-es","cqrs-framework","csharp","dotnet-core","event-driven","event-sourcing","scalability"],"latest_commit_sha":null,"homepage":"","language":"C#","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/leiratech.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}},"created_at":"2020-05-03T22:44:31.000Z","updated_at":"2020-09-17T12:39:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"bfed7e6d-6d6c-4712-bced-d03c9bfecae6","html_url":"https://github.com/leiratech/Event-Sourcing-Core","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/leiratech/Event-Sourcing-Core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leiratech%2FEvent-Sourcing-Core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leiratech%2FEvent-Sourcing-Core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leiratech%2FEvent-Sourcing-Core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leiratech%2FEvent-Sourcing-Core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leiratech","download_url":"https://codeload.github.com/leiratech/Event-Sourcing-Core/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leiratech%2FEvent-Sourcing-Core/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263842954,"owners_count":23518691,"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","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":["cosmosdb","cqrs-es","cqrs-framework","csharp","dotnet-core","event-driven","event-sourcing","scalability"],"created_at":"2025-06-09T10:07:16.605Z","updated_at":"2025-07-06T03:30:32.475Z","avatar_url":"https://github.com/leiratech.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![#](https://img.shields.io/nuget/v/Leira.EventSourcing.svg?style=flat-square)](https://www.nuget.org/packages/Leira.EventSourcing)\n![#](https://img.shields.io/github/license/leiratech/Event-Sourcing-Core?style=flat-square)\n\n# Leira.EventStore\nStrongly typed event sourcing framework that useses CosmosDb as a datastore with strong consistency and resiliency.\n\n## About Event Sourcing\nTraditionally, developers used to store state in databases, however this can be a problem as it doesn't track what happened to get to that end state. Making data auditing and applications debugging more difficult. Event Sourcing is the concept of storing all events that lead to the current state, which allows you to construct your endstate anytime on the fly.\n\n## Terminologies\n### Aggregate\nAn aggregate is your state object, eg. User, this is your UserAggregate. The aggregate can execute Commands \u0026 apply Events.\n\n### Command\nA command is the action that the user takes in order to take an action, eg. Signup, ChangePassword ...etc. The command is responsible to validate the current state and that it can take the necessary action, however it doesn't change the state it self. Each command emmit Event(s) that change the state of the aggregate.\n\n### Event\nEmitted by Commands, events are units of change against an Aggregate. Each event changes the state of the Aggregate appropriately.\n\n## Features\n- Commands Idepmotency (Prevents the same command from running twice.\n- Automatic Commands Storage.\n- Automatic Events Versioning.\n- Automatic Snapshotting.\n- Cross Servers Concurrency and Consistency.\n- Strong or Loose Consistency options.\n\n# Usage\n## Setup\nIn the framework, there 3 main abstract classes that you need to inherit from.\n- Aggregate\n- Event\n- Command\n- Error Class/Struct/Enum which you will use to report errors in command execution\n\n## Prepare\n### Create the Error Enum\n``` c#\npublic enum Error\n{\n  None = 0,\n  UserExist = 1,\n}\n```\n\n### Create the Aggregate\nThe User aggregate must inherit from Aggregate, which means you have to also initialize the constructor. This is simple, everything is done using dependency injection and you don't have to pass those parameters. In reality, you don't even have to create an instance of your Aggregate as it will be created for you. If you need additional parameters in the constructor, add them, we will explain this in details further below.\nNotice that string Id is inherited from `Aggregate\u003cTError\u003e` so you don't have to add that. Add all your other properties.\n``` c#\n public class User : Aggregate\u003cError\u003e\n    {\n        public User(string aggregateId, ConsistencyRestriction consistencyOption, Container eventsContainer, Container snapshotsContainer, Container commandsContainer) : base(aggregateId, consistencyOption, eventsContainer, snapshotsContainer, commandsContainer)\n        {\n        }\n\n        public string Name { get; set; }\n        public DateTime DateOfBirth { get; set; }\n        public DateTime SignupTime { get; set; }\n        public string Country { get; set; }\n        public bool ProfileSet { get; set; }\n    }\n```\n\n### Create the Command\nThe command **MUST** inherit from `Leira.EventSourcing.Abstracts.Command`.\n``` c#\npublic class SignupUser : Command\n{\n  public string Name { get; set; }\n  public DateTime DateOfBirth { get; set; }\n  public string IpAddress { get; set; }\n}\n```\n\n### Create the Event\nThe event **MUST** inherit from `Leira.EventSourcing.Abstracts.Event`.\n``` c#\npublic class UserSignedup : Event\n{\n  public string Name { get; set; }\n  public DateTime DateOfBirth { get; set; }\n  public DateTime SignupTime { get; set; }\n  public string Country { get; set; }\n}\n```\n\n### Expand the aggregate\nHere, we need to tell the aggregate what Commands it accepts, and how to handle Events. \nTo make the Aggregate accept a command, Implment the interface `IAsyncComandExecutor\u003cTCommand, TError\u003e` or `IComandExecutor\u003cTCommand, TError\u003e` depending on your need.\nTo make the Aggregate handle an event, implment the interface `IAsyncEventHandler\u003cTEvent\u003e` or `IAsyncEventHandler\u003cTEvent\u003e` depending on your need.\n``` c#\npublic class User : Aggregate\u003cError\u003e,\n                    IAsyncCommandExecutor\u003cSignupUser, Error\u003e,\n                    IAsyncEventHandler\u003cUserSignedup\u003e\n{\n\n    public User(string aggregateId, ConsistencyRestriction consistencyOption, Container eventsContainer, Container snapshotsContainer, Container commandsContainer) : base(aggregateId, consistencyOption, eventsContainer, snapshotsContainer, commandsContainer)\n    {\n    }\n\n    public string Name { get; set; }\n    public DateTime DateOfBirth { get; set; }\n    public DateTime SignupTime { get; set; }\n    public string Country { get; set; }\n    public bool ProfileSet { get; set; }\n    \n    // This will be called by the framework. Don't call it your self unless you are simulating execution without persisting in the database.\n    public async Task\u003cCommandResult\u003cError\u003e\u003e ExecuteCommandAsync(SignupUser command)\n    {\n        if (this.ProfileSet)\n        {\n            return new CommandResult\u003cError\u003e(Error.UserExist);\n        }\n\n        return new CommandResult\u003cError\u003e(Error.None, new UserSignedup()\n        {\n            // Only fill your own properties, all inherited properties from Event will be overwritten by the framework.\n            Name = command.Name,\n            SignupTime = DateTime.UtcNow,\n            DateOfBirth = command.DateOfBirth,\n            //Country = await FindCountryFromIp(command.IpAddress).ConfigureAwait(false);\n\n        }) ;\n\n    }\n    \n    // This will be called by the framework. Don't call it your self unless you are simulating execution without persisting in the database.    \n    public async Task ApplyEventAsync(UserSignedup @event)\n    {\n        // Here we simply apply the changes.\n        Name = @event.Name;\n        SignupTime = @event.SignupTime;\n        DateOfBirth = @event.DateOfBirth;\n        //... etc\n    }\n}\n```\n\n### Linking Things Together\nNow that all of our classes are setup, we can use our aggregate. It is recommended that you use dependency injection and configure it Singleton by using 'services.AddSingleton()' in order to achieve maximum performance.\nFirst we need the EventStoreClient\u003cTError\u003e, when your application is first run, it will automatically create all the necessary collections and databases (1 database and 3 collections, using 400 RU/S shared).\n \nIf you want to create the Database and Collections yourself, you MUST add 'sequenceNumber' as a unique key in the 'events' Collection ONLY.\n\nIn *startup.cs*, add the following.\n``` c#\nservices.AddSingleton(sp =\u003e new EventStoreClient\u003cError\u003e(sp, new ConfigurationOptions(\"https://{cosmosDbAccount}.documents.azure.com:443/\", \"{CosmosDbAccessKey}\", \"cosmosDbDatabaseName\")));\n// There are additional constructor default values that you can change, which includes the collection names of Snapshots, Commands and Events. Also it allows you to control your creation of DB and Collections RU/s.\n```\n### Getting an Aggregate\nIn any place where the EventStoreClient is injected.\n``` c#\nvar user1 = await eventStoreClient.GetOrCreateAggregateAsync\u003cUser\u003e(\"User1\", ConsistencyRestriction.Loose).ConfigureAwait(false);\n// Additionally, you can pass any additional parameters to the constructor by passing the \"params object[] customParameters\". If your Custom Parameters in the constructor are injected using dependency injection, the framework will automatically load them. Remember to ONLY inject Singletons. The aggregate is a long living object and injecting Transeint or Scoped may result in problems.\n// Now we got the signal from the user to signup. Let's sign them up.\n// Be careful not to use ExecuteCommandAsync which is your method, this will not run anything (as i did in a previous version of this documentation).\nvar result = await user1.ExecuteAsync(new SignupUser()\n  {\n      Name = objectFromFrontend.Name,\n      IpAddress = objectFromFrontend.IpAddress,\n      DateOfBirth = objectFromFrontend.DateOfBirth,\n      Id = Guid.NewGuid().ToString() // Assigning an ID enables Idemptency Check. However this value MUST come from your Frontend. If the Id is not set, the command will not be saved in CosmosDb. This prevents (forexample) the same command from executing twice when the user clicks a button again instead of waiting.\n  }).ConfigureAwait(false);\n```\n\nThe 'result' object contains 3 important properties.\n* `public TError CommandError { get; set; }` This is your Error that you generated while executing the command.\n* `public Error EventSourcingError { get; internal set; }` This is your Error from the framework:\n  * `None` indicates success.\n  * `IdempotencyFailure` indicates that the same command has been executed before and if events persisted, they were reversed.\n  * `ConsistencyConflict` If you chose `ConsistencyRestriction.Strict`. Having this result means that another command changed the state of the aggregate (Even if the change happened on another server) and now the operation is invalid and reversed.\n* `public IEnumerable\u003cEvent\u003e Events { get; set; }` These are the events your execution emitted, you can take these and send them over ServiceBus, RabbitMQ...etc for further async processing.\n\nAnd we are done! simple, yet effective. The framework will not return unless: \n* Command and Events and Snapshot fully persisted in CosmosDb or;\n* Failure is recoverable and will continue to retry until successful or reversed;\n* Failure cannot be recovered from automatically (Due to consistency level).\n\nFor feedback, questions and bugs, please open a new Issue.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleiratech%2Fevent-sourcing-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleiratech%2Fevent-sourcing-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleiratech%2Fevent-sourcing-core/lists"}