// ========================================================================== // 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.Tasks; namespace Squidex.Infrastructure.Commands { public abstract class DomainObjectBase where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private readonly ISemanticLog log; private bool isLoaded; private Guid id; public Guid Id { get { return id; } } public long Version { get { return Snapshot.Version; } } public abstract T Snapshot { get; } protected DomainObjectBase(ISemanticLog log) { Guard.NotNull(log, nameof(log)); this.log = log; } public virtual void Setup(Guid id) { this.id = id; OnSetup(); } public virtual async Task EnsureLoadedAsync(bool silent = false) { if (isLoaded) { return; } if (silent) { await ReadAsync(); } else { var logContext = (id: id.ToString(), name: GetType().Name); using (log.MeasureInformation(logContext, (ctx, w) => w .WriteProperty("action", "ActivateDomainObject") .WriteProperty("domainObjectType", ctx.name) .WriteProperty("domainObjectKey", ctx.id))) { await ReadAsync(); } } isLoaded = true; } protected void RaiseEvent(IEvent @event) { RaiseEvent(Envelope.Create(@event)); } protected virtual void RaiseEvent(Envelope @event) { Guard.NotNull(@event, nameof(@event)); @event.SetAggregateId(id); if (ApplyEvent(@event, false)) { uncomittedEvents.Add(@event); } } public IReadOnlyList> GetUncomittedEvents() { return uncomittedEvents; } public void ClearUncommittedEvents() { uncomittedEvents.Clear(); } protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler, false); } protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToAsync()!, false); } protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler.ToDefault(), false); } protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, false); } protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler, true); } protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToAsync()!, true); } protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToDefault()!, true); } protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, true); } private async Task InvokeAsync(TCommand command, Func> handler, bool isUpdate) where TCommand : class, IAggregateCommand { Guard.NotNull(command, nameof(command)); Guard.NotNull(handler, nameof(handler)); if (isUpdate) { await EnsureLoadedAsync(); } if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) { throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); } if (isUpdate && Version < 0) { throw new DomainObjectNotFoundException(id.ToString(), GetType()); } 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); } } isLoaded = true; return result; } catch { RestorePreviousSnapshot(previousSnapshot, previousVersion); throw; } finally { ClearUncommittedEvents(); } } protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); protected abstract bool ApplyEvent(Envelope @event, bool isLoading); protected abstract Task ReadAsync(); protected abstract Task WriteAsync(Envelope[] newEvents, long previousVersion); public virtual Task RebuildStateAsync() { return Task.CompletedTask; } protected virtual void OnSetup() { } public abstract Task ExecuteAsync(IAggregateCommand command); } }