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