Browse Source

Started filter support.

pull/218/head
Sebastian Stehle 8 years ago
parent
commit
24428e8082
  1. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs
  3. 4
      src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs
  4. 12
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
  5. 60
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
  6. 27
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
  7. 155
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
  8. 97
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs
  9. 9
      src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs
  10. 228
      src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  11. 173
      src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
  12. 135
      src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
  13. 13
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs
  14. 32
      src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs
  15. 39
      src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs
  16. 8
      src/Squidex.Infrastructure/EventSourcing/EventData.cs
  17. 12
      src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
  18. 2
      src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs
  19. 26
      src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs
  20. 4
      src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs
  21. 2
      src/Squidex/Config/Domain/InfrastructureServices.cs
  22. 4
      tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs
  23. 12
      tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs
  24. 14
      tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  25. 53
      tools/Migrate_01/Migration00_ConvertEventStore.cs
  26. 12
      tools/Migrate_01/Rebuilder.cs

4
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs

@ -39,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero });
}
public Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
public Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken ct = default(CancellationToken))
{
return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, cancellationToken);
return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct);
}
public async Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20)

2
src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories
Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall);
Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken));
Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken ct = default(CancellationToken));
Task<int> CountByAppAsync(Guid appId);

4
src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs

@ -71,13 +71,13 @@ namespace Squidex.Domain.Apps.Entities.Rules
timer.SkipCurrentDelay();
}
private async Task QueryAsync(CancellationToken cancellationToken)
private async Task QueryAsync(CancellationToken ct)
{
try
{
var now = clock.GetCurrentInstant();
await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken);
await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct);
}
catch (Exception ex)
{

12
src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Text;
using EventStore.ClientAPI;
using EventStoreData = EventStore.ClientAPI.EventData;
@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing
var body = Encoding.UTF8.GetString(@event.Data);
var meta = Encoding.UTF8.GetString(@event.Metadata);
var eventData = new EventData { Type = @event.EventType, EventId = @event.EventId, Payload = body, Metadata = meta };
var eventData = new EventData { Type = @event.EventType, Payload = body, Metadata = meta };
return new StoredEvent(
resolvedEvent.OriginalEventNumber.ToString(),
@ -30,13 +31,10 @@ namespace Squidex.Infrastructure.EventSourcing
public static EventStoreData Write(EventData eventData)
{
var body = Encoding.UTF8.GetBytes(eventData.Payload);
var meta = Encoding.UTF8.GetBytes(eventData.Metadata);
var body = Encoding.UTF8.GetBytes(eventData.Payload.ToString());
var meta = Encoding.UTF8.GetBytes(eventData.Metadata.ToString());
return new EventStoreData(
eventData.EventId,
eventData.Type,
true, body, meta);
return new EventStoreData(Guid.NewGuid(), eventData.Type, true, body, meta);
}
}
}

60
src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs

@ -11,7 +11,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Projections;
namespace Squidex.Infrastructure.EventSourcing
{
@ -20,18 +19,18 @@ namespace Squidex.Infrastructure.EventSourcing
private const int WritePageSize = 500;
private const int ReadPageSize = 500;
private readonly IEventStoreConnection connection;
private readonly string projectionHost;
private readonly string prefix;
private ProjectionsManager projectionsManager;
private ProjectionClient projectionClient;
public GetEventStore(IEventStoreConnection connection, string prefix, string projectionHost)
{
Guard.NotNull(connection, nameof(connection));
this.connection = connection;
this.projectionHost = projectionHost;
this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex");
projectionClient = new ProjectionClient(connection, prefix, projectionHost);
}
public void Initialize()
@ -45,50 +44,43 @@ namespace Squidex.Infrastructure.EventSourcing
throw new ConfigurationException("Cannot connect to event store.", ex);
}
try
{
projectionsManager = connection.GetProjectionsManagerAsync(projectionHost).Result;
projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials).Wait();
projectionClient.ConnectAsync().Wait();
}
catch (Exception ex)
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
{
throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex);
}
return new GetEventStoreSubscription(connection, subscriber, projectionClient, prefix, position, streamFilter);
}
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
public Task CreateIndexAsync(string property)
{
return new GetEventStoreSubscription(connection, subscriber, projectionsManager, prefix, position, streamFilter);
return projectionClient.CreateProjectionAsync(property, string.Empty);
}
public async Task GetEventsAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken))
public async Task QueryAsync(Func<StoredEvent, Task> callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken))
{
var streamName = await connection.CreateProjectionAsync(projectionsManager, prefix, streamFilter);
var streamName = await projectionClient.CreateProjectionAsync(property, value);
var sliceStart = ProjectionHelper.ParsePosition(position);
var sliceStart = projectionClient.ParsePosition(position);
StreamEventsSlice currentSlice;
do
{
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
await QueryAsync(callback, streamName, sliceStart, ct);
}
if (currentSlice.Status == SliceReadStatus.Success)
public async Task QueryAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken))
{
sliceStart = currentSlice.NextEventNumber;
var streamName = await projectionClient.CreateProjectionAsync(streamFilter);
foreach (var resolved in currentSlice.Events)
{
var storedEvent = Formatter.Read(resolved);
var sliceStart = projectionClient.ParsePosition(position);
await callback(storedEvent);
}
}
await QueryAsync(callback, streamName, sliceStart, ct);
}
while (!currentSlice.IsEndOfStream && !cancellationToken.IsCancellationRequested);
private Task QueryAsync(Func<StoredEvent, Task> callback, string streamName, long sliceStart, CancellationToken ct)
{
return QueryAsync(callback, GetStreamName(streamName), sliceStart, ct);
}
public async Task<IReadOnlyList<StoredEvent>> GetEventsAsync(string streamName, long streamPosition = 0)
public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0)
{
var result = new List<StoredEvent>();
@ -97,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing
StreamEventsSlice currentSlice;
do
{
currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, false);
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
if (currentSlice.Status == SliceReadStatus.Success)
{
@ -116,12 +108,12 @@ namespace Squidex.Infrastructure.EventSourcing
return result;
}
public Task AppendEventsAsync(Guid commitId, string streamName, ICollection<EventData> events)
public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events)
{
return AppendEventsInternalAsync(streamName, EtagVersion.Any, events);
}
public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion));

27
src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs

@ -8,33 +8,32 @@
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing
{
internal sealed class GetEventStoreSubscription : IEventSubscription
{
private readonly IEventStoreConnection eventStoreConnection;
private readonly IEventSubscriber eventSubscriber;
private readonly IEventStoreConnection connection;
private readonly IEventSubscriber subscriber;
private readonly EventStoreCatchUpSubscription subscription;
private readonly long? position;
public GetEventStoreSubscription(
IEventStoreConnection eventStoreConnection,
IEventSubscriber eventSubscriber,
ProjectionsManager projectionsManager,
IEventStoreConnection connection,
IEventSubscriber subscriber,
ProjectionClient projectionClient,
string prefix,
string position,
string streamFilter)
{
Guard.NotNull(eventSubscriber, nameof(eventSubscriber));
Guard.NotNull(subscriber, nameof(subscriber));
this.eventStoreConnection = eventStoreConnection;
this.eventSubscriber = eventSubscriber;
this.position = ProjectionHelper.ParsePositionOrNull(position);
this.connection = connection;
this.position = projectionClient.ParsePositionOrNull(position);
this.subscriber = subscriber;
var streamName = eventStoreConnection.CreateProjectionAsync(projectionsManager, prefix, streamFilter).Result;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
subscription = SubscribeToStream(streamName);
}
@ -50,12 +49,12 @@ namespace Squidex.Infrastructure.EventSourcing
{
var settings = CatchUpSubscriptionSettings.Default;
return eventStoreConnection.SubscribeToStreamFrom(streamName, position, settings,
return connection.SubscribeToStreamFrom(streamName, position, settings,
(s, e) =>
{
var storedEvent = Formatter.Read(e);
eventSubscriber.OnEventAsync(this, storedEvent).Wait();
subscriber.OnEventAsync(this, storedEvent).Wait();
}, null,
(s, reason, ex) =>
{
@ -64,7 +63,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}.");
eventSubscriber.OnErrorAsync(this, ex);
subscriber.OnErrorAsync(this, ex);
}
});
}

155
src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs

@ -0,0 +1,155 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections;
namespace Squidex.Infrastructure.EventSourcing
{
public sealed class ProjectionClient
{
private const string StreamByFilter = "by-{0}-{1}";
private const string StreamByProperty = "by-{0}-{1}-property";
private readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>();
private readonly IEventStoreConnection connection;
private readonly string prefix;
private readonly string projectionHost;
private ProjectionsManager projectionsManager;
public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost)
{
this.connection = connection;
this.prefix = prefix;
this.projectionHost = projectionHost;
}
private string CreateFilterStreamName(string filter)
{
return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), filter.Simplify());
}
private string CreatePropertyStreamName(string property)
{
return string.Format(CultureInfo.InvariantCulture, StreamByFilter, prefix.Simplify(), property.Simplify());
}
public async Task<string> CreateProjectionAsync(string property, object value)
{
var streamName = CreatePropertyStreamName(property);
if (projections.TryAdd(streamName, true))
{
var projectionConfig =
$@"fromAll()
.when({{
$any: function (s, e) {{
if (e.streamId.indexOf('{prefix}') === 0 && e.data.{property}) {{
linkTo('{streamName}-' + e.data.{property}, e);
}}
}}
}});";
try
{
var credentials = connection.Settings.DefaultUserCredentials;
await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials);
}
catch (Exception ex)
{
if (!ex.Is<ProjectionCommandConflictException>())
{
throw;
}
}
}
return streamName + "-" + value;
}
public async Task<string> CreateProjectionAsync(string streamFilter = null)
{
streamFilter = streamFilter ?? ".*";
var streamName = CreateFilterStreamName(streamFilter);
if (projections.TryAdd(streamName, true))
{
var projectionConfig =
$@"fromAll()
.when({{
$any: function (s, e) {{
if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{
linkTo('{streamName}', e);
}}
}}
}});";
try
{
var credentials = connection.Settings.DefaultUserCredentials;
await projectionsManager.CreateContinuousAsync($"{streamName}", projectionConfig, credentials);
}
catch (Exception ex)
{
if (!ex.Is<ProjectionCommandConflictException>())
{
throw;
}
}
}
return streamName;
}
public async Task ConnectAsync()
{
var addressParts = projectionHost.Split(':');
if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port))
{
port = 2113;
}
var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]);
var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port);
projectionsManager =
new ProjectionsManager(
connection.Settings.Log, endpoint,
connection.Settings.OperationTimeout);
try
{
await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials);
}
catch (Exception ex)
{
throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex);
}
}
public long? ParsePositionOrNull(string position)
{
return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null;
}
public long ParsePosition(string position)
{
return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0;
}
}
}

97
src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs

@ -1,97 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections;
namespace Squidex.Infrastructure.EventSourcing
{
public static class ProjectionHelper
{
private const string ProjectionName = "by-{0}-{1}";
private static readonly ConcurrentDictionary<string, bool> SubscriptionsCreated = new ConcurrentDictionary<string, bool>();
private static string ParseFilter(string prefix, string filter)
{
return string.Format(CultureInfo.InvariantCulture, ProjectionName, prefix.Simplify(), filter.Simplify());
}
public static async Task<string> CreateProjectionAsync(this IEventStoreConnection connection, ProjectionsManager projectionsManager, string prefix, string streamFilter = null)
{
streamFilter = streamFilter ?? ".*";
var streamName = ParseFilter(prefix, streamFilter);
if (SubscriptionsCreated.TryAdd(streamName, true))
{
var projectionConfig =
$@"fromAll()
.when({{
$any: function (s, e) {{
if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{
linkTo('{streamName}', e);
}}
}}
}});";
try
{
var credentials = connection.Settings.DefaultUserCredentials;
await projectionsManager.CreateContinuousAsync($"${streamName}", projectionConfig, credentials);
}
catch (Exception ex)
{
if (!ex.Is<ProjectionCommandConflictException>())
{
throw;
}
}
}
return streamName;
}
public static async Task<ProjectionsManager> GetProjectionsManagerAsync(this IEventStoreConnection connection, string projectionHost)
{
var addressParts = projectionHost.Split(':');
if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port))
{
port = 2113;
}
var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]);
var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port);
var projectionsManager =
new ProjectionsManager(
connection.Settings.Log, endpoint,
connection.Settings.OperationTimeout);
return projectionsManager;
}
public static long? ParsePositionOrNull(string position)
{
return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null;
}
public static long ParsePosition(string position)
{
return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0;
}
}
}

9
src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs

@ -7,6 +7,7 @@
using System;
using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.EventSourcing
@ -15,15 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing
{
[BsonElement]
[BsonRequired]
public Guid EventId { get; set; }
public JToken Payload { get; set; }
[BsonElement]
[BsonRequired]
public string Payload { get; set; }
[BsonElement]
[BsonRequired]
public string Metadata { get; set; }
public JToken Metadata { get; set; }
[BsonElement]
[BsonRequired]

228
src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs

@ -5,10 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
@ -16,16 +12,19 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.EventSourcing
{
public class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{
private const int MaxAttempts = 20;
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = Fields.Build(x => x.Timestamp);
private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = Fields.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = Fields.Build(x => x.EventStream);
private readonly IEventNotifier notifier;
public IMongoCollection<BsonDocument> RawCollection
{
get { return Database.GetCollection<BsonDocument>(CollectionName()); }
}
public MongoEventStore(IMongoDatabase database, IEventNotifier notifier)
: base(database)
{
@ -50,220 +49,5 @@ namespace Squidex.Infrastructure.EventSourcing
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Timestamp).Ascending(x => x.EventStream)),
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.EventStream).Descending(x => x.EventStreamOffset), new CreateIndexOptions { Unique = true }));
}
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
{
Guard.NotNull(subscriber, nameof(subscriber));
Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter));
return new PollingSubscription(this, notifier, subscriber, streamFilter, position);
}
public async Task<IReadOnlyList<StoredEvent>> GetEventsAsync(string streamName, long streamPosition = 0)
{
var commits =
await Collection.Find(
Filter.And(
Filter.Eq(EventStreamField, streamName),
Filter.Gte(EventStreamOffsetField, streamPosition - 1)))
.Sort(Sort.Ascending(TimestampField)).ToListAsync();
var result = new List<StoredEvent>();
foreach (var commit in commits)
{
var eventStreamOffset = (int)commit.EventStreamOffset;
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
foreach (var e in commit.Events)
{
eventStreamOffset++;
if (eventStreamOffset >= streamPosition)
{
var eventData = e.ToEventData();
var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
result.Add(new StoredEvent(eventToken, eventStreamOffset, eventData));
}
}
}
return result;
}
public async Task GetEventsAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken))
{
Guard.NotNull(callback, nameof(callback));
StreamPosition lastPosition = position;
var filter = CreateFilter(streamFilter, lastPosition);
await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit =>
{
var eventStreamOffset = (int)commit.EventStreamOffset;
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
foreach (var e in commit.Events)
{
eventStreamOffset++;
if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp)
{
var eventData = e.ToEventData();
var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
await callback(new StoredEvent(eventToken, eventStreamOffset, eventData));
commitOffset++;
}
}
}, cancellationToken);
}
public Task AppendEventsAsync(Guid commitId, string streamName, ICollection<EventData> events)
{
return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events);
}
public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion));
return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events);
}
private async Task AppendEventsInternalAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
Guard.NotNull(events, nameof(events));
if (events.Count == 0)
{
return;
}
var currentVersion = await GetEventStreamOffset(streamName);
if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion)
{
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events);
for (var attempt = 0; attempt < MaxAttempts; attempt++)
{
try
{
await Collection.InsertOneAsync(commit);
notifier.NotifyEventsStored(streamName);
return;
}
catch (MongoWriteException ex)
{
if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
currentVersion = await GetEventStreamOffset(streamName);
if (expectedVersion != EtagVersion.Any)
{
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
else if (attempt < MaxAttempts)
{
expectedVersion = currentVersion;
}
else
{
throw new TimeoutException("Could not acquire a free slot for the commit within the provided time.");
}
}
else
{
throw;
}
}
}
}
private async Task<long> GetEventStreamOffset(string streamName)
{
var document =
await Collection.Find(Filter.Eq(EventStreamField, streamName))
.Project<BsonDocument>(Projection
.Include(EventStreamOffsetField)
.Include(EventsCountField))
.Sort(Sort.Descending(EventStreamOffsetField)).Limit(1)
.FirstOrDefaultAsync();
if (document != null)
{
return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64();
}
return EtagVersion.Empty;
}
private static FilterDefinition<MongoEventCommit> CreateFilter(string streamFilter, StreamPosition streamPosition)
{
var filters = new List<FilterDefinition<MongoEventCommit>>();
if (streamPosition.IsEndOfCommit)
{
filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp));
}
else
{
filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp));
}
if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase))
{
if (streamFilter.Contains("^"))
{
filters.Add(Filter.Regex(EventStreamField, streamFilter));
}
else
{
filters.Add(Filter.Eq(EventStreamField, streamFilter));
}
}
return Filter.And(filters);
}
private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
var commitEvents = new MongoEvent[events.Count];
var i = 0;
foreach (var e in events)
{
var mongoEvent = new MongoEvent(e);
commitEvents[i++] = mongoEvent;
}
var mongoCommit = new MongoEventCommit
{
Id = commitId,
Events = commitEvents,
EventsCount = events.Count,
EventStream = streamName,
EventStreamOffset = expectedVersion,
Timestamp = EmptyTimestamp
};
return mongoCommit;
}
}
}

173
src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs

@ -0,0 +1,173 @@
// ==========================================================================
// 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.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.EventSourcing
{
public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{
public Task CreateIndexAsync(string property)
{
return Collection.Indexes.CreateOneAsync(Index.Ascending(CreateIndexPath(property)));
}
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
{
Guard.NotNull(subscriber, nameof(subscriber));
Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter));
return new PollingSubscription(this, notifier, subscriber, streamFilter, position);
}
public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0)
{
var commits =
await Collection.Find(
Filter.And(
Filter.Eq(EventStreamField, streamName),
Filter.Gte(EventStreamOffsetField, streamPosition - 1)))
.Sort(Sort.Ascending(TimestampField)).ToListAsync();
var result = new List<StoredEvent>();
foreach (var commit in commits)
{
var eventStreamOffset = (int)commit.EventStreamOffset;
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
foreach (var e in commit.Events)
{
eventStreamOffset++;
if (eventStreamOffset >= streamPosition)
{
var eventData = e.ToEventData();
var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
result.Add(new StoredEvent(eventToken, eventStreamOffset, eventData));
}
}
}
return result;
}
public Task QueryAsync(Func<StoredEvent, Task> callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken))
{
Guard.NotNull(callback, nameof(callback));
StreamPosition lastPosition = position;
var filter = CreateFilter(property, value, lastPosition);
return QueryAsync(callback, lastPosition, filter, ct);
}
public Task QueryAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken))
{
Guard.NotNull(callback, nameof(callback));
StreamPosition lastPosition = position;
var filter = CreateFilter(streamFilter, lastPosition);
return QueryAsync(callback, lastPosition, filter, ct);
}
private async Task QueryAsync(Func<StoredEvent, Task> callback, StreamPosition lastPosition, FilterDefinition<MongoEventCommit> filter, CancellationToken ct)
{
await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit =>
{
var eventStreamOffset = (int)commit.EventStreamOffset;
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
foreach (var e in commit.Events)
{
eventStreamOffset++;
if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp)
{
var eventData = e.ToEventData();
var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
await callback(new StoredEvent(eventToken, eventStreamOffset, eventData));
commitOffset++;
}
}
}, ct);
}
private static FilterDefinition<MongoEventCommit> CreateFilter(string property, object value, StreamPosition streamPosition)
{
var filters = new List<FilterDefinition<MongoEventCommit>>();
AddPositionFilter(streamPosition, filters);
AddPropertyFitler(property, value, filters);
return Filter.And(filters);
}
private static FilterDefinition<MongoEventCommit> CreateFilter(string streamFilter, StreamPosition streamPosition)
{
var filters = new List<FilterDefinition<MongoEventCommit>>();
AddPositionFilter(streamPosition, filters);
AddStreamFilter(streamFilter, filters);
return Filter.And(filters);
}
private static void AddPropertyFitler(string property, object value, List<FilterDefinition<MongoEventCommit>> filters)
{
filters.Add(Filter.Eq(CreateIndexPath(property), value));
}
private static void AddStreamFilter(string streamFilter, List<FilterDefinition<MongoEventCommit>> filters)
{
if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase))
{
if (streamFilter.Contains("^"))
{
filters.Add(Filter.Regex(EventStreamField, streamFilter));
}
else
{
filters.Add(Filter.Eq(EventStreamField, streamFilter));
}
}
}
private static void AddPositionFilter(StreamPosition streamPosition, List<FilterDefinition<MongoEventCommit>> filters)
{
if (streamPosition.IsEndOfCommit)
{
filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp));
}
else
{
filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp));
}
}
private static string CreateIndexPath(string property)
{
return $"Events.Payload.{property}";
}
}
}

135
src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs

@ -0,0 +1,135 @@
// ==========================================================================
// 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.Reactive.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace Squidex.Infrastructure.EventSourcing
{
public partial class MongoEventStore
{
private const int MaxWriteAttempts = 20;
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);
public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events)
{
return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events);
}
public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion));
return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events);
}
private async Task AppendEventsInternalAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
Guard.NotNull(events, nameof(events));
if (events.Count == 0)
{
return;
}
var currentVersion = await GetEventStreamOffset(streamName);
if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion)
{
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events);
for (var attempt = 0; attempt < MaxWriteAttempts; attempt++)
{
try
{
await Collection.InsertOneAsync(commit);
notifier.NotifyEventsStored(streamName);
return;
}
catch (MongoWriteException ex)
{
if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
currentVersion = await GetEventStreamOffset(streamName);
if (expectedVersion != EtagVersion.Any)
{
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
if (attempt < MaxWriteAttempts)
{
expectedVersion = currentVersion;
}
else
{
throw new TimeoutException("Could not acquire a free slot for the commit within the provided time.");
}
}
else
{
throw;
}
}
}
}
private async Task<long> GetEventStreamOffset(string streamName)
{
var document =
await Collection.Find(Filter.Eq(EventStreamField, streamName))
.Project<BsonDocument>(Projection
.Include(EventStreamOffsetField)
.Include(EventsCountField))
.Sort(Sort.Descending(EventStreamOffsetField)).Limit(1)
.FirstOrDefaultAsync();
if (document != null)
{
return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64();
}
return EtagVersion.Empty;
}
private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events)
{
var commitEvents = new MongoEvent[events.Count];
var i = 0;
foreach (var e in events)
{
var mongoEvent = new MongoEvent(e);
commitEvents[i++] = mongoEvent;
}
var mongoCommit = new MongoEventCommit
{
Id = commitId,
Events = commitEvents,
EventsCount = events.Count,
EventStream = streamName,
EventStreamOffset = expectedVersion,
Timestamp = EmptyTimestamp
};
return mongoCommit;
}
}
}

13
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs

@ -11,6 +11,7 @@ using System.Reflection;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.MongoDb
{
@ -31,6 +32,18 @@ namespace Squidex.Infrastructure.MongoDb
memberMap.SetSerializer((IBsonSerializer)bsonSerializer);
}
else if (memberMap.MemberType == typeof(JToken))
{
memberMap.SetSerializer(JTokenSerializer<JToken>.Instance);
}
else if (memberMap.MemberType == typeof(JObject))
{
memberMap.SetSerializer(JTokenSerializer<JObject>.Instance);
}
else if (memberMap.MemberType == typeof(JValue))
{
memberMap.SetSerializer(JTokenSerializer<JValue>.Instance);
}
});
ConventionRegistry.Register("json", pack, t => true);

32
src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.MongoDb
{
public sealed class JTokenSerializer<T> : ClassSerializerBase<T> where T : JToken
{
public static readonly JTokenSerializer<T> Instance = new JTokenSerializer<T>();
protected override T DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var jsonReader = new BsonJsonReader(context.Reader);
return (T)JToken.ReadFrom(jsonReader);
}
protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, T value)
{
var jsonWriter = new BsonJsonWriter(context.Writer);
value.WriteTo(jsonWriter);
}
}
}

39
src/Squidex.Infrastructure/EventSourcing/JsonEventDataFormatter.cs → src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs

@ -7,38 +7,37 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.EventSourcing
{
public class JsonEventDataFormatter : IEventDataFormatter
public class DefaultEventDataFormatter : IEventDataFormatter
{
private readonly JsonSerializerSettings serializerSettings;
private readonly JsonSerializer serializer;
private readonly TypeNameRegistry typeNameRegistry;
public JsonEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializerSettings serializerSettings = null)
public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializer serializer = null)
{
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.typeNameRegistry = typeNameRegistry;
this.serializerSettings = serializerSettings ?? new JsonSerializerSettings();
this.serializer = serializer ?? JsonSerializer.CreateDefault();
}
public Envelope<IEvent> Parse(EventData eventData, bool migrate = true)
{
var headers = ReadJson<EnvelopeHeaders>(eventData.Metadata);
var eventType = typeNameRegistry.GetType(eventData.Type);
var eventPayload = ReadJson<IEvent>(eventData.Payload, eventType);
if (migrate && eventPayload is IMigratedEvent migratedEvent)
var headers = eventData.Metadata.ToObject<EnvelopeHeaders>();
var content = eventData.Metadata.ToObject(eventType, serializer) as IEvent;
if (migrate && content is IMigratedEvent migratedEvent)
{
eventPayload = migratedEvent.Migrate();
content = migratedEvent.Migrate();
}
var envelope = new Envelope<IEvent>(eventPayload, headers);
envelope.SetEventId(eventData.EventId);
var envelope = new Envelope<IEvent>(content, headers);
return envelope;
}
@ -56,20 +55,10 @@ namespace Squidex.Infrastructure.EventSourcing
envelope.SetCommitId(commitId);
var headers = WriteJson(envelope.Headers);
var content = WriteJson(envelope.Payload);
var headers = JToken.FromObject(envelope.Headers, serializer);
var content = JToken.FromObject(envelope.Payload, serializer);
return new EventData { EventId = envelope.Headers.EventId(), Type = eventType, Payload = content, Metadata = headers };
}
private T ReadJson<T>(string data, Type type = null)
{
return (T)JsonConvert.DeserializeObject(data, type ?? typeof(T), serializerSettings);
}
private string WriteJson(object value)
{
return JsonConvert.SerializeObject(value, serializerSettings);
return new EventData { Type = eventType, Payload = content, Metadata = headers };
}
}
}

8
src/Squidex.Infrastructure/EventSourcing/EventData.cs

@ -5,17 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.EventSourcing
{
public class EventData
{
public Guid EventId { get; set; }
public JToken Payload { get; set; }
public string Payload { get; set; }
public string Metadata { get; set; }
public JToken Metadata { get; set; }
public string Type { get; set; }
}

12
src/Squidex.Infrastructure/EventSourcing/IEventStore.cs

@ -14,13 +14,17 @@ namespace Squidex.Infrastructure.EventSourcing
{
public interface IEventStore
{
Task<IReadOnlyList<StoredEvent>> GetEventsAsync(string streamName, long streamPosition = 0);
Task CreateIndexAsync(string property);
Task GetEventsAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken cancellationToken = default(CancellationToken));
Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0);
Task AppendEventsAsync(Guid commitId, string streamName, ICollection<EventData> events);
Task QueryAsync(Func<StoredEvent, Task> callback, string streamFilter = null, string position = null, CancellationToken ct = default(CancellationToken));
Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events);
Task QueryAsync(Func<StoredEvent, Task> callback, string property, object value, string position = null, CancellationToken ct = default(CancellationToken));
Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events);
Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events);
IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null);
}

2
src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs

@ -46,7 +46,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
try
{
await eventStore.GetEventsAsync(async storedEvent =>
await eventStore.QueryAsync(async storedEvent =>
{
await eventSubscriber.OnEventAsync(this, storedEvent);

26
src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs

@ -9,33 +9,21 @@ namespace Squidex.Infrastructure.EventSourcing
{
public sealed class StoredEvent
{
private readonly string eventPosition;
private readonly long eventStreamNumber;
private readonly EventData data;
public string EventPosition { get; }
public string EventPosition
{
get { return eventPosition; }
}
public long EventStreamNumber
{
get { return eventStreamNumber; }
}
public long EventStreamNumber { get; }
public EventData Data
{
get { return data; }
}
public EventData Data { get; }
public StoredEvent(string eventPosition, long eventStreamNumber, EventData data)
{
Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition));
Guard.NotNull(data, nameof(data));
this.data = data;
this.eventPosition = eventPosition;
this.eventStreamNumber = eventStreamNumber;
Data = data;
EventPosition = eventPosition;
EventStreamNumber = eventStreamNumber;
}
}
}

4
src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs

@ -101,7 +101,7 @@ namespace Squidex.Infrastructure.States
{
if (UseEventSourcing())
{
var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1);
var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1);
foreach (var @event in events)
{
@ -160,7 +160,7 @@ namespace Squidex.Infrastructure.States
try
{
await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData);
await eventStore.AppendAsync(commitId, GetStreamName(), expectedVersion, eventData);
}
catch (WrongEventVersionException ex)
{

2
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -89,7 +89,7 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ImageSharpAssetThumbnailGenerator>()
.As<IAssetThumbnailGenerator>();
services.AddSingletonAs<JsonEventDataFormatter>()
services.AddSingletonAs<DefaultEventDataFormatter>()
.As<IEventDataFormatter>();
services.AddSingletonAs<Migrator>()

4
tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs

@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.EventSourcing
private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry();
private readonly JsonEventDataFormatter sut;
private readonly DefaultEventDataFormatter sut;
public EventDataFormatterTests()
{
@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing
typeNameRegistry.Map(typeof(MyEvent), "Event");
typeNameRegistry.Map(typeof(MyOldEvent), "OldEvent");
sut = new JsonEventDataFormatter(typeNameRegistry, serializerSettings);
sut = new DefaultEventDataFormatter(typeNameRegistry, JsonSerializer.Create(serializerSettings));
}
[Fact]

12
tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut);
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}
@ -36,7 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var ex = new InvalidOperationException();
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var ex = new OperationCanceledException();
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var ex = new AggregateException(new OperationCanceledException());
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -88,7 +88,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut);
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}
@ -101,7 +101,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut);
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
A.CallTo(() => eventStore.QueryAsync(A<Func<StoredEvent, Task>>.Ignored, "^my-stream", position, A<CancellationToken>.Ignored))
.MustHaveHappened(Repeated.Exactly.Twice);
}

14
tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

@ -118,7 +118,7 @@ namespace Squidex.Infrastructure.States
await sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key);
A.CallTo(() => eventStore.GetEventsAsync(key, 3))
A.CallTo(() => eventStore.QueryAsync(key, 3))
.MustHaveHappened();
}
@ -199,9 +199,9 @@ namespace Squidex.Infrastructure.States
await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent());
await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent());
A.CallTo(() => eventStore.AppendEventsAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.MustHaveHappened();
A.CallTo(() => eventStore.AppendEventsAsync(A<Guid>.Ignored, key, 4, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 4, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.MustHaveHappened();
}
@ -212,7 +212,7 @@ namespace Squidex.Infrastructure.States
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
A.CallTo(() => eventStore.AppendEventsAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.Throws(new WrongEventVersionException(1, 1));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()));
@ -221,7 +221,7 @@ namespace Squidex.Infrastructure.States
[Fact]
public async Task Should_not_remove_from_cache_when_write_failed()
{
A.CallTo(() => eventStore.AppendEventsAsync(A<Guid>.Ignored, A<string>.Ignored, A<long>.Ignored, A<ICollection<EventData>>.Ignored))
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, A<string>.Ignored, A<long>.Ignored, A<ICollection<EventData>>.Ignored))
.Throws(new InvalidOperationException());
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
@ -251,7 +251,7 @@ namespace Squidex.Infrastructure.States
Assert.Same(retrievedStates[0], retrievedState);
}
A.CallTo(() => eventStore.GetEventsAsync(key, 0))
A.CallTo(() => eventStore.QueryAsync(key, 0))
.MustHaveHappened(Repeated.Exactly.Once);
}
@ -284,7 +284,7 @@ namespace Squidex.Infrastructure.States
i++;
}
A.CallTo(() => eventStore.GetEventsAsync(key, readPosition))
A.CallTo(() => eventStore.QueryAsync(key, readPosition))
.Returns(eventsStored);
}
}

53
tools/Migrate_01/Migration00_ConvertEventStore.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01
{
public sealed class Migration00_ConvertEventStore : IMigration
{
private readonly IEventStore eventStore;
public int FromVersion { get; } = 0;
public int ToVersion { get; } = 1;
public Migration00_ConvertEventStore(IEventStore eventStore)
{
this.eventStore = eventStore;
}
public async Task UpdateAsync(IEnumerable<IMigration> previousMigrations)
{
if (eventStore is MongoEventStore mongoEventStore)
{
var collection = mongoEventStore.RawCollection;
var filter = Builders<BsonDocument>.Filter;
await collection.Find(new BsonDocument()).ForEachAsync(async commit =>
{
foreach (BsonDocument @event in commit["Events"].AsBsonArray)
{
@event.Remove("EventId");
@event["Payload"] = BsonDocument.Parse(@event["Payload"].AsString);
@event["Metadata"] = BsonDocument.Parse(@event["Metadata"].AsString);
}
await collection.ReplaceOneAsync(filter.Eq("_id", commit["_id"].AsString), commit);
});
}
}
}
}

12
tools/Migrate_01/Rebuilder.cs

@ -53,7 +53,7 @@ namespace Migrate_01
var handledIds = new HashSet<Guid>();
return eventStore.GetEventsAsync(async storedEvent =>
return eventStore.QueryAsync(async storedEvent =>
{
var @event = ParseKnownEvent(storedEvent);
@ -68,7 +68,7 @@ namespace Migrate_01
await asset.WriteSnapshotAsync();
}
}
}, filter, cancellationToken: CancellationToken.None);
}, filter, ct: CancellationToken.None);
}
public Task RebuildConfigAsync()
@ -77,7 +77,7 @@ namespace Migrate_01
var handledIds = new HashSet<Guid>();
return eventStore.GetEventsAsync(async storedEvent =>
return eventStore.QueryAsync(async storedEvent =>
{
var @event = ParseKnownEvent(storedEvent);
@ -102,7 +102,7 @@ namespace Migrate_01
await app.WriteSnapshotAsync();
}
}
}, filter, cancellationToken: CancellationToken.None);
}, filter, ct: CancellationToken.None);
}
public async Task RebuildContentAsync()
@ -113,7 +113,7 @@ namespace Migrate_01
await snapshotContentStore.ClearAsync();
await eventStore.GetEventsAsync(async storedEvent =>
await eventStore.QueryAsync(async storedEvent =>
{
var @event = ParseKnownEvent(storedEvent);
@ -139,7 +139,7 @@ namespace Migrate_01
// Schema has been deleted.
}
}
}, filter, cancellationToken: CancellationToken.None);
}, filter, ct: CancellationToken.None);
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)

Loading…
Cancel
Save