Line data Source code
1 : #region Copyright
2 : // // -----------------------------------------------------------------------
3 : // // <copyright company="Chinchilla Software Limited">
4 : // // Copyright Chinchilla Software Limited. All rights reserved.
5 : // // </copyright>
6 : // // -----------------------------------------------------------------------
7 : #endregion
8 :
9 : using System;
10 : using System.Collections.Generic;
11 : using System.Collections.ObjectModel;
12 : using System.Linq;
13 : using System.Runtime.Serialization;
14 : using System.Threading;
15 : using cdmdotnet.Logging;
16 : using Cqrs.Commands;
17 : using Cqrs.Configuration;
18 : using Cqrs.Domain.Exceptions;
19 : using Cqrs.Events;
20 : using Cqrs.Infrastructure;
21 :
22 : namespace Cqrs.Domain
23 : {
24 : /// <summary>
25 : /// An independent component that reacts to domain <see cref="IEvent{TAuthenticationToken}"/> in a cross-<see cref="IAggregateRoot{TAuthenticationToken}"/>, eventually consistent manner. Time can also be a trigger. A <see cref="Saga{TAuthenticationToken}"/> can sometimes be purely reactive, and sometimes represent workflows.
26 : ///
27 : /// From an implementation perspective, a <see cref="Saga{TAuthenticationToken}"/> is a state machine that is driven forward by incoming <see cref="IEvent{TAuthenticationToken}"/> (which may come from many <see cref="AggregateRoot{TAuthenticationToken}"/> or other <see cref="Saga{TAuthenticationToken}"/>). Some states will have side effects, such as sending <see cref="ICommand{TAuthenticationToken}"/>, talking to external web services, or sending emails.
28 : /// </summary>
29 : /// <remarks>
30 : /// Isn't a <see cref="Saga{TAuthenticationToken}"/> just leaked domain logic?
31 : /// No.
32 : /// A <see cref="Saga{TAuthenticationToken}"/> can doing things that no individual <see cref="AggregateRoot{TAuthenticationToken}"/> can sensibly do. Thus, it's not a logic leak since the logic didn't belong in an <see cref="AggregateRoot{TAuthenticationToken}"/> anyway. Furthermore, we're not breaking encapsulation in any way, since <see cref="Saga{TAuthenticationToken}"/> operate with <see cref="ICommand{TAuthenticationToken}"/> and <see cref="IEvent{TAuthenticationToken}"/>, which are part of the public API.
33 : ///
34 : /// How can I make my <see cref="Saga{TAuthenticationToken}"/> react to an <see cref="IEvent{TAuthenticationToken}"/> that did not happen?
35 : /// The <see cref="Saga{TAuthenticationToken}"/>, besides reacting to domain <see cref="IEvent{TAuthenticationToken}"/>, can be "woken up" by recurrent internal alarms. Implementing such alarms is easy. See cron in Unix, or triggered WebJobs in Azure for examples.
36 : ///
37 : /// How does the <see cref="Saga{TAuthenticationToken}"/> interact with the write side?
38 : /// By sending an <see cref="ICommand{TAuthenticationToken}"/> to it.
39 : /// </remarks>
40 : public abstract class Saga<TAuthenticationToken> : ISaga<TAuthenticationToken>
41 1 : {
42 : private ReaderWriterLockSlim Lock { get; set; }
43 :
44 : private ICollection<ISagaEvent<TAuthenticationToken>> Changes { get; set; }
45 :
46 : /// <summary>
47 : /// The identifier of this <see cref="ISaga{TAuthenticationToken}"/>.
48 : /// </summary>
49 : [DataMember]
50 : public Guid Rsn
51 : {
52 : get { return Id; }
53 : private set { Id = value; }
54 : }
55 :
56 : /// <summary>
57 : /// The identifier of this <see cref="ISaga{TAuthenticationToken}"/>.
58 : /// </summary>
59 : [DataMember]
60 : public Guid Id { get; protected set; }
61 :
62 : /// <summary>
63 : /// The current version of this <see cref="ISaga{TAuthenticationToken}"/>.
64 : /// </summary>
65 : public int Version { get; protected set; }
66 :
67 : /// <summary>
68 : /// Gets or set the <see cref="ICommandPublisher{TAuthenticationToken}"/>.
69 : /// </summary>
70 : protected ICommandPublisher<TAuthenticationToken> CommandPublisher { get; private set; }
71 :
72 : /// <summary>
73 : /// Gets or set the <see cref="IDependencyResolver"/>.
74 : /// </summary>
75 : protected IDependencyResolver DependencyResolver { get; private set; }
76 :
77 : /// <summary>
78 : /// Gets or set the <see cref="ILogger"/>.
79 : /// </summary>
80 : protected ILogger Logger { get; private set; }
81 :
82 : /// <summary>
83 : /// A constructor for the <see cref="Cqrs.Domain.Factories.IAggregateFactory"/>
84 : /// </summary>
85 1 : protected Saga()
86 : {
87 : Lock = new ReaderWriterLockSlim();
88 : Changes = new ReadOnlyCollection<ISagaEvent<TAuthenticationToken>>(new List<ISagaEvent<TAuthenticationToken>>());
89 : }
90 :
91 : /// <summary>
92 : /// A constructor for the <see cref="Cqrs.Domain.Factories.IAggregateFactory"/>
93 : /// </summary>
94 1 : protected Saga(IDependencyResolver dependencyResolver, ILogger logger)
95 : : this()
96 : {
97 : DependencyResolver = dependencyResolver;
98 : Logger = logger;
99 : CommandPublisher = DependencyResolver.Resolve<ICommandPublisher<TAuthenticationToken>>();
100 : }
101 :
102 : /// <summary>
103 : /// A constructor for the <see cref="Cqrs.Domain.Factories.IAggregateFactory"/>
104 : /// </summary>
105 1 : protected Saga(IDependencyResolver dependencyResolver, ILogger logger, Guid rsn)
106 : : this(dependencyResolver, logger)
107 : {
108 : Rsn = rsn;
109 : }
110 :
111 : /// <summary>
112 : /// Get all applied changes that haven't yet been committed.
113 : /// </summary>
114 1 : public virtual IEnumerable<ISagaEvent<TAuthenticationToken>> GetUncommittedChanges()
115 : {
116 : return Changes;
117 : }
118 :
119 : /// <summary>
120 : /// Mark all applied changes as committed, increment <see cref="Version"/> and flush the <see cref="Changes">internal collection of changes</see>.
121 : /// </summary>
122 1 : public virtual void MarkChangesAsCommitted()
123 : {
124 : Lock.EnterWriteLock();
125 : try
126 : {
127 : Version = Version + Changes.Count;
128 : Changes = new ReadOnlyCollection<ISagaEvent<TAuthenticationToken>>(new List<ISagaEvent<TAuthenticationToken>>());
129 : }
130 : finally
131 : {
132 : Lock.ExitWriteLock();
133 : }
134 : }
135 :
136 : /// <summary>
137 : /// Apply all the <see cref="IEvent{TAuthenticationToken}">events</see> in <paramref name="history"/>
138 : /// using event replay to this instance.
139 : /// </summary>
140 1 : public virtual void LoadFromHistory(IEnumerable<ISagaEvent<TAuthenticationToken>> history)
141 : {
142 : Type sagaType = GetType();
143 : foreach (ISagaEvent<TAuthenticationToken> @event in history.OrderBy(e => e.Version))
144 : {
145 : if (@event.Version != Version + 1)
146 : throw new EventsOutOfOrderException(@event.Id, sagaType, Version + 1, @event.Version);
147 : ApplyChange(@event, true);
148 : }
149 : }
150 :
151 : /// <summary>
152 : /// Call the "Apply" method with a signature matching the provided <paramref name="event"/> without using event replay to this instance.
153 : /// </summary>
154 : /// <remarks>
155 : /// This means a method named "Apply", with return type void and one parameter must exist to be applied.
156 : /// If no method exists, nothing is applied
157 : /// The parameter type must match exactly the <see cref="Type"/> of the provided <paramref name="event"/>.
158 : /// </remarks>
159 1 : protected virtual void ApplyChange(ISagaEvent<TAuthenticationToken> @event)
160 : {
161 : ApplyChange(@event, false);
162 : }
163 :
164 : /// <summary>
165 : /// Calls the "SetId" method dynamically if the method exists, then calls <see cref="ApplyChange(Cqrs.Events.ISagaEvent{TAuthenticationToken})"/>
166 : /// </summary>
167 1 : protected virtual void ApplyChange(IEvent<TAuthenticationToken> @event)
168 : {
169 : var sagaEvent = new SagaEvent<TAuthenticationToken>(@event);
170 : // Set ID
171 : this.AsDynamic().SetId(sagaEvent);
172 : ApplyChange(sagaEvent);
173 : }
174 :
175 : private void ApplyChange(ISagaEvent<TAuthenticationToken> @event, bool isEventReplay)
176 : {
177 : Lock.EnterWriteLock();
178 : try
179 : {
180 : this.AsDynamic().Apply(@event.Event);
181 : if (!isEventReplay)
182 : {
183 : Changes = new ReadOnlyCollection<ISagaEvent<TAuthenticationToken>>(Changes.Concat(new[] { @event }).ToList());
184 : }
185 : else
186 : {
187 : Id = @event.Id;
188 : Version++;
189 : }
190 : }
191 : finally
192 : {
193 : Lock.ExitWriteLock();
194 : }
195 : }
196 : }
197 : }
|