Skip to content

Commit 3a7b218

Browse files
authored
Merge pull request #9 from XerProjects/release-2.0.0
Release 2.0.0
2 parents 65627e2 + f5f71e1 commit 3a7b218

25 files changed

+908
-254
lines changed

GitVersion.yml

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,206 @@
11
using System;
2+
using System.Collections.Generic;
3+
using Xer.DomainDriven.Exceptions;
24

35
namespace Xer.DomainDriven
46
{
57
public abstract class AggregateRoot<TId> : Entity<TId>, IAggregateRoot<TId> where TId : IEquatable<TId>
68
{
7-
public AggregateRoot(TId aggregateRootId)
9+
#region Declarations
10+
11+
private readonly Queue<IDomainEvent<TId>> _uncommittedDomainEvents = new Queue<IDomainEvent<TId>>();
12+
private readonly DomainEventApplierRegistration _domainEventApplierRegistration = new DomainEventApplierRegistration();
13+
14+
#endregion Declarations
15+
16+
#region Constructors
17+
18+
/// <summary>
19+
/// Constructor.
20+
/// </summary>
21+
/// <param name="aggregateRootId">Id of aggregate root.</param>
22+
public AggregateRoot(TId aggregateRootId)
823
: base(aggregateRootId)
924
{
1025
}
26+
27+
/// <summary>
28+
/// Constructor.
29+
/// </summary>
30+
/// <param name="aggregateRootId">Id of aggregate root.</param>
31+
/// <param name="created">Created date.</param>
32+
/// <param name="updated">Updated date.</param>
33+
public AggregateRoot(TId aggregateRootId, DateTime created, DateTime updated)
34+
: base(aggregateRootId, created, updated)
35+
{
36+
}
37+
38+
#endregion Constructors
39+
40+
#region IAggregateRoot implementation
41+
42+
// Note: These methods have been implemented explicitly to avoid cluttering public API.
43+
44+
/// <summary>
45+
/// Get an event stream of all the uncommitted domain events applied to the aggregate.
46+
/// </summary>
47+
/// <returns>Stream of uncommitted domain events.</returns>
48+
IDomainEventStream<TId> IAggregateRoot<TId>.GetUncommitedDomainEvents()
49+
{
50+
return new DomainEventStream<TId>(Id, _uncommittedDomainEvents);
51+
}
52+
53+
// <summary>
54+
// Clear all internally tracked domain events.
55+
// </summary>
56+
void IAggregateRoot<TId>.ClearUncommitedDomainEvents()
57+
{
58+
_uncommittedDomainEvents.Clear();
59+
}
60+
61+
#endregion IAggregateRoot implementation
62+
63+
#region Protected Methods
64+
65+
/// <summary>
66+
/// Register action to apply domain event.
67+
/// </summary>
68+
/// <typeparam name="TDomainEvent">Domain event to be applied.</typeparam>
69+
/// <param name="domainEventApplier">Domain event applier.</param>
70+
protected void RegisterDomainEventApplier<TDomainEvent>(Action<TDomainEvent> domainEventApplier) where TDomainEvent : class, IDomainEvent<TId>
71+
{
72+
_domainEventApplierRegistration.RegisterApplierFor<TDomainEvent>(domainEventApplier);
73+
}
74+
75+
/// <summary>
76+
/// Apply domain event to this entity and mark domain event for commit.
77+
/// </summary>
78+
/// <typeparam name="TDomainEvent">Type of domain event to apply.</typeparam>
79+
/// <param name="domainEvent">Instance of domain event to apply.</param>
80+
protected void ApplyDomainEvent<TDomainEvent>(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent<TId>
81+
{
82+
if (domainEvent == null)
83+
{
84+
throw new ArgumentNullException(nameof(domainEvent));
85+
}
86+
87+
// Invoke and track the event to save to event store.
88+
InvokeDomainEventApplier(domainEvent);
89+
}
90+
91+
#endregion Protected Methods
92+
93+
#region Functions
94+
95+
/// <summary>
96+
/// Invoke the registered action to handle the domain event.
97+
/// </summary>
98+
/// <typeparam name="TDomainEvent">Type of the domain event to handle.</typeparam>
99+
/// <param name="domainEvent">Domain event instance to handle.</param>
100+
/// <param name="markDomainEventForCommit">True, if domain event should be marked/tracked for commit. Otherwise, false - which means domain event should just be replayed.</param>
101+
private void InvokeDomainEventApplier<TDomainEvent>(TDomainEvent domainEvent, bool markDomainEventForCommit = true) where TDomainEvent : IDomainEvent<TId>
102+
{
103+
Action<IDomainEvent<TId>> domainEventApplier = _domainEventApplierRegistration.GetApplierFor(domainEvent);
104+
if (domainEventApplier == null)
105+
{
106+
throw new DomainEventNotAppliedException<TId>(domainEvent,
107+
$@"{GetType().Name} has no registered domain event applier to apply domain event of type {domainEvent.GetType().Name}.
108+
Register domain event appliers by calling {nameof(RegisterDomainEventApplier)} method during object construction.");
109+
}
110+
111+
try
112+
{
113+
domainEventApplier.Invoke(domainEvent);
114+
115+
// Update timestamp.
116+
Updated = domainEvent.TimeStamp;
117+
118+
if (markDomainEventForCommit)
119+
{
120+
MarkAppliedDomainEventForCommit(domainEvent);
121+
}
122+
}
123+
catch (Exception ex)
124+
{
125+
throw new DomainEventNotAppliedException<TId>(domainEvent,
126+
$"Exception occured while trying to apply domain event of type {domainEvent.GetType().Name}.",
127+
ex);
128+
}
129+
}
130+
131+
/// <summary>
132+
/// Add domain event to list of tracked domain events.
133+
/// </summary>
134+
/// <param name="domainEvent">Domain event instance to track.</param>
135+
private void MarkAppliedDomainEventForCommit(IDomainEvent<TId> domainEvent)
136+
{
137+
_uncommittedDomainEvents.Enqueue(domainEvent);
138+
}
139+
140+
#endregion Functions
141+
142+
#region Domain Event Handler Registration
143+
144+
/// <summary>
145+
/// Holds the actions to be executed in handling specific types of domain event.
146+
/// </summary>
147+
private class DomainEventApplierRegistration
148+
{
149+
private readonly IDictionary<Type, Action<IDomainEvent<TId>>> _applierByDomainEventType = new Dictionary<Type, Action<IDomainEvent<TId>>>();
150+
151+
/// <summary>
152+
/// Register action to be executed for the domain event.
153+
/// </summary>
154+
/// <typeparam name="TDomainEvent">Type of domain event to apply.</typeparam>
155+
/// <param name="applier">Action to apply the domain event to the aggregate.</param>
156+
public void RegisterApplierFor<TDomainEvent>(Action<TDomainEvent> applier) where TDomainEvent : class, IDomainEvent<TId>
157+
{
158+
if (applier == null)
159+
{
160+
throw new ArgumentNullException(nameof(applier));
161+
}
162+
163+
Type domainEventType = typeof(TDomainEvent);
164+
165+
if (_applierByDomainEventType.ContainsKey(domainEventType))
166+
{
167+
throw new InvalidOperationException($"A domain event applier that applies {domainEventType.Name} has already been registered.");
168+
}
169+
170+
Action<IDomainEvent<TId>> domainEventApplier = (d) =>
171+
{
172+
TDomainEvent domainEvent = d as TDomainEvent;
173+
if (domainEvent == null)
174+
{
175+
throw new ArgumentException(
176+
$@"An invalid domain event is given to the domain event applier.
177+
Expected domain event is of type {typeof(TDomainEvent).Name} but {d.GetType().Name} was found.");
178+
}
179+
180+
applier.Invoke(domainEvent);
181+
};
182+
183+
_applierByDomainEventType.Add(domainEventType, domainEventApplier);
184+
}
185+
186+
/// <summary>
187+
/// Get action to execute for the applied domain event.
188+
/// </summary>
189+
/// <param name="domainEvent">Domain event to apply.</param>
190+
/// <returns>Action that applies the domain event to the aggregate.</returns>
191+
public Action<IDomainEvent<TId>> GetApplierFor(IDomainEvent<TId> domainEvent)
192+
{
193+
if (domainEvent == null)
194+
{
195+
throw new ArgumentNullException(nameof(domainEvent));
196+
}
197+
198+
_applierByDomainEventType.TryGetValue(domainEvent.GetType(), out Action<IDomainEvent<TId>> domainEventAction);
199+
200+
return domainEventAction;
201+
}
202+
}
203+
204+
#endregion Domain Event Handler Registration
11205
}
12206
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace Xer.DomainDriven
7+
{
8+
public class DomainEventStream<TAggregateRootId> : IDomainEventStream<TAggregateRootId>,
9+
IEnumerable<IDomainEvent<TAggregateRootId>>
10+
where TAggregateRootId : IEquatable<TAggregateRootId>
11+
{
12+
#region Declarations
13+
14+
private readonly ICollection<IDomainEvent<TAggregateRootId>> _domainEvents;
15+
16+
#endregion Declarations
17+
18+
#region Properties
19+
20+
/// <summary>
21+
/// Id of the aggregate root which owns this stream.
22+
/// </summary>
23+
public TAggregateRootId AggregateRootId { get; }
24+
25+
/// <summary>
26+
/// Get number of domain events in the stream.
27+
/// </summary>
28+
public int DomainEventCount { get; }
29+
30+
#endregion Properties
31+
32+
#region Constructors
33+
34+
/// <summary>
35+
/// Constructor to create an empty stream for the aggregate.
36+
/// </summary>
37+
/// <param name="aggreggateRootId">ID of the aggregate root.</param>
38+
public DomainEventStream(TAggregateRootId aggreggateRootId)
39+
{
40+
AggregateRootId = aggreggateRootId;
41+
_domainEvents = new List<IDomainEvent<TAggregateRootId>>();
42+
}
43+
44+
/// <summary>
45+
/// Constructs a new instance of a read-only stream.
46+
/// </summary>
47+
/// <param name="aggregateRootId">Id of the aggregate root which owns this stream.</param>
48+
/// <param name="domainEvents">Domain events.</param>
49+
public DomainEventStream(TAggregateRootId aggregateRootId, IEnumerable<IDomainEvent<TAggregateRootId>> domainEvents)
50+
{
51+
if (domainEvents == null)
52+
{
53+
throw new ArgumentNullException(nameof(domainEvents));
54+
}
55+
56+
_domainEvents = domainEvents.ToList();
57+
58+
AggregateRootId = aggregateRootId;
59+
DomainEventCount = _domainEvents.Count;
60+
}
61+
62+
#endregion Constructors
63+
64+
/// <summary>
65+
/// Creates a new domain event stream which has the appended domain event.
66+
/// </summary>
67+
/// <param name="domainEventToAppend">Domain event to append to the domain event stream.</param>
68+
/// <returns>New instance of domain event stream with the appended domain event.</returns>
69+
public DomainEventStream<TAggregateRootId> AppendDomainEvent(IDomainEvent<TAggregateRootId> domainEventToAppend)
70+
{
71+
if (domainEventToAppend == null)
72+
{
73+
throw new ArgumentNullException(nameof(domainEventToAppend));
74+
}
75+
76+
if (!AggregateRootId.Equals(domainEventToAppend.AggregateRootId))
77+
{
78+
throw new InvalidOperationException("Cannot append domain event belonging to a different aggregate root.");
79+
}
80+
81+
return AppendDomainEventStream(new DomainEventStream<TAggregateRootId>(AggregateRootId, new[] { domainEventToAppend }));
82+
}
83+
84+
/// <summary>
85+
/// Creates a new domain event stream which has the appended domain event stream.
86+
/// </summary>
87+
/// <param name="streamToAppend">Domain event stream to append to this domain event stream.</param>
88+
/// <returns>New instance of domain event stream with the appended domain event stream.</returns>
89+
public DomainEventStream<TAggregateRootId> AppendDomainEventStream(IDomainEventStream<TAggregateRootId> streamToAppend)
90+
{
91+
if(streamToAppend == null)
92+
{
93+
throw new ArgumentNullException(nameof(streamToAppend));
94+
}
95+
96+
if (!AggregateRootId.Equals(streamToAppend.AggregateRootId))
97+
{
98+
throw new InvalidOperationException("Cannot append domain events belonging to a different aggregate root.");
99+
}
100+
101+
return new DomainEventStream<TAggregateRootId>(AggregateRootId, this.Concat(streamToAppend));
102+
}
103+
104+
/// <summary>
105+
/// Get enumerator.
106+
/// </summary>
107+
/// <returns>Enumerator which yields domain events until iterated upon.</returns>
108+
public IEnumerator<IDomainEvent<TAggregateRootId>> GetEnumerator()
109+
{
110+
foreach(IDomainEvent<TAggregateRootId> domainEvent in _domainEvents)
111+
{
112+
yield return domainEvent;
113+
}
114+
}
115+
116+
/// <summary>
117+
/// Get enumerator.
118+
/// </summary>
119+
/// <returns>Enumerator which yields domain events until iterated upon.</returns>
120+
IEnumerator IEnumerable.GetEnumerator()
121+
{
122+
foreach (IDomainEvent<TAggregateRootId> domainEvent in _domainEvents)
123+
{
124+
yield return domainEvent;
125+
}
126+
}
127+
}
128+
}

Src/Xer.DomainDriven/Entity.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,28 @@ public abstract class Entity<TId> : IEntity<TId> where TId : IEquatable<TId>
2222
/// <summary>
2323
/// Constructor.
2424
/// </summary>
25+
/// <remarks>
26+
/// This will set <see cref="Entity.Created"/> and <see cref="Entity.Updated"/> properties to <see cref="DateTime.UtcNow"/>.
27+
/// </remarks>
2528
/// <param name="entityId">ID of entity.</param>
2629
public Entity(TId entityId)
2730
{
2831
Id = entityId;
29-
Created = DateTime.Now;
30-
Updated = DateTime.Now;
32+
Created = DateTime.UtcNow;
33+
Updated = DateTime.UtcNow;
34+
}
35+
36+
/// <summary>
37+
/// Constructor.
38+
/// </summary>
39+
/// <param name="entityId">ID of entity.</param>
40+
/// <param name="created">Created date.</param>
41+
/// <param name="updated">Updated date.</param>
42+
public Entity(TId entityId, DateTime created, DateTime updated)
43+
{
44+
Id = entityId;
45+
Created = created;
46+
Updated = updated;
3147
}
3248
}
3349
}

0 commit comments

Comments
 (0)