DEV Community

Ahmet Küçükoğlu
Ahmet Küçükoğlu

Posted on

9 5

Event Sourcing with ASP.NET Core – 01 Store

This article was originally published at: https://www.ahmetkucukoglu.com/en/event-sourcing-with-asp-net-core-01-store/

1. Introduction

I recommend you to read the article below before applying this example tutorial.

https://www.ahmetkucukoglu.com/en/what-is-event-sourcing/

In the article I have mentioned above, I had formed a sentence as follows.

There is a technology called “Event Store” in the .NET world for Event Sourcing. This technology offers solutions for “Aggregate” and “Projection”. In other words, in addition to providing the store where we can record the events, it also provides the “Messaging” and “Projection” services, which are necessary for us to record in “Query” databases.

In this article, we will deal with the store section of the Event Store. In other words, we will deal with the database feature where we can save events. In the next article, we will deal with the messaging part.

As an sample application, I chose the classic Kanban Board sample.

Our RESTful API endpoints will be as follows.

[POST] api/tasks/{id}/create

[PATCH] api/tasks/{id}/assign

[PATCH] api/tasks/{id}/move

[PATCH] api/tasks/{id}/complete

Our events will be as follows.

CreatedTask : When Task is created, an event with this name will be created.

AssignedTask : An event with this name will be created when Task is assigned to someone.

MovedTask : When Task is moved to a section (In Progress, Done), an event with this name will be created.

CompletedTask : When Task is completed, an event with this name will be created.

2. Installing the Event Store

We run the Event Store in Docker with the following command line.

docker run -d --name eventstore -p 2113:2113 -p 1113:1113 eventstore/eventstore
Enter fullscreen mode Exit fullscreen mode

When the Event Store run, you can enter the panel from the address below. The default username is "admin" and the password is "changeit".

http://localhost:2113/

3. Creating the API Project

We create an ASP.NET Core Web API Application project named "EventSourcingTaskApp". We install the "EventStore.Client" package to the project with the following command line.

dotnet add package EventStore.Client -v 5.0.6
Enter fullscreen mode Exit fullscreen mode

We add Event Store connection information to appsettings.json as follows.

{
"EventStore": {
"ConnectionString": "ConnectTo=tcp://admin:changeit@localhost:1113; DefaultUserCredentials=admin:changeit;",
"ConnectionName": "Task"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

In the Startup.cs file, we connect the Event Store and add it to the DI Container.

namespace EventSourcingTaskApp
{
using EventStore.ClientAPI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var eventStoreConnection = EventStoreConnection.Create(
connectionString: Configuration.GetValue<string>("EventStore:ConnectionString"),
builder: ConnectionSettings.Create().KeepReconnecting(),
connectionName: Configuration.GetValue<string>("EventStore:ConnectionName"));
eventStoreConnection.ConnectAsync().GetAwaiter().GetResult();
services.AddSingleton(eventStoreConnection);
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
view raw Startup.cs hosted with ❤ by GitHub

4. Aggregate Base Class and Aggregate Repository

Let's add two folders named "Core" and "Infrastructure" to the project. We will add Task-related entities, events and exceptions to the Core folder. For the Infrastructure folder, we will add our repository class that will connect the Event Store.

4.1. Aggregate Base Class

Let's add a folder named "Framework" inside the Core folder and add a class named "Aggregate.cs" into it. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core.Framework
{
using System;
using System.Collections.Generic;
using System.Linq;
public abstract class Aggregate
{
readonly IList<object> _changes = new List<object>();
public Guid Id { get; protected set; } = Guid.Empty;
public long Version { get; private set; } = -1;
protected abstract void When(object @event);
public void Apply(object @event)
{
When(@event);
_changes.Add(@event);
}
public void Load(long version, IEnumerable<object> history)
{
Version = version;
foreach (var e in history)
{
When(e);
}
}
public object[] GetChanges() => _changes.ToArray();
}
}
view raw Aggregate.cs hosted with ❤ by GitHub

This is a standard class. Aggregates are derived from this base class. In our example, Task is an aggregate and this will be derived from the base class.

In the line 9, we define the variable where aggregate events will be stored.

In the line 11 we state that aggregate will have an Id.

In the line 12 we state that the default version of aggregate is "-1".

In the line 16, we write the method that will add events to the variable defined on the line 9.

In the line 23, we write the method that will apply the events to aggregate. The final version of aggregate will be created by running this method for each event read from the Event Store.

In the line 33, we write the method that returns the events on aggregate. While sending events to the Event Store, this method will be run and events will be received.

4.2. Aggregate Repository

Let's add a class named "AggregateRepository.cs" inside the Infrastructure folder. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Infrastructure
{
using EventSourcingTaskApp.Core.Framework;
using EventStore.ClientAPI;
using System;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class AggregateRepository
{
private readonly IEventStoreConnection _eventStore;
public AggregateRepository(IEventStoreConnection eventStore)
{
_eventStore = eventStore;
}
public async Task SaveAsync<T>(T aggregate) where T : Aggregate, new()
{
var events = aggregate.GetChanges()
.Select(@event => new EventData(
Guid.NewGuid(),
@event.GetType().Name,
true,
Encoding.UTF8.GetBytes(JsonSerializer.Serialize(@event)),
Encoding.UTF8.GetBytes(@event.GetType().FullName)))
.ToArray();
if (!events.Any())
{
return;
}
var streamName = GetStreamName(aggregate, aggregate.Id);
var result = await _eventStore.AppendToStreamAsync(streamName, ExpectedVersion.Any, events);
}
public async Task<T> LoadAsync<T>(Guid aggregateId) where T : Aggregate, new()
{
if (aggregateId == Guid.Empty)
throw new ArgumentException("Value cannot be null or whitespace.", nameof(aggregateId));
var aggregate = new T();
var streamName = GetStreamName(aggregate, aggregateId);
var nextPageStart = 0L;
do
{
var page = await _eventStore.ReadStreamEventsForwardAsync(
streamName, nextPageStart, 4096, false);
if (page.Events.Length > 0)
{
aggregate.Load(
page.Events.Last().Event.EventNumber,
page.Events.Select(@event => JsonSerializer.Deserialize(Encoding.UTF8.GetString(@event.OriginalEvent.Data), Type.GetType(Encoding.UTF8.GetString(@event.OriginalEvent.Metadata)))
).ToArray());
}
nextPageStart = !page.IsEndOfStream ? page.NextEventNumber : -1;
} while (nextPageStart != -1);
return aggregate;
}
private string GetStreamName<T>(T type, Guid aggregateId) => $"{type.GetType().Name}-{aggregateId}";
}
}

This is also a standard class. We use this repository when sending events to the Event Store or receiving events from the Event Store.

4.2.1. Sending an Event (Append Events to Stream)

In the line 22, we take the events on aggregate and map them to the EventData class. Event Store stores events in the EventData type.

As the first parameter, it takes for the event's id.

As the second parameter, it takes for the name of the event. Example event name; CreatedTask, AssignedTask etc.

As the third parameter, it takes whether the event data is in json type.

As the fourth parameter, it takes the event data. Since it takes in byte array type, serialize and encoding processes are performed.

As the fifth parameter, it takes the metadata. This parameter can be passed as null but we pass the type of the event class as "fullname" so that we can use this class type when deserializing the events. Example fullname; EventSourcingTaskApp.Core.Events.CreatedTask.

In the line 36, we set the stream name. Event aggregates in the Event Store is called stream. So aggregate is expressed as a stream in Event Store. Example stream name; Task-518e7c15-36ac-4edb-8fa5-931fb8ffa3a5.

In the 38th line, events are recorded in the Event Store.

Örnek Stream

4.2.2. Reading an Event (Read Events from Stream)

In the line 47, we set the stream name.

In line 53, events are received from the Event Store in order according to the version numbers in the loop.

In the 58th line, the load method of aggregate is called and the events are applied to aggregate and the final form of aggregate is created.

Let's add AggregateRepository class to DI Container in the Startup.cs file as below.

namespace EventSourcingTaskApp
{
using EventSourcingTaskApp.Infrastructure;
using EventStore.ClientAPI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var eventStoreConnection = EventStoreConnection.Create(
connectionString: Configuration.GetValue<string>("EventStore:ConnectionString"),
builder: ConnectionSettings.Create().KeepReconnecting(),
connectionName: Configuration.GetValue<string>("EventStore:ConnectionName"));
eventStoreConnection.ConnectAsync().GetAwaiter().GetResult();
services.AddSingleton(eventStoreConnection);
services.AddTransient<AggregateRepository>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
view raw Startup.cs hosted with ❤ by GitHub

5. Defining Task and Use Cases

In this part, we will start creating events, exceptions, and use cases related to Task.

5.1. Defining Task

Let's add an aggregate class named "Task.cs" in the Core folder. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core
{
using EventSourcingTaskApp.Core.Framework;
public class Task : Aggregate
{
public string Title { get; private set; }
public BoardSections Section { get; private set; }
public string AssignedTo { get; private set; }
public bool IsCompleted { get; private set; }
protected override void When(object @event)
{
}
}
}
view raw Task.cs hosted with ❤ by GitHub

Task will have the Title, Section, AssignedTo and IsCompleted.

Again, let's add a class named "BoardSections.cs" into Core. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core
{
public enum BoardSections
{
Open = 1,
InProgress = 2,
Done = 3
}
}
5.2. Defining Exceptions

Let's add a folder named "Exceptions" inside the Core folder and add a class named "TaskAlreadyCreatedException.cs" in it. Let's paste the code below into the class. If it tries to create a task with the same id information, we will throw this error.

namespace EventSourcingTaskApp.Core.Exceptions
{
using System;
public class TaskAlreadyCreatedException : Exception
{
public TaskAlreadyCreatedException() : base("Task already created.") { }
}
}

Let's add a class named "TaskCompletedException.cs" in the Exceptions folder inside the Core folder. Let's paste the code below into the class. If any process is tried on the completed task, we will throw this error.

namespace EventSourcingTaskApp.Core.Exceptions
{
using System;
public class TaskNotFoundException : Exception
{
public TaskNotFoundException() : base("Task not found.") { }
}
}

Let's add a class named "TaskNotFoundException.cs" inside the Exceptions folder inside the Core folder. Let's paste the code below into the class. If Task is not found, we will throw this error.

namespace EventSourcingTaskApp.Core.Exceptions
{
using System;
public class TaskCompletedException : Exception
{
public TaskCompletedException() : base("Task is completed.") { }
}
}

Now that we have created the Task aggregate and exceptions, we can start writing use cases related to the Task.

5.3. Create Task

When Task is created; We will keep task id, its title and the information of who created the task, as event data.

CreatedTask

Let's add a folder named "Events" inside the Core folder and add an event class named "CreatedTask.cs" into it. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core.Events
{
using System;
public class CreatedTask
{
public Guid TaskId { get; set; }
public string CreatedBy { get; set; }
public string Title { get; set; }
}
}
view raw CreatedTask.cs hosted with ❤ by GitHub

Let's edit the Task.cs class as follows.

With the Create method, we store the CreatedTask event on aggregate, so that we can send these stored events to the Event Store.

In the line 19, we apply the CreatedTask event from the Event Store to aggregate.

namespace EventSourcingTaskApp.Core
{
using EventSourcingTaskApp.Core.Events;
using EventSourcingTaskApp.Core.Exceptions;
using EventSourcingTaskApp.Core.Framework;
using System;
public class Task : Aggregate
{
public string Title { get; private set; }
public BoardSections Section { get; private set; }
public string AssignedTo { get; private set; }
public bool IsCompleted { get; private set; }
protected override void When(object @event)
{
switch (@event)
{
case CreatedTask x: OnCreated(x); break;
}
}
public void Create(Guid taskId, string title, string createdBy)
{
if (Version >= 0)
{
throw new TaskAlreadyCreatedException();
}
Apply(new CreatedTask
{
TaskId = taskId,
CreatedBy = createdBy,
Title = title,
});
}
#region Event Handlers
private void OnCreated(CreatedTask @event)
{
Id = @event.TaskId;
Title = @event.Title;
Section = BoardSections.Open;
}
#endregion
}
}
view raw Task.cs hosted with ❤ by GitHub
5.4. Assign Task

When Task is assigned to someone; We will keep task id , information of who has assigned the task and to whom the task has been assigned as event data.

AssignedTask

Let's add an event class named "AssignedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core.Events
{
using System;
public class AssignedTask
{
public Guid TaskId { get; set; }
public string AssignedBy { get; set; }
public string AssignedTo { get; set; }
}
}
view raw AssignedTask.cs hosted with ❤ by GitHub

Let's edit the Task.cs class as follows.

With the Assign method, we store the AssignedTask event on aggregate so that we can send these stored events to the Event Store.

In the line 20, we apply AssignedTask event received from Event Store to aggregate.

namespace EventSourcingTaskApp.Core
{
using EventSourcingTaskApp.Core.Events;
using EventSourcingTaskApp.Core.Exceptions;
using EventSourcingTaskApp.Core.Framework;
using System;
public class Task : Aggregate
{
public string Title { get; private set; }
public BoardSections Section { get; private set; }
public string AssignedTo { get; private set; }
public bool IsCompleted { get; private set; }
protected override void When(object @event)
{
switch (@event)
{
case CreatedTask x: OnCreated(x); break;
case AssignedTask x: OnAssigned(x); break;
}
}
public void Create(Guid taskId, string title, string createdBy)
{
if (Version >= 0)
{
throw new TaskAlreadyCreatedException();
}
Apply(new CreatedTask
{
TaskId = taskId,
CreatedBy = createdBy,
Title = title,
});
}
public void Assign(string assignedTo, string assignedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new AssignedTask
{
TaskId = Id,
AssignedBy = assignedBy,
AssignedTo = assignedTo
});
}
#region Event Handlers
private void OnCreated(CreatedTask @event)
{
Id = @event.TaskId;
Title = @event.Title;
Section = BoardSections.Open;
}
private void OnAssigned(AssignedTask @event)
{
AssignedTo = @event.AssignedTo;
}
#endregion
}
}
view raw Task.cs hosted with ❤ by GitHub
5.5. Move Task

When Task is moved to "In Progress" or "Done" section; We will keep the task id, information of who has moved the task and to which part the task has been moved, as event data

MovedTask

Let's add an event class named "MovedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core.Events
{
using System;
public class MovedTask
{
public Guid TaskId { get; set; }
public string MovedBy { get; set; }
public BoardSections Section { get; set; }
}
}
view raw MovedTask.cs hosted with ❤ by GitHub

Let's edit the Task.cs class as follows.

We store the MovedTask event on aggregate with the Move method, so that we can send these stored events to the Event Store.

In the line 21, we apply the MovedTask event received from the Event Store to aggregate.

namespace EventSourcingTaskApp.Core
{
using EventSourcingTaskApp.Core.Events;
using EventSourcingTaskApp.Core.Exceptions;
using EventSourcingTaskApp.Core.Framework;
using System;
public class Task : Aggregate
{
public string Title { get; private set; }
public BoardSections Section { get; private set; }
public string AssignedTo { get; private set; }
public bool IsCompleted { get; private set; }
protected override void When(object @event)
{
switch (@event)
{
case CreatedTask x: OnCreated(x); break;
case AssignedTask x: OnAssigned(x); break;
case MovedTask x: OnMoved(x); break;
}
}
public void Create(Guid taskId, string title, string createdBy)
{
if (Version >= 0)
{
throw new TaskAlreadyCreatedException();
}
Apply(new CreatedTask
{
TaskId = taskId,
CreatedBy = createdBy,
Title = title,
});
}
public void Assign(string assignedTo, string assignedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new AssignedTask
{
TaskId = Id,
AssignedBy = assignedBy,
AssignedTo = assignedTo
});
}
public void Move(BoardSections section, string movedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new MovedTask
{
TaskId = Id,
MovedBy = movedBy,
Section = section
});
}
#region Event Handlers
private void OnCreated(CreatedTask @event)
{
Id = @event.TaskId;
Title = @event.Title;
Section = BoardSections.Open;
}
private void OnAssigned(AssignedTask @event)
{
AssignedTo = @event.AssignedTo;
}
private void OnMoved(MovedTask @event)
{
Section = @event.Section;
}
#endregion
}
}
view raw Task.cs hosted with ❤ by GitHub
5.6. Complete Task

When Task is completed; We will keep task id, who has completed the task as event data.

CompletedTask

Let's add an event class named "CompletedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.

namespace EventSourcingTaskApp.Core.Events
{
using System;
public class CompletedTask
{
public Guid TaskId { get; set; }
public string CompletedBy { get; set; }
}
}

Let's edit the Task.cs class as follows.

With the Complete method, we store the CompletedTask event on aggregate so that we can send these stored events to the Event Store.

In the line 22, we apply the CompletedTask event received from the Event Store to aggregate.

namespace EventSourcingTaskApp.Core
{
using EventSourcingTaskApp.Core.Events;
using EventSourcingTaskApp.Core.Exceptions;
using EventSourcingTaskApp.Core.Framework;
using System;
public class Task : Aggregate
{
public string Title { get; private set; }
public BoardSections Section { get; private set; }
public string AssignedTo { get; private set; }
public bool IsCompleted { get; private set; }
protected override void When(object @event)
{
switch (@event)
{
case CreatedTask x: OnCreated(x); break;
case AssignedTask x: OnAssigned(x); break;
case MovedTask x: OnMoved(x); break;
case CompletedTask x: OnCompleted(x); break;
}
}
public void Create(Guid taskId, string title, string createdBy)
{
if (Version >= 0)
{
throw new TaskAlreadyCreatedException();
}
Apply(new CreatedTask
{
TaskId = taskId,
CreatedBy = createdBy,
Title = title,
});
}
public void Assign(string assignedTo, string assignedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new AssignedTask
{
TaskId = Id,
AssignedBy = assignedBy,
AssignedTo = assignedTo
});
}
public void Move(BoardSections section, string movedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new MovedTask
{
TaskId = Id,
MovedBy = movedBy,
Section = section
});
}
public void Complete(string completedBy)
{
if (Version == -1)
{
throw new TaskNotFoundException();
}
if (IsCompleted)
{
throw new TaskCompletedException();
}
Apply(new CompletedTask
{
TaskId = Id,
CompletedBy = completedBy
});
}
#region Event Handlers
private void OnCreated(CreatedTask @event)
{
Id = @event.TaskId;
Title = @event.Title;
Section = BoardSections.Open;
}
private void OnAssigned(AssignedTask @event)
{
AssignedTo = @event.AssignedTo;
}
private void OnMoved(MovedTask @event)
{
Section = @event.Section;
}
private void OnCompleted(CompletedTask @event)
{
IsCompleted = true;
}
#endregion
}
}
view raw Task.cs hosted with ❤ by GitHub

