Headless CMS and Content Managment Hub
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

388 lines
11 KiB

// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public abstract partial class DomainObject<T> where T : class, IDomainState<T>, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private readonly SnapshotList<T> snapshots = new SnapshotList<T>();
private readonly IPersistenceFactory<T> factory;
private readonly ILogger log;
private IPersistence<T>? persistence;
private bool isLoaded;
private DomainId uniqueId;
public DomainId UniqueId
{
get => uniqueId;
}
public T Snapshot
{
get => snapshots.Current;
}
public long Version
{
get => snapshots.Version;
}
protected int Capacity
{
get => snapshots.Capacity;
set => snapshots.Capacity = value;
}
protected DomainObject(IPersistenceFactory<T> factory,
ILogger log)
{
Guard.NotNull(factory);
Guard.NotNull(log);
this.factory = factory;
this.log = log;
}
public async Task<T> GetSnapshotAsync(long version)
{
var (result, valid) = snapshots.Get(version);
if (result == null && valid)
{
var snapshot = new T
{
Version = EtagVersion.Empty
};
snapshots.Add(snapshot, snapshot.Version, false);
var allEvents = factory.WithEventSourcing(GetType(), UniqueId, @event =>
{
@event = @event.Migrate(snapshot);
var (newSnapshot, isChanged) = ApplyEvent(@event, true, snapshot, snapshot.Version, false);
// Can only be null in case of errors or inconsistent streams.
if (newSnapshot != null)
{
snapshot = newSnapshot;
}
// If all snapshorts from this one here are valid we can stop.
return newSnapshot != null && !snapshots.ContainsThisAndNewer(newSnapshot.Version);
});
await allEvents.ReadAsync();
(result, valid) = snapshots.Get(version);
}
return result ?? new T { Version = EtagVersion.Empty };
}
public virtual void Setup(DomainId uniqueId)
{
this.uniqueId = uniqueId;
persistence = factory.WithSnapshotsAndEventSourcing(GetType(), UniqueId,
new HandleSnapshot<T>((snapshot, version) =>
{
snapshot.Version = version;
snapshots.Add(snapshot, version, true);
}),
@event =>
{
@event = @event.Migrate(Snapshot);
return ApplyEvent(@event, true, Snapshot, Version, true).Success;
});
}
public virtual async Task EnsureLoadedAsync(bool silent = false)
{
if (isLoaded)
{
return;
}
if (silent)
{
await ReadAsync();
}
else
{
var watch = ValueStopwatch.StartNew();
try
{
await ReadAsync();
}
finally
{
log.LogInformation("Activated domain object of type {type} with ID {id} in {time}.", GetType(), UniqueId, watch.Stop());
}
}
isLoaded = true;
}
protected void RaiseEvent(IEvent @event)
{
RaiseEvent(Envelope.Create(@event));
}
protected virtual void RaiseEvent(Envelope<IEvent> @event)
{
Guard.NotNull(@event, nameof(@event));
@event.SetAggregateId(uniqueId);
if (ApplyEvent(@event, false, Snapshot, Version, true).Success)
{
uncomittedEvents.Add(@event);
}
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
{
return uncomittedEvents;
}
public void ClearUncommittedEvents()
{
uncomittedEvents.Clear();
}
private async Task<CommandResult> DeleteCoreAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler) where TCommand : ICommand
{
Guard.NotNull(handler);
var previousSnapshot = Snapshot;
var previousVersion = Version;
try
{
var result = (await handler(command)) ?? None.Value;
var events = uncomittedEvents.ToArray();
if (events != null)
{
var deletedId = DomainId.Combine(UniqueId, DomainId.Create("deleted"));
var deletedStream = factory.WithEventSourcing(GetType(), deletedId, null);
await deletedStream.WriteEventsAsync(events);
if (persistence != null)
{
await persistence.DeleteAsync();
Setup(uniqueId);
}
snapshots.Clear();
}
return new CommandResult(UniqueId, Version, previousVersion, result);
}
catch
{
RestorePreviousSnapshot(previousSnapshot, previousVersion);
throw;
}
finally
{
ClearUncommittedEvents();
}
}
private async Task<CommandResult> UpsertCoreAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler, bool isCreation = false) where TCommand : ICommand
{
Guard.NotNull(handler);
var wasDeleted = IsDeleted(Snapshot);
var previousSnapshot = Snapshot;
var previousVersion = Version;
try
{
var result = (await handler(command)) ?? None.Value;
var events = uncomittedEvents.ToArray();
try
{
await WriteAsync(events);
}
catch (InconsistentStateException)
{
await EnsureLoadedAsync(true);
if (wasDeleted)
{
if (CanRecreate() && isCreation)
{
snapshots.ResetTo(new T(), previousVersion);
foreach (var @event in uncomittedEvents)
{
ApplyEvent(@event, false, Snapshot, Version, true);
}
await WriteAsync(events);
}
else
{
throw new DomainObjectDeletedException(uniqueId.ToString());
}
}
else
{
RestorePreviousSnapshot(previousSnapshot, previousVersion);
throw new DomainObjectConflictException(uniqueId.ToString());
}
}
isLoaded = true;
return new CommandResult(UniqueId, Version, previousVersion, result);
}
catch
{
RestorePreviousSnapshot(previousSnapshot, previousVersion);
throw;
}
finally
{
ClearUncommittedEvents();
}
}
protected virtual bool CanAcceptCreation(ICommand command)
{
return true;
}
protected virtual bool CanAccept(ICommand command)
{
return true;
}
protected virtual bool IsDeleted(T snapshot)
{
return false;
}
protected virtual bool CanRecreate()
{
return false;
}
protected virtual bool CanRecreate(IEvent @event)
{
return false;
}
private void RestorePreviousSnapshot(T previousSnapshot, long previousVersion)
{
snapshots.ResetTo(previousSnapshot, previousVersion);
}
private (T?, bool Success) ApplyEvent(Envelope<IEvent> @event, bool isLoading, T snapshot, long version, bool clean)
{
if (IsDeleted(snapshot))
{
if (!CanRecreate(@event.Payload))
{
return default;
}
snapshot = new T
{
Version = Version
};
}
var newVersion = version + 1;
var newSnapshot = Apply(snapshot, @event);
if (ReferenceEquals(snapshot, newSnapshot) && isLoading)
{
newSnapshot = snapshot.Copy();
}
var isChanged = !ReferenceEquals(snapshot, newSnapshot);
if (isChanged)
{
newSnapshot.Version = newVersion;
snapshots.Add(newSnapshot, newVersion, clean);
}
return (newSnapshot, isChanged);
}
private async Task ReadAsync()
{
if (persistence != null)
{
await persistence.ReadAsync();
if (persistence.IsSnapshotStale)
{
try
{
await persistence.WriteSnapshotAsync(Snapshot);
}
catch (Exception ex)
{
log.LogError(ex, "Failed to repair snapshot for domain object of type {type} with ID {id}.", GetType(), UniqueId);
}
}
}
}
private async Task WriteAsync(Envelope<IEvent>[] newEvents)
{
if (newEvents.Length > 0 && persistence != null)
{
await persistence.WriteEventsAsync(newEvents);
await persistence.WriteSnapshotAsync(Snapshot);
}
}
public async Task RebuildStateAsync()
{
await EnsureLoadedAsync(true);
if (Version <= EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(UniqueId.ToString());
}
if (persistence != null)
{
await persistence.WriteSnapshotAsync(Snapshot);
}
}
protected virtual T Apply(T snapshot, Envelope<IEvent> @event)
{
return snapshot.Apply(@event);
}
public abstract Task<CommandResult> ExecuteAsync(IAggregateCommand command);
}
}