CQRS and Event Sourcing library for .NET Core.
Nuget Packages
Via Package Manager
Install-Package Weapsy.Cqrs
Or via .NET CLI
dotnet add package Weapsy.Cqrs
Or via Paket CLI
paket add Weapsy.Cqrs
For Event Sourcing, an event store data provider needs to be installed. There are few already available but more are coming up. Install one between CosmosDB Sql (DocumnentDB), CosmosDB MongoDB, SqlServer, MySql, PostgreSql and Sqlite. The following example is for the MongoDB package.
Via Package Manager
Install-Package Weapsy.Cqrs.EventStore.CosmosDB.MongoDB
Or via .NET CLI
dotnet add package Weapsy.Cqrs.EventStore.CosmosDB.MongoDB
Or via Paket CLI
paket add Weapsy.Mediator.Cqrs.EventStore.CosmosDB.MongoDB
Working examples for different database providers are available in the examples folder of the repository https://github.com/Weapsy/Weapsy.Cqrs/tree/master/examples
In ConfigureServices method of Startup.cs:
services.AddWeapsyCqrs(typeof(CreateProduct), typeof(GetProduct));
CreateProduct is an sample command and GetProduct is a sample query. In this scenario, commands and queries are in two different assemblies. Both assemblies need to be registered. In order to use the event sourcing functionalities, an event store provider needs to be added as well. Weapsy.Cqrs currently supports the following data providers:
- CosmosDB SQL (DocumentDB)
- CosmosDB MongoDB
- SqlServer
- MySql
- PostgreSql
- Sqlite
Please install the nuget package of your choice and register the event store:
services.AddWeapsyCqrsEventStore(Configuration);
In order to use CosmosDB you need to install the free emulator (https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator) and add some settings to the appsettings.json.
For CosmosDB SQL (DocumentDB):
{
"EventStoreConfiguration": {
"ServerEndpoint": "https://localhost:8081",
"AuthKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"DatabaseId": "EventStore",
"AggregateCollectionId": "Aggregates",
"EventCollectionId": "Events"
}
}
For CosmosDB MongoDB:
{
"EventStoreConfiguration": {
"ConnectionString": "mongodb://localhost:C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==@localhost:10255/admin?ssl=true",
"DatabaseName": "EventStore",
"AggregateCollectionName": "Aggregates",
"EventCollectionName": "Events"
}
}
And add the following check in the Configure method (for CosmosDB SQL (DocumentDB) only):
public void Configure(IApplicationBuilder app, IOptions<CosmosDBSettings> settings)
{
app.EnsureEventStoreDbCreated(settings);
}
For all the others based on Entity Framework Core add just the connection string to appsettings.json:
{
"EventStoreConfiguration": {
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EventStore;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
And the following line can be added to the Configure method to ensure that the database in installed:
public void Configure(IApplicationBuilder app, EventStoreDbContext eventStoreDbContext)
{
eventStoreDbContext.Database.Migrate();
}
There is a single interface to be used, IDispatcher in Weapsy.Cqrs namespace. Note that all handlers are available as asynchronous and as well as synchronous, but for these examples I'm using the asynchronous versions only.
There are 3 kinds of messages:
- Command, single handler
- Event, multiple handlers
- Query/Result, single handler that returns a result
First, create a message:
public class DoSomething : ICommand
{
}
Next, create the handler:
public class DoSomethingHandlerAsync : ICommandHandlerAsync<DoSomething>
{
public Task HandleAsync(DoSomething command)
{
await _myService.MyMethodAsync();
}
}
And finally, send the command using the dispatcher:
var command = new DoSomething();
await _dispatcher.SendAsync(command)
Using the SendAndPublishAsync method, the dispatcher will automatically publish the events returned by the handler.
First, create a command and an event:
public class DoSomething : ICommand
{
}
public class SomethingHappened : IEvent
{
}
Next, create the handler:
public class DoSomethingHandlerAsync : ICommandHandlerWithEventsAsync<DoSomething>
{
public Task<IEnumerable<IEvent>> HandleAsync(DoSomething command)
{
await _myService.MyMethodAsync();
return new List<IEvent>{new SomethingHappened()};
}
}
And finally, send the command and publish the events using the dispatcher:
var command = new DoSomething();
await _dispatcher.SendAndPublishAsync(command)
First, create a message:
public class SomethingHappened : IEvent
{
}
Next, create one or more handlers:
public class SomethingHappenedHandlerAsyncOne : IEventHandlerAsync<SomethingHappened>
{
public Task HandleAsync(SomethingHappened @event)
{
await _myService.MyMethodAsync();
}
}
public class SomethingHappenedHandlerAsyncTwo : IEventHandlerAsync<SomethingHappened>
{
public Task HandleAsync(SomethingHappened @event)
{
await _myService.MyMethodAsync();
}
}
And finally, publish the event using the mediator:
var @event = new SomethingHappened();
await _dispatcher.PublishAsync(@event)
First, create a model and a message:
public class Something
{
public int Id { get; set; }
}
public class GetSomething : ICommand
{
public int Id { get; set; }
}
Next, create the handler:
public class GetSomethingQueryHandlerAsync : IQueryHandlerAsync<GetSomething, Something>
{
public async Task<Something> RetrieveAsync(GetSomething query)
{
return await _db.Somethings.FirstOrDefaultAsync(x => x.Id == query.Id);
}
}
And finally, get the result using the dispatcher:
var query = new GetSomething{ Id = 123 };
var something = await _dispatcher.GetResultAsync<GetSomething, Something>(query);
Using the SendAndPublishAsync<IDomainCommand, IAggregateRoot> method, the dispatcher will automatically publish the events of the aggregate returned by the handler and save those events to the event store. A working example can be found at https://github.com/Weapsy/Weapsy.Cqrs/tree/master/examples
First, create a command and an event:
public class CreateProduct : DomainCommand
{
public string Title { get; set; }
}
public class ProductCreated : DomainEvent
{
public string Title { get; set; }
}
Next, create a domain object that inherits from AggregateRoot. This is how a Procuct class might look like:
public class Product : AggregateRoot
{
public string Title { get; private set; }
public Product()
{
}
public Product(Guid id, string title) : base(id)
{
if (string.IsNullOrEmpty(title))
throw new ApplicationException("Product title is required.");
AddEvent(new ProductCreated
{
AggregateId = Id,
Title = title
});
}
public void UpdateTitle(string title)
{
if (string.IsNullOrEmpty(title))
throw new ApplicationException("Product title is required.");
AddEvent(new ProductTitleUpdated
{
AggregateId = Id,
Title = title
});
}
public void Apply(ProductCreated @event)
{
Id = @event.AggregateId;
Title = @event.Title;
}
public void Apply(ProductTitleUpdated @event)
{
Title = @event.Title;
}
}
Note that the empty constructor is required in order to create a new object. After every command has been executed, an event is added to the pending event list calling the AddEvent method. The Apply methods are called automatically when new events are added and are also used to load the object from the history when GetById method of the Repository is called. Create the first handler:
public class CreateProductHandlerAsync : ICommandHandlerWithAggregateAsync<CreateProduct>
{
public async Task<IAggregateRoot> HandleAsync(CreateProduct command)
{
await Task.CompletedTask;
var product = new Product(command.AggregateId, command.Title);
return product;
}
}
Send the command using the dispatcher:
var command = new CreateProduct
{
AggregateId = Guid.NewGuid(),
Title = "My brand new product"
};
await _dispatcher.SendAndPublishAsync<CreateProduct, Product>(command)
At this stage, we might want to create our read model. It can be achieved by creating an event handler:
public class ProductCreatedHandlerAsync : IEventHandlerAsync<ProductCreated>
{
public async Task HandleAsync(ProductCreated @event)
{
await Task.CompletedTask;
var model = new ProductViewModel
{
Id = @event.AggregateId,
Title = @event.Title
};
FakeReadDatabase.Products.Add(model);
}
}
At this point, the aggregate and the first event have been saved to the event store and the product can be retrieved from the event store using the repository.
New commands, events and handlers can now be added:
public class UpdateProductTitle : DomainCommand
- {
public string Title { get; set; }
}
public class ProductTitleUpdated : DomainEvent
{
public string Title { get; set; }
}
public class UpdateProductTitleHandlerAsync : ICommandHandlerWithAggregate<UpdateProductTitle>
{
private readonly IRepository<Product> _repository;
public UpdateProductTitleHandlerAsync(IRepository<Product> repository)
{
_repository = repository;
}
public async Task<IAggregateRoot> HandleAsync(UpdateProductTitle command)
{
var product = await _repository.GetByIdAsync(command.AggregateId);
if (product == null)
throw new ApplicationException("Product not found.");
product.UpdateTitle(command.Title);
return product;
}
}
public class ProductTitleUpdatedHandlerAsync : IEventHandlerAsync<ProductTitleUpdated>
{
public async Task HandleAsync(ProductTitleUpdated @event)
{
await Task.CompletedTask;
var model = FakeReadDatabase.Products.Find(x => x.Id == @event.AggregateId);
model.Title = @event.Title;
}
}
As per prevoius example, the dispatcher can be used to update the product.
await dispatcher.SendAndPublishAsync<UpdateProductTitle, Product>(new UpdateProductTitle
{
AggregateId = productId,
Title = "Updated product title"
});
A new event is saved and the read model is updated using the event handler. Next time the aggregate is loaded from the repository, two events will be applied in order to recreate the current state.
Note the two optional properties can be saved for the domain events:
- UserId (Guid)
- Source (string)
- Add settings for custom table names for entity framework
- Add more event store providers
- Add message bus integration
- Azure Service Bus
- RabbitMQ