6. Preparing API Endpoints

Let's create a controller named "TasksController" and paste the code below.

namespace EventSourcingTaskApp.Controllers
{
using EventSourcingTaskApp.Core;
using EventSourcingTaskApp.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
[Route("api/tasks/{id}")]
[ApiController]
[Consumes("application/x-www-form-urlencoded")]
public class TasksController : ControllerBase
{
private readonly AggregateRepository _aggregateRepository;
public TasksController(AggregateRepository aggregateRepository)
{
_aggregateRepository = aggregateRepository;
}
[HttpPost, Route("create")]
public async Task<IActionResult> Create(Guid id, [FromForm] string title)
{
var aggregate = await _aggregateRepository.LoadAsync<Core.Task>(id);
aggregate.Create(id, title, "Ahmet KÜÇÜKOĞLU");
await _aggregateRepository.SaveAsync(aggregate);
return Ok();
}
[HttpPatch, Route("assign")]
public async Task<IActionResult> Assign(Guid id, [FromForm] string assignedTo)
{
var aggregate = await _aggregateRepository.LoadAsync<Core.Task>(id);
aggregate.Assign(assignedTo, "Ahmet KÜÇÜKOĞLU");
await _aggregateRepository.SaveAsync(aggregate);
return Ok();
}
[HttpPatch, Route("move")]
public async Task<IActionResult> Move(Guid id, [FromForm] BoardSections section)
{
var aggregate = await _aggregateRepository.LoadAsync<Core.Task>(id);
aggregate.Move(section, "Ahmet KÜÇÜKOĞLU");
await _aggregateRepository.SaveAsync(aggregate);
return Ok();
}
[HttpPatch, Route("complete")]
public async Task<IActionResult> Complete(Guid id)
{
var aggregate = await _aggregateRepository.LoadAsync<Core.Task>(id);
aggregate.Complete("Ahmet KÜÇÜKOĞLU");
await _aggregateRepository.SaveAsync(aggregate);
return Ok();
}
}
}

