Browse Source

Refactored denormlizer

pull/1/head
Sebastian 9 years ago
parent
commit
fe6e43fcfe
  1. 1
      Squidex.sln
  2. 10
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs
  3. 168
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs
  4. 11
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoStreamsRepository.cs
  5. 15
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  6. 4
      src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs
  7. 84
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  8. 12
      src/Squidex.Infrastructure/CQRS/Events/IEventCatchConsumer.cs
  9. 8
      src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs
  10. 4
      src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs
  11. 13
      src/Squidex.Infrastructure/CQRS/Events/ILiveEventConsumer.cs
  12. 32
      src/Squidex.Infrastructure/CQRS/Events/InMemoryEventBus.cs
  13. 18
      src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs
  14. 12
      src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs
  15. 17
      src/Squidex.Infrastructure/CQRS/Replay/IReplayableStore.cs
  16. 107
      src/Squidex.Infrastructure/CQRS/Replay/ReplayGenerator.cs
  17. 17
      src/Squidex.Infrastructure/ICliCommand.cs
  18. 68
      src/Squidex.Infrastructure/Timers/CompletionTimer.cs
  19. 15
      src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs
  20. 100
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository.cs
  21. 111
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  22. 129
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs
  23. 148
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  24. 3
      src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs
  25. 21
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs
  26. 119
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs
  27. 112
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  28. 74
      src/Squidex.Read.MongoDb/Utils/MongoDbConsumerWrapper.cs
  29. 2
      src/Squidex.Read/Apps/Services/IAppProvider.cs
  30. 33
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  31. 4
      src/Squidex.Read/Schemas/Services/ISchemaProvider.cs
  32. 51
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  33. 4
      src/Squidex/Config/Domain/ReadModule.cs
  34. 13
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  35. 4
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs

1
Squidex.sln

@ -103,7 +103,6 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{61F6BBCE-A080-4400-B194-70E2F5D2096E} = {24A3171D-2905-49C9-8A49-A473799014E8}
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{25F66C64-058A-4D44-BC0C-F12A054F9A91} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}

10
src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs

@ -30,14 +30,18 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
[BsonElement]
[BsonRequired]
public string EventStream { get; set; }
public long EventsOffset { get; set; }
[BsonElement]
[BsonRequired]
public int EventsVersion { get; set; }
public long EventStreamOffset { get; set; }
[BsonElement]
[BsonRequired]
public string EventStream { get; set; }
[BsonElement]
[BsonRequired]
public int EventCount { get; set; }
public long EventsCount { get; set; }
}
}

168
src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs

@ -12,7 +12,6 @@ using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Reflection;
@ -25,21 +24,16 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
{
public class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore, IExternalSystem
{
private sealed class EventCountEntity
{
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
[BsonElement]
[BsonRequired]
public int EventCount { get; set; }
}
private const int Retries = 500;
private readonly IEventNotifier notifier;
private string eventsOffsetIndex;
public MongoEventStore(IMongoDatabase database)
public MongoEventStore(IMongoDatabase database, IEventNotifier notifier)
: base(database)
{
Guard.NotNull(notifier, nameof(notifier));
this.notifier = notifier;
}
protected override string CollectionName()
@ -47,9 +41,19 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
return "Events";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection)
protected override MongoCollectionSettings CollectionSettings()
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.EventStream).Ascending(x => x.EventsVersion), new CreateIndexOptions { Unique = true });
return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority };
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection)
{
var indexNames =
await Task.WhenAll(
collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventsOffset), new CreateIndexOptions { Unique = true }),
collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventStreamOffset).Ascending(x => x.EventStream), new CreateIndexOptions { Unique = true }));
eventsOffsetIndex = indexNames[0];
}
public void CheckConnection()
@ -64,61 +68,57 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
}
}
public IObservable<EventData> GetEventsAsync(string streamName)
public IObservable<StoredEvent> GetEventsAsync(string streamName)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
return Observable.Create<EventData>(async (observer, ct) =>
{
try
return Observable.Create<StoredEvent>(async (observer, ct) =>
{
await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit =>
{
var position = commit.EventStreamOffset;
foreach (var @event in commit.Events)
{
var eventData = SimpleMapper.Map(@event, new EventData());
observer.OnNext(eventData);
}
}, ct);
observer.OnNext(new StoredEvent(position, eventData));
observer.OnCompleted();
}
catch (Exception e)
{
observer.OnError(e);
position++;
}
}, ct);
});
}
public IObservable<EventData> GetEventsAsync()
public IObservable<StoredEvent> GetEventsAsync(long lastReceivedPosition = -1)
{
return Observable.Create<EventData>(async (observer, ct) =>
{
try
return Observable.Create<StoredEvent>(async (observer, ct) =>
{
var position = await GetPreviousOffset(lastReceivedPosition);
await Collection.Find(new BsonDocument()).ForEachAsync(commit =>
{
foreach (var @event in commit.Events)
{
if (position >= lastReceivedPosition)
{
var eventData = SimpleMapper.Map(@event, new EventData());
observer.OnNext(eventData);
observer.OnNext(new StoredEvent(position, eventData));
}
}, ct);
observer.OnCompleted();
}
catch (Exception e)
{
observer.OnError(e);
position++;
}
}, ct);
});
}
public async Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events)
{
var currentVersion = await GetEventVersionAsync(streamName);
Guard.NotNullOrEmpty(streamName, nameof(streamName));
Guard.NotNull(events, nameof(events));
var currentVersion = await GetEventStreamOffset(streamName);
if (currentVersion != expectedVersion)
{
@ -127,50 +127,106 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
var now = DateTime.UtcNow;
var commitEvents = events.Select(x => SimpleMapper.Map(x, new MongoEvent())).ToList();
if (commitEvents.Any())
{
var offset = await GetEventOffset();
var commit = new MongoEventCommit
{
Id = commitId,
Events = events.Select(x => SimpleMapper.Map(x, new MongoEvent())).ToList(),
Events = commitEvents,
EventsOffset = offset,
EventsCount = commitEvents.Count,
EventStream = streamName,
EventsVersion = expectedVersion,
EventStreamOffset = expectedVersion,
Timestamp = now
};
if (commit.Events.Any())
for (var retry = 0; retry < Retries; retry++)
{
commit.EventCount = commit.Events.Count;
try
{
await Collection.InsertOneAsync(commit);
notifier.NotifyEventsStored();
return;
}
catch (MongoWriteException e)
{
if (e.WriteError?.Category == ServerErrorCategory.DuplicateKey)
if (e.Message.IndexOf(eventsOffsetIndex, StringComparison.OrdinalIgnoreCase) >= 0)
{
currentVersion = await GetEventVersionAsync(streamName);
if (currentVersion != expectedVersion)
commit.EventsOffset = await GetEventOffset();
}
else if (e.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
currentVersion = await GetEventStreamOffset(streamName);
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
else
{
throw;
}
}
}
}
}
throw;
private async Task<long> GetPreviousOffset(long startPosition)
{
var document =
await Collection.Find(x => x.EventsOffset <= startPosition)
.Project<BsonDocument>(Projection
.Include(x => x.EventStreamOffset)
.Include(x => x.EventsCount))
.SortByDescending(x => x.EventsOffset).Limit(1)
.FirstOrDefaultAsync();
if (document != null)
{
return document["EventsOffset"].ToInt64();
}
return -1;
}
private async Task<long> GetEventOffset()
{
var document =
await Collection.Find(new BsonDocument())
.Project<BsonDocument>(Projection
.Include(x => x.EventsOffset)
.Include(x => x.EventsCount))
.SortByDescending(x => x.EventsOffset).Limit(1)
.FirstOrDefaultAsync();
if (document != null)
{
return document["EventsOffset"].ToInt64() + document["EventsCount"].ToInt64();
}
return -1;
}
private async Task<int> GetEventVersionAsync(string streamName)
private async Task<long> GetEventStreamOffset(string streamName)
{
var allCommits =
await Collection.Find(c => c.EventStream == streamName)
.Project<BsonDocument>(Projection.Include(x => x.EventCount))
.ToListAsync();
var document =
await Collection.Find(x => x.EventStream == streamName)
.Project<BsonDocument>(Projection
.Include(x => x.EventStreamOffset)
.Include(x => x.EventsCount))
.SortByDescending(x => x.EventsOffset).Limit(1)
.FirstOrDefaultAsync();
var currentVersion = allCommits.Sum(x => x["EventCount"].ToInt32()) - 1;
if (document != null)
{
return document["EventStreamOffset"].ToInt64() + document["EventsCount"].ToInt64();
}
return currentVersion;
return -1;
}
}
}

