EventSourcing.NetCore

其他类别 2025-08-06

eventsourcing .net

教程,实用样本和其他有关.NET事件采购的资源。另请参阅我的JVM和Nodejs的类似存储库。

  • eventsourcing .net
    • 1。事件采购
      • 1.1什么是事件采购?
      • 1.2什么是事件?
      • 1.3什么是流?
      • 1.4事件表示
      • 1.5事件存储
      • 1.6从事件中检索当前状态
      • 1.7与Marten的强大ID
    • 2。视频
      • 2.1。与Marten的实用活动采购
      • 2.2。保持溪流短!或如何有效地对事件采购系统建模
      • 2.3。让我们在一小时内建立活动商店!
      • 2.4。 CQR比C#11&net7更简单
      • 2.5。用EventStoredB进行事件采购的实用介绍
      • 2.6。如何在事件采购系统中处理隐私和GDPR
      • 2.7让我们构建最糟糕的事件采购系统!
      • 2.8事件驱动设计的光和阴暗面
      • 2.9实施分布式流程
      • 2.10与伊夫·洛弗林的对话
      • 2.11。永远不要丢失数据 - 事件采购进行救援!
    • 3。支持
    • 4。先决条件
    • 5。使用的工具
    • 6。样品
      • 6.1与Marten的务实事件采购
      • 6.2与Marten的电子商务
      • 6.3使用EventStoredB进行简单的事件库
      • 6.4实施分布式流程
      • 6.5 EventStoredB的电子商务
      • 6.6仓库
      • 6.7仓库最小API
      • 6.8事件版本
      • 6.9事件管道
      • 6.10与Marten的会议管理
      • 6.11电影票与Marten预订
      • 6.12与Marten的物联网
    • 7。自定进度训练套件
      • 7.1事件采购简介
      • 7.2建立自己的活动商店
    • 8。文章
    • 9.活动商店 - 马丁
    • 10。CQRS(命令查询责任分离)
    • 11. Nuget软件包可以帮助您入门。
    • 12。其他资源
      • 12.1简介
      • 12.2生产的活动采购
      • 12.3预测
      • 12.4快照
      • 12.5版本控制
      • 12.6存储
      • 12.7设计和建模
      • 12.8 GDPR
      • 12.9冲突检测
      • 12.10功能编程
      • 12.12测试
      • 12.13 CQRS
      • 12.14工具
      • 12.15事件处理
      • 12.16分布式过程
      • 12.17域驱动设计
      • 12.18白皮书
      • 12.19事件采购问题
      • 12.20这不是事件采购(而是事件流)
      • 每周12.21建筑
    • 执照

1。事件采购

1.1什么是事件采购?

事件采购是一种设计模式,其中业务运营的结果存储为一系列事件。

这是持久数据的另一种方法。与仅保留实体状态的最新版本的面向状态的持久性相反,事件采购将每个状态的变化作为单独的事件而变化。

因此,没有丢失业务数据。每个操作都会导致存储在数据库中的事件。这样可以扩展审核和诊断功能(无论是在技术上还是业务方面)。更重要的是,随着事件包含业务环境,它允许广泛的业务分析和报告。

在此存储库中,我将展示有关从基本到高级实践的事件采购的不同方面和模式。

在我的文章中阅读更多:

  • 在团队的自主权中使用活动如何有助于
  • 什么时候不使用事件采购?

1.2什么是事件?

事件代表过去的事实。他们携带有关已完成的事情的信息。它应该在过去时命名,例如“用户添加”“已确认订单” 。事件不是针对特定收件人的 - 它们是广播的信息。这就像在聚会上讲一个故事。我们希望有人听我们的话,但是我们可能会很快意识到没有人注意。

事件:

  • 不变了: “看不见的东西”
  • 可以忽略但不能缩回(因为您无法更改过去)。
  • 可以用不同的解释。篮球比赛的结果是事实。获胜的团队球迷将积极解释它。失去团队球迷 - 不是很多。

