diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5da5d20..485bec1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
+
diff --git a/DrawTogether.sln b/DrawTogether.sln
index 93503e0..cd15684 100644
--- a/DrawTogether.sln
+++ b/DrawTogether.sln
@@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawTogether.Entities", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawTogether.Actors", "src\DrawTogether.Actors\DrawTogether.Actors.csproj", "{5863DD74-00DE-4DE9-9F0A-FCBC32195E78}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawTogether.Tests", "src\DrawTogether.Tests\DrawTogether.Tests.csproj", "{78CAFA4F-0DBC-45F7-8794-48785FDFDD47}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -109,6 +111,18 @@ Global
{5863DD74-00DE-4DE9-9F0A-FCBC32195E78}.Release|x64.Build.0 = Release|Any CPU
{5863DD74-00DE-4DE9-9F0A-FCBC32195E78}.Release|x86.ActiveCfg = Release|Any CPU
{5863DD74-00DE-4DE9-9F0A-FCBC32195E78}.Release|x86.Build.0 = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|x64.Build.0 = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Debug|x86.Build.0 = Debug|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|x64.ActiveCfg = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|x64.Build.0 = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|x86.ActiveCfg = Release|Any CPU
+ {78CAFA4F-0DBC-45F7-8794-48785FDFDD47}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/DrawTogether.Actors/DrawTogether.Actors.csproj b/src/DrawTogether.Actors/DrawTogether.Actors.csproj
index 8896dcb..54ebfa9 100644
--- a/src/DrawTogether.Actors/DrawTogether.Actors.csproj
+++ b/src/DrawTogether.Actors/DrawTogether.Actors.csproj
@@ -9,10 +9,18 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/DrawTogether.Actors/Serialization/DrawingProtocolSerializer.cs b/src/DrawTogether.Actors/Serialization/DrawingProtocolSerializer.cs
new file mode 100644
index 0000000..db2b1dd
--- /dev/null
+++ b/src/DrawTogether.Actors/Serialization/DrawingProtocolSerializer.cs
@@ -0,0 +1,491 @@
+using System.Collections.Immutable;
+using Akka.Actor;
+using Akka.Hosting;
+using Akka.Serialization;
+using DrawTogether.Actors.Serialization.Proto;
+using DrawTogether.Entities;
+using DrawTogether.Entities.Drawings;
+using DrawTogether.Entities.Drawings.Messages;
+using DrawTogether.Entities.Users;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using ConnectedStroke = DrawTogether.Entities.Drawings.ConnectedStroke;
+using DrawingSessionState = DrawTogether.Entities.Drawings.DrawingSessionState;
+using Point = DrawTogether.Entities.Drawings.Point;
+using Type = System.Type;
+
+namespace DrawTogether.Actors.Serialization;
+
+public static class CustomSerializationAkkaExtensions
+{
+ public static AkkaConfigurationBuilder AddDrawingProtocolSerializer(this AkkaConfigurationBuilder builder)
+ {
+ return builder.WithCustomSerializer("drawing", new[] { typeof(IWithDrawingSessionId) },
+ system => new DrawingProtocolSerializer(system));
+ }
+}
+
+public sealed class DrawingProtocolSerializer : SerializerWithStringManifest
+{
+ // generate manifests for all classes that implement the IWithDrawingSessionId interface - keep them to 2 or 3 characters in length
+ private const string StrokeAddedManifest = "sa";
+ private const string StrokeRemovedManifest = "sr";
+ private const string StrokesClearedManifest = "sc";
+ private const string UserAddedManifest = "ua";
+ private const string UserRemovedManifest = "ur";
+ private const string DrawingSessionClosedManifest = "dc";
+ private const string GetDrawingSessionStateManifest = "gs";
+ private const string SubscribeToDrawingSessionManifest = "su";
+ private const string SubscribeAcknowledgedManifest = "sak";
+ private const string UnsubscribeFromDrawingSessionManifest = "uu";
+ private const string UnsubscribeAcknowledgedManifest = "uak";
+ private const string AddStrokeManifest = "as";
+ private const string RemoveStrokeManifest = "rs";
+ private const string ClearStrokesManifest = "cs";
+ private const string AddUserManifest = "au";
+ private const string RemoveUserManifest = "ru";
+ private const string DrawingActivityUpdateManifest = "da";
+ private const string DrawingSessionStateManifest = "ds";
+
+ // arbitrary id but not within 0-100, which is reserved by Akka.NET
+ public override int Identifier => 481;
+
+ public DrawingProtocolSerializer(ExtendedActorSystem system) : base(system)
+ {
+ }
+
+ public override byte[] ToBinary(object obj)
+ {
+ switch (obj)
+ {
+ case IDrawingSessionEvent e:
+ return e switch
+ {
+ DrawingSessionEvents.StrokeAdded sa => ToProto(sa).ToByteArray(),
+ DrawingSessionEvents.StrokeRemoved sr => ToProto(sr).ToByteArray(),
+ DrawingSessionEvents.StrokesCleared sc => ToProto(sc).ToByteArray(),
+ DrawingSessionEvents.UserAdded ua => ToProto(ua).ToByteArray(),
+ DrawingSessionEvents.UserRemoved ur => ToProto(ur).ToByteArray(),
+ DrawingSessionEvents.DrawingSessionClosed dc => ToProto(dc).ToByteArray(),
+ _ => throw new ArgumentException($"Can't serialize object of type {e.GetType()}")
+ };
+ case IDrawingSessionCommand c:
+ return c switch
+ {
+ DrawingSessionCommands.AddStroke asCmd => ToProto(asCmd).ToByteArray(),
+ DrawingSessionCommands.RemoveStroke rsCmd => ToProto(rsCmd).ToByteArray(),
+ DrawingSessionCommands.ClearStrokes csCmd => ToProto(csCmd).ToByteArray(),
+ DrawingSessionCommands.AddUser auCmd => ToProto(auCmd).ToByteArray(),
+ DrawingSessionCommands.RemoveUser ruCmd => ToProto(ruCmd).ToByteArray(),
+ _ => throw new ArgumentException($"Can't serialize object of type {c.GetType()}")
+ };
+ case IDrawingSessionQuery q:
+ return q switch
+ {
+ DrawingSessionQueries.GetDrawingSessionState gs => ToProto(gs).ToByteArray(),
+ DrawingSessionQueries.SubscribeToDrawingSession su => ToProto(su).ToByteArray(),
+ DrawingSessionQueries.SubscribeAcknowledged sak => ToProto(sak).ToByteArray(),
+ DrawingSessionQueries.UnsubscribeFromDrawingSession uu => ToProto(uu).ToByteArray(),
+ DrawingSessionQueries.UnsubscribeAcknowledged uak => ToProto(uak).ToByteArray(),
+ _ => throw new ArgumentException($"Can't serialize object of type {q.GetType()}")
+ };
+ case DrawingActivityUpdate da:
+ return ToProto(da).ToByteArray();
+ case DrawingSessionState state:
+ return ToProto(state).ToByteArray();
+ default:
+ throw new ArgumentException($"Can't serialize object of type {obj.GetType()}");
+ }
+ }
+
+ private Proto.DrawingActivityUpdated ToProto(DrawingActivityUpdate asCmd)
+ {
+ var protoDrawingActivityUpdated = new Proto.DrawingActivityUpdated()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId,
+ LastUpdated = asCmd.LastUpdate.ToTimestamp(),
+ ActiveUsers = asCmd.ActiveUsers,
+ IsRemoved = asCmd.IsRemoved
+ };
+ return protoDrawingActivityUpdated;
+ }
+
+ private Proto.SubscribeToDrawingSessionState ToProto(DrawingSessionQueries.SubscribeToDrawingSession asCmd)
+ {
+ var protoSubscribeToDrawingSessionState = new Proto.SubscribeToDrawingSessionState()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId
+ };
+ return protoSubscribeToDrawingSessionState;
+ }
+
+ private Proto.SubscribeAcknowledged ToProto(DrawingSessionQueries.SubscribeAcknowledged asCmd)
+ {
+ var protoSubscribeAcknowledged = new Proto.SubscribeAcknowledged()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId
+ };
+ return protoSubscribeAcknowledged;
+ }
+
+ private Proto.UnsubscribeFromDrawingSessionState ToProto(DrawingSessionQueries.UnsubscribeFromDrawingSession asCmd)
+ {
+ var protoUnsubscribeFromDrawingSessionState = new Proto.UnsubscribeFromDrawingSessionState()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId
+ };
+ return protoUnsubscribeFromDrawingSessionState;
+ }
+
+ private Proto.UnsubscribeAcknowledged ToProto(DrawingSessionQueries.UnsubscribeAcknowledged asCmd)
+ {
+ var protoUnsubscribeAcknowledged = new Proto.UnsubscribeAcknowledged()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId
+ };
+ return protoUnsubscribeAcknowledged;
+ }
+
+
+ private Proto.AddStroke ToProto(DrawingSessionCommands.AddStroke asCmd)
+ {
+ var protoAddStroke = new Proto.AddStroke()
+ {
+ DrawingSessionId = asCmd.DrawingSessionId.SessionId,
+ ConnectedStroke = ToProto(asCmd.Stroke)
+ };
+ return protoAddStroke;
+ }
+
+ private Proto.RemoveStroke ToProto(DrawingSessionCommands.RemoveStroke rsCmd)
+ {
+ var protoRemoveStroke = new Proto.RemoveStroke()
+ {
+ DrawingSessionId = rsCmd.DrawingSessionId.SessionId,
+ StrokeId = rsCmd.StrokeId.Id
+ };
+ return protoRemoveStroke;
+ }
+
+ private Proto.ClearStrokes ToProto(DrawingSessionCommands.ClearStrokes csCmd)
+ {
+ var protoClearStrokes = new Proto.ClearStrokes()
+ {
+ DrawingSessionId = csCmd.DrawingSessionId.SessionId
+ };
+ return protoClearStrokes;
+ }
+
+ private Proto.AddUser ToProto(DrawingSessionCommands.AddUser auCmd)
+ {
+ var protoAddUser = new Proto.AddUser()
+ {
+ DrawingSessionId = auCmd.DrawingSessionId.SessionId,
+ UserId = auCmd.UserId.IdentityName
+ };
+ return protoAddUser;
+ }
+
+ private Proto.RemoveUser ToProto(DrawingSessionCommands.RemoveUser ruCmd)
+ {
+ var protoRemoveUser = new Proto.RemoveUser()
+ {
+ DrawingSessionId = ruCmd.DrawingSessionId.SessionId,
+ UserId = ruCmd.UserId.IdentityName
+ };
+ return protoRemoveUser;
+ }
+
+ private Proto.GetDrawingSessionState ToProto(DrawingSessionQueries.GetDrawingSessionState gs)
+ {
+ var protoGetDrawingSessionState = new Proto.GetDrawingSessionState()
+ {
+ DrawingSessionId = gs.DrawingSessionId.SessionId
+ };
+ return protoGetDrawingSessionState;
+ }
+
+
+ private Proto.AddStroke ToProto(DrawingSessionEvents.StrokeAdded stroke)
+ {
+ var protoStrokeAdded = new Proto.AddStroke()
+ {
+ DrawingSessionId = stroke.DrawingSessionId.SessionId,
+ ConnectedStroke = ToProto(stroke.Stroke)
+ };
+ return protoStrokeAdded;
+ }
+
+ private Proto.RemoveStroke ToProto(DrawingSessionEvents.StrokeRemoved stroke)
+ {
+ var protoStrokeRemoved = new Proto.RemoveStroke()
+ {
+ DrawingSessionId = stroke.DrawingSessionId.SessionId,
+ StrokeId = stroke.StrokeId.Id
+ };
+ return protoStrokeRemoved;
+ }
+
+ private Proto.ClearStrokes ToProto(DrawingSessionEvents.StrokesCleared strokes)
+ {
+ var protoStrokesCleared = new Proto.ClearStrokes()
+ {
+ DrawingSessionId = strokes.DrawingSessionId.SessionId
+ };
+ return protoStrokesCleared;
+ }
+
+ private Proto.AddUser ToProto(DrawingSessionEvents.UserAdded user)
+ {
+ var protoUserAdded = new Proto.AddUser()
+ {
+ DrawingSessionId = user.DrawingSessionId.SessionId,
+ UserId = user.UserId.IdentityName
+ };
+ return protoUserAdded;
+ }
+
+ private Proto.RemoveUser ToProto(DrawingSessionEvents.UserRemoved user)
+ {
+ var protoUserRemoved = new Proto.RemoveUser()
+ {
+ DrawingSessionId = user.DrawingSessionId.SessionId,
+ UserId = user.UserId.IdentityName
+ };
+ return protoUserRemoved;
+ }
+
+ private Proto.SessionClosed ToProto(DrawingSessionEvents.DrawingSessionClosed closed)
+ {
+ var protoDrawingSessionClosed = new Proto.SessionClosed()
+ {
+ DrawingSessionId = closed.DrawingSessionId.SessionId
+ };
+ return protoDrawingSessionClosed;
+ }
+
+ public override object FromBinary(byte[] bytes, string manifest)
+ {
+ return manifest switch
+ {
+ StrokeAddedManifest => FromProtoStrokeAdded(Proto.AddStroke.Parser.ParseFrom(bytes)),
+ StrokeRemovedManifest => FromProtoStrokeRemoved(Proto.RemoveStroke.Parser.ParseFrom(bytes)),
+ StrokesClearedManifest => FromProtoStrokesCleared(Proto.ClearStrokes.Parser.ParseFrom(bytes)),
+ UserAddedManifest => FromProtoUserAdded(Proto.AddUser.Parser.ParseFrom(bytes)),
+ UserRemovedManifest => FromProtoUserRemoved(Proto.RemoveUser.Parser.ParseFrom(bytes)),
+ DrawingSessionClosedManifest => FromProtoSessionClosed(Proto.SessionClosed.Parser.ParseFrom(bytes)),
+ DrawingActivityUpdateManifest => FromProto(Proto.DrawingActivityUpdated.Parser.ParseFrom(bytes)),
+ GetDrawingSessionStateManifest => FromProto(Proto.GetDrawingSessionState.Parser.ParseFrom(bytes)),
+ SubscribeToDrawingSessionManifest =>
+ FromProto(Proto.SubscribeToDrawingSessionState.Parser.ParseFrom(bytes)),
+ SubscribeAcknowledgedManifest => FromProto(Proto.SubscribeAcknowledged.Parser.ParseFrom(bytes)),
+ UnsubscribeFromDrawingSessionManifest => FromProto(
+ Proto.UnsubscribeFromDrawingSessionState.Parser.ParseFrom(bytes)),
+ UnsubscribeAcknowledgedManifest => FromProto(Proto.UnsubscribeAcknowledged.Parser.ParseFrom(bytes)),
+ AddStrokeManifest => FromProto(Proto.AddStroke.Parser.ParseFrom(bytes)),
+ RemoveStrokeManifest => FromProto(Proto.RemoveStroke.Parser.ParseFrom(bytes)),
+ ClearStrokesManifest => FromProto(Proto.ClearStrokes.Parser.ParseFrom(bytes)),
+ AddUserManifest => FromProto(Proto.AddUser.Parser.ParseFrom(bytes)),
+ RemoveUserManifest => FromProto(Proto.RemoveUser.Parser.ParseFrom(bytes)),
+ DrawingSessionStateManifest => FromProto(Proto.DrawingSessionState.Parser.ParseFrom(bytes)),
+ _ => throw new ArgumentException($"Can't deserialize object with manifest {manifest}")
+ };
+ }
+
+ private DrawingSessionCommands.RemoveUser FromProto(RemoveUser protoStroke)
+ {
+ return new DrawingSessionCommands.RemoveUser(new DrawingSessionId(protoStroke.DrawingSessionId),
+ new UserId(protoStroke.UserId));
+ }
+
+ private DrawingSessionCommands.AddUser FromProto(AddUser protoStroke)
+ {
+ return new DrawingSessionCommands.AddUser(new DrawingSessionId(protoStroke.DrawingSessionId),
+ new UserId(protoStroke.UserId));
+ }
+
+ private DrawingSessionCommands.ClearStrokes FromProto(ClearStrokes protoStroke)
+ {
+ return new DrawingSessionCommands.ClearStrokes(new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingSessionCommands.RemoveStroke FromProto(RemoveStroke protoStroke)
+ {
+ return new DrawingSessionCommands.RemoveStroke(new DrawingSessionId(protoStroke.DrawingSessionId),
+ new StrokeId(protoStroke.StrokeId));
+ }
+
+ private DrawingSessionCommands.AddStroke FromProto(AddStroke protoStroke)
+ {
+ return new DrawingSessionCommands.AddStroke(new DrawingSessionId(protoStroke.DrawingSessionId),
+ FromProto(protoStroke.ConnectedStroke));
+ }
+
+ private DrawingSessionQueries.UnsubscribeAcknowledged FromProto(UnsubscribeAcknowledged protoStroke)
+ {
+ return new DrawingSessionQueries.UnsubscribeAcknowledged(new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingSessionQueries.UnsubscribeFromDrawingSession FromProto(
+ UnsubscribeFromDrawingSessionState protoStroke)
+ {
+ return new DrawingSessionQueries.UnsubscribeFromDrawingSession(
+ new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingSessionQueries.SubscribeAcknowledged FromProto(SubscribeAcknowledged protoStroke)
+ {
+ return new DrawingSessionQueries.SubscribeAcknowledged(new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingSessionQueries.SubscribeToDrawingSession FromProto(SubscribeToDrawingSessionState protoStroke)
+ {
+ return new DrawingSessionQueries.SubscribeToDrawingSession(new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingSessionQueries.GetDrawingSessionState FromProto(GetDrawingSessionState protoStroke)
+ {
+ return new DrawingSessionQueries.GetDrawingSessionState(new DrawingSessionId(protoStroke.DrawingSessionId));
+ }
+
+ private DrawingActivityUpdate FromProto(DrawingActivityUpdated protoStroke)
+ {
+ return new DrawingActivityUpdate(new DrawingSessionId(protoStroke.DrawingSessionId), protoStroke.ActiveUsers,
+ protoStroke.LastUpdated.ToDateTime(), protoStroke.IsRemoved);
+ }
+
+ private DrawingSessionEvents.DrawingSessionClosed FromProtoSessionClosed(SessionClosed parseFrom)
+ {
+ return new DrawingSessionEvents.DrawingSessionClosed(new DrawingSessionId(parseFrom.DrawingSessionId));
+ }
+
+ private DrawingSessionEvents.UserRemoved FromProtoUserRemoved(RemoveUser parseFrom)
+ {
+ return new DrawingSessionEvents.UserRemoved(new DrawingSessionId(parseFrom.DrawingSessionId),
+ new UserId(parseFrom.UserId));
+ }
+
+ private DrawingSessionEvents.UserAdded FromProtoUserAdded(AddUser parseFrom)
+ {
+ return new DrawingSessionEvents.UserAdded(new DrawingSessionId(parseFrom.DrawingSessionId),
+ new UserId(parseFrom.UserId));
+ }
+
+ private DrawingSessionEvents.StrokesCleared FromProtoStrokesCleared(ClearStrokes parseFrom)
+ {
+ return new DrawingSessionEvents.StrokesCleared(new DrawingSessionId(parseFrom.DrawingSessionId));
+ }
+
+ private DrawingSessionEvents.StrokeRemoved FromProtoStrokeRemoved(RemoveStroke parseFrom)
+ {
+ return new DrawingSessionEvents.StrokeRemoved(new DrawingSessionId(parseFrom.DrawingSessionId),
+ new StrokeId(parseFrom.StrokeId));
+ }
+
+ private DrawingSessionEvents.StrokeAdded FromProtoStrokeAdded(AddStroke parseFrom)
+ {
+ return new DrawingSessionEvents.StrokeAdded(new DrawingSessionId(parseFrom.DrawingSessionId),
+ FromProto(parseFrom.ConnectedStroke));
+ }
+
+ public override string Manifest(object o)
+ {
+ // return the constant string value for the manifest
+ return o switch
+ {
+ IDrawingSessionEvent => GetManifestForEvent(o),
+ IDrawingSessionCommand => GetManifestForCommand(o),
+ IDrawingSessionQuery => GetManifestForQuery(o),
+ DrawingActivityUpdate => DrawingActivityUpdateManifest,
+ DrawingSessionState => DrawingSessionStateManifest,
+ _ => throw new ArgumentException($"Can't serialize object of type {o.GetType()}")
+ };
+ }
+
+ private static string GetManifestForQuery(object o)
+ {
+ return o switch
+ {
+ DrawingSessionQueries.GetDrawingSessionState => GetDrawingSessionStateManifest,
+ DrawingSessionQueries.SubscribeToDrawingSession => SubscribeToDrawingSessionManifest,
+ DrawingSessionQueries.SubscribeAcknowledged => SubscribeAcknowledgedManifest,
+ DrawingSessionQueries.UnsubscribeFromDrawingSession => UnsubscribeFromDrawingSessionManifest,
+ DrawingSessionQueries.UnsubscribeAcknowledged => UnsubscribeAcknowledgedManifest,
+ _ => throw new ArgumentException($"Can't serialize object of type {o.GetType()}")
+ };
+ }
+
+ private static string GetManifestForCommand(object o)
+ {
+ return o switch
+ {
+ DrawingSessionCommands.AddStroke => AddStrokeManifest,
+ DrawingSessionCommands.RemoveStroke => RemoveStrokeManifest,
+ DrawingSessionCommands.ClearStrokes => ClearStrokesManifest,
+ DrawingSessionCommands.AddUser => AddUserManifest,
+ DrawingSessionCommands.RemoveUser => RemoveUserManifest,
+ _ => throw new ArgumentException($"Can't serialize object of type {o.GetType()}")
+ };
+ }
+
+ private static string GetManifestForEvent(object o)
+ {
+ return o switch
+ {
+ DrawingSessionEvents.StrokeAdded => StrokeAddedManifest,
+ DrawingSessionEvents.StrokeRemoved => StrokeRemovedManifest,
+ DrawingSessionEvents.StrokesCleared => StrokesClearedManifest,
+ DrawingSessionEvents.UserAdded => UserAddedManifest,
+ DrawingSessionEvents.UserRemoved => UserRemovedManifest,
+ DrawingSessionEvents.DrawingSessionClosed => DrawingSessionClosedManifest,
+ DrawingActivityUpdate => DrawingActivityUpdateManifest,
+ _ => throw new ArgumentException($"Can't serialize object of type {o.GetType()}")
+ };
+ }
+
+ Proto.ConnectedStroke ToProto(ConnectedStroke stroke)
+ {
+ var protoStroke = new Proto.ConnectedStroke
+ {
+ Id = stroke.Id.Id,
+ Points = { stroke.Points.Select(p => new Proto.Point { X = p.X, Y = p.Y }) },
+ StrokeWidth = stroke.StrokeWidth.Value,
+ StrokeColor = stroke.StrokeColor.HexCodeOrColorName
+ };
+ return protoStroke;
+ }
+
+ ConnectedStroke FromProto(Proto.ConnectedStroke protoStroke)
+ {
+ var stroke = new ConnectedStroke(new StrokeId(protoStroke.Id))
+ {
+ Points = protoStroke.Points.Select(p => new Point(p.X, p.Y)).ToList(),
+ StrokeWidth = new GreaterThanZeroInteger(protoStroke.StrokeWidth),
+ StrokeColor = new Color(protoStroke.StrokeColor)
+ };
+ return stroke;
+ }
+
+ Proto.DrawingSessionState ToProto(DrawingSessionState state)
+ {
+ var protoState = new Proto.DrawingSessionState
+ {
+ DrawingSessionId = state.DrawingSessionId.SessionId,
+ ConnectedStrokes = { state.Strokes.Select(v => ToProto(v.Value)) },
+ ConnectedUsers = { state.ConnectedUsers.Select(u => u.IdentityName) },
+ LastUpdated = state.LastUpdate.ToTimestamp()
+ };
+ return protoState;
+ }
+
+ DrawingSessionState FromProto(Proto.DrawingSessionState protoState)
+ {
+ var state = new DrawingSessionState(new DrawingSessionId(protoState.DrawingSessionId))
+ {
+ Strokes = protoState.ConnectedStrokes.ToImmutableDictionary(s => new StrokeId(s.Id), s => FromProto(s)),
+ ConnectedUsers = protoState.ConnectedUsers.Select(u => new UserId(u)).ToImmutableHashSet(),
+ LastUpdate = protoState.LastUpdated.ToDateTime()
+ };
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/src/DrawTogether.Actors/Serialization/Proto/DrawingProtocol.proto b/src/DrawTogether.Actors/Serialization/Proto/DrawingProtocol.proto
new file mode 100644
index 0000000..2cfd974
--- /dev/null
+++ b/src/DrawTogether.Actors/Serialization/Proto/DrawingProtocol.proto
@@ -0,0 +1,86 @@
+syntax = "proto3";
+
+package DrawTogether.Actors.Serialization.Proto;
+
+import "google/protobuf/timestamp.proto";
+
+/* Primitives */
+message Point {
+ double x = 1;
+ double y = 2;
+}
+
+message ConnectedStroke {
+ int32 id = 1;
+ repeated Point points = 2;
+ int32 strokeWidth = 3;
+ string strokeColor = 4;
+}
+
+/* Commands and events */
+message AddUser {
+ string drawingSessionId = 1;
+ string userId = 2;
+}
+
+message RemoveUser {
+ string drawingSessionId = 1;
+ string userId = 2;
+
+}
+
+message AddStroke {
+ string drawingSessionId = 2;
+ ConnectedStroke connectedStroke = 3;
+}
+
+message RemoveStroke {
+ string drawingSessionId = 1;
+ int32 strokeId = 2;
+}
+
+message ClearStrokes {
+ string drawingSessionId = 1;
+}
+
+message SessionClosed {
+ string drawingSessionId = 1;
+}
+
+/* State */
+
+message DrawingSessionState{
+ string drawingSessionId = 1;
+ repeated ConnectedStroke connectedStrokes = 2;
+ repeated string connectedUsers = 3;
+ google.protobuf.Timestamp lastUpdated = 4;
+}
+
+/* Queries */
+
+message GetDrawingSessionState {
+ string drawingSessionId = 1;
+}
+
+message SubscribeToDrawingSessionState {
+ string drawingSessionId = 1;
+}
+
+message SubscribeAcknowledged {
+ string drawingSessionId = 1;
+}
+
+message UnsubscribeFromDrawingSessionState {
+ string drawingSessionId = 1;
+}
+
+message UnsubscribeAcknowledged {
+ string drawingSessionId = 1;
+}
+
+message DrawingActivityUpdated {
+ string drawingSessionId = 1;
+ google.protobuf.Timestamp lastUpdated = 2;
+ int32 activeUsers = 3;
+ bool isRemoved = 4;
+}
\ No newline at end of file
diff --git a/src/DrawTogether.Entities/Drawings/ConnectedStroke.cs b/src/DrawTogether.Entities/Drawings/ConnectedStroke.cs
index e062a51..03432b2 100644
--- a/src/DrawTogether.Entities/Drawings/ConnectedStroke.cs
+++ b/src/DrawTogether.Entities/Drawings/ConnectedStroke.cs
@@ -40,4 +40,16 @@ public sealed record ConnectedStroke(StrokeId Id)
public GreaterThanZeroInteger StrokeWidth { get; init; } = GreaterThanZeroInteger.Default;
public Color StrokeColor { get; init; } = Color.Black;
+
+ public bool Equals(ConnectedStroke? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return StrokeWidth.Equals(other.StrokeWidth) && StrokeColor.Equals(other.StrokeColor) && Id.Equals(other.Id) && Points.SequenceEqual(other.Points);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Points, StrokeWidth, StrokeColor, Id);
+ }
}
\ No newline at end of file
diff --git a/src/DrawTogether.Entities/Drawings/DrawingSessionState.cs b/src/DrawTogether.Entities/Drawings/DrawingSessionState.cs
index ef249c7..eecdebf 100644
--- a/src/DrawTogether.Entities/Drawings/DrawingSessionState.cs
+++ b/src/DrawTogether.Entities/Drawings/DrawingSessionState.cs
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
+using System.Text;
using DrawTogether.Entities.Drawings.Messages;
using DrawTogether.Entities.Users;
using static DrawTogether.Entities.Drawings.Messages.DrawingSessionCommands;
@@ -13,8 +14,31 @@ public sealed record DrawingSessionState(DrawingSessionId DrawingSessionId) : IW
public ImmutableHashSet ConnectedUsers { get; init; } = ImmutableHashSet.Empty;
public DateTime LastUpdate { get; init; } = DateTime.UtcNow;
-
+
public bool IsEmpty => Strokes.IsEmpty && ConnectedUsers.IsEmpty;
+
+ public bool Equals(DrawingSessionState? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return LastUpdate.Equals(other.LastUpdate) && DrawingSessionId.Equals(other.DrawingSessionId)
+ && Strokes.Values.SequenceEqual(other.Strokes.Values) &&
+ ConnectedUsers.SetEquals(other.ConnectedUsers);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Strokes, ConnectedUsers, LastUpdate, DrawingSessionId);
+ }
+
+ private bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append(nameof(DrawingSessionId)).Append(" = ").Append(DrawingSessionId)
+ .Append(", ").Append(nameof(Strokes)).Append(" = ").AppendFormat("Count = {0}", Strokes.Count)
+ .Append(", ").Append(nameof(ConnectedUsers)).Append(" = ").AppendFormat("Count = {0}", ConnectedUsers.Count)
+ .Append(", ").Append(nameof(LastUpdate)).Append(" = ").Append(LastUpdate);
+ return true;
+ }
}
public static class DrawingSessionStateExtensions
@@ -96,13 +120,17 @@ public static (CommandResult commandResult, IDrawingSessionEvent[] events) Proce
}
}
}
-
+
public static DrawingSessionState Apply(this DrawingSessionState currentState, IDrawingSessionEvent @event)
{
var e = @event switch
{
DrawingSessionEvents.StrokeAdded strokeAdded =>
- currentState with { Strokes = currentState.Strokes.SetItem(strokeAdded.Stroke.Id, strokeAdded.Stroke), LastUpdate = DateTime.UtcNow},
+ currentState with
+ {
+ Strokes = currentState.Strokes.SetItem(strokeAdded.Stroke.Id, strokeAdded.Stroke),
+ LastUpdate = DateTime.UtcNow
+ },
DrawingSessionEvents.StrokeRemoved strokeRemoved =>
currentState with { Strokes = currentState.Strokes.Remove(strokeRemoved.StrokeId) },
DrawingSessionEvents.StrokesCleared =>
@@ -113,7 +141,7 @@ public static DrawingSessionState Apply(this DrawingSessionState currentState, I
currentState with { ConnectedUsers = currentState.ConnectedUsers.Remove(userRemoved.UserId) },
_ => currentState
};
-
+
e = e with { LastUpdate = DateTime.UtcNow };
return e;
}
diff --git a/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionEvents.cs b/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionEvents.cs
index 0debaa7..60930ce 100644
--- a/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionEvents.cs
+++ b/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionEvents.cs
@@ -6,8 +6,6 @@ public interface IDrawingSessionEvent : IWithDrawingSessionId { }
public static class DrawingSessionEvents
{
- public sealed record DrawingSessionCreated(DrawingSessionId DrawingSessionId) : IDrawingSessionEvent;
-
public sealed record StrokeAdded(DrawingSessionId DrawingSessionId, ConnectedStroke Stroke) : IDrawingSessionEvent;
public sealed record StrokeRemoved(DrawingSessionId DrawingSessionId, StrokeId StrokeId) : IDrawingSessionEvent;
diff --git a/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionQueries.cs b/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionQueries.cs
index 2016a1e..aa512d5 100644
--- a/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionQueries.cs
+++ b/src/DrawTogether.Entities/Drawings/Messages/DrawingSessionQueries.cs
@@ -6,8 +6,6 @@ public static class DrawingSessionQueries
{
public sealed record GetDrawingSessionState(DrawingSessionId DrawingSessionId) : IDrawingSessionQuery;
- public sealed record GetDrawingSessionUsers(DrawingSessionId DrawingSessionId) : IDrawingSessionQuery;
-
public sealed record SubscribeToDrawingSession(DrawingSessionId DrawingSessionId) : IDrawingSessionQuery;
public sealed record SubscribeAcknowledged(DrawingSessionId DrawingSessionId) : IDrawingSessionQuery;
diff --git a/src/DrawTogether.Entities/Drawings/Messages/IWithDrawingSessionId.cs b/src/DrawTogether.Entities/Drawings/Messages/IWithDrawingSessionId.cs
index 67d4163..f8e54a5 100644
--- a/src/DrawTogether.Entities/Drawings/Messages/IWithDrawingSessionId.cs
+++ b/src/DrawTogether.Entities/Drawings/Messages/IWithDrawingSessionId.cs
@@ -1,6 +1,4 @@
-using DrawTogether.Entities.Users;
-
-namespace DrawTogether.Entities.Drawings.Messages;
+namespace DrawTogether.Entities.Drawings.Messages;
///
/// Marker interface for messages that have a .
diff --git a/src/DrawTogether.Tests/DrawTogether.Tests.csproj b/src/DrawTogether.Tests/DrawTogether.Tests.csproj
new file mode 100644
index 0000000..0d475e2
--- /dev/null
+++ b/src/DrawTogether.Tests/DrawTogether.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DrawTogether.Tests/Serialization/DrawingProtocolSerializerSpecs.cs b/src/DrawTogether.Tests/Serialization/DrawingProtocolSerializerSpecs.cs
new file mode 100644
index 0000000..6b514f4
--- /dev/null
+++ b/src/DrawTogether.Tests/Serialization/DrawingProtocolSerializerSpecs.cs
@@ -0,0 +1,62 @@
+using System.Collections.Immutable;
+using Akka.Hosting;
+using Akka.Hosting.TestKit;
+using Akka.Serialization;
+using DrawTogether.Actors.Serialization;
+using DrawTogether.Entities;
+using DrawTogether.Entities.Drawings;
+using DrawTogether.Entities.Users;
+using FluentAssertions;
+
+namespace DrawTogether.Tests.Serialization;
+
+public class DrawingProtocolSerializerSpecs : TestKit
+{
+ // generate a test case for serializing each type that implements IWithDrawingSessionId
+ [Fact]
+ public void ShouldSerializeDrawingSessionState()
+ {
+ var drawingSessionState = new DrawingSessionState(new DrawingSessionId("iD1"))
+ {
+ ConnectedUsers = ImmutableHashSet.Empty.Add(new UserId("user1")).Add(new UserId("user2")),
+ Strokes = ImmutableDictionary.Empty.Add(new StrokeId(1),
+ new ConnectedStroke(new StrokeId(1))
+ {
+ StrokeWidth = new GreaterThanZeroInteger(4), StrokeColor = new Color("white"), Points = new[]
+ {
+ new Point(2, 3),
+ new Point(2, 2)
+ }
+ })
+ };
+
+ VerifySerialization(drawingSessionState);
+ }
+
+ [Fact]
+ public void ShouldSerializeDrawingActivityUpdate()
+ {
+ var drawingActivityUpdate1 = new DrawingActivityUpdate(new DrawingSessionId("foo"), 4, DateTime.UtcNow, true);
+ var drawingActivityUpdate2 = new DrawingActivityUpdate(new DrawingSessionId("foo"), 0, DateTime.UtcNow, false);
+
+ VerifySerialization(drawingActivityUpdate1);
+ VerifySerialization(drawingActivityUpdate2);
+ }
+
+ private void VerifySerialization(TMessage message)
+ {
+ var serializerFor = (SerializerWithStringManifest)Sys.Serialization.FindSerializerFor(message);
+ serializerFor.Should().BeOfType();
+
+ var manifest = serializerFor.Manifest(message);
+ var bytes = serializerFor.ToBinary(message);
+
+ var deserialized = (TMessage)serializerFor.FromBinary(bytes, manifest);
+ deserialized.Should().BeEquivalentTo(message);
+ }
+
+ protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
+ {
+ builder.AddDrawingProtocolSerializer();
+ }
+}
\ No newline at end of file
diff --git a/src/DrawTogether/Config/AkkaConfiguration.cs b/src/DrawTogether/Config/AkkaConfiguration.cs
index ae75773..c798717 100644
--- a/src/DrawTogether/Config/AkkaConfiguration.cs
+++ b/src/DrawTogether/Config/AkkaConfiguration.cs
@@ -6,6 +6,7 @@
using DrawTogether.Actors;
using DrawTogether.Actors.Drawings;
using DrawTogether.Actors.Local;
+using DrawTogether.Actors.Serialization;
using LinqToDB;
namespace DrawTogether.Config;
@@ -29,6 +30,7 @@ public static IServiceCollection ConfigureAkka(this IServiceCollection services,
{
builder.WithRemoting(akkaSettings.RemoteOptions)
.WithClustering(akkaSettings.ClusterOptions)
+ .AddDrawingProtocolSerializer()
.WithSqlPersistence(
connectionString: connectionString,
providerName: ProviderName.SqlServer2022,