<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kairi</title>
    <description>The latest articles on DEV Community by kairi (@kary_0009).</description>
    <link>https://dev.to/kary_0009</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118744%2F056d5f37-da3f-4aab-bde8-1e7ce710249b.png</url>
      <title>DEV Community: kairi</title>
      <link>https://dev.to/kary_0009</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kary_0009"/>
    <language>en</language>
    <item>
      <title>【Event Sourcing】Trying Sekiban DCB: Implementing a Materialized View</title>
      <dc:creator>kairi</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:06:28 +0000</pubDate>
      <link>https://dev.to/kary_0009/event-sourcing-trying-sekiban-dcb-implementing-a-materialized-view-2dp7</link>
      <guid>https://dev.to/kary_0009/event-sourcing-trying-sekiban-dcb-implementing-a-materialized-view-2dp7</guid>
      <description>&lt;p&gt;In this article, we will use the student enrollment management system created in a &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076"&gt;previous article&lt;/a&gt; to explore how Materialized Views work and how to implement them.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076" class="crayons-story__hidden-navigation-link"&gt;[Event Sourcing] Trying out Sekiban DCB: Implementation&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kary_0009" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118744%2F056d5f37-da3f-4aab-bde8-1e7ce710249b.png" alt="kary_0009 profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kary_0009" class="crayons-story__secondary fw-medium m:hidden"&gt;
              kairi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                kairi
                
                
              
              &lt;div id="story-author-preview-content-4663803" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kary_0009" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118744%2F056d5f37-da3f-4aab-bde8-1e7ce710249b.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;kairi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 16&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076" id="article-link-4663803"&gt;
          [Event Sourcing] Trying out Sekiban DCB: Implementation
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/eventsourcing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;eventsourcing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dcb"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dcb&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;In this system, actions such as creating a student, updating a profile, and enrolling in a class are stored as events. We will create read-side tables from these events so that we can retrieve an enrollment list containing student names and class names using SQL.&lt;/p&gt;

&lt;p&gt;A Materialized View (MV) stores read-side data calculated from the original data. In this implementation, Sekiban reads events and reflects them in SQL tables.&lt;/p&gt;

&lt;p&gt;The stack used in this article is &lt;strong&gt;.NET 10 / Sekiban DCB 10.19.0 / Orleans / PostgreSQL&lt;/strong&gt;. The code examples include only the parts needed for the explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use a Materialized View?
&lt;/h2&gt;

&lt;p&gt;The event store contains events such as "a student was created" and "a student enrolled in a class." However, what we want to retrieve in an enrollment list is data that combines student names and class names.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Student Name&lt;/th&gt;
&lt;th&gt;Class Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One option is to retrieve the student list and class list separately and match them in the application. However, if the MV contains tables for students, classes, and enrollments, we can retrieve the required data with SQL JOINs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enrollment JOIN Student JOIN Class
  → Filter by conditions
  → Return an enrollment list containing student and class names
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The role of the MV is to provide a data structure optimized for reading, separately from the format used to store events. Filtering and aggregation can also be written directly in SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall Architecture
&lt;/h2&gt;

&lt;p&gt;In this system, we use one database for events and another for the MV.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Create student / Update profile / Enroll in class] --&amp;gt; B[Validate business rules in the command]
    B --&amp;gt; C[(Event Store)]
    C --&amp;gt; D[Successful command response]
    C --&amp;gt; E[Sekiban MV Runtime]
    E --&amp;gt; F[Projector&amp;lt;br/&amp;gt;Convert events into SQL]
    F --&amp;gt; G[(MV Database&amp;lt;br/&amp;gt;Student / Class / Enrollment tables)]
    H[Enrollment List API] --&amp;gt; I[Get the currently active MV tables&amp;lt;br/&amp;gt;JOIN them with SQL]
    G --&amp;gt; I
    I --&amp;gt; J[Enrollment list containing student and class names]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The event store keeps the event history, while the MV database stores read-side data built from that history.&lt;/p&gt;

&lt;p&gt;For example, when a student's name is updated, the process works as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The command validates the requested change.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;StudentProfileUpdated&lt;/code&gt; event is stored.&lt;/li&gt;
&lt;li&gt;The MV runtime reads the event.&lt;/li&gt;
&lt;li&gt;The student table is updated using SQL generated by the Projector.&lt;/li&gt;
&lt;li&gt;The enrollment list API returns the updated student name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Event persistence and MV updates are separate processes, so there is a delay between a successful command response and the update becoming visible in the MV.&lt;/p&gt;

&lt;p&gt;MV synchronization may complete within a few seconds, but when there are many events, it may take several minutes or longer. Because of this behavior, the MV should not be used to validate consistency or enforce business rules.&lt;/p&gt;

&lt;p&gt;To guarantee consistency when writing data, retrieve the current state for each tag (aggregate) and perform validation on the command side.&lt;/p&gt;

&lt;p&gt;The events remain the source of truth. The MV should be treated as read-side data that can be rebuilt from those events.&lt;/p&gt;

&lt;p&gt;In this article, the MV is implemented as a set of read-side tables that are updated based on events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages and Database Connections
&lt;/h2&gt;

&lt;p&gt;The EventSource project, which contains the MV definitions, references &lt;code&gt;Sekiban.Dcb.MaterializedView&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The API host also uses packages for PostgreSQL and Orleans integration.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://www.sekiban.dev/jp/docs/dcb" rel="noopener noreferrer"&gt;official Sekiban documentation&lt;/a&gt;, providers are available not only for PostgreSQL but also for databases such as SQL Server, MySQL, and SQLite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Sekiban.Dcb.MaterializedView"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.19.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Sekiban.Dcb.MaterializedView.Postgres"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.19.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Sekiban.Dcb.MaterializedView.Orleans"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.19.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same configuration used in the existing sample.&lt;/p&gt;

&lt;p&gt;Dapper is used to execute SQL and map query results to objects.&lt;/p&gt;

&lt;p&gt;In AppHost, the MV database is passed to the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;materializedViewPostgres&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postgresServer&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DcbMaterializedViewPostgres"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;apiService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apiService&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;materializedViewPostgres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;materializedViewPostgres&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this sample, the same PostgreSQL server contains two databases: &lt;code&gt;DcbPostgres&lt;/code&gt; for storing events and &lt;code&gt;DcbMaterializedViewPostgres&lt;/code&gt; for storing the MV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the Projector
&lt;/h2&gt;

&lt;p&gt;The system already had an MV implementation. I added support for the &lt;code&gt;StudentProfileUpdated&lt;/code&gt; event so that changes to a student's name and enrollment limit are also reflected in the read-side tables.&lt;/p&gt;

&lt;p&gt;A Projector is a class that converts events into read-side data.&lt;/p&gt;

&lt;p&gt;In this example, &lt;code&gt;ClassRoomEnrollmentMvV2&lt;/code&gt; implements &lt;code&gt;IMaterializedViewProjector&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClassRoomEnrollmentMvV2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMaterializedViewProjector&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ViewName&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"ClassRoomEnrollment"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ViewVersion&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Implement InitializeAsync and ApplyToViewAsync.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two main methods are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InitializeAsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registers and creates the MV tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ApplyToViewAsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates SQL for each event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Defining the Tables
&lt;/h3&gt;

&lt;p&gt;The enrollment list uses the following three tables.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Main Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;students&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Student ID, name, enrollment limit, number of enrollments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;classrooms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Class ID, name, capacity, number of enrolled students&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enrollments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Student ID, class ID, enrollment date and time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;InitializeAsync&lt;/code&gt;, we register a logical table name and then create the table using the physical name provided by the runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Students&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentsLogicalTable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is an excerpt from the DDL for the student table.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{Students.PhysicalName}&lt;/code&gt; is the table name inserted by C# string interpolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Students&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhysicalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;student_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_class_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;enrolled_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_last_sortable_unique_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_last_applied_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This DDL is executed with &lt;code&gt;ctx.ExecuteAsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By obtaining the physical table name from the runtime instead of hard-coding it, we can work with tables that correspond to different MV versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Returning SQL for Each Event
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;ApplyToViewAsync&lt;/code&gt;, the logic is selected according to the event type, and a list of &lt;code&gt;MvSqlStatement&lt;/code&gt; objects is returned.&lt;/p&gt;

&lt;p&gt;The student-related part looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;StudentCreated&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InsertStudent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentSortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="n"&gt;StudentProfileUpdated&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;UpdateStudent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentSortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relationship between events and MV updates is as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Update to the MV&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StudentCreated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Insert a student&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClassRoomCreated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Insert a class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StudentProfileUpdated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update the student's name and enrollment limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StudentEnrolledInClassRoom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Insert an enrollment and update enrollment counts for the student and class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StudentDroppedFromClassRoom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete an enrollment and update enrollment counts for the student and class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Business rules, such as the enrollment limit, are validated on the command side.&lt;/p&gt;

&lt;p&gt;The Projector converts stored events into SQL, and the MV runtime applies that SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reflecting Profile Updates
&lt;/h3&gt;

&lt;p&gt;When a profile is updated, the student's name and enrollment limit are changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Students&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhysicalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_class_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;MaxClassCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_last_sortable_unique_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;SortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_last_applied_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;student_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;StudentId&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;_last_sortable_unique_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;SortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values are passed as SQL parameters.&lt;/p&gt;

&lt;p&gt;This update does not modify the enrollment count or enrollment records, so the existing enrollment information is preserved even when the student's name changes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_last_sortable_unique_id&lt;/code&gt; represents the position of the latest event applied to the row.&lt;/p&gt;

&lt;p&gt;The update is performed only when the incoming event is newer. This condition helps prevent duplicate processing and older events from overwriting newer data, while assuming that the MV runtime applies events in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering the MV Runtime
&lt;/h2&gt;

&lt;p&gt;In the API's &lt;code&gt;Program.cs&lt;/code&gt;, we register the Projector and the MV storage.&lt;/p&gt;

&lt;p&gt;The following is an excerpt from the registration code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSekibanDcbMaterializedView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowDefaultServiceId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollInterval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMaterializedView&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ClassRoomEnrollmentMvV2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSekibanDcbMaterializedViewPostgres&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connectionStringName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"DcbMaterializedViewPostgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;registerHostedWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSekibanDcbMaterializedViewOrleans&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, Orleans manages MV processing, so &lt;code&gt;registerHostedWorker&lt;/code&gt; is set to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AllowDefaultServiceId&lt;/code&gt; is configured for the single-service setup used in this sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieving the Enrollment List from the MV
&lt;/h2&gt;

&lt;p&gt;On the read side, &lt;code&gt;IMvOrleansQueryAccessor&lt;/code&gt; is used to obtain the database connection information and table definitions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mvQueryAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;projector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;studentsTable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRequiredTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ClassRoomEnrollmentMvV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentsLogicalTable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class and enrollment tables are retrieved in the same way.&lt;/p&gt;

&lt;p&gt;The main part of the SQL query uses the physical table names to perform JOINs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"""
&lt;/span&gt;    &lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;student_id&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"StudentId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"StudentName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_room_id&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"ClassRoomId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"ClassName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enrolled_at&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"EnrolledAt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_sortable_unique_id&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"LastSortableUniqueId"&lt;/span&gt;
    &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;enrollmentsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhysicalTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;INNER&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;studentsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhysicalTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
        &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;student_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;student_id&lt;/span&gt;
    &lt;span class="n"&gt;INNER&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;classRoomsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhysicalTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_room_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class_room_id&lt;/span&gt;
    &lt;span class="s"&gt;""";
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual implementation also adds filtering by student ID and class ID, as well as sorting.&lt;/p&gt;

&lt;p&gt;Dapper maps the result to the response type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnrollmentListMvItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CommandDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connection&lt;/code&gt; is an Npgsql connection to the MV database, and &lt;code&gt;parameters&lt;/code&gt; contains the filter conditions.&lt;/p&gt;

&lt;p&gt;The endpoint is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/mv/enrollments/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the result already contains both the student name and class name, the caller does not need to retrieve separate lists and match them manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting for the MV After an Update
&lt;/h2&gt;

&lt;p&gt;If we query the MV immediately after an update, it may still return the previous data.&lt;/p&gt;

&lt;p&gt;In this sample, the command API returns a &lt;code&gt;sortableUniqueId&lt;/code&gt;. By passing this value to the query API, we can wait until the MV has processed events up to that position.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the profile
  → Get sortableUniqueId from the response
  → Pass it to the enrollment API as waitForSortableUniqueId
  → Wait until the MV reaches that event position
  → Retrieve the data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The waiting logic checks the processing position with the following call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsSortableUniqueIdReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation also handles timeouts and cancellation.&lt;/p&gt;

&lt;p&gt;What we are waiting for here is the event processing position. Therefore, if the Projector does not contain logic for the profile update event, waiting for the event to be processed will not update the student's name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Versions with ViewVersion
&lt;/h2&gt;

&lt;p&gt;An MV has its own read-model version, separate from the Sekiban package version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ViewName&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"ClassRoomEnrollment"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ViewVersion&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ViewName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Name of the read model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ViewVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version of the table structure and projection logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The existing version 1 did not process &lt;code&gt;StudentProfileUpdated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If events have already been processed without handling this event, simply adding the new logic does not automatically cause previously processed events to be read again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Version 1
  Student created       → Insert student
  Profile updated       → Skip without processing
  Student enrolled      → Insert enrollment

Version 2
  Student created       → Insert student
  Profile updated       → Update name and enrollment limit
  Student enrolled      → Insert enrollment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this reason, after adding support for &lt;code&gt;StudentProfileUpdated&lt;/code&gt;, I changed &lt;code&gt;ViewVersion&lt;/code&gt; to 2.&lt;/p&gt;

&lt;p&gt;This allows the MV to be rebuilt from historical events using the new projection logic.&lt;/p&gt;

&lt;p&gt;Even if the table columns do not change, changing the version is useful when the data produced from historical events changes.&lt;/p&gt;

&lt;p&gt;On the other hand, if we only add more fields to a query by JOINing existing tables, rebuilding the MV is usually unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting the Currently Active MV Tables
&lt;/h3&gt;

&lt;p&gt;Because the tables are separated by version, the API needs to determine which version is currently active.&lt;/p&gt;

&lt;p&gt;This is handled by &lt;code&gt;IMvOrleansQueryAccessor&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use version 1
  → Build the data for version 2
  → Once version 2 is ready, switch queries to version 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By obtaining the table names through the Accessor instead of hard-coding physical table names in the API, the query destination can be switched between versions.&lt;/p&gt;

&lt;p&gt;It is not enough to rename the class to &lt;code&gt;V2&lt;/code&gt;. &lt;code&gt;ViewVersion&lt;/code&gt; and the registered Projector must also be updated together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the API Behavior
&lt;/h2&gt;

&lt;p&gt;First, create a student named "Alice" and a class named "Math", enroll Alice in the class, and retrieve the enrollment list.&lt;/p&gt;

&lt;p&gt;Then change the student's name to "Alice Updated", wait for the update event to be reflected in the MV, and retrieve the list again.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Before Update&lt;/th&gt;
&lt;th&gt;After Update&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Student name&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;Alice Updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class name&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enrollment&lt;/td&gt;
&lt;td&gt;Alice is enrolled in Math&lt;/td&gt;
&lt;td&gt;The same enrollment is preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If we get this result, we can confirm that the profile update has been reflected in the student table and that the updated name is also used in the enrollment list generated through the JOIN.&lt;/p&gt;

&lt;p&gt;When the student drops the class, we also verify that the corresponding enrollment disappears from the list and that the enrollment counts for both the student and class are updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification Results
&lt;/h2&gt;

&lt;p&gt;Using the actual API, Orleans, and PostgreSQL, I verified the following behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates to the student's name and enrollment limit were reflected in the MV, and the updated name appeared in the joined enrollment list.&lt;/li&gt;
&lt;li&gt;The existing enrollment information and enrollment timestamp were preserved after the profile update, and additional enrollments were reflected correctly.&lt;/li&gt;
&lt;li&gt;Updates that violated business rules were rejected, and the MV was not changed.&lt;/li&gt;
&lt;li&gt;Filtering by student and class, as well as dropping an enrollment, was reflected correctly in the query results.&lt;/li&gt;
&lt;li&gt;After restarting with version 2 instead of version 1, historical profile updates were reflected, and the API also switched its query target to version 2.&lt;/li&gt;
&lt;li&gt;I re-projected 13 events into an empty MV database and confirmed that the student names, enrollment limits, enrollment relationships, and counts matched the data before the rebuild. The original events were not modified.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we used Sekiban DCB's Materialized View functionality to build an enrollment list that can be queried with SQL from stored events.&lt;/p&gt;

&lt;p&gt;In addition to reflecting updates, we also confirmed version switching and rebuilding the MV from historical events.&lt;/p&gt;

&lt;p&gt;Materialized Views are useful when you want to join or aggregate multiple pieces of information with SQL, or when you want to expose read-side data as database tables.&lt;/p&gt;

&lt;p&gt;A good place to start is a familiar list endpoint like this one. Try updating tables from events and see how the read model changes as new events are processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sekiban.dev/jp/docs/dcb" rel="noopener noreferrer"&gt;Sekiban DCB Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/J-Tech-Japan/Sekiban/blob/main/docs/dcb_llm_ja/20_materialized_view.md" rel="noopener noreferrer"&gt;Materialized View Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/J-Tech-Japan/Sekiban/blob/main/docs/decisions/SEK-G29-materialized-view-generation-switching.md" rel="noopener noreferrer"&gt;Design for Version Switching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>eventsourcing</category>
      <category>dcb</category>
      <category>materializedview</category>
    </item>
    <item>
      <title>[Event Sourcing] Building an Application Fast Using the Event Sourcing Pattern with Sekiban DCB and AI</title>
      <dc:creator>kairi</dc:creator>
      <pubDate>Thu, 17 Sep 2026 08:15:43 +0000</pubDate>
      <link>https://dev.to/kary_0009/event-sourcing-building-an-application-fast-using-the-event-sourcing-pattern-with-sekiban-dcb-and-1oa6</link>
      <guid>https://dev.to/kary_0009/event-sourcing-building-an-application-fast-using-the-event-sourcing-pattern-with-sekiban-dcb-and-1oa6</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;In this article, we will build an event-sourced application using Sekiban DCB and AI. Along with implementing the UI and APIs, we will check whether the application maintains consistency when multiple operations happen at the same time.&lt;/p&gt;

&lt;p&gt;For this project, I used &lt;strong&gt;Codex × GPT-6 Astra&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Sekiban DCB Decider template includes sample implementations that we can use as references when giving instructions to AI.&lt;/p&gt;

&lt;p&gt;These samples cover the full flow from the UI to APIs, business rules, and event persistence. We will explore how far we can take feature development and verification with AI by following these existing implementations.&lt;/p&gt;

&lt;p&gt;As an example, we will build a simple library management application that supports registering, borrowing, and returning books. We will use PostgreSQL for storage and create a basic Blazor UI to try out the APIs.&lt;/p&gt;

&lt;p&gt;The sample code is available here:&lt;br&gt;
&lt;a href="https://github.com/kdaigo/SekibanSamples/tree/main/BookManagement" rel="noopener noreferrer"&gt;BookManagement sample on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Creating a Project from the Template
&lt;/h2&gt;

&lt;p&gt;First, run the following commands to create a project from the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new &lt;span class="nb"&gt;install &lt;/span&gt;Sekiban.Dcb.Templates
dotnet new sekiban-dcb-decider &lt;span class="nt"&gt;-n&lt;/span&gt; BookManagement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also find instructions for getting started with the Decider template in the &lt;a href="https://www.sekiban.dev" rel="noopener noreferrer"&gt;Quick Start on the official website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this project, I used the .NET 10 SDK and Linux containers in Docker Desktop. The UI was built with Blazor.&lt;/p&gt;

&lt;p&gt;The generated project already included samples for Student, ClassRoom, and Enrollment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Student: Students and their enrollment limits&lt;/li&gt;
&lt;li&gt;ClassRoom: Classes and their capacity&lt;/li&gt;
&lt;li&gt;Enrollment: Enrolling in and withdrawing from classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will ask AI to implement new features using these samples as references.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Implementing the Features
&lt;/h2&gt;

&lt;p&gt;The detailed business requirements, consistency rules, and verification scenarios are available in &lt;code&gt;docs/spec.md&lt;/code&gt; in the sample code. Below is an excerpt from the instructions I gave AI to keep the implementation consistent with the existing samples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before you start implementing, read the existing Student, ClassRoom, and
Enrollment implementations from end to end, including the UI, APIs,
commands, Deciders, events, states, and queries.

Treat these implementations as the standard for this project.
Rather than introducing a new design or coding style, implement the
library management features by adapting similar existing code.

