Skip to content
Browse files

Merge pull request #262 from hyrmn/commondomain

CommonDomain brought in from joliver/CommonDomain - Initial stab at #261
  • Loading branch information...
2 parents 05efa73 + 9fdf647 commit 59aa91cb0e965ef4bb0c97d7bc6a591cb929e689 @damianh damianh committed Oct 1, 2013
Showing with 1,531 additions and 0 deletions.
  1. +58 −0 src/NEventStore.Tests/CommonDomain/IAggregateTests.cs
  2. +18 −0 src/NEventStore.Tests/CommonDomain/Persistence/IAggregatePersistenceTestHelpers.cs
  3. +142 −0 src/NEventStore.Tests/CommonDomain/Persistence/IAggregatePersistenceTests.cs
  4. +54 −0 src/NEventStore.Tests/CommonDomain/TestAggregate.cs
  5. +4 −0 src/NEventStore.Tests/NEventStore.Tests.csproj
  6. +104 −0 src/NEventStore/CommonDomain/Core/AggregateBase.cs
  7. +58 −0 src/NEventStore/CommonDomain/Core/ConflictDetector.cs
  8. +88 −0 src/NEventStore/CommonDomain/Core/ConventionEventRouter.cs
  9. +21 −0 src/NEventStore/CommonDomain/Core/ExtensionMethods.cs
  10. +23 −0 src/NEventStore/CommonDomain/Core/HandlerForDomainEventNotFoundException.cs
  11. +39 −0 src/NEventStore/CommonDomain/Core/RegistrationEventRouter.cs
  12. +73 −0 src/NEventStore/CommonDomain/Core/SagaBase.cs
  13. +17 −0 src/NEventStore/CommonDomain/IAggregate.cs
  14. +13 −0 src/NEventStore/CommonDomain/IDetectConflicts.cs
  15. +11 −0 src/NEventStore/CommonDomain/IMemento.cs
  16. +13 −0 src/NEventStore/CommonDomain/IRouteEvents.cs
  17. +22 −0 src/NEventStore/CommonDomain/ISaga.cs
  18. +44 −0 src/NEventStore/CommonDomain/Persistence/ConflictingCommandException.cs
  19. +212 −0 src/NEventStore/CommonDomain/Persistence/EventStore/EventStoreRepository.cs
  20. +90 −0 src/NEventStore/CommonDomain/Persistence/EventStore/ExceptionMessages.Designer.cs
  21. +129 −0 src/NEventStore/CommonDomain/Persistence/EventStore/ExceptionMessages.resx
  22. +158 −0 src/NEventStore/CommonDomain/Persistence/EventStore/SagaEventStoreRepository.cs
  23. +9 −0 src/NEventStore/CommonDomain/Persistence/IConstructAggregates.cs
  24. +20 −0 src/NEventStore/CommonDomain/Persistence/IRepository.cs
  25. +12 −0 src/NEventStore/CommonDomain/Persistence/ISagaRepository.cs
  26. +44 −0 src/NEventStore/CommonDomain/Persistence/PersistenceException.cs
  27. +17 −0 src/NEventStore/CommonDomain/Persistence/RepositoryExtensions.cs
  28. +14 −0 src/NEventStore/CommonDomain/StringExtensions.cs
  29. +24 −0 src/NEventStore/NEventStore.csproj
