Skip to content

Commit cbbe4c2

Browse files
committed
Expanded Event Schema Versioning description
1 parent 0dd275a commit cbbe4c2

File tree

4 files changed

+58
-18
lines changed

4 files changed

+58
-18
lines changed

Core.EventStoreDB/Events/AggregateStreamExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static class AggregateStreamExtensions
1717
ulong? fromVersion = null
1818
) where T : class, IProjection
1919
{
20-
var readResult = eventStore.ReadStreamAsync(
20+
await using var readResult = eventStore.ReadStreamAsync(
2121
Direction.Forwards,
2222
StreamNameMapper.ToStreamId<T>(id),
2323
fromVersion ?? StreamPosition.Start,
@@ -36,4 +36,4 @@ public static class AggregateStreamExtensions
3636

3737
return aggregate;
3838
}
39-
}
39+
}

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ Samples are using CQRS architecture. They're sliced based on the business module
459459
- No Event Sourcing! Using Entity Framework to show that CQRS is not bounded to Event Sourcing or any type of storage,
460460
- No Aggregates! CQRS do not need DDD. Business logic can be handled in handlers.
461461

462-
### 6.6 [Event Versioning](./Sample/EventVersioning)
462+
### 6.6 [Event Versioning](./Sample/EventsVersioning)
463463
Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):
464464
- [Simple mapping](./Sample/EventsVersioning/#simple-mapping)
465465
- [New not required property](./Sample/EventsVersioning/#new-not-required-property)

Sample/EventsVersioning/README.md

+55-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Downcasters](#downcasters)
1212
- [Events Transformations](#events-transformations)
1313
- [Stream Transformation](#stream-transformation)
14+
- [Migrations](#migrations)
1415
- [Summary](#summary)
1516

1617
As time flow, the events' definition may change. Our business is changing, and we need to add more information. Sometimes we have to fix a bug or modify the definition for a better developer experience.
@@ -23,7 +24,7 @@ Migrations are never easy, even in relational databases. You always have to thin
2324

2425
We should always try to perform the change in a non-breaking manner. I explained that in [Let's take care of ourselves! Thoughts on compatibility](https://event-driven.io/en/lets_take_care_of_ourselves_thoughts_about_comptibility/) article.
2526

26-
The same "issues" happens for event data model. Greg Young wrote a book about it: https://leanpub.com/esversioning/read. I recommend you to read it.
27+
The same "issues" happens for event data model. Greg Young wrote a book about it. You can read it for free: https://leanpub.com/esversioning/read. I recommend you to read it.
2728

2829
This sample shows how to do basic Event Schema versioning. Those patterns can be applied to any event store.
2930

@@ -37,6 +38,8 @@ or read blog article [Simple patterns for events schema versioning](https://even
3738

3839
There are some simple mappings that we could handle on the code structure or serialisation level. I'm using `System.Text.Json` in samples, other serialises may be smarter, but the patterns will be similar.
3940

41+
### New not required property
42+
4043
Having event defined as such:
4144

4245
```csharp
@@ -46,8 +49,6 @@ public record ShoppingCartInitialized(
4649
);
4750
```
4851

49-
### New not required property
50-
5152
If we'd like to add a new not required property, e.g. `IntializedAt`, we can add it just as a new nullable property. The essential fact to decide if that's the right strategy is if we're good with not having it defined. It can be handled as:
5253

5354
```csharp
@@ -59,14 +60,16 @@ public record ShoppingCartInitialized(
5960
);
6061
```
6162

63+
Then, most serialisers will put the null value by default and not fail unless we use strict mode. The new events will contain whole information, for the old ones we'll have to live with that.
64+
6265
See full sample: [NewNotRequiredProperty.cs](./EventsVersioning.Tests/SimpleMappings/NewNotRequiredProperty.cs).
6366

6467

6568
### New required property
6669

6770
We must define a default value if we'd like to add a new required property and make it non-breaking. It's the same as you'd add a new column to the relational table.
6871

69-
For instance, we decide that we'd like to add a validation step when the shopping cart is open (e.g. for fraud or spam detection), and our shopping cart can be opened with a pending state. We could solve that by adding the new property with the status information and setting it to `Initialised`, assuming that all old events were appended using the older logic.
72+
For instance, we decide that we'd like to add a validation step when the shopping cart is open (e.g. for fraud or spam detection), and our shopping cart can be opened with a pending state. We could solve that by adding the new property with the status information and setting it to `Initialized`, assuming that all old events were appended using the older logic.
7073

7174
```csharp
7275
public enum ShoppingCartStatus
@@ -112,6 +115,29 @@ public class ShoppingCartInitialized
112115
}
113116
}
114117
```
118+
119+
The benefit is that both old and the new structure will be backward and forward compatible. The downside of this solution is that we're still keeping the old JSON structure, so all consumers need to be aware of that and do mapping if they want to use the new structure. Some serialisers like Newtonsoft Json.NET allows to do such magic:
120+
121+
```csharp
122+
public class ShoppingCartIntialised
123+
{
124+
public Guid CartId { get; init; }
125+
public Guid ClientId { get; init; }
126+
127+
public ShoppingCartIntialised(
128+
Guid? cartId,
129+
Guid clientId,
130+
Guid? shoppingCartId = null
131+
)
132+
{
133+
CartId = cartId ?? shoppingCartId!.Value;
134+
ClientId = clientId;
135+
}
136+
}
137+
```
138+
139+
We'll either use the new property name or, if it's not available, then an old one. The downside is that we had to pollute our code with additional fields and nullable markers. As always, pick your poison.
140+
115141
See full sample: [NewRequiredProperty.cs](./EventsVersioning.Tests/SimpleMappings/NewRequiredProperty.cs).
116142

117143
## Upcasting
@@ -126,14 +152,14 @@ For instance, we decide to send also other information about the client, instead
126152

127153
```csharp
128154
public record Client(
129-
Guid Id,
130-
string Name = "Unknown"
131-
);
155+
Guid Id,
156+
string Name = "Unknown"
157+
);
132158

133-
public record ShoppingCartInitialized(
134-
Guid ShoppingCartId,
135-
Client Client
136-
);
159+
public record ShoppingCartInitialized(
160+
Guid ShoppingCartId,
161+
Client Client
162+
);
137163
```
138164

139165
We can define upcaster as a function that'll later plug in the deserialisation process.
@@ -156,12 +182,16 @@ Or we can map it from JSON
156182

157183
```csharp
158184
public static ShoppingCartInitialized Upcast(
159-
V1.ShoppingCartInitialized oldEvent
185+
string oldEventJson
160186
)
161187
{
188+
var oldEvent = JsonDocument.Parse(oldEventJson).RootElement;
189+
162190
return new ShoppingCartInitialized(
163-
oldEvent.ShoppingCartId,
164-
new Client(oldEvent.ClientId)
191+
oldEvent.GetProperty("ShoppingCartId").GetGuid(),
192+
new Client(
193+
oldEvent.GetProperty("ClientId").GetGuid()
194+
)
165195
);
166196
}
167197
```
@@ -556,8 +586,18 @@ private EventData ToShoppingCartInitializedWithProducts(
556586

557587
See a full sample in [StreamTransformations.cs](./EventsVersioning.Tests/Transformations/StreamTransformations.cs).
558588

589+
## Migrations
590+
591+
You can say that, well, those patterns are not migrations. Events will stay as they were, and you'll have to keep the old structure forever. That's quite true. Still, this is fine, as typically, you should not change the past. Having precise information, even including bugs, is a valid scenario. It allows you to get insights and see the precise history. However, pragmatically you may sometimes want to have a "clean" event log with only a new schema.
592+
593+
It appears that composing the patterns described above can support such a case. For example, if you're running EventStoreDB or Marten, you can read/subscribe to the event stream, store events in the new stream, or even a new EventStoreDB cluster or Postgres schema. Having that, you could even rewrite the whole log and switch databases once the new one caught up.
594+
595+
I hope that those samples will show you that you can support many versioning scenarios with basic composition techniques.
596+
597+
![migrations](./assets/migrations.png)
598+
559599
## Summary
560600

561601
I hope that those samples will show you that you can support many versioning scenarios with basic composition techniques.
562602

563-
Nevertheless, the best approach is to [not need to do versioning at all](https://event-driven.io/en/how_to_do_event_versioning/). If you're facing such a need, before using the strategies described above, make sure that your business scenario cannot be solved by talking to the business. It may appear that's some flaw in the business process modelling. We should not be trying to fix the issue, but the root cause.
603+
Nevertheless, the best approach is to [not need to do versioning at all](https://event-driven.io/en/how_to_do_event_versioning/). If you're facing such a need, before using the strategies described above, make sure that your business scenario cannot be solved by talking to the business. It may appear that's some flaw in the business process modelling. We should not be trying to fix the issue, but the root cause.
1.51 MB
Loading

0 commit comments

Comments
 (0)