在我的文章中阅读更多:

  • 命令和事件有什么区别?
  • 事件应该尽可能小,对吗?
  • 事件建模中的反模式 - 财产采购
  • 事件建模中的反诉讼 - 状态痴迷

1.3什么是流?

事件在逻辑上分组为流。在事件采购中,流是实体的表示。所有实体状态突变最终都随着持续的事件而最终出现。通过阅读所有流事件并按照外观顺序逐一应用它们来检索实体状态。

流应该具有代表特定对象的唯一标识符。每个事件在流中都有自己独特的位置。该位置通常由数字,增量值表示。该数字可用于在检索状态时定义事件的顺序。它也可以用于检测并发问题。

1.4事件表示

从技术上讲,事件是消息。

它们可以代表,例如JSON,二进制,XML格式。除了数据外,它们通常包含:

  • ID :唯一的事件标识符。
  • 类型:事件的名称,例如“发票已发行”
  • 流ID :已注册事件的对象ID(例如,开票ID)。
  • 流位置(也命名为版本发生的顺序等):用于决定特定对象(流)事件发生顺序的数字。
  • 时间戳:代表事件发生的时间。
  • 其他元数据,例如相关ID,因果关系ID等。

示例事件JSON看起来像:

{
  "id" : " e44f813c-1a2f-4747-aed5-086805c6450e " ,
  "type" : " invoice-issued " ,
  "streamId" : " INV/2021/11/01 " ,
  "streamPosition" : 1 ,
  "timestamp" : " 2021-11-01T00:05:32.000Z " ,

  "data" :
  {
    "issuedTo" : {
      "name" : " Oscar the Grouch " ,
      "address" : " 123 Sesame Street "
    },
    "amount" : 34.12 ,
    "number" : " INV/2021/11/01 " ,
    "issuedAt" : " 2021-11-01T00:05:32.000Z "
  },

  "metadata" :
  {
    "correlationId" : " 1fecc92e-3197-4191-b929-bd306e1110a4 " ,
    "causationId" : " c3cf07e8-9f2f-4c2d-a8e9-f8a612b4a7f1 "
  }
}

在我的文章中阅读更多:

  • 限制映射事件类型
  • 事件采购中的明确事件序列化

1.5事件存储

事件采购与任何类型的存储实现无关。只要它符合假设,就可以在具有任何支持数据库(关系,文档等)的情况下实现它。国家必须由事件的附加日志表示。这些事件按时间顺序存储,并将新事件附加到上一个事件中。事件商店是为此目的明确设计的数据库类别。

在我的文章中阅读更多:

  • 让我们在一小时内建立活动商店!
  • 如果我告诉您关系数据库实际上是事件商店怎么办?

1.6从事件中检索当前状态

在事件采购中,该州存储在活动中。事件在逻辑上分组为流。可以将流视为实体的表示。传统上(例如,在关系或文档方法中),每个实体都被存储为单独的记录。

ID 发行 ISSUERADDRESS 数量 数字 发行
E44F813C 奥斯卡粗鲁 芝麻街123号 34.12 Inv/2021/11/01 2021-11-01

在事件采购中,该实体被存储为该特定对象发生的一系列事件,例如开票,开票,发票,发票。