11
src/Squidex.Infrastructure.MongoDb/EventStore/MongoStreamsRepository.cs

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.MongoDb.EventStore
{
public class MongoStreamsRepository
{
}
}

15
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -20,26 +20,22 @@ namespace Squidex.Infrastructure.CQRS.Commands
private readonly IStreamNameResolver nameResolver;
private readonly IDomainObjectFactory factory;
private readonly IEventStore eventStore;
private readonly IEventPublisher eventPublisher;
private readonly EventDataFormatter formatter;
public DefaultDomainObjectRepository(
IDomainObjectFactory factory,
IEventStore eventStore,
IEventPublisher eventPublisher,
IStreamNameResolver nameResolver,
EventDataFormatter formatter)
{
Guard.NotNull(factory, nameof(factory));
Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventPublisher, nameof(eventPublisher));
Guard.NotNull(nameResolver, nameof(nameResolver));
this.factory = factory;
this.eventStore = eventStore;
this.formatter = formatter;
this.eventPublisher = eventPublisher;
this.eventStore = eventStore;
this.nameResolver = nameResolver;
}
@ -58,9 +54,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
var domainObject = (TDomainObject)factory.CreateNew(typeof(TDomainObject), id);
foreach (var eventData in events)
foreach (var storedEvent in events)
{
var envelope = formatter.Parse(eventData);
var envelope = formatter.Parse(storedEvent.Data);
domainObject.ApplyEvent(envelope);
}
@ -93,11 +89,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
throw new DomainObjectVersionException(domainObject.Id.ToString(), domainObject.GetType(), versionCurrent, versionExpected);
}
foreach (var eventData in eventsToSave)
{
eventPublisher.Publish(eventData);
}
}
}
}

4
src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs

@ -14,12 +14,12 @@ namespace Squidex.Infrastructure.CQRS
{
public static class EnvelopeExtensions
{
public static int EventNumber(this EnvelopeHeaders headers)
public static long EventNumber(this EnvelopeHeaders headers)
{
return headers[CommonHeaders.EventNumber].ToInt32(CultureInfo.InvariantCulture);
}
public static Envelope<T> SetEventNumber<T>(this Envelope<T> envelope, int value) where T : class
public static Envelope<T> SetEventNumber<T>(this Envelope<T> envelope, long value) where T : class
{
envelope.Headers.Set(CommonHeaders.EventNumber, value);

84
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -7,93 +7,93 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Timers;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class EventReceiver
public sealed class EventReceiver : DisposableObject
{
private readonly EventDataFormatter formatter;
private readonly bool canCatch;
private readonly IEnumerable<ILiveEventConsumer> liveConsumers;
private readonly IEnumerable<ICatchEventConsumer> catchConsumers;
private readonly IEventStream eventStream;
private readonly IEventStore eventStore;
private readonly IEventNotifier eventNotifier;
private readonly IEventCatchConsumer eventConsumer;
private readonly ILogger<EventReceiver> logger;
private bool isSubscribed;
private CompletionTimer timer;
public EventReceiver(
ILogger<EventReceiver> logger,
IEventStream eventStream,
IEnumerable<ILiveEventConsumer> liveConsumers,
IEnumerable<ICatchEventConsumer> catchConsumers,
EventDataFormatter formatter,
bool canCatch = true)
IEventStore eventStore,
IEventNotifier eventNotifier,
IEventCatchConsumer eventConsumer,
ILogger<EventReceiver> logger)
{
Guard.NotNull(logger, nameof(logger));
Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStream, nameof(eventStream));
Guard.NotNull(liveConsumers, nameof(liveConsumers));
Guard.NotNull(catchConsumers, nameof(catchConsumers));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventNotifier, nameof(eventNotifier));
Guard.NotNull(eventConsumer, nameof(eventConsumer));
this.logger = logger;
this.formatter = formatter;
this.canCatch = canCatch;
this.eventStream = eventStream;
this.liveConsumers = liveConsumers;
this.catchConsumers = catchConsumers;
this.eventStore = eventStore;
this.eventNotifier = eventNotifier;
this.eventConsumer = eventConsumer;
}
public void Subscribe()
protected override void DisposeObject(bool disposing)
{
if (isSubscribed)
if (disposing)
{
return;
timer?.Dispose();
}
}
eventStream.Connect("squidex", eventData =>
public void Subscribe(int delay = 5000)
{
var @event = ParseEvent(eventData);
if (@event == null)
if (timer != null)
{
return;
}
if (canCatch)
var lastReceivedPosition = long.MinValue;
timer = new CompletionTimer(delay, async ct =>
{
DispatchConsumers(catchConsumers, @event);
}
else
if (lastReceivedPosition == long.MinValue)
{
DispatchConsumers(liveConsumers, @event);
lastReceivedPosition = await eventConsumer.GetLastHandledEventNumber();
}
logger.LogDebug("Event {0} handled", @event.Payload.GetType().Name);
});
await eventStore.GetEventsAsync(lastReceivedPosition).ForEachAsync(async storedEvent =>
{
var @event = ParseEvent(storedEvent.Data);
isSubscribed = true;
}
@event.SetEventNumber(storedEvent.EventNumber);
private void DispatchConsumers(IEnumerable<IEventConsumer> consumers, Envelope<IEvent> @event)
{
Task.WaitAll(consumers.Select(c => DispatchConsumer(@event, c)).ToArray());
await DispatchConsumer(@event, eventConsumer, storedEvent.EventNumber);
}, ct);
});
eventNotifier.Subscribe(timer.Trigger);
}
private async Task DispatchConsumer(Envelope<IEvent> @event, IEventConsumer consumer)
private async Task DispatchConsumer(Envelope<IEvent> @event, IEventCatchConsumer consumer, long eventNumber)
{
try
{
await consumer.On(@event);
await consumer.On(@event, eventNumber);
}
catch (Exception ex)
{
logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, "[{0}]: Failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId());
throw;
}
}
@ -109,7 +109,7 @@ namespace Squidex.Infrastructure.CQRS.Events
{
logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex, "Failed to parse event {0}", eventData.EventId);
return null;
throw;
}
}
}

12
src/Squidex.Infrastructure/CQRS/Events/ICatchEventConsumer.cs → src/Squidex.Infrastructure/CQRS/Events/IEventCatchConsumer.cs

@ -1,13 +1,19 @@
// ==========================================================================
// ICatchEventConsumer.cs
// ==========================================================================
// IEventCatchConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
public interface ICatchEventConsumer : IEventConsumer
public interface IEventCatchConsumer
{
Task<long> GetLastHandledEventNumber();
Task On(Envelope<IEvent> @event, long eventNumber);
}
}

8
src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs → src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IEventStream.cs
// IEventsPushedNotifier.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,8 +10,10 @@ using System;
namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventStream : IDisposable
public interface IEventNotifier
{
void Connect(string queuePrefix, Action<EventData> received);
void NotifyEventsStored();
void Subscribe(Action handler);
}
}

4
src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs

@ -14,9 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventStore
{
IObservable<EventData> GetEventsAsync();
IObservable<StoredEvent> GetEventsAsync(long lastReceivedPosition = -1);
IObservable<EventData> GetEventsAsync(string streamName);
IObservable<StoredEvent> GetEventsAsync(string streamName);
Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events);
}

13
src/Squidex.Infrastructure/CQRS/Events/ILiveEventConsumer.cs

@ -1,13 +0,0 @@
// ==========================================================================
// ILiveEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events
{
public interface ILiveEventConsumer : IEventConsumer
{
}
}

32
src/Squidex.Infrastructure/CQRS/Events/InMemoryEventBus.cs

@ -1,32 +0,0 @@
// ==========================================================================
// InMemoryEventBus.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Reactive.Subjects;
namespace Squidex.Infrastructure.CQRS.Events
{
public class InMemoryEventBus : IEventPublisher, IEventStream
{
private readonly Subject<EventData> subject = new Subject<EventData>();
public void Dispose()
{
}
public void Publish(EventData eventData)
{
subject.OnNext(eventData);
}
public void Connect(string queuePrefix, Action<EventData> received)
{
subject.Subscribe(received);
}
}
}

18
src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs → src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs

@ -1,15 +1,25 @@
// ==========================================================================
// IEventPublisher.cs
// StoredEvent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventPublisher
public sealed class StoredEvent
{
public long EventNumber { get; }
public EventData Data { get; }
public StoredEvent(long eventNumber, EventData data)
{
void Publish(EventData eventData);
Guard.NotNull(data, nameof(data));
EventNumber = eventNumber;
Data = data;
}
}
}

12
src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs

@ -12,20 +12,20 @@ namespace Squidex.Infrastructure.CQRS.Events
{
public class WrongEventVersionException : Exception
{
private readonly int currentVersion;
private readonly int expectedVersion;
private readonly long currentVersion;
private readonly long expectedVersion;
public int CurrentVersion
public long CurrentVersion
{
get { return currentVersion; }
}
public int ExpectedVersion
public long ExpectedVersion
{
get { return expectedVersion; }
}
public WrongEventVersionException(int currentVersion, int expectedVersion)
public WrongEventVersionException(long currentVersion, long expectedVersion)
: base(FormatMessage(currentVersion, expectedVersion))
{
this.currentVersion = currentVersion;
@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.CQRS.Events
this.expectedVersion = expectedVersion;
}
private static string FormatMessage(int currentVersion, int expectedVersion)
private static string FormatMessage(long currentVersion, long expectedVersion)
{
return $"Requested version {expectedVersion}, but found {currentVersion}.";
}

17
src/Squidex.Infrastructure/CQRS/Replay/IReplayableStore.cs

@ -1,17 +0,0 @@
// ==========================================================================
// IReplayableStore.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Replay
{
public interface IReplayableStore
{
Task ClearAsync();
}
}

107
src/Squidex.Infrastructure/CQRS/Replay/ReplayGenerator.cs

@ -1,107 +0,0 @@
// ==========================================================================
// ReplayGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Infrastructure.CQRS.Replay
{
public sealed class ReplayGenerator : ICliCommand
{
private readonly ILogger<ReplayGenerator> logger;
private readonly IEventStore eventStore;
private readonly IEventPublisher eventPublisher;
private readonly IEnumerable<IReplayableStore> stores;
public string Name { get; } = "replay";
public ReplayGenerator(
ILogger<ReplayGenerator> logger,
IEventStore eventStore,
IEventPublisher eventPublisher,
IEnumerable<IReplayableStore> stores)
{
Guard.NotNull(logger, nameof(logger));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventPublisher, nameof(eventPublisher));
Guard.NotNull(stores, nameof(stores));
this.stores = stores;
this.logger = logger;
this.eventStore = eventStore;
this.eventPublisher = eventPublisher;
}
public void Execute(string[] args)
{
ReplayAllAsync().Wait();
}
public async Task ReplayAllAsync()
{
logger.LogDebug("Starting to replay all events");
if (!await ClearAsync())
{
return;
}
await ReplayEventsAsync();
logger.LogDebug("Finished to replay all events");
}
private async Task ReplayEventsAsync()
{
try
{
logger.LogDebug("Replaying all messages");
await eventStore.GetEventsAsync().ForEachAsync(eventData =>
{
eventPublisher.Publish(eventData);
});
logger.LogDebug("Replayed all messages");
}
catch (Exception e)
{
logger.LogCritical(InfrastructureErrors.ReplayPublishingFailed, e, "Failed to publish events to {0}", eventPublisher);
}
}
private async Task<bool> ClearAsync()
{
logger.LogDebug("Clearing replayable stores");
foreach (var store in stores)
{
try
{
await store.ClearAsync();
logger.LogDebug("Cleared store {0}", store);
}
catch (Exception e)
{
logger.LogCritical(InfrastructureErrors.ReplayClearingFailed, e, "Failed to clear store {0}", store);
return false;
}
}
logger.LogDebug("Cleared replayable stores");
return true;
}
}
}

17
src/Squidex.Infrastructure/ICliCommand.cs

@ -1,17 +0,0 @@
// ==========================================================================
// ICommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure
{
public interface ICliCommand
{
string Name { get; }
void Execute(string[] args);
}
}

68
src/Squidex.Infrastructure/Timers/CompletionTimer.cs

@ -0,0 +1,68 @@
// ==========================================================================
// CompletionTimer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.Timers
{
public sealed class CompletionTimer : DisposableObject
{
private readonly CancellationTokenSource disposeCancellationTokenSource = new CancellationTokenSource();
private readonly Task runTask;
private CancellationTokenSource delayCancellationSource;
public CompletionTimer(int delay, Func<CancellationToken, Task> callback)
{
Guard.NotNull(callback, nameof(callback));
Guard.GreaterThan(delay, 0, nameof(delay));
runTask = RunInternal(delay, callback);
}
private async Task RunInternal(int delay, Func<CancellationToken, Task> callback)
{
while (!disposeCancellationTokenSource.IsCancellationRequested)
{
try
{
await callback(disposeCancellationTokenSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
Console.WriteLine("Task in TriggerTimer has been cancelled.");
}
delayCancellationSource = new CancellationTokenSource();
await Task.Delay(delay, delayCancellationSource.Token).ConfigureAwait(false);
}
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
delayCancellationSource?.Cancel();
disposeCancellationTokenSource.Cancel();
runTask.Wait();
}
}
public void Trigger()
{
ThrowIfDisposed();
delayCancellationSource?.Cancel();
}
}
}

15
src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs

@ -27,15 +27,15 @@ namespace Squidex.Read.MongoDb.Apps
[BsonRequired]
[BsonElement]
public HashSet<string> Languages { get; set; }
public HashSet<string> Languages { get; } = new HashSet<string>();
[BsonRequired]
[BsonElement]
public Dictionary<string, MongoAppClientEntity> Clients { get; set; }
public Dictionary<string, MongoAppClientEntity> Clients { get; } = new Dictionary<string, MongoAppClientEntity>();
[BsonRequired]
[BsonElement]
public Dictionary<string, MongoAppContributorEntity> Contributors { get; set; }
public Dictionary<string, MongoAppContributorEntity> Contributors { get; } = new Dictionary<string, MongoAppContributorEntity>();
IReadOnlyCollection<IAppClientEntity> IAppEntity.Clients
{
@ -56,14 +56,5 @@ namespace Squidex.Read.MongoDb.Apps
{
get { return Language.GetLanguage(MasterLanguage); }
}
public MongoAppEntity()
{
Contributors = new Dictionary<string, MongoAppContributorEntity>();
Clients = new Dictionary<string, MongoAppClientEntity>();
Languages = new HashSet<string>();
}
}
}

100
src/Squidex.Read.MongoDb/Apps/MongoAppRepository.cs

@ -10,25 +10,25 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.MongoDb.Utils;
using Squidex.Read.Apps.Services;
namespace Squidex.Read.MongoDb.Apps
{
public class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, ICatchEventConsumer, IReplayableStore
public partial class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, IEventConsumer
{
public MongoAppRepository(IMongoDatabase database)
private readonly IAppProvider appProvider;
public MongoAppRepository(IMongoDatabase database, IAppProvider appProvider)
: base(database)
{
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
protected override string CollectionName()
@ -41,11 +41,6 @@ namespace Squidex.Read.MongoDb.Apps
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Name));
}
public Task ClearAsync()
{
return TryDropCollectionAsync();
}
public async Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId)
{
var entities =
@ -69,84 +64,5 @@ namespace Squidex.Read.MongoDb.Apps
return entity;
}
protected Task On(AppCreated @event, EnvelopeHeaders headers)
{
return Collection.CreateAsync(headers, a =>
{
SimpleMapper.Map(@event, a);
});
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
SimpleMapper.Map(@event, contributor);
});
}
protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Contributors.Remove(@event.ContributorId);
});
}
protected Task On(AppClientAttached @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Clients.Add(@event.Id, SimpleMapper.Map(@event, new MongoAppClientEntity()));
});
}
protected Task On(AppClientRevoked @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Clients.Remove(@event.Id);
});
}
protected Task On(AppClientRenamed @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Clients[@event.Id].Name = @event.Name;
});
}
protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Languages.Add(@event.Language.Iso2Code);
});
}
protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Languages.Remove(@event.Language.Iso2Code);
});
}
protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.MasterLanguage = @event.Language.Iso2Code;
});
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
}
}

111
src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -0,0 +1,111 @@
// ==========================================================================
// MongoAppRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.MongoDb.Utils;
namespace Squidex.Read.MongoDb.Apps
{
public partial class MongoAppRepository
{
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected async Task On(AppCreated @event, EnvelopeHeaders headers)
{
await Collection.CreateAsync(headers, a =>
{
SimpleMapper.Map(@event, a);
});
appProvider.Remove(headers.AggregateId());
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
SimpleMapper.Map(@event, contributor);
});
}
protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Contributors.Remove(@event.ContributorId);
});
}
protected Task On(AppClientAttached @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppClientEntity());
});
}
protected Task On(AppClientRevoked @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Clients.Remove(@event.Id);
});
}
protected Task On(AppClientRenamed @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Clients[@event.Id].Name = @event.Name;
});
}
protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Languages.Add(@event.Language.Iso2Code);
});
}
protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.Languages.Remove(@event.Language.Iso2Code);
});
}
protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
return UpdateAsync(headers, a =>
{
a.MasterLanguage = @event.Language.Iso2Code;
});
}
public async Task UpdateAsync(EnvelopeHeaders headers, Action<MongoAppEntity> updater)
{
await Collection.UpdateAsync(headers, updater);
appProvider.Remove(headers.AggregateId());
}
}
}

129
src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs

@ -11,40 +11,23 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.Core;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Core.Schemas;
using Squidex.Events;
using Squidex.Events.Contents;
using Squidex.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.Contents;
using Squidex.Read.Contents.Repositories;
using Squidex.Read.MongoDb.Contents.Visitors;
using Squidex.Read.MongoDb.Utils;
using Squidex.Read.Schemas.Services;
namespace Squidex.Read.MongoDb.Contents
{
public class MongoContentRepository : IContentRepository, ICatchEventConsumer, IReplayableStore
public partial class MongoContentRepository : IContentRepository, IEventConsumer
{
private const string Prefix = "Projections_Content_";
private readonly IMongoDatabase database;
private readonly ISchemaProvider schemaProvider;
protected UpdateDefinitionBuilder<MongoContentEntity> Update
{
get
{
return Builders<MongoContentEntity>.Update;
}
}
protected IndexKeysDefinitionBuilder<MongoContentEntity> IndexKeys
{
get
@ -63,25 +46,6 @@ namespace Squidex.Read.MongoDb.Contents
this.schemaProvider = schemaProvider;
}
public async Task ClearAsync()
{
using (var collections = await database.ListCollectionsAsync())
{
while (await collections.MoveNextAsync())
{
foreach (var collection in collections.Current)
{
var name = collection["name"].ToString();
if (name.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
{
await database.DropCollectionAsync(name);
}
}
}
}
}
public async Task<List<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages)
{
List<IContentEntity> result = null;
@ -167,83 +131,6 @@ namespace Squidex.Read.MongoDb.Contents
return result;
}
protected Task On(ContentCreated @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{
return collection.CreateAsync(headers, x =>
{
SimpleMapper.Map(@event, x);
x.SetData(schema, @event.Data);
});
});
}
protected Task On(ContentUpdated @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{
return collection.UpdateAsync(headers, x =>
{
x.SetData(schema, @event.Data);
});
});
}
protected Task On(ContentPublished @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = true;
});
});
}
protected Task On(ContentUnpublished @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = false;
});
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsDeleted = true;
});
});
}
protected Task On(FieldDeleted @event, EnvelopeHeaders headers)
{
var collection = GetCollection(headers.SchemaId());
return collection.UpdateManyAsync(new BsonDocument(), Update.Unset(new StringFieldDefinition<MongoContentEntity>($"Data.{@event.FieldId}")));
}
protected async Task On(SchemaCreated @event, EnvelopeHeaders headers)
{
var collection = GetCollection(headers.AggregateId());
await collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(IndexKeys.Text(x => x.Text));
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
private async Task ForSchemaAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Schema, Task> action)
{
var collection = GetCollection(schemaId);
@ -257,19 +144,5 @@ namespace Squidex.Read.MongoDb.Contents
await action(collection, schemaEntity.Schema);
}
private async Task ForSchemaAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{
var collection = GetCollection(schemaId);
await action(collection);
}
private IMongoCollection<MongoContentEntity> GetCollection(Guid schemaId)
{
var name = $"{Prefix}{schemaId}";
return database.GetCollection<MongoContentEntity>(name);
}
}
}

148
src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -0,0 +1,148 @@
// ==========================================================================
// MongoContentRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Events;
using Squidex.Events.Contents;
using Squidex.Events.Schemas;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.MongoDb.Utils;
// ReSharper disable ConvertToLambdaExpression
namespace Squidex.Read.MongoDb.Contents
{
public partial class MongoContentRepository
{
protected UpdateDefinitionBuilder<MongoContentEntity> Update
{
get
{
return Builders<MongoContentEntity>.Update;
}
}
public async Task ClearAsync()
{
using (var collections = await database.ListCollectionsAsync())
{
while (await collections.MoveNextAsync())
{
foreach (var collection in collections.Current)
{
var name = collection["name"].ToString();
if (name.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
{
await database.DropCollectionAsync(name);
}
}
}
}
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected Task On(SchemaCreated @event, EnvelopeHeaders headers)
{
return ForSchemaIdAsync(headers.AggregateId(), async collection =>
{
await collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(IndexKeys.Text(x => x.Text));
});
}
protected Task On(ContentCreated @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{
return collection.CreateAsync(headers, x =>
{
SimpleMapper.Map(@event, x);
x.SetData(schema, @event.Data);
});
});
}
protected Task On(ContentUpdated @event, EnvelopeHeaders headers)
{
return ForSchemaAsync(headers.SchemaId(), (collection, schema) =>
{
return collection.UpdateAsync(headers, x =>
{
x.SetData(schema, @event.Data);
});
});
}
protected Task On(ContentPublished @event, EnvelopeHeaders headers)
{
return ForSchemaIdAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = true;
});
});
}
protected Task On(ContentUnpublished @event, EnvelopeHeaders headers)
{
return ForSchemaIdAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = false;
});
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
return ForSchemaIdAsync(headers.SchemaId(), collection =>
{
return collection.UpdateAsync(headers, x =>
{
x.IsDeleted = true;
});
});
}
protected Task On(FieldDeleted @event, EnvelopeHeaders headers)
{
return ForSchemaIdAsync(headers.SchemaId(), collection =>
{
return collection.UpdateManyAsync(new BsonDocument(), Update.Unset(new StringFieldDefinition<MongoContentEntity>($"Data.{@event.FieldId}")));
});
}
private async Task ForSchemaIdAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{
var collection = GetCollection(schemaId);
await action(collection);
}
private IMongoCollection<MongoContentEntity> GetCollection(Guid schemaId)
{
var name = $"{Prefix}{schemaId}";
return database.GetCollection<MongoContentEntity>(name);
}
}
}

3
src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -14,7 +14,6 @@ using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read.History;
using Squidex.Read.History.Repositories;
@ -22,7 +21,7 @@ using Squidex.Read.MongoDb.Utils;
namespace Squidex.Read.MongoDb.History
{
public class MongoHistoryEventRepository : MongoRepositoryBase<MongoHistoryEventEntity>, IHistoryEventRepository, ICatchEventConsumer, IReplayableStore
public class MongoHistoryEventRepository : MongoRepositoryBase<MongoHistoryEventEntity>, IHistoryEventRepository, IEventConsumer
{
private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();

21
src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs

@ -58,11 +58,26 @@ namespace Squidex.Read.MongoDb.Schemas
get { return schema.Value; }
}
public Lazy<Schema> DeserializeSchema(SchemaJsonSerializer serializer)
public void SerializeSchema(Schema newSchema, SchemaJsonSerializer serializer)
{
Label = newSchema.Properties.Label ?? newSchema.Name;
Schema = serializer.Serialize(newSchema).ToString();
schema = new Lazy<Schema>(() => newSchema);
IsPublished = newSchema.IsPublished;
}
public void UpdateSchema(SchemaJsonSerializer serializer, Func<Schema, Schema> updater)
{
schema = new Lazy<Schema>(() => Schema != null ? serializer.Deserialize(JObject.Parse(Schema)) : null);
DeserializeSchema(serializer);
SerializeSchema(updater(schema.Value), serializer);
}
return schema;
public Lazy<Schema> DeserializeSchema(SchemaJsonSerializer serializer)
{
return schema ?? (schema = new Lazy<Schema>(() => Schema != null ? serializer.Deserialize(JObject.Parse(Schema)) : null));
}
}
}

119
src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs

@ -13,35 +13,31 @@ using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json;
using Squidex.Events.Schemas;
using Squidex.Events.Schemas.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.MongoDb.Utils;
using Squidex.Read.Schemas;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas.Services;
namespace Squidex.Read.MongoDb.Schemas
{
public class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ICatchEventConsumer, IReplayableStore
public partial class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, IEventConsumer
{
private readonly SchemaJsonSerializer serializer;
private readonly FieldRegistry registry;
private readonly ISchemaProvider schemaProvider;
public MongoSchemaRepository(IMongoDatabase database, SchemaJsonSerializer serializer, FieldRegistry registry)
public MongoSchemaRepository(IMongoDatabase database, SchemaJsonSerializer serializer, FieldRegistry registry, ISchemaProvider schemaProvider)
: base(database)
{
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(registry, nameof(registry));
this.serializer = serializer;
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(schemaProvider, nameof(schemaProvider));
this.registry = registry;
this.serializer = serializer;
this.schemaProvider = schemaProvider;
}
protected override string CollectionName()
@ -105,104 +101,5 @@ namespace Squidex.Read.MongoDb.Schemas
return entity?.Id;
}
protected Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, s => s.IsDeleted = true);
}
protected Task On(FieldDeleted @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldDisabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldEnabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldHidden @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldShown @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldUpdated @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaUpdated @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaPublished @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaUnpublished @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldAdded @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry));
}
protected Task On(SchemaCreated @event, EnvelopeHeaders headers)
{
var schema = Schema.Create(@event.Name, @event.Properties);
return Collection.CreateAsync(headers, s => { UpdateSchema(s, schema); SimpleMapper.Map(@event, s); });
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
private Task UpdateSchema(EnvelopeHeaders headers, Func<Schema, Schema> updater)
{
return Collection.UpdateAsync(headers, e => UpdateSchema(e, updater));
}
private void UpdateSchema(MongoSchemaEntity entity, Func<Schema, Schema> updater)
{
var currentSchema = Deserialize(entity);
currentSchema = updater(currentSchema);
UpdateSchema(entity, currentSchema);
UpdateProperties(entity, currentSchema);
}
private static void UpdateProperties(MongoSchemaEntity entity, Schema currentSchema)
{
entity.Label = currentSchema.Properties.Label;
entity.IsPublished = currentSchema.IsPublished;
}
private void UpdateSchema(MongoSchemaEntity entity, Schema schema)
{
entity.Schema = serializer.Serialize(schema).ToString();
}
private Schema Deserialize(MongoSchemaEntity entity)
{
return entity.DeserializeSchema(serializer).Value;
}
}
}

112
src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -0,0 +1,112 @@
// ==========================================================================
// MongoSchemaRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Core.Schemas;
using Squidex.Events.Schemas;
using Squidex.Events.Schemas.Utils;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Read.MongoDb.Utils;
namespace Squidex.Read.MongoDb.Schemas
{
public partial class MongoSchemaRepository
{
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected async Task On(SchemaCreated @event, EnvelopeHeaders headers)
{
var schema = SchemaEventDispatcher.Dispatch(@event);
await Collection.CreateAsync(headers, s => { UpdateSchema(s, schema); SimpleMapper.Map(@event, s); });
schemaProvider.Remove(headers.AggregateId());
}
protected Task On(FieldDeleted @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldDisabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldEnabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldHidden @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldShown @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldUpdated @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaUpdated @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaPublished @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(SchemaUnpublished @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldAdded @event, EnvelopeHeaders headers)
{
return UpdateSchema(headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry));
}
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
await Collection.UpdateAsync(headers, s => s.IsDeleted = true);
schemaProvider.Remove(headers.AggregateId());
}
private async Task UpdateSchema(EnvelopeHeaders headers, Func<Schema, Schema> updater)
{
await Collection.UpdateAsync(headers, e => UpdateSchema(e, updater));
schemaProvider.Remove(headers.AggregateId());
}
private void UpdateSchema(MongoSchemaEntity entity, Func<Schema, Schema> updater)
{
entity.UpdateSchema(serializer, updater);
}
private void UpdateSchema(MongoSchemaEntity entity, Schema schema)
{
entity.SerializeSchema(schema, serializer);
}
}
}

74
src/Squidex.Read.MongoDb/Utils/MongoDbConsumerWrapper.cs

@ -0,0 +1,74 @@
// ==========================================================================
// MongoDbStore.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Read.MongoDb.Utils
{
public sealed class EventPosition
{
public string Name { get; set; }
public long EventNumber { get; set; }
}
public sealed class MongoDbConsumerWrapper : MongoRepositoryBase<EventPosition>, IEventCatchConsumer
{
private static readonly UpdateOptions upsert = new UpdateOptions { IsUpsert = true };
private readonly IEventConsumer eventConsumer;
private readonly string eventStoreName;
public MongoDbConsumerWrapper(IMongoDatabase database, IEventConsumer eventConsumer)
: base(database)
{
Guard.NotNull(eventConsumer, nameof(eventConsumer));
this.eventConsumer = eventConsumer;
eventStoreName = GetType().Name;
}
protected override string CollectionName()
{
return "EventPositions";
}
protected override Task SetupCollectionAsync(IMongoCollection<EventPosition> collection)
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Name), new CreateIndexOptions { Unique = true });
}
public async Task On(Envelope<IEvent> @event, long eventNumber)
{
await eventConsumer.On(@event);
await SetLastHandledEventNumber(eventNumber);
}
private Task SetLastHandledEventNumber(long eventNumber)
{
return Collection.ReplaceOneAsync(x => x.Name == eventStoreName, new EventPosition { Name = eventStoreName, EventNumber = eventNumber }, upsert);
}
public async Task<long> GetLastHandledEventNumber()
{
var collectionPosition =
await Collection
.Find(new BsonDocument()).SortByDescending(x => x.EventNumber).Limit(1)
.FirstOrDefaultAsync();
return collectionPosition?.EventNumber ?? -1;
}
}
}

2
src/Squidex.Read/Apps/Services/IAppProvider.cs

@ -16,5 +16,7 @@ namespace Squidex.Read.Apps.Services
Task<IAppEntity> FindAppByIdAsync(Guid id);
Task<IAppEntity> FindAppByNameAsync(string name);
void Remove(Guid id);
}
}

33
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -9,10 +9,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.Utils;
@ -20,7 +17,7 @@ using Squidex.Read.Utils;
namespace Squidex.Read.Apps.Services.Implementations
{
public class CachingAppProvider : CachingProvider, IAppProvider, ICatchEventConsumer, ILiveEventConsumer
public class CachingAppProvider : CachingProvider, IAppProvider
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
private readonly IAppRepository repository;
@ -49,7 +46,7 @@ namespace Squidex.Read.Apps.Services.Implementations
{
var entity = await repository.FindAppAsync(appId);
cacheItem = new CacheItem { Entity = entity, Name = entity?.Name };
cacheItem = new CacheItem { Entity = entity, Name = entity.Name };
Cache.Set(cacheKey, cacheItem, CacheDuration);
@ -86,18 +83,9 @@ namespace Squidex.Read.Apps.Services.Implementations
return cacheItem.Entity;
}
public Task On(Envelope<IEvent> @event)
public void Remove(Guid id)
{
if (@event.Payload is AppContributorAssigned ||
@event.Payload is AppContributorRemoved ||
@event.Payload is AppClientAttached ||
@event.Payload is AppClientRevoked ||
@event.Payload is AppClientRenamed ||
@event.Payload is AppLanguageAdded ||
@event.Payload is AppLanguageRemoved ||
@event.Payload is AppMasterLanguageSet)
{
var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId());
var cacheKey = BuildIdCacheKey(id);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
@ -108,19 +96,6 @@ namespace Squidex.Read.Apps.Services.Implementations
Cache.Remove(cacheKey);
}
else
{
var appCreated = @event.Payload as AppCreated;
if (appCreated != null)
{
Cache.Remove(BuildIdCacheKey(@event.Headers.AggregateId()));
Cache.Remove(BuildNameCacheKey(appCreated.Name));
}
}
return Task.FromResult(true);
}
private static string BuildNameCacheKey(string name)
{

4
src/Squidex.Read/Schemas/Services/ISchemaProvider.cs

@ -13,8 +13,10 @@ namespace Squidex.Read.Schemas.Services
{
public interface ISchemaProvider
{
Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid schemaId);
Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid id);
Task<ISchemaEntityWithSchema> FindSchemaByNameAsync(Guid appId, string name);
void Remove(Guid id);
}
}

51
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -9,19 +9,16 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Utils;
using Squidex.Events;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf
namespace Squidex.Read.Schemas.Services.Implementations
{
public class CachingSchemaProvider : CachingProvider, ISchemaProvider, ICatchEventConsumer, ILiveEventConsumer
public class CachingSchemaProvider : CachingProvider, ISchemaProvider
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly ISchemaRepository repository;
@ -30,6 +27,8 @@ namespace Squidex.Read.Schemas.Services.Implementations
{
public ISchemaEntityWithSchema Entity;
public Guid AppId;
public string Name;
}
@ -41,16 +40,23 @@ namespace Squidex.Read.Schemas.Services.Implementations
this.repository = repository;
}
public async Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid schemaId)
public async Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid id)
{
var cacheKey = BuildIdCacheKey(schemaId);
var cacheKey = BuildIdCacheKey(id);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem == null)
{
var entity = await repository.FindSchemaAsync(schemaId);
var entity = await repository.FindSchemaAsync(id);
cacheItem = new CacheItem { Entity = entity, Name = entity?.Name };
if (entity == null)
{
cacheItem = new CacheItem();
}
else
{
cacheItem = new CacheItem { Entity = entity, Name = entity.Name, AppId = entity.AppId };
}
Cache.Set(cacheKey, cacheItem, CacheDuration);
@ -74,7 +80,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
{
var entity = await repository.FindSchemaAsync(appId, name);
cacheItem = new CacheItem { Entity = entity, Name = name };
cacheItem = new CacheItem { Entity = entity, Name = name, AppId = appId };
Cache.Set(cacheKey, cacheItem, CacheDuration);
@ -87,38 +93,19 @@ namespace Squidex.Read.Schemas.Services.Implementations
return cacheItem.Entity;
}
public Task On(Envelope<IEvent> @event)
public void Remove(Guid id)
{
if (@event.Payload is SchemaDeleted ||
@event.Payload is SchemaPublished ||
@event.Payload is SchemaUnpublished ||
@event.Payload is SchemaUpdated ||
@event.Payload is FieldEvent)
{
var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId());
var cacheKey = BuildIdCacheKey(id);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem?.Name != null)
{
Cache.Remove(BuildNameCacheKey(@event.Headers.AppId(), cacheItem.Name));
Cache.Remove(BuildNameCacheKey(cacheItem.AppId, cacheItem.Name));
}
Cache.Remove(cacheKey);
}
else
{
var schemaCreated = @event.Payload as SchemaCreated;
if (schemaCreated != null)
{
Cache.Remove(BuildIdCacheKey(@event.Headers.AggregateId()));
Cache.Remove(BuildNameCacheKey(@event.Headers.AppId(), schemaCreated.Name));
}
}
return Task.FromResult(true);
}
private static string BuildNameCacheKey(Guid appId, string name)
{

4
src/Squidex/Config/Domain/ReadModule.cs

@ -33,14 +33,10 @@ namespace Squidex.Config.Domain
{
builder.RegisterType<CachingAppProvider>()
.As<IAppProvider>()
.As<ILiveEventConsumer>()
.As<ICatchEventConsumer>()
.SingleInstance();
builder.RegisterType<CachingSchemaProvider>()
.As<ISchemaProvider>()
.As<ILiveEventConsumer>()
.As<ICatchEventConsumer>()
.SingleInstance();
builder.RegisterType<AppHistoryEventsCreator>()

13
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -15,7 +15,6 @@ using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Replay;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.Contents.Repositories;
using Squidex.Read.History.Repositories;
@ -102,29 +101,25 @@ namespace Squidex.Config.Domain
builder.RegisterType<MongoContentRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.As<IContentRepository>()
.As<ICatchEventConsumer>()
.As<IReplayableStore>()
.As<IEventCatchConsumer>()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.As<IHistoryEventRepository>()
.As<ICatchEventConsumer>()
.As<IReplayableStore>()
.As<IEventCatchConsumer>()
.SingleInstance();
builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.As<ISchemaRepository>()
.As<ICatchEventConsumer>()
.As<IReplayableStore>()
.As<IEventCatchConsumer>()
.SingleInstance();
builder.RegisterType<MongoAppRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.As<IAppRepository>()
.As<ICatchEventConsumer>()
.As<IReplayableStore>()
.As<IEventCatchConsumer>()
.SingleInstance();
}
}

4
tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs

@ -44,8 +44,8 @@ namespace Squidex.Infrastructure.CQRS.Events
private readonly Mock<ILiveEventConsumer> liveConsumer1 = new Mock<ILiveEventConsumer>();
private readonly Mock<ILiveEventConsumer> liveConsumer2 = new Mock<ILiveEventConsumer>();
private readonly Mock<ICatchEventConsumer> catchConsumer1 = new Mock<ICatchEventConsumer>();
private readonly Mock<ICatchEventConsumer> catchConsumer2 = new Mock<ICatchEventConsumer>();
private readonly Mock<IEventCatchConsumer> catchConsumer1 = new Mock<IEventCatchConsumer>();
private readonly Mock<IEventCatchConsumer> catchConsumer2 = new Mock<IEventCatchConsumer>();
private readonly Mock<IEventStream> eventStream = new Mock<IEventStream>();
private readonly Mock<EventDataFormatter> formatter = new Mock<EventDataFormatter>(new TypeNameRegistry(), null);
private readonly EventData eventData = new EventData();

Loading…
Cancel
Save