From b1d79a95d0a3c474540d51d91830dea7e59a6469 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 3 Jan 2018 17:46:34 +0100 Subject: [PATCH 1/4] Fix --- .../Events/GetEventStoreSubscription.cs | 152 ------------------ .../EventSourcing/{Events => }/Formatter.cs | 0 .../{Events => }/GetEventStore.cs | 0 .../GetEventStoreSubscription.cs | 78 +++++++++ .../EventSourcing/ProjectionHelper.cs | 88 ++++++++++ 5 files changed, 166 insertions(+), 152 deletions(-) delete mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStoreSubscription.cs rename src/Squidex.Infrastructure.GetEventStore/EventSourcing/{Events => }/Formatter.cs (100%) rename src/Squidex.Infrastructure.GetEventStore/EventSourcing/{Events => }/GetEventStore.cs (100%) create mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs create mode 100644 src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStoreSubscription.cs deleted file mode 100644 index 98c0f1067..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStoreSubscription.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ========================================================================== -// GetEventStoreSubscription.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -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; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class GetEventStoreSubscription : IEventSubscription - { - private const string ProjectionName = "by-{0}-{1}"; - private static readonly ConcurrentDictionary SubscriptionsCreated = new ConcurrentDictionary(); - private readonly IEventStoreConnection eventStoreConnection; - private readonly IEventSubscriber eventSubscriber; - private readonly string prefix; - private readonly string streamFilter; - private readonly string projectionHost; - private readonly EventStoreCatchUpSubscription subscription; - private readonly long? position; - - public GetEventStoreSubscription( - IEventStoreConnection eventStoreConnection, - IEventSubscriber eventSubscriber, - string projectionHost, - string prefix, - string position, - string streamFilter) - { - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); - Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); - - this.eventStoreConnection = eventStoreConnection; - this.eventSubscriber = eventSubscriber; - this.position = ParsePosition(position); - this.prefix = prefix; - this.projectionHost = projectionHost; - this.streamFilter = streamFilter; - - var streamName = ParseFilter(prefix, streamFilter); - - InitializeAsync(streamName).Wait(); - - subscription = SubscribeToStream(streamName); - } - - public Task StopAsync() - { - subscription.Stop(); - - return TaskHelper.Done; - } - - private EventStoreCatchUpSubscription SubscribeToStream(string streamName) - { - var settings = CatchUpSubscriptionSettings.Default; - - return eventStoreConnection.SubscribeToStreamFrom(streamName, position, settings, - (s, e) => - { - var storedEvent = Formatter.Read(e); - - eventSubscriber.OnEventAsync(this, storedEvent).Wait(); - }, null, - (s, reason, ex) => - { - if (reason != SubscriptionDropReason.ConnectionClosed && - reason != SubscriptionDropReason.UserInitiated) - { - ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); - - eventSubscriber.OnErrorAsync(this, ex); - } - }); - } - - private async Task InitializeAsync(string streamName) - { - if (SubscriptionsCreated.TryAdd(streamName, true)) - { - var projectsManager = await ConnectToProjections(); - - 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 = eventStoreConnection.Settings.DefaultUserCredentials; - - await projectsManager.CreateContinuousAsync($"${streamName}", projectionConfig, credentials); - } - catch (Exception ex) - { - if (!ex.Is()) - { - throw; - } - } - } - } - - private async Task ConnectToProjections() - { - 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( - eventStoreConnection.Settings.Log, endpoint, - eventStoreConnection.Settings.OperationTimeout); - - return projectionsManager; - } - - private static string ParseFilter(string prefix, string filter) - { - return string.Format(CultureInfo.InvariantCulture, ProjectionName, prefix.Simplify(), filter.Simplify()); - } - - private static long? ParsePosition(string position) - { - return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs similarity index 100% rename from src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/Formatter.cs rename to src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs similarity index 100% rename from src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs rename to src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs new file mode 100644 index 000000000..a7dfdd025 --- /dev/null +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// GetEventStoreSubscription.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class GetEventStoreSubscription : IEventSubscription + { + private readonly IEventStoreConnection eventStoreConnection; + private readonly IEventSubscriber eventSubscriber; + private readonly EventStoreCatchUpSubscription subscription; + private readonly long? position; + + public GetEventStoreSubscription( + IEventStoreConnection eventStoreConnection, + IEventSubscriber eventSubscriber, + string projectionHost, + string prefix, + string position, + string streamFilter) + { + Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); + Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); + + this.eventStoreConnection = eventStoreConnection; + this.eventSubscriber = eventSubscriber; + this.position = ParsePosition(position); + + var streamName = eventStoreConnection.CreateProjectionAsync(projectionHost, prefix, streamFilter).Result; + + subscription = SubscribeToStream(streamName); + } + + public Task StopAsync() + { + subscription.Stop(); + + return TaskHelper.Done; + } + + private EventStoreCatchUpSubscription SubscribeToStream(string streamName) + { + var settings = CatchUpSubscriptionSettings.Default; + + return eventStoreConnection.SubscribeToStreamFrom(streamName, position, settings, + (s, e) => + { + var storedEvent = Formatter.Read(e); + + eventSubscriber.OnEventAsync(this, storedEvent).Wait(); + }, null, + (s, reason, ex) => + { + if (reason != SubscriptionDropReason.ConnectionClosed && + reason != SubscriptionDropReason.UserInitiated) + { + ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); + + eventSubscriber.OnErrorAsync(this, ex); + } + }); + } + + private static long? ParsePosition(string position) + { + return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; + } + } +} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs new file mode 100644 index 000000000..7ad93b8c7 --- /dev/null +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// ProjectionHelper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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 SubscriptionsCreated = new ConcurrentDictionary(); + + private static string ParseFilter(string prefix, string filter) + { + return string.Format(CultureInfo.InvariantCulture, ProjectionName, prefix.Simplify(), filter.Simplify()); + } + + public static async Task CreateProjectionAsync(this IEventStoreConnection connection, string projectionHost, string prefix, string streamFilter = null) + { + var streamName = ParseFilter(prefix, streamFilter); + + if (SubscriptionsCreated.TryAdd(streamName, true)) + { + var projectsManager = await ConnectToProjections(connection, projectionHost); + + 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 projectsManager.CreateContinuousAsync($"${streamName}", projectionConfig, credentials); + } + catch (Exception ex) + { + if (!ex.Is()) + { + throw; + } + } + } + + return streamName; + } + + private static async Task ConnectToProjections(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; + } + } +} From 397de74a9836c94517ada794b51f9ff012fa7af2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 3 Jan 2018 17:50:26 +0100 Subject: [PATCH 2/4] Fix finished --- .../EventSourcing/GetEventStore.cs | 25 +++++++++++++++++-- .../GetEventStoreSubscription.cs | 7 +----- .../EventSourcing/ProjectionHelper.cs | 5 ++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index cd6659044..12cbd708b 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -54,9 +54,30 @@ namespace Squidex.Infrastructure.EventSourcing return new GetEventStoreSubscription(connection, subscriber, projectionHost, prefix, position, streamFilter); } - public Task GetEventsAsync(Func callback, CancellationToken cancellationToken, string streamFilter = null, string position = null) + public async Task GetEventsAsync(Func callback, CancellationToken cancellationToken, string streamFilter = null, string position = null) { - throw new NotSupportedException(); + var streamName = await connection.CreateProjectionAsync(projectionHost, prefix, streamFilter); + + var sliceStart = ProjectionHelper.ParsePosition(position) ?? -1; + + StreamEventsSlice currentSlice; + do + { + currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, false); + + if (currentSlice.Status == SliceReadStatus.Success) + { + sliceStart = currentSlice.NextEventNumber; + + foreach (var resolved in currentSlice.Events) + { + var storedEvent = Formatter.Read(resolved); + + await callback(storedEvent); + } + } + } + while (!currentSlice.IsEndOfStream && !cancellationToken.IsCancellationRequested); } public async Task> GetEventsAsync(string streamName, long streamPosition = 0) diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index a7dfdd025..b2fc0c24f 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.EventSourcing this.eventStoreConnection = eventStoreConnection; this.eventSubscriber = eventSubscriber; - this.position = ParsePosition(position); + this.position = ProjectionHelper.ParsePosition(position); var streamName = eventStoreConnection.CreateProjectionAsync(projectionHost, prefix, streamFilter).Result; @@ -69,10 +69,5 @@ namespace Squidex.Infrastructure.EventSourcing } }); } - - private static long? ParsePosition(string position) - { - return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; - } } } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs index 7ad93b8c7..3c35b5aac 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs @@ -84,5 +84,10 @@ namespace Squidex.Infrastructure.EventSourcing return projectionsManager; } + + public static long? ParsePosition(string position) + { + return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; + } } } From 3b7c668cfc77207814e5f28abe1a0768c6c39040 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 4 Jan 2018 11:52:32 +0100 Subject: [PATCH 3/4] 1) Separate interface IRunnable 2) ExternalSystem => IInitializable --- .../Rules/RuleDequeuer.cs | 4 +-- .../Assets/AzureBlobAssetStore.cs | 4 +-- .../EventSourcing/GetEventStore.cs | 4 +-- .../Assets/GoogleCloudAssetStore.cs | 4 +-- .../MongoDb/MongoRepositoryBase.cs | 4 +-- .../States/MongoSnapshotStore.cs | 2 +- .../CQRS/Events/RabbitMqEventConsumer.cs | 4 +-- .../RedisPubSub.cs | 4 +-- .../Assets/FolderAssetStore.cs | 4 +-- .../Grains/EventConsumerGrainManager.cs | 4 +-- src/Squidex.Infrastructure/IInitializable.cs | 15 ++++++++++ .../{IExternalSystem.cs => IRunnable.cs} | 6 ++-- src/Squidex.Infrastructure/Log/FileChannel.cs | 4 +-- .../States/StateFactory.cs | 4 +-- src/Squidex/Config/Domain/AssetServices.cs | 6 ++-- .../Config/Domain/EventPublishersServices.cs | 2 +- .../Config/Domain/EventStoreServices.cs | 4 +-- .../Config/Domain/InfrastructureServices.cs | 2 +- src/Squidex/Config/Domain/PubSubServices.cs | 2 +- src/Squidex/Config/Domain/ReadServices.cs | 6 ++-- src/Squidex/Config/Domain/StoreServices.cs | 30 +++++++++---------- src/Squidex/Config/Domain/SystemExtensions.cs | 16 ++++++++-- src/Squidex/WebStartup.cs | 3 +- .../Assets/AssetStoreTests.cs | 10 +++---- .../Assets/AzureBlobAssetStoreTests.cs | 2 +- .../Assets/FolderAssetStoreTests.cs | 6 ++-- .../Assets/GoogleCloudAssetStoreTests.cs | 2 +- .../Grains/EventConsumerManagerTests.cs | 10 +++---- .../States/StateEventSourcingTests.cs | 2 +- .../States/StateSnapshotTests.cs | 2 +- 30 files changed, 99 insertions(+), 73 deletions(-) create mode 100644 src/Squidex.Infrastructure/IInitializable.cs rename src/Squidex.Infrastructure/{IExternalSystem.cs => IRunnable.cs} (81%) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs index 5ae2477bf..da523ac95 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDequeuer : DisposableObjectBase, IExternalSystem + public class RuleDequeuer : DisposableObjectBase, IRunnable { private readonly ActionBlock requestBlock; private readonly IRuleEventRepository ruleEventRepository; @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Rules } } - public void Connect() + public void Run() { } diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index 87a5d16a2..7300e7720 100644 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -14,7 +14,7 @@ using Microsoft.WindowsAzure.Storage.Blob; namespace Squidex.Infrastructure.Assets { - public class AzureBlobAssetStore : IAssetStore, IExternalSystem + public class AzureBlobAssetStore : IAssetStore, IInitializable { private const string AssetVersion = "AssetVersion"; private const string AssetId = "AssetId"; @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.Assets this.containerName = containerName; } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 12cbd708b..f224dfe3f 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -15,7 +15,7 @@ using EventStore.ClientAPI; namespace Squidex.Infrastructure.EventSourcing { - public sealed class GetEventStore : IEventStore, IExternalSystem + public sealed class GetEventStore : IEventStore, IInitializable { private const int WritePageSize = 500; private const int ReadPageSize = 500; @@ -37,7 +37,7 @@ namespace Squidex.Infrastructure.EventSourcing { } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index 6df5f83a8..7c6b68e0b 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -16,7 +16,7 @@ using Google.Cloud.Storage.V1; namespace Squidex.Infrastructure.Assets { - public sealed class GoogleCloudAssetStore : IAssetStore, IExternalSystem + public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable { private readonly string bucketName; private StorageClient storageClient; @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Assets this.bucketName = bucketName; } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 056dd8595..746195863 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.MongoDb { - public abstract class MongoRepositoryBase : IExternalSystem + public abstract class MongoRepositoryBase : IInitializable { private const string CollectionFormat = "{0}Set"; @@ -106,7 +106,7 @@ namespace Squidex.Infrastructure.MongoDb } } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 8628d75ec..bbdb89a98 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IInitializable { private readonly JsonSerializer serializer; diff --git a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index ecf9ce5e9..24f6611db 100644 --- a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing { - public sealed class RabbitMqEventConsumer : DisposableObjectBase, IExternalSystem, IEventConsumer + public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer { private readonly JsonSerializerSettings serializerSettings; private readonly string eventPublisherName; @@ -61,7 +61,7 @@ namespace Squidex.Infrastructure.EventSourcing } } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs index 238b912d8..c548a178f 100644 --- a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs +++ b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs @@ -13,7 +13,7 @@ using StackExchange.Redis; namespace Squidex.Infrastructure { - public sealed class RedisPubSub : IPubSub, IExternalSystem + public sealed class RedisPubSub : IPubSub, IInitializable { private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary(); private readonly Lazy redisClient; @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure redisSubscriber = new Lazy(() => redis.Value.GetSubscriber()); } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index 951503235..557a3e7a5 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Assets { - public sealed class FolderAssetStore : IAssetStore, IExternalSystem + public sealed class FolderAssetStore : IAssetStore, IInitializable { private readonly ISemanticLog log; private readonly DirectoryInfo directory; @@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.Assets directory = new DirectoryInfo(path); } - public void Connect() + public void Initialize() { try { diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs index 70ef98c40..8d4497e4e 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.EventSourcing.Grains { - public sealed class EventConsumerGrainManager : DisposableObjectBase, IExternalSystem + public sealed class EventConsumerGrainManager : DisposableObjectBase, IRunnable { private readonly IStateFactory factory; private readonly IPubSub pubSub; @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains this.consumers = consumers.ToList(); } - public void Connect() + public void Run() { var actors = new Dictionary(); diff --git a/src/Squidex.Infrastructure/IInitializable.cs b/src/Squidex.Infrastructure/IInitializable.cs new file mode 100644 index 000000000..44a853641 --- /dev/null +++ b/src/Squidex.Infrastructure/IInitializable.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IInitializable.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public interface IInitializable + { + void Initialize(); + } +} diff --git a/src/Squidex.Infrastructure/IExternalSystem.cs b/src/Squidex.Infrastructure/IRunnable.cs similarity index 81% rename from src/Squidex.Infrastructure/IExternalSystem.cs rename to src/Squidex.Infrastructure/IRunnable.cs index 8943f9de6..b36a94eeb 100644 --- a/src/Squidex.Infrastructure/IExternalSystem.cs +++ b/src/Squidex.Infrastructure/IRunnable.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IExternalSystem.cs +// IRunnable.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,8 +8,8 @@ namespace Squidex.Infrastructure { - public interface IExternalSystem + public interface IRunnable { - void Connect(); + void Run(); } } diff --git a/src/Squidex.Infrastructure/Log/FileChannel.cs b/src/Squidex.Infrastructure/Log/FileChannel.cs index bf70e22ee..c351aa7d2 100644 --- a/src/Squidex.Infrastructure/Log/FileChannel.cs +++ b/src/Squidex.Infrastructure/Log/FileChannel.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Log.Internal; namespace Squidex.Infrastructure.Log { - public sealed class FileChannel : DisposableObjectBase, ILogChannel, IExternalSystem + public sealed class FileChannel : DisposableObjectBase, ILogChannel, IInitializable { private readonly FileLogProcessor processor; @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.Log processor.EnqueueMessage(new LogMessageEntry { Message = message }); } - public void Connect() + public void Initialize() { processor.Connect(); } diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index abeb95d8a..dedc268f3 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public sealed class StateFactory : DisposableObjectBase, IExternalSystem, IStateFactory + public sealed class StateFactory : DisposableObjectBase, IInitializable, IStateFactory { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private readonly IPubSub pubSub; @@ -70,7 +70,7 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public void Connect() + public void Initialize() { pubSubSubscription = pubSub.Subscribe(m => { diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index 3694cde33..b958f6ff0 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -26,7 +26,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) .As() - .As(); + .As(); }, ["GoogleCloud"] = () => { @@ -34,7 +34,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) .As() - .As(); + .As(); }, ["AzureBlob"] = () => { @@ -43,7 +43,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) .As() - .As(); + .As(); } }); } diff --git a/src/Squidex/Config/Domain/EventPublishersServices.cs b/src/Squidex/Config/Domain/EventPublishersServices.cs index b191350f6..a645708c5 100644 --- a/src/Squidex/Config/Domain/EventPublishersServices.cs +++ b/src/Squidex/Config/Domain/EventPublishersServices.cs @@ -56,7 +56,7 @@ namespace Squidex.Config.Domain { services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) .As() - .As(); + .As(); } } else diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index 59b2bb727..e5287f813 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -33,7 +33,7 @@ namespace Squidex.Config.Domain return new MongoEventStore(mongDatabase, c.GetRequiredService()); }) - .As() + .As() .As(); }, ["GetEventStore"] = () => @@ -45,7 +45,7 @@ namespace Squidex.Config.Domain var connection = EventStoreConnection.Create(eventStoreConfiguration); services.AddSingletonAs(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost)) - .As() + .As() .As(); } }); diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index b8cd37168..5ea0bd2b9 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -45,7 +45,7 @@ namespace Squidex.Config.Domain { services.AddSingletonAs(new FileChannel(loggingFile)) .As() - .As(); + .As(); } services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) diff --git a/src/Squidex/Config/Domain/PubSubServices.cs b/src/Squidex/Config/Domain/PubSubServices.cs index b19b462ba..fe3cc14b3 100644 --- a/src/Squidex/Config/Domain/PubSubServices.cs +++ b/src/Squidex/Config/Domain/PubSubServices.cs @@ -33,7 +33,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new RedisPubSub(redis, c.GetRequiredService())) .As() - .As(); + .As(); } }); } diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index 54c9958ce..eda0d92a0 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -44,9 +44,9 @@ namespace Squidex.Config.Domain services.AddTransient(); services.AddSingletonAs() - .As(); + .As(); services.AddSingletonAs() - .As(); + .As(); } var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); @@ -58,7 +58,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As() + .As() .As(); services.AddSingletonAs(c => c.GetService>()?.Value?.Plans.OrEmpty()); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index ce4fe184d..2a3224ab8 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -64,74 +64,74 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new MongoXmlRepository(mongoDatabase)) .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase)) .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) .As>() .As() .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) .As>() .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase)) .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoUsageStore(mongoDatabase)) .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) .As() .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) .As() .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase)) .As() .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) .As() .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() .As>() .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) .As() .As() - .As(); + .As(); services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) .As() .As() - .As(); + .As(); } }); } diff --git a/src/Squidex/Config/Domain/SystemExtensions.cs b/src/Squidex/Config/Domain/SystemExtensions.cs index 95538ef4c..ccd4543ed 100644 --- a/src/Squidex/Config/Domain/SystemExtensions.cs +++ b/src/Squidex/Config/Domain/SystemExtensions.cs @@ -16,13 +16,23 @@ namespace Squidex.Config.Domain { public static class SystemExtensions { - public static void TestExternalSystems(this IServiceProvider services) + public static void InitializeAll(this IServiceProvider services) { - var systems = services.GetRequiredService>(); + var systems = services.GetRequiredService>(); foreach (var system in systems) { - system.Connect(); + system.Initialize(); + } + } + + public static void RunAll(this IServiceProvider services) + { + var systems = services.GetRequiredService>(); + + foreach (var system in systems) + { + system.Run(); } } diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index d88b28055..6c9bdd9f2 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -39,8 +39,9 @@ namespace Squidex public void Configure(IApplicationBuilder app) { app.ApplicationServices.LogConfiguration(); + app.ApplicationServices.InitializeAll(); app.ApplicationServices.Migrate(); - app.ApplicationServices.TestExternalSystems(); + app.ApplicationServices.RunAll(); app.UseMyCors(); app.UseMyForwardingRules(); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs index bf6c35b64..56c74f7c3 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public Task Should_throw_exception_if_asset_to_download_is_not_found() { - ((IExternalSystem)Sut).Connect(); + ((IInitializable)Sut).Initialize(); return Assert.ThrowsAsync(() => Sut.DownloadAsync(Id(), 1, "suffix", new MemoryStream())); } @@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public Task Should_throw_exception_if_asset_to_copy_is_not_found() { - ((IExternalSystem)Sut).Connect(); + ((IInitializable)Sut).Initialize(); return Assert.ThrowsAsync(() => Sut.CopyTemporaryAsync(Id(), Id(), 1, null)); } @@ -50,7 +50,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_read_and_write_file() { - ((IExternalSystem)Sut).Connect(); + ((IInitializable)Sut).Initialize(); var assetId = Id(); var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); @@ -67,7 +67,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_commit_temporary_file() { - ((IExternalSystem)Sut).Connect(); + ((IInitializable)Sut).Initialize(); var tempId = Id(); @@ -87,7 +87,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public async Task Should_ignore_when_deleting_twice() { - ((IExternalSystem)Sut).Connect(); + ((IInitializable)Sut).Initialize(); var tempId = Id(); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs index dcab0c97b..47953e893 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Assets // [Fact] public void Should_calculate_source_url() { - Sut.Connect(); + Sut.Initialize(); var id = Guid.NewGuid().ToString(); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs index 721b8465f..e1b5b0f16 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs @@ -34,13 +34,13 @@ namespace Squidex.Infrastructure.Assets [Fact] public void Should_throw_when_creating_directory_failed() { - Assert.Throws(() => new FolderAssetStore(CreateInvalidPath(), A.Dummy()).Connect()); + Assert.Throws(() => new FolderAssetStore(CreateInvalidPath(), A.Dummy()).Initialize()); } [Fact] public void Should_create_directory_when_connecting() { - Sut.Connect(); + Sut.Initialize(); Assert.True(Directory.Exists(testFolder)); } @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.Assets [Fact] public void Should_calculate_source_url() { - Sut.Connect(); + Sut.Initialize(); var id = Guid.NewGuid().ToString(); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs index 2318a2208..f89a7e9d5 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Assets // [Fact] public void Should_calculate_source_url() { - Sut.Connect(); + Sut.Initialize(); var id = Guid.NewGuid().ToString(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs index e6d1edd31..84d336a02 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_activate_all_actors() { - sut.Connect(); + sut.Run(); A.CallTo(() => actor1.Activate(consumer1)) .MustHaveHappened(); @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_start_correct_actor() { - sut.Connect(); + sut.Run(); pubSub.Publish(new StartConsumerMessage { ConsumerName = consumerName1 }, true); @@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_stop_correct_actor() { - sut.Connect(); + sut.Run(); pubSub.Publish(new StopConsumerMessage { ConsumerName = consumerName1 }, true); @@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_reset_correct_actor() { - sut.Connect(); + sut.Run(); pubSub.Publish(new ResetConsumerMessage { ConsumerName = consumerName2 }, true); @@ -96,7 +96,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_get_state_from_all_actors() { - sut.Connect(); + sut.Run(); A.CallTo(() => actor1.GetState()) .Returns(new EventConsumerInfo { Name = consumerName1, Position = "123 " }); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index ac1581d2e..7af4608b9 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -88,7 +88,7 @@ namespace Squidex.Infrastructure.States .Returns(key); sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); - sut.Connect(); + sut.Initialize(); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index 1a84194a0..0243c0c55 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.States .Returns(snapshotStore); sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); - sut.Connect(); + sut.Initialize(); } public void Dispose() From 24f887f4baaf70df251cd8f0d1db514adccf6ac3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 4 Jan 2018 12:52:47 +0100 Subject: [PATCH 4/4] Position fix --- .../EventSourcing/GetEventStore.cs | 2 +- .../EventSourcing/GetEventStoreSubscription.cs | 2 +- .../EventSourcing/ProjectionHelper.cs | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index f224dfe3f..70036d8ee 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.EventSourcing { var streamName = await connection.CreateProjectionAsync(projectionHost, prefix, streamFilter); - var sliceStart = ProjectionHelper.ParsePosition(position) ?? -1; + var sliceStart = ProjectionHelper.ParsePosition(position); StreamEventsSlice currentSlice; do diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index b2fc0c24f..607089bb1 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.EventSourcing this.eventStoreConnection = eventStoreConnection; this.eventSubscriber = eventSubscriber; - this.position = ProjectionHelper.ParsePosition(position); + this.position = ProjectionHelper.ParsePositionOrNull(position); var streamName = eventStoreConnection.CreateProjectionAsync(projectionHost, prefix, streamFilter).Result; diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs index 3c35b5aac..ee9905dd5 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs @@ -85,9 +85,14 @@ namespace Squidex.Infrastructure.EventSourcing return projectionsManager; } - public static long? ParsePosition(string position) + 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; + } } }