EventSourcing.NetCore

2025-12-07 0 384

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

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 EventSourcing.NetCore https://www.zuozi.net/31718.html

pitstop
上一篇: pitstop
opentelemetry dotnet
下一篇: opentelemetry dotnet
常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务