[
    {
        "id" : " e44f813c-1a2f-4747-aed5-086805c6450e " ,
        "type" : " invoice-initiated " ,
        "streamId" : " INV/2021/11/01 " ,
        "streamPosition" : 1 ,
        "timestamp" : " 2021-11-01T00:05:32.000Z " ,

        "data" :
        {
            "issuer" : {
                "name" : " Oscar the Grouch " ,
                "address" : " 123 Sesame Street " ,
            },
            "amount" : 34.12 ,
            "number" : " INV/2021/11/01 " ,
            "initiatedAt" : " 2021-11-01T00:05:32.000Z "
        }
    },
    {
        "id" : " 5421d67d-d0fe-4c4c-b232-ff284810fb59 " ,
        "type" : " invoice-issued " ,
        "streamId" : " INV/2021/11/01 " ,
        "streamPosition" : 2 ,
        "timestamp" : " 2021-11-01T00:11:32.000Z " ,

        "data" :
        {
            "issuedTo" : " Cookie Monster " ,
            "issuedAt" : " 2021-11-01T00:11:32.000Z "
        }
    },
    {
        "id" : " 637cfe0f-ed38-4595-8b17-2534cc706abf " ,
        "type" : " invoice-sent " ,
        "streamId" : " INV/2021/11/01 " ,
        "streamPosition" : 3 ,
        "timestamp" : " 2021-11-01T00:12:01.000Z " ,

        "data" :
        {
            "sentVia" : " email " ,
            "sentAt" : " 2021-11-01T00:12:01.000Z "
        }
    }
]

所有这些事件共享流ID(“流”:“ Inv/2021/11/01”),并具有增量的流位置。

在事件采购中,每个实体都由其流表示:事件的顺序与流位置排序的流ID相关。

为了获得实体的当前状态,我们需要执行流汇总过程。我们将事件集转换为一个实体。这可以通过以下步骤完成:

  1. 阅读特定流的所有事件。
  2. 订购他们以外观顺序(通过事件的流位置)上升。
  3. 构建实体类型的空对象(例如带有默认构造函数)。
  4. 将每个事件应用于实体。

此过程也称为流聚合状态补液

我们可以将其实施为:

 public record Person (
    string Name ,
    string Address
) ;

public record InvoiceInitiated (
    double Amount ,
    string Number ,
    Person IssuedTo ,
    DateTime InitiatedAt
) ;

public record InvoiceIssued (
    string IssuedBy ,
    DateTime IssuedAt
) ;

public enum InvoiceSendMethod
{
    Email ,
    Post
}

public record InvoiceSent (
    InvoiceSendMethod SentVia ,
    DateTime SentAt
) ;

public enum InvoiceStatus
{
    Initiated = 1 ,
    Issued = 2 ,
    Sent = 3
}

public class Invoice
{
    public string Id { get ; set ; }
    public double Amount { get ; private set ; }
    public string Number { get ; private set ; }

    public InvoiceStatus Status { get ; private set ; }

    public Person IssuedTo { get ; private set ; }
    public DateTime InitiatedAt { get ; private set ; }

    public string IssuedBy { get ; private set ; }
    public DateTime IssuedAt { get ; private set ; }

    public InvoiceSendMethod SentVia { get ; private set ; }
    public DateTime SentAt { get ; private set ; }

    public void Evolve ( object @event )
    {
        switch ( @event )
        {
            case InvoiceInitiated invoiceInitiated :
                Apply ( invoiceInitiated ) ;
                break ;
            case InvoiceIssued invoiceIssued :
                Apply ( invoiceIssued ) ;
                break ;
            case InvoiceSent invoiceSent :
                Apply ( invoiceSent ) ;
                break ;
        }
    }

    private void Apply ( InvoiceInitiated @event )
    {
        Id = @event . Number ;
        Amount = @event . Amount ;
        Number = @event . Number ;
        IssuedTo = @event . IssuedTo ;
        InitiatedAt = @event . InitiatedAt ;
        Status = InvoiceStatus . Initiated ;
    }

    private void Apply ( InvoiceIssued @event )
    {
        IssuedBy = @event . IssuedBy ;
        IssuedAt = @event . IssuedAt ;
        Status = InvoiceStatus . Issued ;
    }

    private void Apply ( InvoiceSent @event )
    {
        SentVia = @event . SentVia ;
        SentAt = @event . SentAt ;
        Status = InvoiceStatus . Sent ;
    }
}