If the existing code uses multiple approaches, choose the one closest
to the business operation you are adding.
Do not introduce custom helpers, new abstractions, or shorthand syntax.
Only if the existing approach cannot meet the requirements, explain why,
propose a change, and ask for confirmation before proceeding.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire implementation took less than an hour, including reviewing the existing code, implementing the features, building the project, running integration and concurrency tests against a real PostgreSQL database, and checking and fixing the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Business Rules with the Decider Pattern
&lt;/h3&gt;

&lt;p&gt;In this template, business rules live in Deciders associated with events and states. Their main responsibilities are &lt;code&gt;Validate&lt;/code&gt;, which checks whether an operation is allowed, and &lt;code&gt;Evolve&lt;/code&gt;, which applies an event to a state.&lt;/p&gt;

&lt;p&gt;Below is an excerpt from the borrowing logic we implemented. &lt;code&gt;Book&lt;/code&gt; represents the book being borrowed, &lt;code&gt;User&lt;/code&gt; represents the borrower, and &lt;code&gt;Loan&lt;/code&gt; represents a single loan. If the book is already on loan, another borrowing attempt is rejected. When the borrowing event is applied, the resulting state stores the LoanId and UserId.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BookBorrowedDecider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;BorrowedBookState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"Book &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BookId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is already borrowed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;BorrowedBookState&lt;/span&gt; &lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;AvailableBookState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BookBorrowed&lt;/span&gt; &lt;span class="n"&gt;borrowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BookId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;borrowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;borrowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Handler coordinates state retrieval and validation, then returns an event if validation succeeds. The Decider itself does not need any code for connecting to PostgreSQL.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    UI[Blazor UI] --&amp;gt; API[API]
    API --&amp;gt; H[Command Handler]
    H --&amp;gt; V[State retrieval and Decider validation]
    V --&amp;gt; E[Event creation]
    E --&amp;gt; S[Conflict detection and persistence by Sekiban]
    S --&amp;gt; P[Projector / List projection]
    P --&amp;gt; Q[Query]
    Q --&amp;gt; UI&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In this project, AI implemented the new business rules using the same Decider structure as the existing code. Keeping the code structure consistent as features are added also makes it easier for developers to know where to look for business rules. This is where I found the approach particularly useful for AI-assisted coding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintaining Consistency Across Multiple Entities with DCB
&lt;/h3&gt;

&lt;p&gt;Checking the current state alone does not solve concurrency problems. Two requests can read the same state before either updates it, and both can conclude that the operation is allowed.&lt;/p&gt;

&lt;p&gt;For example, if two users try to borrow the same book at almost the same time, both requests might read its status as “available” before it changes. Similarly, if one user borrows different books at the same time, each request might appear to be within the borrowing limit, while the combined result exceeds it.&lt;/p&gt;

&lt;p&gt;This is where DCB, or &lt;strong&gt;Dynamic Consistency Boundary&lt;/strong&gt;, comes in. It uses tags to define which entities must remain consistent together for a particular operation. Sekiban DCB provides optimistic concurrency control based on these tags.&lt;/p&gt;

&lt;p&gt;Our borrowing event has the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;BookBorrowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;LoanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;BookId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;BorrowedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEventPayload&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;EventPayloadWithTags&lt;/span&gt; &lt;span class="nf"&gt;GetEventWithTags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoanTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoanId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BookTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BookId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We attach three tags—Loan, Book, and User—to a single fact: a book was borrowed. Each is defined as a consistency tag, and the Handler reads and validates the corresponding state.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity to check&lt;/th&gt;
&lt;th&gt;Rule to enforce&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Book&lt;/td&gt;
&lt;td&gt;Prevent the same book from being loaned out twice at the same time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;Keep the user within the borrowing limit, even when requests involve different books&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loan&lt;/td&gt;
&lt;td&gt;Identify each loan and check for duplicates and whether it has been returned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sekiban handles the concurrency checks that determine whether the events can be saved based on the state we read. The application returns an error for a request that encounters a conflict.&lt;/p&gt;

&lt;p&gt;The key is deciding which business conditions to validate and which entities to include in the consistency boundary. Adding tags alone does not automatically enforce arbitrary business rules. We define those rules explicitly in the Deciders and Handlers, then let the framework detect conflicts when saving events.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Blazor UI for Trying Out the APIs
&lt;/h3&gt;

&lt;p&gt;The UI is a simple interface for trying out the APIs. The screenshots below show it with sample data prepared for this article (The UI was originally built in Japanese. The screenshots below have been edited to show the labels and sample data in English for this article.).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gm7ki5n6a24cv53uwsi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gm7ki5n6a24cv53uwsi.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The book list shows each book’s status. The button for removing a book from the catalog is disabled while the book is on loan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcm0wgq1xoezrkovjn1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcm0wgq1xoezrkovjn1z.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the borrowing and returns screen, you can select a user and a book to create a loan. Loan records remain in the history after the books are returned, and the Return button is disabled for those records. The API’s business rules also check whether each operation is allowed.&lt;/p&gt;

&lt;p&gt;To verify that consistency was maintained, we tested the following scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Concurrent attempts to borrow the same book&lt;/li&gt;
&lt;li&gt;The same user borrowing different books concurrently&lt;/li&gt;
&lt;li&gt;Concurrent attempts to return the same Loan&lt;/li&gt;
&lt;li&gt;Concurrent requests to create a new loan, return an already returned loan again, and remove the book from the catalog&lt;/li&gt;
&lt;li&gt;For a book currently on loan, concurrent requests to return the current loan, create a new loan, and remove the book from the catalog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every scenario, we sent multiple requests at almost the same time to an API connected to a real PostgreSQL database. We confirmed that operations violating the business rules were rejected and that the related states remained consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;By asking AI to add features that followed the existing samples, we were able to implement and verify the application in a short time. Letting Sekiban handle conflict detection and persistence also allowed us to focus on implementing the business rules.&lt;/p&gt;

&lt;p&gt;I hope this article encourages you to try building an event-sourced application with Sekiban DCB and AI.&lt;/p&gt;

</description>
      <category>eventsourcing</category>
      <category>dcb</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>[Event Sourcing] Running an Existing C# Business Model on Wasm with SekibanWasmRuntime</title>
      <dc:creator>kairi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:05:09 +0000</pubDate>
      <link>https://dev.to/kary_0009/event-sourcing-running-an-existing-c-business-model-on-wasm-with-sekibanwasmruntime-1gcf</link>
      <guid>https://dev.to/kary_0009/event-sourcing-running-an-existing-c-business-model-on-wasm-with-sekibanwasmruntime-1gcf</guid>
      <description>&lt;p&gt;In the previous article, we created a C# application using Sekiban DCB to handle students, classes, and class enrollment. This time, we will try running that application with &lt;strong&gt;SekibanWasmRuntime&lt;/strong&gt;, which allows Sekiban to run with Wasm (WebAssembly).&lt;/p&gt;

&lt;p&gt;In this article, we will run a project written in C# as Wasm. SekibanWasmRuntime currently provides primary support for C# and Rust, while Go, TypeScript, MoonBit, and Swift are also available as experimental implementations.&lt;/p&gt;

&lt;p&gt;The sample code used in this article is available &lt;a href="https://github.com/kdaigo/SekibanSamples/tree/main/DCBNativeWasmProject" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Roles of Sekiban and Wasm
&lt;/h2&gt;