As you can see in the actions, firstly, events are taken from the Event Store with the AggregateRepository's Load method and aggregate is created. Then, new events are stored in aggregate with the use case methods on aggregate. With the AggregateRepository’s Save method, these stored events are sent to the Event Store.

Let's run the API and send requests to the API.

Let's create a task with the curl command line below.

curl -d "title=Event Store kurulacak" -H "Content-Type: application/x-www-form-urlencoded" -X POST https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/create
Enter fullscreen mode Exit fullscreen mode

Let's assign the task to someone with the curl command line below.

curl -d "assignedTo=Aziz CETİN" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/assign
Enter fullscreen mode Exit fullscreen mode

Let's pull the task to In-Progress with the curl command line below.

curl -d "section=2" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move
Enter fullscreen mode Exit fullscreen mode

Let's pull the task to Done with the curl command line below.

curl -d "section=3" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move
Enter fullscreen mode Exit fullscreen mode

Let's complete the task with the curl command line below.

curl -d -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/complete
Enter fullscreen mode Exit fullscreen mode

You can check the stream by entering the Event Store panel.

http://localhost:2113/web/index.html#/streams/Task-3a7daba9-872c-4f4d-8d6f-e9700d78c4f5

You can access the final version of the project from Github.

Good luck.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (2)

Collapse
 
justoke profile image
justoke

Excellent example. Thank you for detailing this and sharing code too. I tried to get this going but got stuck with the event store setup on docker. I managed to resolve using this thread github.com/EventStore/EventStore/i... and amending the docker command to set dev mode :
docker run -d --name eventstore -p 2113:2113 -p 1113:1113 -e EVENTSTORE_DEV=true eventstore/eventstore

Collapse
 
bronxsystem profile image
bronxsystem

omg no one replied to this? this is fantastic thank you.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay