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 }); 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) 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 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); 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(); timer.SkipCurrentDelay();
} }
private async Task QueryAsync(CancellationToken cancellationToken) private async Task QueryAsync(CancellationToken ct)
{ {
try try
{ {
var now = clock.GetCurrentInstant(); var now = clock.GetCurrentInstant();
await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken); await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct);
} }
catch (Exception ex) catch (Exception ex)
{ {

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Text; using System.Text;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStoreData = EventStore.ClientAPI.EventData; using EventStoreData = EventStore.ClientAPI.EventData;
@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing
var body = Encoding.UTF8.GetString(@event.Data); var body = Encoding.UTF8.GetString(@event.Data);
var meta = Encoding.UTF8.GetString(@event.Metadata); 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( return new StoredEvent(
resolvedEvent.OriginalEventNumber.ToString(), resolvedEvent.OriginalEventNumber.ToString(),
@ -30,13 +31,10 @@ namespace Squidex.Infrastructure.EventSourcing
public static EventStoreData Write(EventData eventData) public static EventStoreData Write(EventData eventData)
{ {
var body = Encoding.UTF8.GetBytes(eventData.Payload); var body = Encoding.UTF8.GetBytes(eventData.Payload.ToString());
var meta = Encoding.UTF8.GetBytes(eventData.Metadata); var meta = Encoding.UTF8.GetBytes(eventData.Metadata.ToString());
return new EventStoreData( return new EventStoreData(Guid.NewGuid(), eventData.Type, true, body, meta);
eventData.EventId,
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;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStore.ClientAPI.Projections;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
{ {
@ -20,18 +19,18 @@ namespace Squidex.Infrastructure.EventSourcing
private const int WritePageSize = 500; private const int WritePageSize = 500;
private const int ReadPageSize = 500; private const int ReadPageSize = 500;
private readonly IEventStoreConnection connection; private readonly IEventStoreConnection connection;
private readonly string projectionHost;
private readonly string prefix; private readonly string prefix;
private ProjectionsManager projectionsManager; private ProjectionClient projectionClient;
public GetEventStore(IEventStoreConnection connection, string prefix, string projectionHost) public GetEventStore(IEventStoreConnection connection, string prefix, string projectionHost)
{ {
Guard.NotNull(connection, nameof(connection)); Guard.NotNull(connection, nameof(connection));
this.connection = connection; this.connection = connection;
this.projectionHost = projectionHost;
this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex"); this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex");
projectionClient = new ProjectionClient(connection, prefix, projectionHost);
} }
public void Initialize() public void Initialize()
@ -45,50 +44,43 @@ namespace Squidex.Infrastructure.EventSourcing
throw new ConfigurationException("Cannot connect to event store.", ex); throw new ConfigurationException("Cannot connect to event store.", ex);
} }
try projectionClient.ConnectAsync().Wait();
{
projectionsManager = connection.GetProjectionsManagerAsync(projectionHost).Result;
projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials).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; await QueryAsync(callback, streamName, sliceStart, ct);
do }
{
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
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 sliceStart = projectionClient.ParsePosition(position);
{
var storedEvent = Formatter.Read(resolved);
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>(); var result = new List<StoredEvent>();
@ -97,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing
StreamEventsSlice currentSlice; StreamEventsSlice currentSlice;
do do
{ {
currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, false); currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
if (currentSlice.Status == SliceReadStatus.Success) if (currentSlice.Status == SliceReadStatus.Success)
{ {
@ -116,12 +108,12 @@ namespace Squidex.Infrastructure.EventSourcing
return result; 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); 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)); Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion));

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

@ -8,33 +8,32 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions; using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
{ {
internal sealed class GetEventStoreSubscription : IEventSubscription internal sealed class GetEventStoreSubscription : IEventSubscription
{ {
private readonly IEventStoreConnection eventStoreConnection; private readonly IEventStoreConnection connection;
private readonly IEventSubscriber eventSubscriber; private readonly IEventSubscriber subscriber;
private readonly EventStoreCatchUpSubscription subscription; private readonly EventStoreCatchUpSubscription subscription;
private readonly long? position; private readonly long? position;
public GetEventStoreSubscription( public GetEventStoreSubscription(
IEventStoreConnection eventStoreConnection, IEventStoreConnection connection,
IEventSubscriber eventSubscriber, IEventSubscriber subscriber,
ProjectionsManager projectionsManager, ProjectionClient projectionClient,
string prefix, string prefix,
string position, string position,
string streamFilter) string streamFilter)
{ {
Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); Guard.NotNull(subscriber, nameof(subscriber));
this.eventStoreConnection = eventStoreConnection; this.connection = connection;
this.eventSubscriber = eventSubscriber; this.position = projectionClient.ParsePositionOrNull(position);
this.position = ProjectionHelper.ParsePositionOrNull(position); this.subscriber = subscriber;
var streamName = eventStoreConnection.CreateProjectionAsync(projectionsManager, prefix, streamFilter).Result; var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
subscription = SubscribeToStream(streamName); subscription = SubscribeToStream(streamName);
} }
@ -50,12 +49,12 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var settings = CatchUpSubscriptionSettings.Default; var settings = CatchUpSubscriptionSettings.Default;
return eventStoreConnection.SubscribeToStreamFrom(streamName, position, settings, return connection.SubscribeToStreamFrom(streamName, position, settings,
(s, e) => (s, e) =>
{ {
var storedEvent = Formatter.Read(e); var storedEvent = Formatter.Read(e);
eventSubscriber.OnEventAsync(this, storedEvent).Wait(); subscriber.OnEventAsync(this, storedEvent).Wait();
}, null, }, null,
(s, reason, ex) => (s, reason, ex) =>
{ {
@ -64,7 +63,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); 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 System;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
@ -15,15 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
[BsonElement] [BsonElement]
[BsonRequired] [BsonRequired]
public Guid EventId { get; set; } public JToken Payload { get; set; }
[BsonElement] [BsonElement]
[BsonRequired] [BsonRequired]
public string Payload { get; set; } public JToken Metadata { get; set; }
[BsonElement]
[BsonRequired]
public string Metadata { get; set; }
[BsonElement] [BsonElement]
[BsonRequired] [BsonRequired]

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

@ -5,10 +5,6 @@
// All rights reserved. Licensed under the MIT license. // 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 System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -16,16 +12,19 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.EventSourcing 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, 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> EventsCountField = Fields.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); 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 static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = Fields.Build(x => x.EventStream);
private readonly IEventNotifier notifier; private readonly IEventNotifier notifier;
public IMongoCollection<BsonDocument> RawCollection
{
get { return Database.GetCollection<BsonDocument>(CollectionName()); }
}
public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) public MongoEventStore(IMongoDatabase database, IEventNotifier notifier)
: base(database) : 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.Timestamp).Ascending(x => x.EventStream)),
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.EventStream).Descending(x => x.EventStreamOffset), new CreateIndexOptions { Unique = true })); 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;
using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Conventions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
@ -31,6 +32,18 @@ namespace Squidex.Infrastructure.MongoDb
memberMap.SetSerializer((IBsonSerializer)bsonSerializer); 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); 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 System;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
{ {
public class JsonEventDataFormatter : IEventDataFormatter public class DefaultEventDataFormatter : IEventDataFormatter
{ {
private readonly JsonSerializerSettings serializerSettings; private readonly JsonSerializer serializer;
private readonly TypeNameRegistry typeNameRegistry; private readonly TypeNameRegistry typeNameRegistry;
public JsonEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializerSettings serializerSettings = null) public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, JsonSerializer serializer = null)
{ {
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.typeNameRegistry = typeNameRegistry; this.typeNameRegistry = typeNameRegistry;
this.serializerSettings = serializerSettings ?? new JsonSerializerSettings(); this.serializer = serializer ?? JsonSerializer.CreateDefault();
} }
public Envelope<IEvent> Parse(EventData eventData, bool migrate = true) public Envelope<IEvent> Parse(EventData eventData, bool migrate = true)
{ {
var headers = ReadJson<EnvelopeHeaders>(eventData.Metadata);
var eventType = typeNameRegistry.GetType(eventData.Type); 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); var envelope = new Envelope<IEvent>(content, headers);
envelope.SetEventId(eventData.EventId);
return envelope; return envelope;
} }
@ -56,20 +55,10 @@ namespace Squidex.Infrastructure.EventSourcing
envelope.SetCommitId(commitId); envelope.SetCommitId(commitId);
var headers = WriteJson(envelope.Headers); var headers = JToken.FromObject(envelope.Headers, serializer);
var content = WriteJson(envelope.Payload); var content = JToken.FromObject(envelope.Payload, serializer);
return new EventData { EventId = envelope.Headers.EventId(), Type = eventType, Payload = content, Metadata = headers }; return new EventData { 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);
} }
} }
} }

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

@ -5,17 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using Newtonsoft.Json.Linq;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
{ {
public class EventData public class EventData
{ {
public Guid EventId { get; set; } public JToken Payload { get; set; }
public string Payload { get; set; } public JToken Metadata { get; set; }
public string Metadata { get; set; }
public string Type { 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 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); 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 try
{ {
await eventStore.GetEventsAsync(async storedEvent => await eventStore.QueryAsync(async storedEvent =>
{ {
await eventSubscriber.OnEventAsync(this, 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 public sealed class StoredEvent
{ {
private readonly string eventPosition; public string EventPosition { get; }
private readonly long eventStreamNumber;
private readonly EventData data;
public string EventPosition public long EventStreamNumber { get; }
{
get { return eventPosition; }
}
public long EventStreamNumber
{
get { return eventStreamNumber; }
}
public EventData Data public EventData Data { get; }
{
get { return data; }
}
public StoredEvent(string eventPosition, long eventStreamNumber, EventData data) public StoredEvent(string eventPosition, long eventStreamNumber, EventData data)
{ {
Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition)); Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition));
Guard.NotNull(data, nameof(data)); Guard.NotNull(data, nameof(data));
this.data = data; Data = data;
this.eventPosition = eventPosition;
this.eventStreamNumber = eventStreamNumber; EventPosition = eventPosition;
EventStreamNumber = eventStreamNumber;
} }
} }
} }

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

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

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

@ -89,7 +89,7 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ImageSharpAssetThumbnailGenerator>() services.AddSingletonAs<ImageSharpAssetThumbnailGenerator>()
.As<IAssetThumbnailGenerator>(); .As<IAssetThumbnailGenerator>();
services.AddSingletonAs<JsonEventDataFormatter>() services.AddSingletonAs<DefaultEventDataFormatter>()
.As<IEventDataFormatter>(); .As<IEventDataFormatter>();
services.AddSingletonAs<Migrator>() 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 JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry();
private readonly JsonEventDataFormatter sut; private readonly DefaultEventDataFormatter sut;
public EventDataFormatterTests() public EventDataFormatterTests()
{ {
@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing
typeNameRegistry.Map(typeof(MyEvent), "Event"); typeNameRegistry.Map(typeof(MyEvent), "Event");
typeNameRegistry.Map(typeof(MyOldEvent), "OldEvent"); typeNameRegistry.Map(typeof(MyOldEvent), "OldEvent");
sut = new JsonEventDataFormatter(typeNameRegistry, serializerSettings); sut = new DefaultEventDataFormatter(typeNameRegistry, JsonSerializer.Create(serializerSettings));
} }
[Fact] [Fact]

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

@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut); 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); .MustHaveHappened(Repeated.Exactly.Once);
} }
@ -36,7 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var ex = new InvalidOperationException(); 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); .Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var ex = new OperationCanceledException(); 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); .Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var ex = new AggregateException(new OperationCanceledException()); 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); .Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
@ -88,7 +88,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut); 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); .MustHaveHappened(Repeated.Exactly.Once);
} }
@ -101,7 +101,7 @@ namespace Squidex.Infrastructure.EventSourcing
await WaitAndStopAsync(sut); 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); .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); await sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key);
A.CallTo(() => eventStore.GetEventsAsync(key, 3)) A.CallTo(() => eventStore.QueryAsync(key, 3))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -199,9 +199,9 @@ namespace Squidex.Infrastructure.States
await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()); await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent());
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(); .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(); .MustHaveHappened();
} }
@ -212,7 +212,7 @@ namespace Squidex.Infrastructure.States
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key); 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)); .Throws(new WrongEventVersionException(1, 1));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent())); await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()));
@ -221,7 +221,7 @@ namespace Squidex.Infrastructure.States
[Fact] [Fact]
public async Task Should_not_remove_from_cache_when_write_failed() 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()); .Throws(new InvalidOperationException());
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key); var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
@ -251,7 +251,7 @@ namespace Squidex.Infrastructure.States
Assert.Same(retrievedStates[0], retrievedState); Assert.Same(retrievedStates[0], retrievedState);
} }
A.CallTo(() => eventStore.GetEventsAsync(key, 0)) A.CallTo(() => eventStore.QueryAsync(key, 0))
.MustHaveHappened(Repeated.Exactly.Once); .MustHaveHappened(Repeated.Exactly.Once);
} }
@ -284,7 +284,7 @@ namespace Squidex.Infrastructure.States
i++; i++;
} }
A.CallTo(() => eventStore.GetEventsAsync(key, readPosition)) A.CallTo(() => eventStore.QueryAsync(key, readPosition))
.Returns(eventsStored); .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>(); var handledIds = new HashSet<Guid>();
return eventStore.GetEventsAsync(async storedEvent => return eventStore.QueryAsync(async storedEvent =>
{ {
var @event = ParseKnownEvent(storedEvent); var @event = ParseKnownEvent(storedEvent);
@ -68,7 +68,7 @@ namespace Migrate_01
await asset.WriteSnapshotAsync(); await asset.WriteSnapshotAsync();
} }
} }
}, filter, cancellationToken: CancellationToken.None); }, filter, ct: CancellationToken.None);
} }
public Task RebuildConfigAsync() public Task RebuildConfigAsync()
@ -77,7 +77,7 @@ namespace Migrate_01
var handledIds = new HashSet<Guid>(); var handledIds = new HashSet<Guid>();
return eventStore.GetEventsAsync(async storedEvent => return eventStore.QueryAsync(async storedEvent =>
{ {
var @event = ParseKnownEvent(storedEvent); var @event = ParseKnownEvent(storedEvent);
@ -102,7 +102,7 @@ namespace Migrate_01
await app.WriteSnapshotAsync(); await app.WriteSnapshotAsync();
} }
} }
}, filter, cancellationToken: CancellationToken.None); }, filter, ct: CancellationToken.None);
} }
public async Task RebuildContentAsync() public async Task RebuildContentAsync()
@ -113,7 +113,7 @@ namespace Migrate_01
await snapshotContentStore.ClearAsync(); await snapshotContentStore.ClearAsync();
await eventStore.GetEventsAsync(async storedEvent => await eventStore.QueryAsync(async storedEvent =>
{ {
var @event = ParseKnownEvent(storedEvent); var @event = ParseKnownEvent(storedEvent);
@ -139,7 +139,7 @@ namespace Migrate_01
// Schema has been deleted. // Schema has been deleted.
} }
} }
}, filter, cancellationToken: CancellationToken.None); }, filter, ct: CancellationToken.None);
} }
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent) private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)

Loading…
Cancel
Save