&lt;p&gt;Sekiban is an event sourcing framework that provides a mechanism for storing events that occur in an application and building the current state from those events.&lt;/p&gt;

&lt;p&gt;Wasm is an executable format produced by compiling code. In this example, &lt;strong&gt;SekibanWasmRuntime&lt;/strong&gt; running on the server loads and executes the Wasm file. The application contains the business code related to students, while the Runtime provides the environment for executing that code and the mechanism for storing events.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What Runs in Wasm
&lt;/h2&gt;

&lt;p&gt;The architecture used in this example is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scalar / PowerShell
    ↓ 
API
    ↓ ISekibanExecutor
RemoteSekibanExecutor
    ├── Executes existing Command handlers on the API side
    └── Requests state retrieval, event commit, and Query execution over HTTP
            ↓ 
SekibanWasmRuntime
    ├── Executes the Projector and list Query in Wasm
    └── Stores events in PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Command handlers run on the API side, while the Projector that builds state from events and the student list processing run as Wasm inside the Runtime.&lt;/p&gt;

&lt;p&gt;When a student is created, the process works in the following order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HTTP endpoint receives the request to create a student.&lt;/li&gt;
&lt;li&gt;The existing Command handler runs on the API side and creates an event according to the business rules.&lt;/li&gt;
&lt;li&gt;The API requests the Runtime to store the event through the Remote connection.&lt;/li&gt;
&lt;li&gt;The Runtime stores the event, and the Projector in Wasm applies the event when state is retrieved or the list is updated.&lt;/li&gt;
&lt;li&gt;The retrieval API can then be used to check the state of the created student or the student list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The business logic remains unchanged, but we need to add an entry point for calling Wasm, type registration, and API connection settings. The entire Web API and all Commands do not run inside Wasm.&lt;/p&gt;

&lt;p&gt;For this example, I used the following versions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.NET&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sekiban.Dcb&lt;/td&gt;
&lt;td&gt;10.19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sekiban.Dcb.WasmRuntime.Aspire&lt;/td&gt;
&lt;td&gt;1.0.0-preview.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sekiban.Dcb.WasmRuntime.Remote&lt;/td&gt;
&lt;td&gt;1.0.0-preview.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime container&lt;/td&gt;
&lt;td&gt;1.0.0-preview.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The NuGet packages and the Runtime container use different version series.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What Was Added or Changed
&lt;/h2&gt;

&lt;p&gt;To reuse the existing business logic, I added the Wasm entry point and execution environment.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://github.com/J-Tech-Japan/SekibanWasmRuntime/blob/main/docs/templates/sekiban-wasm-decider.md" rel="noopener noreferrer"&gt;C# Wasm template&lt;/a&gt; specifies using Docker when building Wasm on Windows or macOS. Following this procedure, we prepare the build environment for compiling C# to Wasm inside Docker. The generated Wasm file is then loaded and executed by SekibanWasmRuntime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Added or changed item&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wasm project&lt;/td&gt;
&lt;td&gt;References the existing EventSource and builds it as Wasm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm entry point and type registration&lt;/td&gt;
&lt;td&gt;Connects the Runtime to the existing Projector and provides the type information required for ahead-of-time compilation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;manifest&lt;/td&gt;
&lt;td&gt;Tells the Runtime which Wasm, events, Projectors, and Queries are associated with each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build scripts&lt;/td&gt;
&lt;td&gt;Build Wasm inside Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppHost settings&lt;/td&gt;
&lt;td&gt;Start the Runtime container and a dedicated PostgreSQL instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote connection for the API&lt;/td&gt;
&lt;td&gt;Replaces the &lt;code&gt;ISekibanExecutor&lt;/code&gt; implementation with &lt;code&gt;RemoteSekibanExecutor&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Wasm project references the existing code as follows. The following code snippets show the main settings and implementations that were added.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ProjectReference Include="..\DCBNativeWasmProject.EventSource\DCBNativeWasmProject.EventSource.csproj" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the entry point called by the Runtime, we use the existing Projector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instance.State = StudentProjector.Project(instance.State, ev);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the API side, we register the implementation used to communicate with the Runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddHttpClient&amp;lt;RemoteSekibanExecutor&amp;gt;(http =&amp;gt;
{
    http.BaseAddress = new Uri(builder.Configuration["Runtime:BaseUrl"]!);
    http.Timeout = TimeSpan.FromSeconds(90);
});

