mirror of https://github.com/Squidex/squidex.git
30 changed files with 480 additions and 222 deletions
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IPersistent.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IPersistence<TState> |
||||
|
{ |
||||
|
Task WriteEventsAsync(params Envelope<IEvent>[] @events); |
||||
|
|
||||
|
Task WriteSnapShotAsync(TState state); |
||||
|
|
||||
|
Task ReadAsync(bool force = false); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IStore.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IStore |
||||
|
{ |
||||
|
IPersistence<object> WithEventSourcing<TOwner>(string key, Func<Envelope<IEvent>, Task> applyEvent); |
||||
|
|
||||
|
IPersistence<TState> WithSnapshots<TOwner, TState>(string key, Func<TState, Task> applySnapshot); |
||||
|
|
||||
|
IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(string key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Persistance.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class Persistance<TOwner, TState> : IPersistence<TState> |
||||
|
{ |
||||
|
private readonly string ownerKey; |
||||
|
private readonly ISnapshotStore snapshotStore; |
||||
|
private readonly IStreamNameResolver streamNameResolver; |
||||
|
private readonly IEventStore eventStore; |
||||
|
private readonly EventDataFormatter eventDataFormatter; |
||||
|
private readonly Action invalidate; |
||||
|
private readonly Func<TState, Task> applyState; |
||||
|
private readonly Func<Envelope<IEvent>, Task> applyEvent; |
||||
|
private Task readTask; |
||||
|
private int positionSnapshot = -1; |
||||
|
private int positionEvent = -1; |
||||
|
|
||||
|
public Persistance(string ownerKey, |
||||
|
ISnapshotStore snapshotStore, |
||||
|
IStreamNameResolver streamNameResolver, |
||||
|
IEventStore eventStore, |
||||
|
EventDataFormatter eventDataFormatter, |
||||
|
Action invalidate, |
||||
|
Func<TState, Task> applyState, |
||||
|
Func<Envelope<IEvent>, Task> applyEvent) |
||||
|
{ |
||||
|
Guard.NotNull(ownerKey, nameof(ownerKey)); |
||||
|
|
||||
|
this.ownerKey = ownerKey; |
||||
|
this.applyState = applyState; |
||||
|
this.applyEvent = applyEvent; |
||||
|
this.invalidate = invalidate; |
||||
|
this.eventStore = eventStore; |
||||
|
this.eventDataFormatter = eventDataFormatter; |
||||
|
this.snapshotStore = snapshotStore; |
||||
|
this.streamNameResolver = streamNameResolver; |
||||
|
} |
||||
|
|
||||
|
public Task ReadAsync(bool force = false) |
||||
|
{ |
||||
|
if (force) |
||||
|
{ |
||||
|
return ReadInternalAsync(); |
||||
|
} |
||||
|
|
||||
|
if (readTask == null) |
||||
|
{ |
||||
|
readTask = ReadInternalAsync(); |
||||
|
} |
||||
|
|
||||
|
return readTask; |
||||
|
} |
||||
|
|
||||
|
private async Task ReadInternalAsync() |
||||
|
{ |
||||
|
positionSnapshot = -1; |
||||
|
positionEvent = -1; |
||||
|
|
||||
|
if (snapshotStore != null) |
||||
|
{ |
||||
|
var (state, etag) = await snapshotStore.ReadAsync<TState>(ownerKey); |
||||
|
|
||||
|
if (int.TryParse(etag, out var position)) |
||||
|
{ |
||||
|
positionSnapshot = position; |
||||
|
positionEvent = position; |
||||
|
|
||||
|
if (applyState != null) |
||||
|
{ |
||||
|
await applyState(state); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (eventStore != null && streamNameResolver != null) |
||||
|
{ |
||||
|
var events = await eventStore.GetEventsAsync(GetStreamName(), positionSnapshot); |
||||
|
|
||||
|
foreach (var @event in events) |
||||
|
{ |
||||
|
var parsedEvent = eventDataFormatter.Parse(@event.Data, true); |
||||
|
|
||||
|
if (applyEvent != null) |
||||
|
{ |
||||
|
await applyEvent(parsedEvent); |
||||
|
} |
||||
|
|
||||
|
positionEvent = (int)@event.EventStreamNumber; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task WriteSnapShotAsync(TState state) |
||||
|
{ |
||||
|
if (snapshotStore == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Snapshots are not supported."); |
||||
|
} |
||||
|
|
||||
|
var newPosition = |
||||
|
eventStore != null ? |
||||
|
positionEvent : |
||||
|
positionSnapshot + 1; |
||||
|
|
||||
|
if (newPosition != positionSnapshot) |
||||
|
{ |
||||
|
await snapshotStore.WriteAsync(ownerKey, state, positionSnapshot.ToString(), newPosition.ToString()); |
||||
|
|
||||
|
positionSnapshot = newPosition; |
||||
|
} |
||||
|
|
||||
|
invalidate(); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteEventsAsync(params Envelope<IEvent>[] @events) |
||||
|
{ |
||||
|
Guard.NotNull(events, nameof(@events)); |
||||
|
|
||||
|
if (eventStore == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Events are not supported."); |
||||
|
} |
||||
|
|
||||
|
if (@events.Length > 0) |
||||
|
{ |
||||
|
var commitId = Guid.NewGuid(); |
||||
|
|
||||
|
var eventStream = GetStreamName(); |
||||
|
var eventData = GetEventData(events, commitId); |
||||
|
|
||||
|
await eventStore.AppendEventsAsync(commitId, GetStreamName(), positionEvent, eventData); |
||||
|
|
||||
|
positionEvent += events.Length; |
||||
|
} |
||||
|
|
||||
|
invalidate(); |
||||
|
} |
||||
|
|
||||
|
private EventData[] GetEventData(Envelope<IEvent>[] events, Guid commitId) |
||||
|
{ |
||||
|
return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); |
||||
|
} |
||||
|
|
||||
|
private string GetStreamName() |
||||
|
{ |
||||
|
return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,56 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// StateHolder.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.States |
|
||||
{ |
|
||||
public sealed class StateHolder<T> : IStateHolder<T> |
|
||||
{ |
|
||||
private readonly Action written; |
|
||||
private readonly IStateStore store; |
|
||||
private readonly string key; |
|
||||
private string etag; |
|
||||
|
|
||||
public T State { get; set; } |
|
||||
|
|
||||
public StateHolder(string key, Action written, IStateStore store) |
|
||||
{ |
|
||||
this.key = key; |
|
||||
this.store = store; |
|
||||
this.written = written; |
|
||||
} |
|
||||
|
|
||||
public async Task ReadAsync() |
|
||||
{ |
|
||||
(State, etag) = await store.ReadAsync<T>(key); |
|
||||
|
|
||||
if (Equals(State, default(T))) |
|
||||
{ |
|
||||
State = Activator.CreateInstance<T>(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task WriteAsync() |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var newEtag = Guid.NewGuid().ToString(); |
|
||||
|
|
||||
await store.WriteAsync(key, State, etag, newEtag); |
|
||||
|
|
||||
etag = newEtag; |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
written(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,65 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// StatefulActor.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.States |
|
||||
{ |
|
||||
public abstract class StatefulObject<T> |
|
||||
{ |
|
||||
private IStateHolder<T> stateHolder; |
|
||||
|
|
||||
public T State |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
if (stateHolder != null) |
|
||||
{ |
|
||||
return stateHolder.State; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return default(T); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected set |
|
||||
{ |
|
||||
if (stateHolder != null) |
|
||||
{ |
|
||||
stateHolder.State = value; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public Task ActivateAsync(IStateHolder<T> stateHolder) |
|
||||
{ |
|
||||
Guard.NotNull(stateHolder, nameof(stateHolder)); |
|
||||
|
|
||||
this.stateHolder = stateHolder; |
|
||||
|
|
||||
return ReadStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task ReadStateAsync() |
|
||||
{ |
|
||||
if (stateHolder != null) |
|
||||
{ |
|
||||
await stateHolder.ReadAsync(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public virtual async Task WriteStateAsync() |
|
||||
{ |
|
||||
if (stateHolder != null) |
|
||||
{ |
|
||||
await stateHolder.WriteAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,52 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Store.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class Store : IStore |
||||
|
{ |
||||
|
private readonly Action invalidate; |
||||
|
private readonly ISnapshotStore snapshotStore; |
||||
|
private readonly IStreamNameResolver streamNameResolver; |
||||
|
private readonly IEventStore eventStore; |
||||
|
private readonly EventDataFormatter eventDataFormatter; |
||||
|
|
||||
|
public Store( |
||||
|
ISnapshotStore snapshotStore, |
||||
|
IStreamNameResolver streamNameResolver, |
||||
|
IEventStore eventStore, |
||||
|
EventDataFormatter eventDataFormatter, |
||||
|
Action invalidate) |
||||
|
{ |
||||
|
this.eventStore = eventStore; |
||||
|
this.eventDataFormatter = eventDataFormatter; |
||||
|
this.invalidate = invalidate; |
||||
|
this.snapshotStore = snapshotStore; |
||||
|
this.streamNameResolver = streamNameResolver; |
||||
|
} |
||||
|
|
||||
|
public IPersistence<object> WithEventSourcing<TOwner>(string key, Func<Envelope<IEvent>, Task> applyEvent) |
||||
|
{ |
||||
|
return new Persistance<TOwner, object>(key, null, streamNameResolver, eventStore, eventDataFormatter, invalidate, null, applyEvent); |
||||
|
} |
||||
|
|
||||
|
public IPersistence<TState> WithSnapshots<TOwner, TState>(string key, Func<TState, Task> applySnapshot) |
||||
|
{ |
||||
|
return new Persistance<TOwner, TState>(key, snapshotStore, null, null, null, invalidate, applySnapshot, null); |
||||
|
} |
||||
|
|
||||
|
public IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(string key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent) |
||||
|
{ |
||||
|
return new Persistance<TOwner, TState>(key, snapshotStore, streamNameResolver, eventStore, eventDataFormatter, invalidate, applySnapshot, applyEvent); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
// ==========================================================================
|
||||
|
// StoreExtensions.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public static class StoreExtensions |
||||
|
{ |
||||
|
public static IPersistence<object> WithEventSourcing<TOwner>(this IStore store, string key, Action<Envelope<IEvent>> applyEvent) |
||||
|
{ |
||||
|
return store.WithEventSourcing<TOwner>(key, x => |
||||
|
{ |
||||
|
applyEvent(x); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IPersistence<TState> WithSnapshots<TOwner, TState>(this IStore store, string key, Action<TState> applySnapshot) |
||||
|
{ |
||||
|
return store.WithSnapshots<TOwner, TState>(key, x => |
||||
|
{ |
||||
|
applySnapshot(x); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(this IStore store, string key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent) |
||||
|
{ |
||||
|
return store.WithSnapshotsAndEventSourcing<TOwner, TState>(key, x => |
||||
|
{ |
||||
|
applySnapshot(x); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}, x => |
||||
|
{ |
||||
|
applyEvent(x); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue