You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
23
24
24
25
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.
25
26
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.
27
28
28
29
This sample shows how to do basic Event Schema versioning. Those patterns can be applied to any event store.
29
30
@@ -37,6 +38,8 @@ or read blog article [Simple patterns for events schema versioning](https://even
37
38
38
39
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.
39
40
41
+
### New not required property
42
+
40
43
Having event defined as such:
41
44
42
45
```csharp
@@ -46,8 +49,6 @@ public record ShoppingCartInitialized(
46
49
);
47
50
```
48
51
49
-
### New not required property
50
-
51
52
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:
52
53
53
54
```csharp
@@ -59,14 +60,16 @@ public record ShoppingCartInitialized(
59
60
);
60
61
```
61
62
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
+
62
65
See full sample: [NewNotRequiredProperty.cs](./EventsVersioning.Tests/SimpleMappings/NewNotRequiredProperty.cs).
63
66
64
67
65
68
### New required property
66
69
67
70
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.
68
71
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.
70
73
71
74
```csharp
72
75
publicenumShoppingCartStatus
@@ -112,6 +115,29 @@ public class ShoppingCartInitialized
112
115
}
113
116
}
114
117
```
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
+
publicclassShoppingCartIntialised
123
+
{
124
+
publicGuidCartId { get; init; }
125
+
publicGuidClientId { get; init; }
126
+
127
+
publicShoppingCartIntialised(
128
+
Guid? cartId,
129
+
GuidclientId,
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
+
115
141
See full sample: [NewRequiredProperty.cs](./EventsVersioning.Tests/SimpleMappings/NewRequiredProperty.cs).
116
142
117
143
## Upcasting
@@ -126,14 +152,14 @@ For instance, we decide to send also other information about the client, instead
126
152
127
153
```csharp
128
154
publicrecordClient(
129
-
GuidId,
130
-
stringName="Unknown"
131
-
);
155
+
GuidId,
156
+
stringName="Unknown"
157
+
);
132
158
133
-
publicrecordShoppingCartInitialized(
134
-
GuidShoppingCartId,
135
-
ClientClient
136
-
);
159
+
publicrecordShoppingCartInitialized(
160
+
GuidShoppingCartId,
161
+
ClientClient
162
+
);
137
163
```
138
164
139
165
We can define upcaster as a function that'll later plug in the deserialisation process.
See a full sample in [StreamTransformations.cs](./EventsVersioning.Tests/Transformations/StreamTransformations.cs).
558
588
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
+

598
+
559
599
## Summary
560
600
561
601
I hope that those samples will show you that you can support many versioning scenarios with basic composition techniques.
562
602
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.
0 commit comments