mirror of https://github.com/Squidex/squidex.git
26 changed files with 679 additions and 466 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue