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

Aggregate snapshots #51

Open
wants to merge 2 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions src/Elders.Cronus.DomainModeling/AggregateRoot.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

namespace Elders.Cronus;
Expand All @@ -15,12 +16,11 @@ public class AggregateRoot<TState> : IAggregateRoot

public AggregateRoot()
{
state = InitializeState();
state = InitializeState(new TState());
uncommittedEvents = new List<IEvent>();
uncommittedPublicEvents = new List<IPublicEvent>();
revision = 0;

var mapping = new DomainObjectEventHandlerMapping();
handlers = new EventHandlerRegistrations();
var arHandlers = DomainObjectEventHandlerMapping.GetEventHandlers(() => this.state);
foreach (var handler in arHandlers)
Expand All @@ -29,12 +29,13 @@ public AggregateRoot()
}
}

public TState InitializeState()
public TState InitializeState() => InitializeState(new TState());

public TState InitializeState(IAggregateRootState initialState)
{
var state = new TState();
var dynamicState = (dynamic)state;
var dynamicState = (dynamic)initialState;
dynamicState.Root = (dynamic)this;
return state;
return dynamicState;
}

IAggregateRootState IHaveState<IAggregateRootState>.State { get { return state; } }
Expand All @@ -59,12 +60,16 @@ internal protected void Apply(IPublicEvent @event)

void IAmEventSourced.ReplayEvents(List<IEvent> events, int revision)
{
if (events is null) throw new ArgumentNullException(nameof(events));

state = InitializeState();

foreach (IEvent @event in events)
{
var handler = handlers.GetEventHandler(@event, out IEvent realEvent);
handler(realEvent);
}

this.revision = revision;

if (state.Id == null || state.Id.RawId == default(byte[]))
Expand All @@ -80,6 +85,29 @@ void IAmEventSourced.RegisterEventHandler(EntityId entityId, Type eventType, Act
{
handlers.Register(entityId, eventType, handleAction);
}

void IAmEventSourced.ReplayEvents(List<IEvent> events, int currentRevision, object snapshot)
{
if (events is null) throw new ArgumentNullException(nameof(events));
if (snapshot is null) throw new ArgumentNullException(nameof(snapshot));
if (GetType().IsSnapshotable() == false) throw new InvalidOperationException("Trying to restore a non-snapshotable aggregate from a snapshot.");

state = InitializeState();

var method = GetType().GetMethod(nameof(IAmSnapshotable<object>.RestoreFromSnapshot), BindingFlags.Public | BindingFlags.Instance);
method.Invoke(this, new object[] { snapshot });

foreach (IEvent @event in events)
{
var handler = handlers.GetEventHandler(@event, out IEvent realEvent);
handler(realEvent);
}

revision = currentRevision;

if (state.Id == null || state.Id.RawId == default(byte[]))
throw new AggregateRootException("Invalid aggregate root state. The initial event which created the aggregate root is missing.");
}
}

public sealed class EventHandlerRegistrations // internal?
Expand All @@ -95,11 +123,18 @@ public EventHandlerRegistrations()

public void Register(Type eventType, Action<IEvent> handler)
{
if (eventType is null) throw new ArgumentNullException(nameof(eventType));
if (handler is null) throw new ArgumentNullException(nameof(handler));

aggregateRootHandlers.Add(eventType, handler);
}

public void Register(EntityId entityId, Type eventType, Action<IEvent> handler)
{
if (entityId is null) throw new ArgumentNullException(nameof(entityId));
if (eventType is null) throw new ArgumentNullException(nameof(eventType));
if (handler is null) throw new ArgumentNullException(nameof(handler));

Dictionary<Type, Action<IEvent>> specificEntityHandlers;
if (entityHandlers.TryGetValue(entityId, out specificEntityHandlers))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Elders.Cronus;

internal class DomainObjectEventHandlerMapping
internal static class DomainObjectEventHandlerMapping
{
public static Dictionary<Type, Action<IEvent>> GetEventHandlers(Func<object> target)
{
Expand Down
9 changes: 4 additions & 5 deletions src/Elders.Cronus.DomainModeling/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ public abstract class Entity<TAggregateRoot, TEntityState> : IEntity
protected Entity(TAggregateRoot root, EntityId entityId)
{
this.root = root;
this.state = new TEntityState();
var dynamicState = (dynamic)this.state;
state = new TEntityState();
var dynamicState = (dynamic)state;
dynamicState.EntityId = (dynamic)entityId;
var mapping = new DomainObjectEventHandlerMapping();
foreach (var handlerAction in DomainObjectEventHandlerMapping.GetEventHandlers(() => this.state))
foreach (var handlerAction in DomainObjectEventHandlerMapping.GetEventHandlers(() => state))
{
root.RegisterEventHandler(state.EntityId, handlerAction.Key, handlerAction.Value);
this.root.RegisterEventHandler(state.EntityId, handlerAction.Key, handlerAction.Value);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Elders.Cronus.DomainModeling/IAmEventSourced.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Elders.Cronus;
public interface IAmEventSourced
{
void ReplayEvents(List<IEvent> events, int currentRevision);
void ReplayEvents(List<IEvent> events, int currentRevision, object snapshot);
void RegisterEventHandler(Type eventType, Action<IEvent> handleAction);
void RegisterEventHandler(EntityId entityId, Type eventType, Action<IEvent> handleAction);
IEnumerable<IEvent> UncommittedEvents { get; }
Expand Down
8 changes: 8 additions & 0 deletions src/Elders.Cronus.DomainModeling/IAmSnapshotable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Elders.Cronus;

public interface IAmSnapshotable<T>
where T : class, new()
{
T CreateSnapshot();
void RestoreFromSnapshot(T snapshot);
}
15 changes: 15 additions & 0 deletions src/Elders.Cronus.DomainModeling/MessageInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ public static string GetContractId(this Type messageType)
return messageId;
}

public static bool IsSnapshotable(this Type rootType)
{
return rootType.GetTypeInfo().ImplementedInterfaces
.Where(x => x.IsGenericType)
.Any(x => x.GetGenericTypeDefinition() == typeof(IAmSnapshotable<>).GetGenericTypeDefinition());
}

public static bool IsSnapshotable(this IAggregateRoot root)
{
if (root is null)
return false;

return IsSnapshotable(root.GetType());
}

public static string GetBoundedContext(this Type messageType, string defaultBoundedContext = "implicit")
{
string boundedContext;
Expand Down