mirror of https://github.com/Squidex/squidex.git
18 changed files with 452 additions and 213 deletions
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public enum StatusChange |
|||
{ |
|||
Archived, |
|||
Published, |
|||
Restored, |
|||
Unpublished |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Log; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.Commands |
|||
{ |
|||
public abstract class DomainObjectGrainBase<T> : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() |
|||
{ |
|||
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>(); |
|||
private readonly ISemanticLog log; |
|||
private Guid id; |
|||
|
|||
public Guid Id |
|||
{ |
|||
get { return id; } |
|||
} |
|||
|
|||
public long Version |
|||
{ |
|||
get { return Snapshot.Version; } |
|||
} |
|||
|
|||
public long NewVersion |
|||
{ |
|||
get { return Snapshot.Version + uncomittedEvents.Count; } |
|||
} |
|||
|
|||
public abstract T Snapshot { get; } |
|||
|
|||
protected DomainObjectGrainBase(ISemanticLog log) |
|||
{ |
|||
Guard.NotNull(log, nameof(log)); |
|||
|
|||
this.log = log; |
|||
} |
|||
|
|||
public sealed override async Task OnActivateAsync(Guid key) |
|||
{ |
|||
using (log.MeasureInformation(w => w |
|||
.WriteProperty("action", "ActivateDomainObject") |
|||
.WriteProperty("domainObjectType", GetType().Name) |
|||
.WriteProperty("domainObjectKey", key.ToString()))) |
|||
{ |
|||
id = key; |
|||
|
|||
await ReadAsync(GetType(), id); |
|||
} |
|||
} |
|||
|
|||
public void RaiseEvent(IEvent @event) |
|||
{ |
|||
RaiseEvent(Envelope.Create(@event)); |
|||
} |
|||
|
|||
public virtual void RaiseEvent(Envelope<IEvent> @event) |
|||
{ |
|||
Guard.NotNull(@event, nameof(@event)); |
|||
|
|||
@event.SetAggregateId(id); |
|||
|
|||
ApplyEvent(@event); |
|||
|
|||
uncomittedEvents.Add(@event); |
|||
} |
|||
|
|||
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents() |
|||
{ |
|||
return uncomittedEvents; |
|||
} |
|||
|
|||
public void ClearUncommittedEvents() |
|||
{ |
|||
uncomittedEvents.Clear(); |
|||
} |
|||
|
|||
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler, false); |
|||
} |
|||
|
|||
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler?.ToAsync(), false); |
|||
} |
|||
|
|||
protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), false); |
|||
} |
|||
|
|||
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), false); |
|||
} |
|||
|
|||
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler, true); |
|||
} |
|||
|
|||
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler?.ToAsync(), true); |
|||
} |
|||
|
|||
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), true); |
|||
} |
|||
|
|||
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand |
|||
{ |
|||
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), true); |
|||
} |
|||
|
|||
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, bool isUpdate) where TCommand : class, IAggregateCommand |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version) |
|||
{ |
|||
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); |
|||
} |
|||
|
|||
if (isUpdate && Version < 0) |
|||
{ |
|||
try |
|||
{ |
|||
DeactivateOnIdle(); |
|||
} |
|||
catch (InvalidOperationException) |
|||
{ |
|||
} |
|||
|
|||
throw new DomainObjectNotFoundException(id.ToString(), GetType()); |
|||
} |
|||
|
|||
if (!isUpdate && Version >= 0) |
|||
{ |
|||
throw new DomainException("Object has already been created."); |
|||
} |
|||
|
|||
var previousSnapshot = Snapshot; |
|||
var previousVersion = Version; |
|||
try |
|||
{ |
|||
var result = await handler(command); |
|||
|
|||
var events = uncomittedEvents.ToArray(); |
|||
|
|||
await WriteAsync(events, previousVersion); |
|||
|
|||
if (result == null) |
|||
{ |
|||
if (isUpdate) |
|||
{ |
|||
result = new EntitySavedResult(Version); |
|||
} |
|||
else |
|||
{ |
|||
result = EntityCreatedResult.Create(id, Version); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
catch |
|||
{ |
|||
RestorePreviousSnapshot(previousSnapshot, previousVersion); |
|||
|
|||
throw; |
|||
} |
|||
finally |
|||
{ |
|||
uncomittedEvents.Clear(); |
|||
} |
|||
} |
|||
|
|||
protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); |
|||
|
|||
protected abstract void ApplyEvent(Envelope<IEvent> @event); |
|||
|
|||
protected abstract Task ReadAsync(Type type, Guid id); |
|||
|
|||
protected abstract Task WriteAsync(Envelope<IEvent>[] events, long previousVersion); |
|||
|
|||
public async Task<J<object>> ExecuteAsync(J<IAggregateCommand> command) |
|||
{ |
|||
var result = await ExecuteAsync(command.Value); |
|||
|
|||
return result.AsJ(); |
|||
} |
|||
|
|||
protected abstract Task<object> ExecuteAsync(IAggregateCommand command); |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Log; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Infrastructure.Commands |
|||
{ |
|||
public abstract class MultiSnapshotDomainObjectGrain<T> : DomainObjectGrainBase<T> where T : IDomainState, new() |
|||
{ |
|||
private readonly IStore<Guid> store; |
|||
private readonly List<T> snapshots = new List<T> { new T { Version = EtagVersion.Empty } }; |
|||
private IPersistence persistence; |
|||
|
|||
public override T Snapshot |
|||
{ |
|||
get { return snapshots.Last(); } |
|||
} |
|||
|
|||
protected MultiSnapshotDomainObjectGrain(IStore<Guid> store, ISemanticLog log) |
|||
: base(log) |
|||
{ |
|||
Guard.NotNull(log, nameof(log)); |
|||
|
|||
this.store = store; |
|||
} |
|||
|
|||
public T GetSnapshot(long version) |
|||
{ |
|||
if (version == EtagVersion.Any) |
|||
{ |
|||
return Snapshot; |
|||
} |
|||
|
|||
if (version == EtagVersion.Empty) |
|||
{ |
|||
return snapshots[0]; |
|||
} |
|||
|
|||
if (version >= 0 && version < snapshots.Count - 1) |
|||
{ |
|||
return snapshots[(int)version + 1]; |
|||
} |
|||
|
|||
return default(T); |
|||
} |
|||
|
|||
protected sealed override void ApplyEvent(Envelope<IEvent> @event) |
|||
{ |
|||
var snapshot = OnEvent(@event); |
|||
|
|||
snapshot.Version = NewVersion + 1; |
|||
snapshots.Add(OnEvent(@event)); |
|||
} |
|||
|
|||
protected sealed override Task ReadAsync(Type type, Guid id) |
|||
{ |
|||
persistence = store.WithEventSourcing<Guid>(type, id, ApplyEvent); |
|||
|
|||
return persistence.ReadAsync(); |
|||
} |
|||
|
|||
protected sealed override async Task WriteAsync(Envelope<IEvent>[] events, long previousVersion) |
|||
{ |
|||
if (events.Length > 0) |
|||
{ |
|||
var snaphosts = store.GetSnapshotStore<T>(); |
|||
|
|||
await persistence.WriteEventsAsync(events); |
|||
await snaphosts.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); |
|||
} |
|||
} |
|||
|
|||
protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) |
|||
{ |
|||
while (snapshots.Count > previousVersion) |
|||
{ |
|||
snapshots.RemoveAt(snapshots.Count - 1); |
|||
} |
|||
} |
|||
|
|||
protected abstract T OnEvent(Envelope<IEvent> @event); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Infrastructure.States |
|||
{ |
|||
public interface IPersistence<TState> |
|||
{ |
|||
long Version { get; } |
|||
|
|||
Task WriteEventsAsync(IEnumerable<Envelope<IEvent>> @events); |
|||
|
|||
Task WriteSnapshotAsync(TState state); |
|||
|
|||
Task ReadAsync(long expectedVersion = EtagVersion.Any); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Reflection; |
|||
using FakeItEasy; |
|||
using Orleans; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Commands |
|||
{ |
|||
public class DomainObjectGrainFormatterTests |
|||
{ |
|||
private readonly IGrainCallContext context = A.Fake<IGrainCallContext>(); |
|||
|
|||
[Fact] |
|||
public void Should_return_fallback_if_no_method_is_defined() |
|||
{ |
|||
A.CallTo(() => context.InterfaceMethod) |
|||
.Returns(null); |
|||
|
|||
var result = DomainObjectGrainFormatter.Format(context); |
|||
|
|||
Assert.Equal("Unknown", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_method_name_if_not_domain_object_method() |
|||
{ |
|||
var methodInfo = A.Fake<MethodInfo>(); |
|||
|
|||
A.CallTo(() => methodInfo.Name) |
|||
.Returns("Calculate"); |
|||
|
|||
A.CallTo(() => context.InterfaceMethod) |
|||
.Returns(methodInfo); |
|||
|
|||
var result = DomainObjectGrainFormatter.Format(context); |
|||
|
|||
Assert.Equal("Calculate", result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_nice_method_name_if_domain_object_execute() |
|||
{ |
|||
var methodInfo = A.Fake<MethodInfo>(); |
|||
|
|||
A.CallTo(() => methodInfo.Name) |
|||
.Returns(nameof(IDomainObjectGrain.ExecuteAsync)); |
|||
|
|||
A.CallTo(() => context.Arguments) |
|||
.Returns(new object[] { new MyCommand() }); |
|||
|
|||
A.CallTo(() => context.InterfaceMethod) |
|||
.Returns(methodInfo); |
|||
|
|||
var result = DomainObjectGrainFormatter.Format(context); |
|||
|
|||
Assert.Equal("ExecuteAsync(MyCommand)", result); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue