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什么是事件采购?
事件采购是一种设计模式,其中业务运营的结果存储为一系列事件。
这是持久数据的另一种方法。与仅保留实体状态的最新版本的面向状态的持久性相反,事件采购将每个状态的变化作为单独的事件而变化。
因此,没有丢失业务数据。每个操作都会导致存储在数据库中的事件。这样可以扩展审核和诊断功能(无论是在技术上还是业务方面)。更重要的是,随着事件包含业务环境,它允许广泛的业务分析和报告。
在此存储库中,我将展示有关从基本到高级实践的事件采购的不同方面和模式。
在我的文章中阅读更多:
- 在团队的自主权中使用活动如何有助于
- 什么时候不使用事件采购?
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相关。
为了获得实体的当前状态,我们需要执行流汇总过程。我们将事件集转换为一个实体。这可以通过以下步骤完成:
- 阅读特定流的所有事件。
- 订购他们以外观顺序(通过事件的流位置)上升。
- 构建实体类型的空对象(例如带有默认构造函数)。
- 将每个事件应用于实体。
此过程也称为流聚合或状态补液。
我们可以将其实施为:
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。先决条件
对于运行活动商店示例,您需要拥有:
- .NET 6安装-https://dotnet.microsoft.com/download/dotnet/6.0
- Docker安装了。然后转到Docker文件夹并运行:
