Skip to content

Commit

Permalink
Add EventInfo wrapper type for serializing events (#957)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Dec 7, 2023
1 parent 464bcc2 commit b8df013
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 5 deletions.
2 changes: 1 addition & 1 deletion TeachingRecordSystem/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<LangVersion>11.0</LangVersion>
<LangVersion>12.0</LangVersion>
<TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace TeachingRecordSystem.Core.DataStore.Postgres.Models;

public class Event
{
private static readonly JsonSerializerOptions _serializerOptions = new();
public static JsonSerializerOptions JsonSerializerOptions { get; } = new();

public long EventId { get; set; }
public long EventId { get; }
public required string EventName { get; init; }
public required DateTime Created { get; init; }
public required string Payload { get; init; }
Expand All @@ -16,7 +16,7 @@ public class Event
public static Event FromEventBase(EventBase @event)
{
var eventName = @event.GetType().Name;
var payload = JsonSerializer.Serialize(@event, inputType: @event.GetType(), _serializerOptions);
var payload = JsonSerializer.Serialize(@event, inputType: @event.GetType(), JsonSerializerOptions);

return new Event()
{
Expand All @@ -32,6 +32,6 @@ public EventBase ToEventBase()
var eventType = Type.GetType(eventTypeName) ??
throw new Exception($"Could not find event type '{eventTypeName}'.");

return (EventBase)JsonSerializer.Deserialize(Payload, eventType, _serializerOptions)!;
return (EventBase)JsonSerializer.Deserialize(Payload, eventType, JsonSerializerOptions)!;
}
}
124 changes: 124 additions & 0 deletions TeachingRecordSystem/src/TeachingRecordSystem.Core/EventInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Events;

namespace TeachingRecordSystem.Core;

/// <inheritdoc cref="EventInfo"/>
public sealed class EventInfo<TEvent> : EventInfo
where TEvent : EventBase
{
public EventInfo(TEvent @event)
: base(@event)
{
}

public static implicit operator TEvent(EventInfo<TEvent> @event) => @event.Event;

public new TEvent Event => (TEvent)base.Event;
}

/// <summary>
/// Wrapper type for serializing an event to JSON.
/// </summary>
public abstract class EventInfo
{
private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
{
Converters =
{
new EventInfoJsonConverter()
}
};

private protected EventInfo(EventBase @event)
{
Event = @event;
}

public EventBase Event { get; }

public static EventInfo<TEvent> Create<TEvent>(TEvent @event) where TEvent : EventBase => new EventInfo<TEvent>(@event);

public static EventInfo Deserialize(string payload) =>
JsonSerializer.Deserialize<EventInfo>(payload, _serializerOptions)!;

public static EventInfo<TEvent> Deserialize<TEvent>(string payload) where TEvent : EventBase =>
Deserialize(payload) is EventInfo<TEvent> typedEvent ? typedEvent :
throw new InvalidCastException();

public string Serialize() => JsonSerializer.Serialize(this, typeof(EventInfo), _serializerOptions);
}

file class EventInfoJsonConverter : JsonConverter<EventInfo>
{
public override EventInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

Type? eventType = null;

while (reader.Read())
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}

var propertyName = reader.GetString();

if (propertyName == "EventTypeName")
{
reader.Read();

if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}

var eventTypeName = typeof(EventBase).Namespace + "." + reader.GetString()!;
eventType = Type.GetType(eventTypeName);
}
else if (propertyName == "Event")
{
reader.Read();

if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

if (eventType is null)
{
throw new JsonException();
}

var @event = JsonSerializer.Deserialize(ref reader, eventType, Event.JsonSerializerOptions);

reader.Read();

var eventInfoType = typeof(EventInfo<>).MakeGenericType(eventType);
return (EventInfo)Activator.CreateInstance(eventInfoType, [@event])!;
}
}

throw new Exception();
}

public override void Write(Utf8JsonWriter writer, EventInfo value, JsonSerializerOptions options)
{
var eventType = value.Event.GetType();
var eventTypeName = eventType.Name;

writer.WriteStartObject();
writer.WritePropertyName("EventTypeName");
writer.WriteStringValue(eventTypeName);
writer.WritePropertyName("Event");
JsonSerializer.Serialize(writer, value.Event, eventType, Event.JsonSerializerOptions);
writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using TeachingRecordSystem.Core.Events;

namespace TeachingRecordSystem.Core.Tests;

public class EventInfoTests
{
[Fact]
public void EventSerializesCorrectly()
{
// Arrange
var @e = new UserActivatedEvent()
{
CreatedUtc = DateTime.UtcNow,
SourceUserId = DataStore.Postgres.Models.User.SystemUserId,
User = new()
{
AzureAdUserId = "ad-user-id",
Email = "[email protected]",
Name = "Test User",
Roles = ["Administrator"],
UserId = Guid.NewGuid(),
UserType = UserType.Person
}
};

var eventInfo = EventInfo.Create(@e);

// Act
var serialized = eventInfo.Serialize();
var deserialized = EventInfo.Deserialize(serialized);

// Assert
var roundTripped = Assert.IsType<EventInfo<UserActivatedEvent>>(deserialized);
Assert.Equal(e.CreatedUtc, roundTripped.Event.CreatedUtc);
Assert.Equal(e.SourceUserId, roundTripped.Event.SourceUserId);
Assert.Equal(e.User.AzureAdUserId, roundTripped.Event.User.AzureAdUserId);
Assert.Equal(e.User.Email, roundTripped.Event.User.Email);
Assert.Equal(e.User.Name, roundTripped.Event.User.Name);
Assert.Equal(e.User.Roles, roundTripped.Event.User.Roles);
Assert.Equal(e.User.UserId, roundTripped.Event.User.UserId);
Assert.Equal(e.User.UserType, roundTripped.Event.User.UserType);
}
}

0 comments on commit b8df013

Please sign in to comment.