Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Schema Versioning #75

Merged
merged 6 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions EventSourcing.NetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventPipelines.MediatR", "S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Serialization", "Core.Serialization\Core.Serialization.csproj", "{8C102EF7-ED16-43AC-9511-DB20BDE5E743}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EventsVersioning", "EventsVersioning", "{0C3B70E2-26A6-4707-A537-74D649EDC2B8}"
ProjectSection(SolutionItems) = preProject
Sample\EventsVersioning\README.md = Sample\EventsVersioning\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECommerce.V1", "Sample\EventsVersioning\ECommerce.V1\ECommerce.V1.csproj", "{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsVersioning.Tests", "Sample\EventsVersioning\EventsVersioning.Tests\EventsVersioning.Tests.csproj", "{B8C099AE-3585-4228-9C79-81BB340A5215}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -523,6 +532,14 @@ Global
{8C102EF7-ED16-43AC-9511-DB20BDE5E743}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C102EF7-ED16-43AC-9511-DB20BDE5E743}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C102EF7-ED16-43AC-9511-DB20BDE5E743}.Release|Any CPU.Build.0 = Release|Any CPU
{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8}.Release|Any CPU.Build.0 = Release|Any CPU
{B8C099AE-3585-4228-9C79-81BB340A5215}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8C099AE-3585-4228-9C79-81BB340A5215}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8C099AE-3585-4228-9C79-81BB340A5215}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C099AE-3585-4228-9C79-81BB340A5215}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -617,6 +634,9 @@ Global
{57CBBAB0-D609-4628-9E98-44CBD074D20D} = {D9799DB3-9D11-4909-8133-64CD96F6E1AC}
{504EF1B3-0E8E-42D8-8E30-2348539E3639} = {D9799DB3-9D11-4909-8133-64CD96F6E1AC}
{8C102EF7-ED16-43AC-9511-DB20BDE5E743} = {0570E45A-2EB6-4C4C-84E4-2C80E1FECEB5}
{0C3B70E2-26A6-4707-A537-74D649EDC2B8} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
{7B7E8DF0-8D21-4BC0-8A61-966A34E77DD8} = {0C3B70E2-26A6-4707-A537-74D649EDC2B8}
{B8C099AE-3585-4228-9C79-81BB340A5215} = {0C3B70E2-26A6-4707-A537-74D649EDC2B8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ Tutorial, practical samples and other resources about Event Sourcing in .NET Cor
- [6.2 Simple EventSourcing with EventStoreDB](#62-simple-eventsourcing-with-eventstoredb)
- [6.3 ECommerce with EventStoreDB](#63-ecommerce-with-eventstoredb)
- [6.5 Warehouse](#65-warehouse)
- [6.6 Event Pipelines](#66-event-pipelines)
- [6.7 Meetings Management with Marten](#67-meetings-management-with-marten)
- [6.8 Cinema Tickets Reservations with Marten](#68-cinema-tickets-reservations-with-marten)
- [6.9 SmartHome IoT with Marten](#69-smarthome-iot-with-marten)
- [6.6 Event Versioning](#66-event-versioning)
- [6.7 Event Pipelines](#67-event-pipelines)
- [6.8 Meetings Management with Marten](#68-meetings-management-with-marten)
- [6.9 Cinema Tickets Reservations with Marten](#69-cinema-tickets-reservations-with-marten)
- [6.10 SmartHome IoT with Marten](#610-smarthome-iot-with-marten)
- [7. Self-paced training Kit](#7-self-paced-training-kit)
- [8. Articles](#8-articles)
- [9. Event Store - Marten](#9-event-store---marten)
Expand Down Expand Up @@ -453,7 +454,21 @@ Samples are using CQRS architecture. They're sliced based on the business module
- No Event Sourcing! Using Entity Framework to show that CQRS is not bounded to Event Sourcing or any type of storage,
- No Aggregates! CQRS do not need DDD. Business logic can be handled in handlers.

### 6.6 [Event Pipelines](./Sample/EventPipelines)
### 6.6 [Event Versioning](./Sample/EventVersioning)
Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):
- [Simple mapping](./Sample/EventsVersioning/#simple-mapping)
- [New not required property](./Sample/EventsVersioning/#new-not-required-property)
- [New required property](./Sample/EventsVersioning/#new-required-property)
- [Renamed property](./Sample/EventsVersioning/#renamed-property)
- [Upcasting](./Sample/EventsVersioning/#upcasting)
- [Changed Structure](./Sample/EventsVersioning/#changed-structure)
- [New required property](./Sample/EventsVersioning/#new-required-property-1)
- [Downcasters](./Sample/EventsVersioning/#downcasters)
- [Events Transformations](./Sample/EventsVersioning/#events-transformations)
- [Stream Transformation](./Sample/EventsVersioning/#stream-transformation)
- [Summary](./Sample/EventsVersioning/#summary)

### 6.7 [Event Pipelines](./Sample/EventPipelines)
Shows how to compose event handlers in the processing pipelines to:
- filter events,
- transform them,
Expand All @@ -464,20 +479,20 @@ Shows how to compose event handlers in the processing pipelines to:
- can be used with Dependency Injection, but also without through builder,
- integrates with MediatR if you want to.

### 6.7 [Meetings Management with Marten](./Sample/MeetingsManagement/)
### 6.8 [Meetings Management with Marten](./Sample/MeetingsManagement/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- microservices example,
- stores events to Marten,
- Kafka as a messaging platform to integrate microservices,
- read models handled in separate microservice and stored to other database (ElasticSearch)

### 6.8 [Cinema Tickets Reservations with Marten](./Sample/Tickets/)
### 6.9 [Cinema Tickets Reservations with Marten](./Sample/Tickets/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- stores events to Marten.

### 6.9 [SmartHome IoT with Marten](./Sample/AsyncProjections/)
### 6.10 [SmartHome IoT with Marten](./Sample/AsyncProjections/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- stores events to Marten,
Expand Down
2 changes: 1 addition & 1 deletion Sample/EventPipelines/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# [Event Pipelines](./EventPipelines)
# Event Pipelines

Shows how to compose event handlers in the processing pipelines to:
- filter events,
Expand Down
2 changes: 1 addition & 1 deletion Sample/EventStoreDB/Simple/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Simple, practical EventSourcing with EventStoreDB and EntityFramework

The PR is adding a new sample that contains the simple Event Sourcing setup with EventStoreDB. For the Read Model, Postgres and Entity Framework are used.
The is the simple Event Sourcing setup with EventStoreDB. For the Read Model, Postgres and Entity Framework are used.

You can watch the webinar on YouTube where I'm explaining the details of the implementation:

Expand Down
9 changes: 9 additions & 0 deletions Sample/EventsVersioning/ECommerce.V1/ECommerce.V1.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

</Project>
40 changes: 40 additions & 0 deletions Sample/EventsVersioning/ECommerce.V1/Events.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;

namespace ECommerce.V1;

public record ProductItem(
Guid ProductId,
int Quantity
);

public record PricedProductItem(
ProductItem ProductItem,
decimal UnitPrice
);

public record ShoppingCartInitialized(
Guid ShoppingCartId,
Guid ClientId
);

public record ProductItemAddedToShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ProductItemRemovedFromShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ShoppingCartConfirmed(
Guid ShoppingCartId,
DateTime ConfirmedAt
);

public enum ShoppingCartStatus
{
Pending = 1,
Confirmed = 2,
Cancelled = 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Text.Json;
using FluentAssertions;
using Xunit;
using V1 = ECommerce.V1;

namespace EventsVersioning.Tests.Downcasters;

public class ChangedStructure
{
public record Client(
Guid Id,
string Name = "Unknown"
);

public record ShoppingCartInitialized(
Guid ShoppingCartId,
Client Client
);

public static V1.ShoppingCartInitialized Downcast(
ShoppingCartInitialized newEvent
)
{
return new V1.ShoppingCartInitialized(
newEvent.ShoppingCartId,
newEvent.Client.Id
);
}

public static V1.ShoppingCartInitialized Downcast(
string newEventJson
)
{
var newEvent = JsonDocument.Parse(newEventJson).RootElement;

return new V1.ShoppingCartInitialized(
newEvent.GetProperty("ShoppingCartId").GetGuid(),
newEvent.GetProperty("Client").GetProperty("Id").GetGuid()
);
}

[Fact]
public void UpcastObjects_Should_BeForwardCompatible()
{
// Given
var newEvent = new ShoppingCartInitialized(
Guid.NewGuid(),
new Client( Guid.NewGuid(), "Oskar the Grouch")
);

// When
var @event = Downcast(newEvent);

@event.Should().NotBeNull();
@event.ShoppingCartId.Should().Be(newEvent.ShoppingCartId);
@event.ClientId.Should().Be(newEvent.Client.Id);
}

[Fact]
public void UpcastJson_Should_BeForwardCompatible()
{
// Given
var newEvent = new ShoppingCartInitialized(
Guid.NewGuid(),
new Client( Guid.NewGuid(), "Oskar the Grouch")
);
// When
var @event = Downcast(
JsonSerializer.Serialize(newEvent)
);

@event.Should().NotBeNull();
@event.ShoppingCartId.Should().Be(newEvent.ShoppingCartId);
@event.ClientId.Should().Be(newEvent.Client.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VSTestLogger>trx%3bLogFileName=$(MSBuildProjectName).trx</VSTestLogger>
<VSTestResultsDirectory>$(MSBuildThisFileDirectory)/bin/TestResults/$(TargetFramework)</VSTestResultsDirectory>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ECommerce.V1\ECommerce.V1.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json;

namespace EventsVersioning.Tests.Serializers;

public static class SystemTextJsonSerializer
{
public static string Serialize(this object @event) =>
JsonSerializer.Serialize(@event);


public static T? Deserialize<T>(this string @event) =>
JsonSerializer.Deserialize<T>(@event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Text.Json;
using FluentAssertions;
using Xunit;
using V1 = ECommerce.V1;

namespace EventsVersioning.Tests.SimpleMappings;

public class NewNotRequiredProperty
{
public record ShoppingCartInitialized(
Guid ShoppingCartId,
Guid ClientId,
// Adding new not required property as nullable
DateTime? IntializedAt
);

[Fact]
public void Should_BeForwardCompatible()
{
// Given
var oldEvent = new V1.ShoppingCartInitialized(Guid.NewGuid(), Guid.NewGuid());
var json = JsonSerializer.Serialize(oldEvent);

// When
var @event = JsonSerializer.Deserialize<ShoppingCartInitialized>(json);

@event.Should().NotBeNull();
@event!.ShoppingCartId.Should().Be(oldEvent.ShoppingCartId);
@event.ClientId.Should().Be(oldEvent.ClientId);
@event.IntializedAt.Should().BeNull();
}

[Fact]
public void Should_BeBackwardCompatible()
{
// Given
var @event = new ShoppingCartInitialized(Guid.NewGuid(), Guid.NewGuid(), DateTime.UtcNow);
var json = JsonSerializer.Serialize(@event);

// When
var oldEvent = JsonSerializer.Deserialize<V1.ShoppingCartInitialized>(json);

oldEvent.Should().NotBeNull();
oldEvent!.ShoppingCartId.Should().Be(@event.ShoppingCartId);
oldEvent.ClientId.Should().Be(@event.ClientId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Text.Json;
using FluentAssertions;
using Xunit;
using V1 = ECommerce.V1;

namespace EventsVersioning.Tests.SimpleMappings;

public class NewRequiredProperty
{
public enum ShoppingCartStatus
{
Pending = 1,
Initialized = 2,
Confirmed = 3,
Cancelled = 4
}

public record ShoppingCartInitialized(
Guid ShoppingCartId,
Guid ClientId,
// Adding new not required property as nullable
ShoppingCartStatus Status = ShoppingCartStatus.Initialized
);

[Fact]
public void Should_BeForwardCompatible()
{
// Given
var oldEvent = new V1.ShoppingCartInitialized(Guid.NewGuid(), Guid.NewGuid());
var json = JsonSerializer.Serialize(oldEvent);

// When
var @event = JsonSerializer.Deserialize<ShoppingCartInitialized>(json);

@event.Should().NotBeNull();
@event!.ShoppingCartId.Should().Be(oldEvent.ShoppingCartId);
@event.ClientId.Should().Be(oldEvent.ClientId);
@event.Status.Should().Be(ShoppingCartStatus.Initialized);
}

[Fact]
public void Should_BeBackwardCompatible()
{
// Given
var @event = new ShoppingCartInitialized(Guid.NewGuid(), Guid.NewGuid(), ShoppingCartStatus.Pending);
var json = JsonSerializer.Serialize(@event);

// When
var oldEvent = JsonSerializer.Deserialize<V1.ShoppingCartInitialized>(json);

oldEvent.Should().NotBeNull();
oldEvent!.ShoppingCartId.Should().Be(@event.ShoppingCartId);
oldEvent.ClientId.Should().Be(@event.ClientId);
}
}