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