并将其用作:

 var invoiceInitiated = new InvoiceInitiated (
    34.12 ,
    "INV/2021/11/01" ,
    new Person ( "Oscar the Grouch" , "123 Sesame Street" ) ,
    DateTime . UtcNow
) ;
var invoiceIssued = new InvoiceIssued (
    "Cookie Monster" ,
    DateTime . UtcNow
) ;
var invoiceSent = new InvoiceSent (
    InvoiceSendMethod . Email ,
    DateTime . UtcNow
) ;

// 1,2. Get all events and sort them in the order of appearance
var events = new object [ ] { invoiceInitiated , invoiceIssued , invoiceSent } ;

// 3. Construct empty Invoice object
var invoice = new Invoice ( ) ;

// 4. Apply each event on the entity.
foreach ( var @event in events )
{
    invoice . Evolve ( @event ) ;
}

并将其概括为汇总基类:

 public abstract class Aggregate < T >
{
    public T Id { get ; protected set ; }

    protected Aggregate ( ) { }

    public virtual void Evolve ( object @event ) { }
}

“在线”流汇总的最大优势是它始终使用最新的业务逻辑。因此,在更改应用方法之后,它会自动反射在下一个运行中。如果事件数据很好,则不需要进行任何迁移或更新。

在Marten Evolve方法中不需要。 Marten使用命名约定,并在内部调用应用程序。它必须:

  • 具有事件对象的单个参数,
  • 结果具有无效类型。

请参阅样品:

  • 通用流聚合
  • EventStoredB

在我的文章中阅读更多:

  • 如何从事件中获取当前的实体状态?
  • 从事件重建状态时,您应该引发例外吗?

1.7与Marten的强大ID

强烈键入的ID(或通常是适当的类型系统)可以使您的代码更可预测。它减少了琐碎错误的机会,例如意外更改相同原始类型的参数顺序。

因此,对于这样的代码:

 var reservationId = "RES/01" ;
var seatId = "SEAT/22" ;
var customerId = "CUS/291" ;

var reservation = new Reservation (
    reservationId ,
    seatId ,
    customerId
) ;

如果您用SEATID切换保留ID,则编译器将不会捕获。

如果您使用强烈键入的ID,则编译将捕获该问题:

 var reservationId = new ReservationId ( "RES/01" ) ;
var seatId = new SeatId ( "SEAT/22" ) ;
var customerId = new CustomerId ( "CUS/291" ) ;

var reservation = new Reservation (
    reservationId ,
    seatId ,
    customerId
) ;

它们不是理想的,因为它们通常与存储引擎的运行效果不佳。典型的问题是:序列化,LINQ查询等。在某些情况下,它们可能只是过度杀伤。您需要选择毒药。

为了减少乏味的复制/粘贴代码,值得定义强大的ID基类,例如:

 public class StronglyTypedValue < T > : IEquatable < StronglyTypedValue < T > > where T : IComparable < T >
{
    public T Value { get ; }

    public StronglyTypedValue ( T value )
    {
        Value = value ;
    }

    public bool Equals ( StronglyTypedValue < T > ? other )
    {
        if ( ReferenceEquals ( null , other ) ) return false ;
        if ( ReferenceEquals ( this , other ) ) return true ;
        return EqualityComparer < T > . Default . Equals ( Value , other . Value ) ;
    }

    public override bool Equals ( object ? obj )
    {
        if ( ReferenceEquals ( null , obj ) ) return false ;
        if ( ReferenceEquals ( this , obj ) ) return true ;
        if ( obj . GetType ( ) != this . GetType ( ) ) return false ;
        return Equals ( ( StronglyTypedValue < T > ) obj ) ;
    }

    public override int GetHashCode ( )
    {
        return EqualityComparer < T > . Default . GetHashCode ( Value ) ;
    }

    public static bool operator == ( StronglyTypedValue < T > ? left , StronglyTypedValue < T > ? right )
    {
        return Equals ( left , right ) ;
    }