builder.Services.AddTransient&amp;lt;ISekibanExecutor&amp;gt;(
    sp =&amp;gt; sp.GetRequiredService&amp;lt;RemoteSekibanExecutor&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration, the existing endpoints can continue calling &lt;code&gt;executor.ExecuteAsync(command)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package and Wasm Configuration
&lt;/h3&gt;

&lt;p&gt;Run the following commands from the project root directory. Prepare the .NET 10 SDK and Docker Desktop beforehand. On Windows, use Git Bash, and on macOS, use a terminal. Because the build tools are provided inside Docker, you do not need to install the WASI SDK directly.&lt;/p&gt;

&lt;p&gt;The packages added to AppHost and the API are as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;/DCBNativeWasmProject.AppHost\DCBNativeWasmProject.AppHost.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sekiban.Dcb.WasmRuntime.Aspire&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1.0.0-preview.6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;/DCBNativeWasmProject.ApiService\DCBNativeWasmProject.ApiService.csproj&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sekiban.Dcb.WasmRuntime.Remote&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1.0.0-preview.6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following settings are added to &lt;code&gt;DCBNativeWasmProject.Wasm/DCBNativeWasmProject.Wasm.csproj&lt;/code&gt; to generate Wasm output. These are the main settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;TargetFramework&amp;gt;net10.0&amp;lt;/TargetFramework&amp;gt;
&amp;lt;RuntimeIdentifier&amp;gt;wasi-wasm&amp;lt;/RuntimeIdentifier&amp;gt;
&amp;lt;OutputType&amp;gt;library&amp;lt;/OutputType&amp;gt;
&amp;lt;AllowUnsafeBlocks&amp;gt;true&amp;lt;/AllowUnsafeBlocks&amp;gt;
&amp;lt;InvariantGlobalization&amp;gt;true&amp;lt;/InvariantGlobalization&amp;gt;
&amp;lt;IsAotCompatible&amp;gt;true&amp;lt;/IsAotCompatible&amp;gt;
&amp;lt;SelfContained&amp;gt;true&amp;lt;/SelfContained&amp;gt;
&amp;lt;IlcExportUnmanagedEntrypoints&amp;gt;true&amp;lt;/IlcExportUnmanagedEntrypoints&amp;gt;
&amp;lt;NativeLib&amp;gt;Shared&amp;lt;/NativeLib&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual file also includes a package reference for NativeAOT-LLVM and linker settings that expose the functions called by the Runtime. In &lt;code&gt;WasmExports.cs&lt;/code&gt;, type information is explicitly registered, and entry points are provided for applying events, saving and restoring state, and retrieving lists. &lt;code&gt;NuGet.wasm.config&lt;/code&gt; specifies the package sources required for this build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map Wasm and Processing in the manifest
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;config/sekiban-manifest.json&lt;/code&gt; tells the Runtime which Projectors and Queries are provided by each Wasm module. The following is an excerpt related to the student list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "defaultModulePath": "/app/modules/dcb-native.wasm",
  "queryAssemblyVersion": "wasm",
  "projectors": [
    {
      "projectorName": "StudentListProjection",
      "projectorVersion": "1.0.1",
      "modulePath": "/app/modules/dcb-native.wasm",
      "abiKind": "wasi-preview1",
      "moduleVersion": "1.0.0"
    }
  ],
  "queryProjectors": {
    "GetStudentListQuery": "StudentListProjection"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connect the Runtime and API from AppHost
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;DCBNativeWasmProject.AppHost/WasmAppHost.cs&lt;/code&gt;, we start the Runtime and a PostgreSQL instance. &lt;code&gt;root&lt;/code&gt; is the absolute path to the project root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var pg = builder.AddPostgres("wasm-postgres");
var events = pg.AddDatabase("SekibanDcb", "dcb_native_wasm_events");

var runtime = builder.AddSekibanWasmRuntime(
    "wasm-runtime",
    new SekibanWasmRuntimeOptions
    {
        ConfigDirectory = Path.Combine(root, "config"),
        ModulesDirectory = Path.Combine(root, "modules"),
        WasmModulePath = "/app/modules/dcb-native.wasm",
        EventStoreDatabase = events,
        ProjectionMode = "memory-only",
        HostPort = 5280
    })
    .WithEnvironment("SEKIBAN_WASM_POOL_SIZE", "0")
    .WaitFor(events);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;memory-only&lt;/code&gt; is the projection mode used to build projections by applying events. The events themselves are stored in PostgreSQL. &lt;code&gt;SEKIBAN_WASM_POOL_SIZE=0&lt;/code&gt; is set as a workaround for an issue in this preview version where consecutive list updates could wait indefinitely. This is not a recommended value that has been evaluated for production use.&lt;/p&gt;

&lt;p&gt;Next, we pass the Runtime URL to the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.AddProject&amp;lt;DCBNativeWasmProject_ApiService&amp;gt;(
        "wasm-api", launchProfileName: null)
    .WithEnvironment("Runtime__BaseUrl", runtime.GetEndpoint("http"))
    .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
    .WithHttpEndpoint(
        port: 5241, targetPort: 5241, name: "http", isProxied: false)
    .WithEnvironment("ASPNETCORE_URLS", "http://localhost:5241")
    .WaitFor(runtime);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Runtime__BaseUrl&lt;/code&gt; passes the SekibanWasmRuntime endpoint to the API. The API uses this endpoint to request state retrieval and event storage from the Runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Build the Wasm File
&lt;/h2&gt;

&lt;p&gt;Start Docker Desktop and move to the project root directory in your terminal. Adjust the path to match your working directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/build-wasm.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script uses &lt;code&gt;scripts/Dockerfile.wasm-build&lt;/code&gt; to create a Docker image for the build, and then &lt;code&gt;scripts/build-wasm-container.sh&lt;/code&gt; runs the following command inside the container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet publish DCBNativeWasmProject.Wasm/DCBNativeWasmProject.Wasm.csproj \
  -c Release -r wasi-wasm \
  -o artifacts/wasm/publish --configfile NuGet.wasm.config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build result is copied to the location used by the Runtime, and &lt;code&gt;wasm-tools validate&lt;/code&gt; is used to validate the Wasm format. When the build succeeds, a log similar to the following is displayed. The file size may change depending on the implementation.&lt;/p&gt;

&lt;p&gt;Example output when the Wasm build completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[build-wasm] OK: modules/dcb-native.wasm (25282599 bytes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Output Locations of the Build Files
&lt;/h3&gt;

&lt;p&gt;The following paths are relative to the project root.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;artifacts/wasm/publish/DCBNativeWasmProject.Wasm.wasm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wasm generated by &lt;code&gt;publish&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modules/dcb-native.wasm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A copy of the generated Wasm file that is loaded by the Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DCBNativeWasmProject.Wasm/bin/&lt;/code&gt; and &lt;code&gt;obj/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Build outputs and intermediate files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config/sekiban-manifest.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime configuration created manually. This is not a build output.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;modules&lt;/code&gt; folder on the host is mounted into the Runtime container. Therefore, the manifest and AppHost specify &lt;code&gt;/app/modules/dcb-native.wasm&lt;/code&gt;, which is the path &lt;strong&gt;inside the container&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Wasm project used in this example is built separately from the normal solution build. Run the dedicated build script before starting AppHost.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Start the Application
&lt;/h2&gt;

&lt;p&gt;In the same terminal, set the environment variables and start AppHost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ASPIRE_ALLOW_UNSECURED_TRANSPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
export &lt;/span&gt;&lt;span class="nv"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:15900
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:15901
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:15902

dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; DCBNativeWasmProject.AppHost &lt;span class="nt"&gt;--no-launch-profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These settings are for local execution. AppHost starts the dedicated PostgreSQL instance, the Runtime container, and the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Execution Results
&lt;/h2&gt;

&lt;p&gt;Create a student from Scalar, and then retrieve the student using the same ID. The following is the actual response returned by the API for the request shown below. The IDs will be different each time you run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/students/
Content-Type: application/json

{
  "studentId": "10c8f945-8314-4631-8c3d-627dbd22934a",
  "name": "山田太郎",
  "maxClassCount": 5
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10c8f945-8314-4631-8c3d-627dbd22934a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01a08f5c-d51e-7cf9-9e22-ad1348d4137e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sortableUniqueId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"063924708420894567502099121181"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Student created successfully"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, retrieve the student that was created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/students/10c8f945-8314-4631-8c3d-627dbd22934a
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10c8f945-8314-4631-8c3d-627dbd22934a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10c8f945-8314-4631-8c3d-627dbd22934a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"山田太郎"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maxClassCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enrolledClassRoomIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The student creation event is applied by the Projector running in Wasm, and we can retrieve the student's state. List retrieval and updates can also be executed from the same Scalar screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;By using SekibanWasmRuntime, we can change the execution environment while continuing to use existing models and Projectors, as in this example, by adding a Wasm entry point and connection settings. The business logic we have already created can also be used with this architecture.&lt;/p&gt;

&lt;p&gt;I also felt that by leaving event storage and Wasm execution to SekibanWasmRuntime, the application side can focus more on implementing the business logic: what kinds of operations the application accepts and what kinds of events those operations produce. Because the API can use the Runtime through a Remote connection, the part that calls the business processing can be separated from the part responsible for execution and storage.&lt;/p&gt;

&lt;p&gt;Samples for Go and TypeScript are also available as experimental/reference implementations, and I am looking forward to seeing further support added in the future.&lt;/p&gt;

</description>
      <category>eventsourcing</category>
      <category>webassembly</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>[Event Sourcing] Trying out Sekiban DCB: Implementation</title>
      <dc:creator>kairi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:50:24 +0000</pubDate>
      <link>https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076</link>
      <guid>https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-implementation-2076</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This article is a continuation of "&lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78"&gt;[Event Sourcing] Trying out Sekiban DCB: Introduction&lt;/a&gt;". In the previous post, we covered setting up the DCB Native environment and its processing flow.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78" class="crayons-story__hidden-navigation-link"&gt;[Event Sourcing] Trying out Sekiban DCB: Introduction&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kary_0009" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118744%2F056d5f37-da3f-4aab-bde8-1e7ce710249b.png" alt="kary_0009 profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kary_0009" class="crayons-story__secondary fw-medium m:hidden"&gt;
              kairi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                kairi
                
                
              
              &lt;div id="story-author-preview-content-4662857" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kary_0009" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118744%2F056d5f37-da3f-4aab-bde8-1e7ce710249b.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;kairi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 16&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78" id="article-link-4662857"&gt;
          [Event Sourcing] Trying out Sekiban DCB: Introduction
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/eventsourcing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;eventsourcing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dcb"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dcb&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Initially, features like creating a Student, fetching individual and list data, and enrolling/dropping classes are already implemented. The student's state is built from recorded events. The information a student holds includes Student ID, Name, Max Class Count, and a list of currently enrolled classes.&lt;/p&gt;

&lt;p&gt;In this post, we will add a feature to update a student's name and their maximum class limit. The Student ID and enrolled classes will remain unchanged. Decreasing the max limit below the current number of enrolled classes will be prohibited.&lt;/p&gt;

&lt;p&gt;When updating, we don't directly overwrite the current data. Instead, we record a &lt;code&gt;StudentProfileUpdated&lt;/code&gt; event. A Projector then processes this event to build the updated student state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update Feature Specifications
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StudentId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies the student to update. It is not changed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required, 1 to 100 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MaxClassCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 to 10. Changing it to a value lower than the current enrollment count is not allowed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enrollment list&lt;/td&gt;
&lt;td&gt;Keeps the contents before the update.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-existing student&lt;/td&gt;
&lt;td&gt;The update is rejected.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The API receives JSON containing the student ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/students/update
Content-Type: application/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9912a519-a6f6-41fe-92bf-07b99045efff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxClassCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will add the implementation in the order of Event, Decider, Command, Projector, and API.&lt;/p&gt;

&lt;p&gt;The sample code is available on &lt;a href="https://github.com/kdaigo/SekibanSamples/tree/main/DCBNativeProject" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Add an Event That Represents the Update
&lt;/h2&gt;

&lt;p&gt;We define the fact that the student information was updated as &lt;code&gt;StudentProfileUpdated&lt;/code&gt; event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.Tags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Sekiban.Dcb.Events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.Events.Student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;StudentProfileUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;StudentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxClassCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEventPayload&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;EventPayloadWithTags&lt;/span&gt; &lt;span class="nf"&gt;GetEventWithTags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StudentTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event records the target student ID, the updated name, and the updated maximum number of classes. By adding &lt;code&gt;StudentTag&lt;/code&gt;, this event is associated with the target student.&lt;/p&gt;

&lt;p&gt;The enrollment list is not included in the event because it is not part of this update.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Implement Business Rules and State Updates in Deciders
&lt;/h2&gt;

&lt;p&gt;In Deciders, we implement validation based on the current state and the processing that applies the event to the state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.Events.Student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.States.Student.Deciders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StudentProfileUpdatedDecider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxClassCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxClassCount&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnrolledClassRoomIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApplicationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"MaxClassCount cannot be less than the current enrollment count."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;StudentProfileUpdated&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;MaxClassCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxClassCount&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;Validate&lt;/code&gt;, the updated maximum class count is compared with the current number of enrolled classes. For example, if a student is enrolled in three classes, the maximum class count cannot be changed to two.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Evolve&lt;/code&gt;, the state with the contents of the event applied is returned. Only the name and maximum class count can be changed, while the student ID and enrollment list are kept unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add a Command to Handle Update Requests
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;UpdateStudent&lt;/code&gt; is responsible for defining the input values and handling the processing from retrieving the state to generating the event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.ComponentModel.DataAnnotations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.Events.Student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.States.Student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.States.Student.Deciders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dcb.ImmutableModels.Tags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Sekiban.Dcb.Commands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Sekiban.Dcb.Events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Dcb.EventSource.Student&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;UpdateStudent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommandWithHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UpdateStudent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;StudentId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Name is required"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StringLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MinimumLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Name must be between 1 and 100 characters"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MaxClassCount must be between 1 and 10"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxClassCount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UpdateStudent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxClassCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;StudentId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;MaxClassCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxClassCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EventOrNone&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UpdateStudent&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ICommandContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StudentTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tagState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetStateAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StudentProjector&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tagState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApplicationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Student does not exist."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;StudentProfileUpdatedDecider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxClassCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StudentProfileUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxClassCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEventWithTags&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Validation&lt;/th&gt;
&lt;th&gt;Implementation location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name is required and has a valid length&lt;/td&gt;
&lt;td&gt;Command attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum class count is between 1 and 10&lt;/td&gt;
&lt;td&gt;Command attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The student to update exists&lt;/td&gt;
&lt;td&gt;Command handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum class count is greater than or equal to the current enrollment count&lt;/td&gt;
&lt;td&gt;Decider&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After Sekiban validates the Command attributes, the handler retrieves the current state of the student. If the business rule validation passes, it generates and returns &lt;code&gt;StudentProfileUpdated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sekiban handles event persistence, so there is no need to write database persistence logic in the handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Add Update Event Handling to the Projector
&lt;/h2&gt;

&lt;p&gt;Saving the event alone does not reflect the update in the student state. We add handling for the update event and &lt;code&gt;Evolve&lt;/code&gt; to the Projector that builds the state.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;StudentProjector&lt;/code&gt;, which handles the state of a single student, add the following branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentProfileUpdated&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processing for each event becomes as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ITagStatePayload&lt;/span&gt; &lt;span class="nf"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ITagStatePayload&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmptyTagStatePayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentCreated&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StudentCreatedDecider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentProfileUpdated&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentEnrolledInClassRoom&lt;/span&gt; &lt;span class="n"&gt;enrolled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrolled&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentDroppedFromClassRoom&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also add handling for the same event to &lt;code&gt;StudentListProjection&lt;/code&gt;, which builds the student list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;StudentCreated&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StudentCreatedDecider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StudentProfileUpdated&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StudentEnrolledInClassRoom&lt;/span&gt; &lt;span class="n"&gt;enrolled&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrolled&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StudentDroppedFromClassRoom&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Evolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, the updated name and maximum class count are reflected in both the state of a single student and the student list.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add the Update API
&lt;/h2&gt;

&lt;p&gt;Add an endpoint to &lt;code&gt;StudentEndpoints&lt;/code&gt; that receives the update Command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateStudentAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UpdateStudent"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateStudentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;UpdateStudent&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;ISekibanExecutor&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;studentId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;eventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sortableUniqueId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortableUniqueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Student updated successfully"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API directly receives &lt;code&gt;UpdateStudent&lt;/code&gt; and asks Sekiban to execute it. The check for whether the student exists and the validation of the maximum class count are handled through the Command and Decider processing.&lt;/p&gt;

&lt;p&gt;The flow from the update request to event persistence is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/students/update
    ↓
UpdateStudent
    ↓
Validate input values
    ↓
Retrieve the current student state
    ↓
Check the existence of the student
and the consistency of the maximum class count
    ↓
Generate StudentProfileUpdated
    ↓
Sekiban stores the event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When building or updating the state from the stored event, the Projector calls the corresponding &lt;code&gt;Evolve&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events Stored in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Let's check the events actually stored in the database. In &lt;code&gt;dcb_events&lt;/code&gt;, the following creation event and update event are stored. The data below was retrieved in ascending order of &lt;code&gt;SortableUniqueId&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01a0840a-df7f-7c43-9665-0564c61c7718"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SortableUniqueId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"063924518500223460000021724853"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"EventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StudentCreated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9912a519-a6f6-41fe-92bf-07b99045efff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Student update persistence test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxClassCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Student:9912a519-a6f6-41fe-92bf-07b99045efff"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-09T02:41:40.235475+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CausationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01a0840a-df7f-7c43-9665-0564c61c7718"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CorrelationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CreateStudent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ExecutedUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GeneralSekibanExecutor"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ServiceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01a0840a-e817-7b00-8c14-4f869aa9a78d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SortableUniqueId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"063924518502423536500632865263"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"EventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StudentProfileUpdated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"studentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9912a519-a6f6-41fe-92bf-07b99045efff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Student update persistence test updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxClassCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Student:9912a519-a6f6-41fe-92bf-07b99045efff"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-09T02:41:42.43232+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CausationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01a0840a-e817-7b00-8c14-4f869aa9a78d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CorrelationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UpdateStudent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ExecutedUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GeneralSekibanExecutor"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the same student ID, the name and maximum class count at the time of creation and the updated name and maximum class count remain as separate events.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;StudentProfileUpdated&lt;/code&gt; is applied to the state created by &lt;code&gt;StudentCreated&lt;/code&gt;, the name becomes &lt;code&gt;Student update persistence test updated&lt;/code&gt;, and the maximum class count becomes &lt;code&gt;3&lt;/code&gt;. The Projector branch and the Decider's &lt;code&gt;Evolve&lt;/code&gt; added this time reproduce this state change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;By implementing the student information update step by step, I felt that I gained a deeper understanding of the event sourcing flow of "validating a change, recording it as an event, and building the state from that history," as well as the role of each process. I also thought it was very useful that persistence can be left to Sekiban, allowing us to focus on business rules and state changes.&lt;/p&gt;

&lt;p&gt;This time, I used Codex for the implementation. Instead of asking it to implement the entire feature at once, I proceeded while checking the purpose and code of each step. Since the processing in this structure is separated by responsibility, it worked well with this kind of implementation through dialogue, and I was able to implement the feature while building up my understanding step by step.&lt;/p&gt;

</description>
      <category>eventsourcing</category>
      <category>tutorial</category>
      <category>dcb</category>
    </item>
    <item>
      <title>[Event Sourcing] Trying out Sekiban DCB: Introduction</title>
      <dc:creator>kairi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 02:51:17 +0000</pubDate>
      <link>https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78</link>
      <guid>https://dev.to/kary_0009/event-sourcing-trying-out-sekiban-dcb-introduction-4m78</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While researching ways to ensure data consistency using NoSQL in systems with frequent reads and writes, I became interested in Event Sourcing and CQRS. During this process, I found a framework called &lt;a href="https://www.sekiban.dev/" rel="noopener noreferrer"&gt;Sekiban&lt;/a&gt;, which is designed for Event Sourcing/CQRS, and decided to try it out.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.sekiban.dev/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.sekiban.dev%2Fog-image-en.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.sekiban.dev/" rel="noopener noreferrer" class="c-link"&gt;
            Sekiban - Event Sourcing / CQRS Framework
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Sekiban is an open-source Event Sourcing and CQRS framework for .NET using C#. Store events in Azure Cosmos DB, PostgreSQL, or AWS DynamoDB.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.sekiban.dev%2Ffavicon.ico" width="48" height="48"&gt;
          sekiban.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What is Sekiban DCB?
&lt;/h2&gt;

&lt;p&gt;According to the official website, Sekiban is defined as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"An open-source Event Sourcing and CQRS framework for .NET. It stores not just the current state, but all changes as immutable events."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within this framework, Sekiban DCB is an Event Sourcing framework that implements a Dynamic Consistency Boundary (DCB). It records all events in a single global stream.&lt;/p&gt;

&lt;p&gt;Looking at their documentation, they offer three approaches: DCB Native, DCB Wasm, and Sekiban Cloud (which seems to be still in development and not yet released). For this guide, I will run it using Native C#.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Environment &amp;amp; Setup
&lt;/h2&gt;

&lt;p&gt;Prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OS: Windows 11 Pro&lt;/li&gt;
&lt;li&gt;Environment: ASP.NET development environment ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your environment is set up, you can create a new project with just a few commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the Sekiban DCB templates&lt;/span&gt;
dotnet new &lt;span class="nb"&gt;install &lt;/span&gt;Sekiban.Dcb.Templates

&lt;span class="c"&gt;# Create project with Decider pattern&lt;/span&gt;
dotnet new sekiban-dcb-decider &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp

&lt;span class="c"&gt;# Run with Aspire&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; MyApp.AppHost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the last command will start the Aspire dashboard. The &lt;a href="https://dcb.events/#how-it-works" rel="noopener noreferrer"&gt;official DCB documentation&lt;/a&gt; uses a Student and Class relationship as an example. The default project automatically includes sample code for registering students and enrolling them in classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Here is a quick overview of the created project structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;DCBNativeProject.AppHost&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manages dependent services, connections, ports, and startup order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;DCBNativeProject.ApiService&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Handles API routing and authentication.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;DCBNativeProject.EventSource&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contains Commands, Handlers, Projectors, and data search processing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;DCBNativeProject.ImmutableModels&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines Student/Class events, Tags, State, and Deciders.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To give you an idea of how it works, here is the flow for creating and getting a student record.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6iyofqiv1h3soqg2iks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6iyofqiv1h3soqg2iks.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Student
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Read the incoming JSON and convert it to a &lt;code&gt;CreateStudent&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt; Execute the command using &lt;code&gt;ExecuteAsync&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt; Generate a "StudentCreated" event (in this sample, it only uses a &lt;code&gt;StudentTag&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; The framework saves the event to the database.&lt;/li&gt;
&lt;li&gt; Return the result to the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Retrieving a Student
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Receive the Student ID and find the target student using the &lt;code&gt;StudentTag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Define the expected data state using a &lt;code&gt;StudentProjector&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Call &lt;code&gt;GetTagStateAsync&lt;/code&gt; to restore the current state from the past events.&lt;/li&gt;
&lt;li&gt; Return the result to the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you can see, database saving operations are completely handled by the framework. I was able to save and load events without worrying about the database code at all.&lt;/p&gt;

&lt;p&gt;Next time, I plan to add more Entities and try out some different event registrations!&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>eventsourcing</category>
      <category>dcb</category>
    </item>
  </channel>
</rss>