View
58 src/NEventStore.Tests/CommonDomain/IAggregateTests.cs
@@ -0,0 +1,58 @@
+namespace CommonDomain
+{
+ using System;
+
+ using NEventStore.Persistence.AcceptanceTests.BDD;
+
+ using Xunit;
+ using Xunit.Should;
+
+ public class when_an_aggregate_is_created : SpecificationBase
+ {
+ private TestAggregate _testAggregate;
+
+ protected override void Because()
+ {
+ this._testAggregate = new TestAggregate(Guid.NewGuid(), "Test");
+ }
+
+ [Fact]
+ public void should_have_name()
+ {
+ this._testAggregate.Name.ShouldBe("Test");
+ }
+
+ [Fact]
+ public void aggregate_version_should_be_one()
+ {
+ this._testAggregate.Version.ShouldBe(1);
+ }
+ }
+
+ public class when_updating_an_aggregate : SpecificationBase
+ {
+ private TestAggregate _testAggregate;
+
+ protected override void Context()
+ {
+ this._testAggregate = new TestAggregate(Guid.NewGuid(), "Test");
+ }
+
+ protected override void Because()
+ {
+ _testAggregate.ChangeName("UpdatedTest");
+ }
+
+ [Fact]
+ public void name_change_should_be_applied()
+ {
+ this._testAggregate.Name.ShouldBe("UpdatedTest");
+ }
+
+ [Fact]
+ public void applying_events_automatically_increments_version()
+ {
+ this._testAggregate.Version.ShouldBe(2);
+ }
+ }
+}
View
18 src/NEventStore.Tests/CommonDomain/Persistence/IAggregatePersistenceTestHelpers.cs
@@ -0,0 +1,18 @@
+namespace CommonDomain
+{
+ using System;
+ using System.Reflection;
+
+ using CommonDomain.Persistence;
+
+ internal class AggregateFactory : IConstructAggregates
+ {
+ public IAggregate Build(Type type, Guid id, IMemento snapshot)
+ {
+ ConstructorInfo constructor = type.GetConstructor(
+ BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(Guid) }, null);
+
+ return constructor.Invoke(new object[] { id }) as IAggregate;
+ }
+ }
+}
View
142 src/NEventStore.Tests/CommonDomain/Persistence/IAggregatePersistenceTests.cs
@@ -0,0 +1,142 @@
+namespace CommonDomain
+{
+ using System;
+
+ using NEventStore;
+ using NEventStore.Persistence.AcceptanceTests.BDD;
+
+ using Xunit;
+ using Xunit.Should;
+
+ using global::CommonDomain.Core;
+ using global::CommonDomain.Persistence;
+ using global::CommonDomain.Persistence.EventStore;
+
+ public class using_a_configured_repository : SpecificationBase
+ {
+ protected IRepository _repository;
+
+ protected IStoreEvents _storeEvents;
+
+ protected override void Context()
+ {
+ this._storeEvents = Wireup.Init().UsingInMemoryPersistence().Build();
+ this._repository = new EventStoreRepository(this._storeEvents, new AggregateFactory(), new ConflictDetector());
+ }
+ }
+
+ public class when_an_aggregate_is_persisted : using_a_configured_repository
+ {
+ private TestAggregate _testAggregate;
+
+ private Guid _id;
+
+ protected override void Context()
+ {
+ base.Context();
+ _id = Guid.NewGuid();
+ _testAggregate = new TestAggregate(_id, "Test");
+ }
+
+ protected override void Because()
+ {
+ _repository.Save(_testAggregate, Guid.NewGuid(), null);
+ }
+
+ [Fact]
+ public void should_be_returned_when_loaded_by_id()
+ {
+ _repository.GetById<TestAggregate>(_id).Name.ShouldBe(_testAggregate.Name);
+ }
+ }
+
+ public class when_a_persisted_aggregate_is_updated : using_a_configured_repository
+ {
+ private Guid _id;
+
+ private const string NewName = "UpdatedName";
+
+ protected override void Context()
+ {
+ base.Context();
+ _id = Guid.NewGuid();
+ _repository.Save(new TestAggregate(_id, "Test"), Guid.NewGuid(), null);
+ }
+
+ protected override void Because()
+ {
+ var aggregate = _repository.GetById<TestAggregate>(_id);
+ aggregate.ChangeName(NewName);
+ _repository.Save(aggregate,Guid.NewGuid(), null);
+ }
+
+ [Fact]
+ public void should_have_updated_name()
+ {
+ _repository.GetById<TestAggregate>(_id).Name.ShouldBe(NewName);
+ }
+
+ [Fact]
+ public void should_have_updated_version()
+ {
+ _repository.GetById<TestAggregate>(_id).Version.ShouldBe(2);
+ }
+ }
+
+ public class when_a_loading_a_specific_aggregate_version : using_a_configured_repository
+ {
+ private Guid _id;
+
+ private const string VersionOneName = "Test";
+ private const string NewName = "UpdatedName";
+
+ protected override void Context()
+ {
+ base.Context();
+ _id = Guid.NewGuid();
+ _repository.Save(new TestAggregate(_id, VersionOneName), Guid.NewGuid(), null);
+ }
+
+ protected override void Because()
+ {
+ var aggregate = _repository.GetById<TestAggregate>(_id);
+ aggregate.ChangeName(NewName);
+ _repository.Save(aggregate, Guid.NewGuid(), null);
+ _repository.Dispose();
+ }
+
+ [Fact]
+ public void should_be_able_to_load_initial_version()
+ {
+ _repository.GetById<TestAggregate>(_id, 1).Name.ShouldBe(VersionOneName);
+ }
+ }
+
+ public class when_an_aggregate_is_persisted_to_specific_bucket : using_a_configured_repository
+ {
+ private TestAggregate _testAggregate;
+
+ private Guid _id;
+
+ private string _bucket;
+
+ protected override void Context()
+ {
+ base.Context();
+ _id = Guid.NewGuid();
+ _bucket = "TenantB";
+ _testAggregate = new TestAggregate(_id, "Test");
+ }
+
+ protected override void Because()
+ {
+ _repository.Save(_bucket, _testAggregate, Guid.NewGuid(), null);
+ }
+
+ [Fact]
+ public void should_be_returned_when_loaded_by_id()
+ {
+ _repository.GetById<TestAggregate>(_bucket, _id).Name.ShouldBe(_testAggregate.Name);
+ }
+ }
+}
View
54 src/NEventStore.Tests/CommonDomain/TestAggregate.cs
@@ -0,0 +1,54 @@
+namespace CommonDomain
+{
+ using System;
+
+ using global::CommonDomain.Core;
+
+ internal class TestAggregate : AggregateBase
+ {
+ private TestAggregate(Guid id)
+ {
+ this.Id = id;
+ }
+
+ public TestAggregate(Guid id, string name)
+ : this(id)
+ {
+ this.RaiseEvent(new TestAggregateCreatedEvent { Id = this.Id, Name = name });
+ }
+
+ public string Name { get; set; }
+
+ public void ChangeName(string newName)
+ {
+ this.RaiseEvent(new NameChangedEvent { Name = newName });
+ }
+
+ private void Apply(TestAggregateCreatedEvent @event)
+ {
+ this.Name = @event.Name;
+ }
+
+ private void Apply(NameChangedEvent @event)
+ {
+ this.Name = @event.Name;
+ }
+ }
+
+ public interface IDomainEvent
+ {}
+
+ [Serializable]
+ public class NameChangedEvent : IDomainEvent
+ {
+ public string Name { get; set; }
+ }
+
+ [Serializable]
+ public class TestAggregateCreatedEvent : IDomainEvent
+ {
+ public Guid Id { get; set; }
+
+ public string Name { get; set; }
+ }
+}
View
4 src/NEventStore.Tests/NEventStore.Tests.csproj
@@ -72,6 +72,10 @@
</Compile>
<Compile Include="Client\PollingClientTests.cs" />
<Compile Include="CommitHelper.cs" />
+ <Compile Include="CommonDomain\IAggregateTests.cs" />
+ <Compile Include="CommonDomain\Persistence\IAggregatePersistenceTestHelpers.cs" />
+ <Compile Include="CommonDomain\Persistence\IAggregatePersistenceTests.cs" />
+ <Compile Include="CommonDomain\TestAggregate.cs" />
<Compile Include="ConversionTests\EventUpconverterPipelineHookTests.cs" />
<Compile Include="DispatchCommitHookTests.cs" />
<Compile Include="DispatcherTests\AsynchronousDispatcherTests.cs" />
View
104 src/NEventStore/CommonDomain/Core/AggregateBase.cs
@@ -0,0 +1,104 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+
+ public abstract class AggregateBase : IAggregate, IEquatable<IAggregate>
+ {
+ private readonly ICollection<object> uncommittedEvents = new LinkedList<object>();
+
+ private IRouteEvents registeredRoutes;
+
+ protected AggregateBase()
+ : this(null)
+ {}
+
+ protected AggregateBase(IRouteEvents handler)
+ {
+ if (handler == null)
+ {
+ return;
+ }
+
+ this.RegisteredRoutes = handler;
+ this.RegisteredRoutes.Register(this);
+ }
+
+ protected IRouteEvents RegisteredRoutes
+ {
+ get
+ {
+ return this.registeredRoutes ?? (this.registeredRoutes = new ConventionEventRouter(true, this));
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new InvalidOperationException("AggregateBase must have an event router to function");
+ }
+
+ this.registeredRoutes = value;
+ }
+ }
+
+ public Guid Id { get; protected set; }
+
+ public int Version { get; protected set; }
+
+ void IAggregate.ApplyEvent(object @event)
+ {
+ this.RegisteredRoutes.Dispatch(@event);
+ this.Version++;
+ }
+
+ ICollection IAggregate.GetUncommittedEvents()
+ {
+ return (ICollection)this.uncommittedEvents;
+ }
+
+ void IAggregate.ClearUncommittedEvents()
+ {
+ this.uncommittedEvents.Clear();
+ }
+
+ IMemento IAggregate.GetSnapshot()
+ {
+ IMemento snapshot = this.GetSnapshot();
+ snapshot.Id = this.Id;
+ snapshot.Version = this.Version;
+ return snapshot;
+ }
+
+ public virtual bool Equals(IAggregate other)
+ {
+ return null != other && other.Id == this.Id;
+ }
+
+ protected void Register<T>(Action<T> route)
+ {
+ this.RegisteredRoutes.Register(route);
+ }
+
+ protected void RaiseEvent(object @event)
+ {
+ ((IAggregate)this).ApplyEvent(@event);
+ this.uncommittedEvents.Add(@event);
+ }
+
+ protected virtual IMemento GetSnapshot()
+ {
+ return null;
+ }
+
+ public override int GetHashCode()
+ {
+ return this.Id.GetHashCode();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as IAggregate);
+ }
+ }
+}
View
58 src/NEventStore/CommonDomain/Core/ConflictDetector.cs
@@ -0,0 +1,58 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ /// <summary>
+ /// The conflict detector is used to determine if the events to be committed represent
+ /// a true business conflict as compared to events that have already been committed, thus
+ /// allowing reconciliation of optimistic concurrency problems.
+ /// </summary>
+ /// <remarks>
+ /// The implementation contains some internal lambda "magic" which allows casting between
+ /// TCommitted, TUncommitted, and System.Object and in a completely type-safe way.
+ /// </remarks>
+ public class ConflictDetector : IDetectConflicts
+ {
+ private readonly IDictionary<Type, IDictionary<Type, ConflictDelegate>> actions =
+ new Dictionary<Type, IDictionary<Type, ConflictDelegate>>();
+
+ public void Register<TUncommitted, TCommitted>(ConflictDelegate handler) where TUncommitted : class
+ where TCommitted : class
+ {
+ IDictionary<Type, ConflictDelegate> inner;
+ if (!this.actions.TryGetValue(typeof(TUncommitted), out inner))
+ {
+ this.actions[typeof(TUncommitted)] = inner = new Dictionary<Type, ConflictDelegate>();
+ }
+
+ inner[typeof(TCommitted)] = (uncommitted, committed) => handler(uncommitted as TUncommitted, committed as TCommitted);
+ }
+
+ public bool ConflictsWith(IEnumerable<object> uncommittedEvents, IEnumerable<object> committedEvents)
+ {
+ return (from object uncommitted in uncommittedEvents
+ from object committed in committedEvents
+ where this.Conflicts(uncommitted, committed)
+ select uncommittedEvents).Any();
+ }
+
+ private bool Conflicts(object uncommitted, object committed)
+ {
+ IDictionary<Type, ConflictDelegate> registration;
+ if (!this.actions.TryGetValue(uncommitted.GetType(), out registration))
+ {
+ return uncommitted.GetType() == committed.GetType(); // no reg, only conflict if the events are the same time
+ }
+
+ ConflictDelegate callback;
+ if (!registration.TryGetValue(committed.GetType(), out callback))
+ {
+ return true;
+ }
+
+ return callback(uncommitted, committed);
+ }
+ }
+}
View
88 src/NEventStore/CommonDomain/Core/ConventionEventRouter.cs
@@ -0,0 +1,88 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+
+ public class ConventionEventRouter : IRouteEvents
+ {
+ private readonly IDictionary<Type, Action<object>> handlers = new Dictionary<Type, Action<object>>();
+
+ private readonly bool throwOnApplyNotFound;
+
+ private IAggregate registered;
+
+ public ConventionEventRouter()
+ : this(true)
+ {}
+
+ public ConventionEventRouter(bool throwOnApplyNotFound)
+ {
+ this.throwOnApplyNotFound = throwOnApplyNotFound;
+ }
+
+ public ConventionEventRouter(bool throwOnApplyNotFound, IAggregate aggregate)
+ : this(throwOnApplyNotFound)
+ {
+ this.Register(aggregate);
+ }
+
+ public virtual void Register<T>(Action<T> handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException("handler");
+ }
+
+ this.Register(typeof(T), @event => handler((T)@event));
+ }
+
+ public virtual void Register(IAggregate aggregate)
+ {
+ if (aggregate == null)
+ {
+ throw new ArgumentNullException("aggregate");
+ }
+
+ this.registered = aggregate;
+
+ // Get instance methods named Apply with one parameter returning void
+ var applyMethods =
+ aggregate.GetType()
+ .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(
+ m => m.Name == "Apply" && m.GetParameters().Length == 1 && m.ReturnParameter.ParameterType == typeof(void))
+ .Select(m => new { Method = m, MessageType = m.GetParameters().Single().ParameterType });
+
+ foreach (var apply in applyMethods)
+ {
+ MethodInfo applyMethod = apply.Method;
+ this.handlers.Add(apply.MessageType, m => applyMethod.Invoke(aggregate, new[] { m }));
+ }
+ }
+
+ public virtual void Dispatch(object eventMessage)
+ {
+ if (eventMessage == null)
+ {
+ throw new ArgumentNullException("eventMessage");
+ }
+
+ Action<object> handler;
+ if (this.handlers.TryGetValue(eventMessage.GetType(), out handler))
+ {
+ handler(eventMessage);
+ }
+ else if (this.throwOnApplyNotFound)
+ {
+ this.registered.ThrowHandlerNotFound(eventMessage);
+ }
+ }
+
+ private void Register(Type messageType, Action<object> handler)
+ {
+ this.handlers[messageType] = handler;
+ }
+ }
+}
View
21 src/NEventStore/CommonDomain/Core/ExtensionMethods.cs
@@ -0,0 +1,21 @@
+namespace CommonDomain.Core
+{
+ using System.Globalization;
+
+ internal static class ExtensionMethods
+ {
+ public static string FormatWith(this string format, params object[] args)
+ {
+ return string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args);
+ }
+
+ public static void ThrowHandlerNotFound(this IAggregate aggregate, object eventMessage)
+ {
+ string exceptionMessage =
+ "Aggregate of type '{0}' raised an event of type '{1}' but not handler could be found to handle the message."
+ .FormatWith(aggregate.GetType().Name, eventMessage.GetType().Name);
+
+ throw new HandlerForDomainEventNotFoundException(exceptionMessage);
+ }
+ }
+}
View
23 src/NEventStore/CommonDomain/Core/HandlerForDomainEventNotFoundException.cs
@@ -0,0 +1,23 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Runtime.Serialization;
+
+ public class HandlerForDomainEventNotFoundException : Exception
+ {
+ public HandlerForDomainEventNotFoundException()
+ {}
+
+ public HandlerForDomainEventNotFoundException(string message)
+ : base(message)
+ {}
+
+ public HandlerForDomainEventNotFoundException(string message, Exception innerException)
+ : base(message, innerException)
+ {}
+
+ public HandlerForDomainEventNotFoundException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {}
+ }
+}
View
39 src/NEventStore/CommonDomain/Core/RegistrationEventRouter.cs
@@ -0,0 +1,39 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Collections.Generic;
+
+ public class RegistrationEventRouter : IRouteEvents
+ {
+ private readonly IDictionary<Type, Action<object>> handlers = new Dictionary<Type, Action<object>>();
+
+ private IAggregate regsitered;
+
+ public virtual void Register<T>(Action<T> handler)
+ {
+ this.handlers[typeof(T)] = @event => handler((T)@event);
+ }
+
+ public virtual void Register(IAggregate aggregate)
+ {
+ if (aggregate == null)
+ {
+ throw new ArgumentNullException("aggregate");
+ }
+
+ this.regsitered = aggregate;
+ }
+
+ public virtual void Dispatch(object eventMessage)
+ {
+ Action<object> handler;
+
+ if (!this.handlers.TryGetValue(eventMessage.GetType(), out handler))
+ {
+ this.regsitered.ThrowHandlerNotFound(eventMessage);
+ }
+
+ handler(eventMessage);
+ }
+ }
+}
View
73 src/NEventStore/CommonDomain/Core/SagaBase.cs
@@ -0,0 +1,73 @@
+namespace CommonDomain.Core
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+
+ public class SagaBase<TMessage> : ISaga, IEquatable<ISaga>
+ where TMessage : class
+ {
+ private readonly IDictionary<Type, Action<TMessage>> handlers = new Dictionary<Type, Action<TMessage>>();
+
+ private readonly ICollection<TMessage> uncommitted = new LinkedList<TMessage>();
+
+ private readonly ICollection<TMessage> undispatched = new LinkedList<TMessage>();
+
+ public virtual bool Equals(ISaga other)
+ {
+ return null != other && other.Id == this.Id;
+ }
+
+ public Guid Id { get; protected set; }
+
+ public int Version { get; private set; }
+
+ public void Transition(object message)
+ {
+ this.handlers[message.GetType()](message as TMessage);
+ this.uncommitted.Add(message as TMessage);
+ this.Version++;
+ }
+
+ ICollection ISaga.GetUncommittedEvents()
+ {
+ return this.uncommitted as ICollection;
+ }
+
+ void ISaga.ClearUncommittedEvents()
+ {
+ this.uncommitted.Clear();
+ }
+
+ ICollection ISaga.GetUndispatchedMessages()
+ {
+ return this.undispatched as ICollection;
+ }
+
+ void ISaga.ClearUndispatchedMessages()
+ {
+ this.undispatched.Clear();
+ }
+
+ protected void Register<TRegisteredMessage>(Action<TRegisteredMessage> handler)
+ where TRegisteredMessage : class, TMessage
+ {
+ this.handlers[typeof(TRegisteredMessage)] = message => handler(message as TRegisteredMessage);
+ }
+
+ protected void Dispatch(TMessage message)
+ {
+ this.undispatched.Add(message);
+ }
+
+ public override int GetHashCode()
+ {
+ return this.Id.GetHashCode();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as ISaga);
+ }
+ }
+}
View
17 src/NEventStore/CommonDomain/IAggregate.cs
@@ -0,0 +1,17 @@
+namespace CommonDomain
+{
+ using System;
+ using System.Collections;
+
+ public interface IAggregate
+ {
+ Guid Id { get; }
+ int Version { get; }
+
+ void ApplyEvent(object @event);
+ ICollection GetUncommittedEvents();
+ void ClearUncommittedEvents();
+
+ IMemento GetSnapshot();
+ }
+}
View
13 src/NEventStore/CommonDomain/IDetectConflicts.cs
@@ -0,0 +1,13 @@
+namespace CommonDomain
+{
+ using System.Collections.Generic;
+
+ public interface IDetectConflicts
+ {
+ void Register<TUncommitted, TCommitted>(ConflictDelegate handler) where TUncommitted : class where TCommitted : class;
+
+ bool ConflictsWith(IEnumerable<object> uncommittedEvents, IEnumerable<object> committedEvents);
+ }
+
+ public delegate bool ConflictDelegate(object uncommitted, object committed);
+}
View
11 src/NEventStore/CommonDomain/IMemento.cs
@@ -0,0 +1,11 @@
+namespace CommonDomain
+{
+ using System;
+
+ public interface IMemento
+ {
+ Guid Id { get; set; }
+
+ int Version { get; set; }
+ }
+}
View
13 src/NEventStore/CommonDomain/IRouteEvents.cs
@@ -0,0 +1,13 @@
+namespace CommonDomain
+{
+ using System;
+
+ public interface IRouteEvents
+ {
+ void Register<T>(Action<T> handler);
+
+ void Register(IAggregate aggregate);
+
+ void Dispatch(object eventMessage);
+ }
+}
View
22 src/NEventStore/CommonDomain/ISaga.cs
@@ -0,0 +1,22 @@
+namespace CommonDomain
+{
+ using System;
+ using System.Collections;
+
+ public interface ISaga
+ {
+ Guid Id { get; }
+
+ int Version { get; }
+
+ void Transition(object message);
+
+ ICollection GetUncommittedEvents();
+
+ void ClearUncommittedEvents();
+
+ ICollection GetUndispatchedMessages();
+
+ void ClearUndispatchedMessages();
+ }
+}
View
44 src/NEventStore/CommonDomain/Persistence/ConflictingCommandException.cs
@@ -0,0 +1,44 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+ using System.Runtime.Serialization;
+
+ /// <summary>
+ /// Represents a command that could not be executed because it conflicted with the command of another user or actor.
+ /// </summary>
+ [Serializable]
+ public class ConflictingCommandException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the ConflictingCommandException class.
+ /// </summary>
+ public ConflictingCommandException()
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the ConflictingCommandException class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public ConflictingCommandException(string message)
+ : base(message)
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the ConflictingCommandException class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ /// <param name="innerException">The message that is the cause of the current exception.</param>
+ public ConflictingCommandException(string message, Exception innerException)
+ : base(message, innerException)
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the ConflictingCommandException class.
+ /// </summary>
+ /// <param name="info">The SerializationInfo that holds the serialized object data of the exception being thrown.</param>
+ /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
+ protected ConflictingCommandException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {}
+ }
+}
View
212 src/NEventStore/CommonDomain/Persistence/EventStore/EventStoreRepository.cs
@@ -0,0 +1,212 @@
+namespace CommonDomain.Persistence.EventStore
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ using NEventStore;
+ using NEventStore.Persistence;
+
+ public class EventStoreRepository : IRepository, IDisposable
+ {
+ private const string AggregateTypeHeader = "AggregateType";
+
+ private readonly IDetectConflicts conflictDetector;
+
+ private readonly IStoreEvents eventStore;
+
+ private readonly IConstructAggregates factory;
+
+ private readonly IDictionary<string, ISnapshot> snapshots = new Dictionary<string, ISnapshot>();
+
+ private readonly IDictionary<string, IEventStream> streams = new Dictionary<string, IEventStream>();
+
+ public EventStoreRepository(IStoreEvents eventStore, IConstructAggregates factory, IDetectConflicts conflictDetector)
+ {
+ this.eventStore = eventStore;
+ this.factory = factory;
+ this.conflictDetector = conflictDetector;
+ }
+
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public virtual TAggregate GetById<TAggregate>(Guid id) where TAggregate : class, IAggregate
+ {
+ return this.GetById<TAggregate>(Bucket.Default, id);
+ }
+
+ public virtual TAggregate GetById<TAggregate>(Guid id, int versionToLoad) where TAggregate : class, IAggregate
+ {
+ return this.GetById<TAggregate>(Bucket.Default, id, versionToLoad);
+ }
+
+ public TAggregate GetById<TAggregate>(string bucketId, Guid id) where TAggregate : class, IAggregate
+ {
+ return this.GetById<TAggregate>(bucketId, id, int.MaxValue);
+ }
+
+ public TAggregate GetById<TAggregate>(string bucketId, Guid id, int versionToLoad) where TAggregate : class, IAggregate
+ {
+ ISnapshot snapshot = this.GetSnapshot(bucketId, id, versionToLoad);
+ IEventStream stream = this.OpenStream(bucketId, id, versionToLoad, snapshot);
+ IAggregate aggregate = this.GetAggregate<TAggregate>(snapshot, stream);
+
+ ApplyEventsToAggregate(versionToLoad, stream, aggregate);
+
+ return aggregate as TAggregate;
+ }
+
+ public virtual void Save(IAggregate aggregate, Guid commitId, Action<IDictionary<string, object>> updateHeaders)
+ {
+ Save(Bucket.Default, aggregate, commitId, updateHeaders);
+
+ }
+
+ public void Save(string bucketId, IAggregate aggregate, Guid commitId, Action<IDictionary<string, object>> updateHeaders)
+ {
+ Dictionary<string, object> headers = PrepareHeaders(aggregate, updateHeaders);
+ while (true)
+ {
+ IEventStream stream = this.PrepareStream(bucketId, aggregate, headers);
+ int commitEventCount = stream.CommittedEvents.Count;
+
+ try
+ {
+ stream.CommitChanges(commitId);
+ aggregate.ClearUncommittedEvents();
+ return;
+ }
+ catch (DuplicateCommitException)
+ {
+ stream.ClearChanges();
+ return;
+ }
+ catch (ConcurrencyException e)
+ {
+ if (this.ThrowOnConflict(stream, commitEventCount))
+ {
+ throw new ConflictingCommandException(e.Message, e);
+ }
+
+ stream.ClearChanges();
+ }
+ catch (StorageException e)
+ {
+ throw new PersistenceException(e.Message, e);
+ }
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ lock (this.streams)
+ {
+ foreach (var stream in this.streams)
+ {
+ stream.Value.Dispose();
+ }
+
+ this.snapshots.Clear();
+ this.streams.Clear();
+ }
+ }
+
+ private static void ApplyEventsToAggregate(int versionToLoad, IEventStream stream, IAggregate aggregate)
+ {
+ if (versionToLoad == 0 || aggregate.Version < versionToLoad)
+ {
+ foreach (var @event in stream.CommittedEvents.Select(x => x.Body))
+ {
+ aggregate.ApplyEvent(@event);
+ }
+ }
+ }
+
+ private IAggregate GetAggregate<TAggregate>(ISnapshot snapshot, IEventStream stream)
+ {
+ IMemento memento = snapshot == null ? null : snapshot.Payload as IMemento;
+ return this.factory.Build(typeof(TAggregate), stream.StreamId.ToGuid(), memento);
+ }
+
+ private ISnapshot GetSnapshot(string bucketId, Guid id, int version)
+ {
+ ISnapshot snapshot;
+ var snapshotId = bucketId + id;
+ if (!this.snapshots.TryGetValue(snapshotId, out snapshot))
+ {
+ this.snapshots[snapshotId] = snapshot = this.eventStore.Advanced.GetSnapshot(bucketId, id, version);
+ }
+
+ return snapshot;
+ }
+
+ private IEventStream OpenStream(string bucketId, Guid id, int version, ISnapshot snapshot)
+ {
+ IEventStream stream;
+ var streamsId = bucketId + id;
+ if (this.streams.TryGetValue(streamsId, out stream))
+ {
+ return stream;
+ }
+
+ stream = snapshot == null
+ ? this.eventStore.OpenStream(bucketId, id, 0, version)
+ : this.eventStore.OpenStream(snapshot, version);
+
+ return this.streams[streamsId] = stream;
+ }
+
+ private IEventStream PrepareStream(string bucketId, IAggregate aggregate, Dictionary<string, object> headers)
+ {
+ IEventStream stream;
+ var streamsId = bucketId + aggregate.Id;
+ if (!this.streams.TryGetValue(streamsId, out stream))
+ {
+ this.streams[streamsId] = stream = this.eventStore.CreateStream(bucketId, aggregate.Id);
+ }
+
+ foreach (var item in headers)
+ {
+ stream.UncommittedHeaders[item.Key] = item.Value;
+ }
+
+ aggregate.GetUncommittedEvents()
+ .Cast<object>()
+ .Select(x => new EventMessage { Body = x })
+ .ToList()
+ .ForEach(stream.Add);
+
+ return stream;
+ }
+
+ private static Dictionary<string, object> PrepareHeaders(
+ IAggregate aggregate, Action<IDictionary<string, object>> updateHeaders)
+ {
+ var headers = new Dictionary<string, object>();
+
+ headers[AggregateTypeHeader] = aggregate.GetType().FullName;
+ if (updateHeaders != null)
+ {
+ updateHeaders(headers);
+ }
+
+ return headers;
+ }
+
+ private bool ThrowOnConflict(IEventStream stream, int skip)
+ {
+ IEnumerable<object> committed = stream.CommittedEvents.Skip(skip).Select(x => x.Body);
+ IEnumerable<object> uncommitted = stream.UncommittedEvents.Select(x => x.Body);
+ return this.conflictDetector.ConflictsWith(uncommitted, committed);
+ }
+ }
+}
View
90 src/NEventStore/CommonDomain/Persistence/EventStore/ExceptionMessages.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace CommonDomain.Persistence.EventStore {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ExceptionMessages {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ExceptionMessages() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CommonDomain.Persistence.EventStore.ExceptionMessages", typeof(ExceptionMessages).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The command issued conflicted with another command that was sent by another user, actor, or process in the system. The change could not be automatically merged. Please review the data that has changed and try your change again..
+ /// </summary>
+ internal static string ConflictingCommand {
+ get {
+ return ResourceManager.GetString("ConflictingCommand", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There were no uncommitted changes to persist. When attempting to save an aggregate there must be at least one uncommitted event to persist..
+ /// </summary>
+ internal static string NoWork {
+ get {
+ return ResourceManager.GetString("NoWork", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The argument cannot be null..
+ /// </summary>
+ internal static string NullArgument {
+ get {
+ return ResourceManager.GetString("NullArgument", resourceCulture);
+ }
+ }
+ }
+}
View
129 src/NEventStore/CommonDomain/Persistence/EventStore/ExceptionMessages.resx
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ConflictingCommand" xml:space="preserve">
+ <value>The command issued conflicted with another command that was sent by another user, actor, or process in the system. The change could not be automatically merged. Please review the data that has changed and try your change again.</value>
+ </data>
+ <data name="NoWork" xml:space="preserve">
+ <value>There were no uncommitted changes to persist. When attempting to save an aggregate there must be at least one uncommitted event to persist.</value>
+ </data>
+ <data name="NullArgument" xml:space="preserve">
+ <value>The argument cannot be null.</value>
+ </data>
+</root>
View
158 src/NEventStore/CommonDomain/Persistence/EventStore/SagaEventStoreRepository.cs
@@ -0,0 +1,158 @@
+namespace CommonDomain.Persistence.EventStore
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ using NEventStore;
+ using NEventStore.Persistence;
+
+ public class SagaEventStoreRepository : ISagaRepository, IDisposable
+ {
+ private const string SagaTypeHeader = "SagaType";
+
+ private const string UndispatchedMessageHeader = "UndispatchedMessage.";
+
+ private readonly IStoreEvents eventStore;
+
+ private readonly IDictionary<Guid, IEventStream> streams = new Dictionary<Guid, IEventStream>();
+
+ public SagaEventStoreRepository(IStoreEvents eventStore)
+ {
+ this.eventStore = eventStore;
+ }
+
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public TSaga GetById<TSaga>(Guid sagaId) where TSaga : class, ISaga, new()
+ {
+ return BuildSaga<TSaga>(this.OpenStream(sagaId));
+ }
+
+ public void Save(ISaga saga, Guid commitId, Action<IDictionary<string, object>> updateHeaders)
+ {
+ if (saga == null)
+ {
+ throw new ArgumentNullException("saga", ExceptionMessages.NullArgument);
+ }
+
+ Dictionary<string, object> headers = PrepareHeaders(saga, updateHeaders);
+ IEventStream stream = this.PrepareStream(saga, headers);
+
+ Persist(stream, commitId);
+
+ saga.ClearUncommittedEvents();
+ saga.ClearUndispatchedMessages();
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ lock (this.streams)
+ {
+ foreach (var stream in this.streams)
+ {
+ stream.Value.Dispose();
+ }
+
+ this.streams.Clear();
+ }
+ }
+
+ private IEventStream OpenStream(Guid sagaId)
+ {
+ IEventStream stream;
+ if (this.streams.TryGetValue(sagaId, out stream))
+ {
+ return stream;
+ }
+
+ try
+ {
+ stream = this.eventStore.OpenStream(sagaId, 0, int.MaxValue);
+ }
+ catch (StreamNotFoundException)
+ {
+ stream = this.eventStore.CreateStream(sagaId);
+ }
+
+ return this.streams[sagaId] = stream;
+ }
+
+ private static TSaga BuildSaga<TSaga>(IEventStream stream) where TSaga : class, ISaga, new()
+ {
+ var saga = new TSaga();
+ foreach (var @event in stream.CommittedEvents.Select(x => x.Body))
+ {
+ saga.Transition(@event);
+ }
+
+ saga.ClearUncommittedEvents();
+ saga.ClearUndispatchedMessages();
+
+ return saga;
+ }
+
+ private static Dictionary<string, object> PrepareHeaders(
+ ISaga saga, Action<IDictionary<string, object>> updateHeaders)
+ {
+ var headers = new Dictionary<string, object>();
+
+ headers[SagaTypeHeader] = saga.GetType().FullName;
+ if (updateHeaders != null)
+ {
+ updateHeaders(headers);
+ }
+
+ int i = 0;
+ foreach (var command in saga.GetUndispatchedMessages())
+ {
+ headers[UndispatchedMessageHeader + i++] = command;
+ }
+
+ return headers;
+ }
+
+ private IEventStream PrepareStream(ISaga saga, Dictionary<string, object> headers)
+ {
+ IEventStream stream;
+ if (!this.streams.TryGetValue(saga.Id, out stream))
+ {
+ this.streams[saga.Id] = stream = this.eventStore.CreateStream(saga.Id);
+ }
+
+ foreach (var item in headers)
+ {
+ stream.UncommittedHeaders[item.Key] = item.Value;
+ }
+
+ saga.GetUncommittedEvents().Cast<object>().Select(x => new EventMessage { Body = x }).ToList().ForEach(stream.Add);
+
+ return stream;
+ }
+
+ private static void Persist(IEventStream stream, Guid commitId)
+ {
+ try
+ {
+ stream.CommitChanges(commitId);
+ }
+ catch (DuplicateCommitException)
+ {
+ stream.ClearChanges();
+ }
+ catch (StorageException e)
+ {
+ throw new PersistenceException(e.Message, e);
+ }
+ }
+ }
+}
View
9 src/NEventStore/CommonDomain/Persistence/IConstructAggregates.cs
@@ -0,0 +1,9 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+
+ public interface IConstructAggregates
+ {
+ IAggregate Build(Type type, Guid id, IMemento snapshot);
+ }
+}
View
20 src/NEventStore/CommonDomain/Persistence/IRepository.cs
@@ -0,0 +1,20 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+ using System.Collections.Generic;
+
+ public interface IRepository : IDisposable
+ {
+ TAggregate GetById<TAggregate>(Guid id) where TAggregate : class, IAggregate;
+
+ TAggregate GetById<TAggregate>(Guid id, int version) where TAggregate : class, IAggregate;
+
+ TAggregate GetById<TAggregate>(string bucketId, Guid id) where TAggregate : class, IAggregate;
+
+ TAggregate GetById<TAggregate>(string bucketId, Guid id, int version) where TAggregate : class, IAggregate;
+
+ void Save(IAggregate aggregate, Guid commitId, Action<IDictionary<string, object>> updateHeaders);
+
+ void Save(string bucketId, IAggregate aggregate, Guid commitId, Action<IDictionary<string, object>> updateHeaders);
+ }
+}
View
12 src/NEventStore/CommonDomain/Persistence/ISagaRepository.cs
@@ -0,0 +1,12 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+ using System.Collections.Generic;
+
+ public interface ISagaRepository
+ {
+ TSaga GetById<TSaga>(Guid sagaId) where TSaga : class, ISaga, new();
+
+ void Save(ISaga saga, Guid commitId, Action<IDictionary<string, object>> updateHeaders);
+ }
+}
View
44 src/NEventStore/CommonDomain/Persistence/PersistenceException.cs
@@ -0,0 +1,44 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+ using System.Runtime.Serialization;
+
+ /// <summary>
+ /// Represents a general failure of the persistence infrastructure.
+ /// </summary>
+ [Serializable]
+ public class PersistenceException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the PersistenceException class.
+ /// </summary>
+ public PersistenceException()
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the PersistenceException class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public PersistenceException(string message)
+ : base(message)
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the PersistenceException class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ /// <param name="innerException">The message that is the cause of the current exception.</param>
+ public PersistenceException(string message, Exception innerException)
+ : base(message, innerException)
+ {}
+
+ /// <summary>
+ /// Initializes a new instance of the PersistenceException class.
+ /// </summary>
+ /// <param name="info">The SerializationInfo that holds the serialized object data of the exception being thrown.</param>
+ /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
+ protected PersistenceException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {}
+ }
+}
View
17 src/NEventStore/CommonDomain/Persistence/RepositoryExtensions.cs
@@ -0,0 +1,17 @@
+namespace CommonDomain.Persistence
+{
+ using System;
+
+ public static class RepositoryExtensions
+ {
+ public static void Save(this IRepository repository, IAggregate aggregate, Guid commitId)
+ {
+ repository.Save(aggregate, commitId, a => { });
+ }
+
+ public static void Save(this IRepository repository, string bucketId, IAggregate aggregate, Guid commitId)
+ {
+ repository.Save(bucketId, aggregate, commitId, a => { });
+ }
+ }
+}
View
14 src/NEventStore/CommonDomain/StringExtensions.cs
@@ -0,0 +1,14 @@
+namespace CommonDomain
+{
+ using System;
+
+ internal static class StringExtensions
+ {
+ public static Guid ToGuid(this string value)
+ {
+ Guid guid = Guid.Empty;
+ Guid.TryParse(value, out guid);
+ return guid;
+ }
+ }
+}
View
24 src/NEventStore/NEventStore.csproj
@@ -67,6 +67,28 @@
<Compile Include="CommitAttemptExtensions.cs" />
<Compile Include="CommitEqualityComparer.cs" />
<Compile Include="CommitEventsExtensions.cs" />
+ <Compile Include="CommonDomain\Core\AggregateBase.cs" />
+ <Compile Include="CommonDomain\Core\ConflictDetector.cs" />
+ <Compile Include="CommonDomain\Core\ConventionEventRouter.cs" />
+ <Compile Include="CommonDomain\Core\ExtensionMethods.cs" />
+ <Compile Include="CommonDomain\Core\HandlerForDomainEventNotFoundException.cs" />
+ <Compile Include="CommonDomain\Core\RegistrationEventRouter.cs" />
+ <Compile Include="CommonDomain\Core\SagaBase.cs" />
+ <Compile Include="CommonDomain\IAggregate.cs" />
+ <Compile Include="CommonDomain\IDetectConflicts.cs" />
+ <Compile Include="CommonDomain\IMemento.cs" />
+ <Compile Include="CommonDomain\IRouteEvents.cs" />
+ <Compile Include="CommonDomain\ISaga.cs" />
+ <Compile Include="CommonDomain\Persistence\ConflictingCommandException.cs" />
+ <Compile Include="CommonDomain\Persistence\EventStore\EventStoreRepository.cs" />
+ <Compile Include="CommonDomain\Persistence\EventStore\ExceptionMessages.Designer.cs" />
+ <Compile Include="CommonDomain\Persistence\EventStore\SagaEventStoreRepository.cs" />
+ <Compile Include="CommonDomain\Persistence\IConstructAggregates.cs" />
+ <Compile Include="CommonDomain\Persistence\IRepository.cs" />
+ <Compile Include="CommonDomain\Persistence\ISagaRepository.cs" />
+ <Compile Include="CommonDomain\Persistence\PersistenceException.cs" />
+ <Compile Include="CommonDomain\Persistence\RepositoryExtensions.cs" />
+ <Compile Include="CommonDomain\StringExtensions.cs" />
<Compile Include="ConcurrencyException.cs">
<SubType>Code</SubType>
</Compile>
@@ -255,6 +277,7 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Include="CommonDomain\Persistence\EventStore\ExceptionMessages.resx" />
<EmbeddedResource Include="Messages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
@@ -308,6 +331,7 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
+ <ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
</Project>

0 comments on commit 59aa91c

Please sign in to comment.
Something went wrong with that request. Please try again.