    public static bool operator != ( StronglyTypedValue < T > ? left , StronglyTypedValue < T > ? right )
    {
        return ! Equals ( left , right ) ;
    }
}

然后,您可以将特定ID类定义为:

 public class ReservationId : StronglyTypedValue < Guid >
{
    public ReservationId ( Guid value ) : base ( value )
    {
    }
}

您甚至可以添加其他规则:

 public class ReservationNumber : StronglyTypedValue < string >
{
    public ReservationNumber ( string value ) : base ( value )
    {
        if ( string . IsNullOrEmpty ( value ) || ! value . StartsWith ( "RES/" ) || value . Length <= 4 )
            throw new ArgumentOutOfRangeException ( nameof ( value ) ) ;
    }
}

与Marten合作的基类可以定义为:

 public abstract class Aggregate < TKey , T >
    where TKey : StronglyTypedValue < T >
    where T : IComparable < T >
{
    public TKey Id { get ; set ; } = default ! ;

    [ Identity ]
    public T AggregateId    {
        get => Id . Value ;
        set { }
    }

    public int Version { get ; protected set ; }

    [ JsonIgnore ] private readonly Queue < object > uncommittedEvents = new ( ) ;

    public object [ ] DequeueUncommittedEvents ( )
    {
        var dequeuedEvents = uncommittedEvents . ToArray ( ) ;

        uncommittedEvents . Clear ( ) ;

        return dequeuedEvents ;
    }

    protected void Enqueue ( object @event )
    {
        uncommittedEvents . Enqueue ( @event ) ;
    }
}

Marten需要使用公共设置器和字符串或GUID的Getter ID。我们使用了这个技巧,并在强烈的背景字段中添加了聚集体。我们还告知Marten在其内部使用此字段的身份属性。

示例汇总看起来像:

 public class Reservation : Aggregate < ReservationId , Guid >
{
    public CustomerId CustomerId { get ; private set ; } = default ! ;

    public SeatId SeatId { get ; private set ; } = default ! ;

    public ReservationNumber Number { get ; private set ; } = default ! ;

    public ReservationStatus Status { get ; private set ; }

    public static Reservation CreateTentative (
        SeatId seatId ,
        CustomerId customerId )
    {
        return new Reservation (
            new ReservationId ( Guid . NewGuid ( ) ) ,
            seatId ,
            customerId ,
            new ReservationNumber ( Guid . NewGuid ( ) . ToString ( ) )
        ) ;
    }

    // (...)
}

请参阅此处的完整示例。

在文章中阅读更多:

  • 与Marten一起使用强大的标识符
  • 不变的价值对象比您想象的要简单,更有用!

2。视频

2.1。与Marten的实用活动采购

2.2。保持溪流短!或如何有效地对事件采购系统建模

2.3。让我们在一小时内建立活动商店!

2.4。 CQR比C#11&net7更简单

2.5。用EventStoredB进行事件采购的实用介绍

2.6。如何在事件采购系统中处理隐私和GDPR

2.7让我们构建最糟糕的事件采购系统!

2.8事件驱动设计的光和阴暗面

2.9实施分布式流程

2.10与伊夫·洛弗林的对话

2.11。永远不要丢失数据 - 事件采购进行救援!

3。支持

如果您有任何疑问或要求更多的解释或样本,请随时创建问题。我也接受拉请请求

?如果这个存储库为您提供帮助 - 如果您加入我的官方支持者小组,我会很高兴:

Github赞助商

明星在github上或与您的朋友分享也将有所帮助!

4。先决条件

对于运行活动商店示例,您需要拥有:

  1. .NET 6安装-https://dotnet.microsoft.com/download/dotnet/6.0
  2. Docker安装了。然后转到Docker文件夹并运行:
下载源码

通过命令行克隆项目:

git clone https://github.com/oskardudycz/EventSourcing.NetCore.git