public async Task <T> Load <T>(string id) where T : Aggregate, new() { Ensure.NotEmptyString(id, nameof(id)); var stream = StreamName.For <T>(id); var aggregate = new T(); try { await _eventStore.ReadStream(stream, StreamReadPosition.Start, Fold); } catch (Exceptions.StreamNotFound e) { throw new Exceptions.AggregateNotFound <T>(id, e); } return(aggregate); void Fold(StreamEvent streamEvent) { var evt = Deserialize(streamEvent); if (evt == null) { return; } aggregate !.Fold(evt); } object?Deserialize(StreamEvent streamEvent) => _serializer.Deserialize(streamEvent.Data.AsSpan(), streamEvent.EventType); }
public async Task ShouldProjectImported() { var evt = DomainFixture.CreateImportBooking(); var stream = StreamName.For <Booking>(evt.BookingId); var append = await Instance.EventStore.AppendEvents( stream, ExpectedStreamVersion.Any, new[] { new StreamEvent(Guid.NewGuid(), evt, new Metadata(), "application/json", 0) }, CancellationToken.None ); await Task.Delay(500); var expected = new BookingDocument(evt.BookingId) { RoomId = evt.RoomId, CheckInDate = evt.CheckIn, CheckOutDate = evt.CheckOut, Position = append.GlobalPosition }; var actual = await Instance.Mongo.LoadDocument <BookingDocument>(evt.BookingId); actual.Should().Be(expected); }
public async Task read_snapshot_when_loading_aggregate() { var now = DateTime.UtcNow; var esStore = new EsEventStore(GetEventStoreClient(), "snapshot_test"); var esAggregateStore = new EsAggregateStore(esStore, 5); var aggregate = new Day(); var slots = new List <ScheduledSlot> { new ScheduledSlot(TimeSpan.FromMinutes(10), now), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(10)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(20)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(30)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(40)) }; aggregate.Schedule(new DoctorId(Guid.NewGuid()), DateTime.UtcNow, slots, Guid.NewGuid); await esAggregateStore.Save(aggregate, new CommandMetadata(new CorrelationId(Guid.NewGuid()), new CausationId(Guid.NewGuid()))); await esStore.TruncateStream(StreamName.For <Day>(aggregate.Id), Convert.ToUInt64(aggregate.GetChanges().Count())); var reloadedAggregate = await esAggregateStore.Load <Day>(aggregate.Id); Assert.Equal(5, reloadedAggregate.Version); }
public async Task write_snapshot_if_threshold_reached() { var now = DateTime.UtcNow; var esStore = new EsEventStore(GetEventStoreClient(), "snapshot_test"); var esAggregateStore = new EsAggregateStore(esStore, 5); var aggregate = new Day(); var slots = new List <ScheduledSlot> { new ScheduledSlot(TimeSpan.FromMinutes(10), now), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(10)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(20)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(30)), new ScheduledSlot(TimeSpan.FromMinutes(10), now.AddMinutes(40)) }; aggregate.Schedule(new DoctorId(Guid.NewGuid()), DateTime.UtcNow, slots, Guid.NewGuid); await esAggregateStore.Save(aggregate, new CommandMetadata(new CorrelationId(Guid.NewGuid()), new CausationId(Guid.NewGuid()))); var snapshotEnvelope = await esStore.LoadSnapshot(StreamName.For <Day>(aggregate.Id)); var snapshot = snapshotEnvelope?.Snapshot as DaySnapshot; Assert.NotNull(snapshot); }
public async Task <T> Load <T>(string id, CancellationToken cancellationToken) where T : Aggregate, new() { Ensure.NotEmptyString(id, nameof(id)); var stream = StreamName.For <T>(id); var aggregate = new T(); try { await _eventStore.ReadStream(stream, StreamReadPosition.Start, Fold, cancellationToken).Ignore(); } catch (Exceptions.StreamNotFound e) { throw new Exceptions.AggregateNotFound <T>(id, e); } return(aggregate); void Fold(StreamEvent streamEvent) { var evt = Deserialize(streamEvent); if (evt == null) { return; } aggregate !.Fold(evt); } }
public async Task MapEnrichedCommand() { var store = new InMemoryEventStore(); var app = new WebApplicationFactory <Program>() .WithWebHostBuilder( builder => builder.ConfigureServices(services => services.AddAggregateStore(_ => store)) ); var httpClient = app.CreateClient(); using var client = new RestClient(httpClient, disposeHttpClient: true).UseSerializer( () => new SystemTextJsonSerializer( new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) ) ); var cmd = new BookRoom( "123", "123", LocalDate.FromDateTime(DateTime.Now), LocalDate.FromDateTime(DateTime.Now.AddDays(1)), 100 ); var response = await client.PostJsonAsync("/book", cmd); response.Should().Be(HttpStatusCode.OK); var storedEvents = await store.ReadEvents( StreamName.For <Booking>(cmd.BookingId), StreamReadPosition.Start, 100, default ); var actual = storedEvents.FirstOrDefault(); actual.Should().NotBeNull(); actual !.Payload.Should() .BeEquivalentTo( new BookingEvents.RoomBooked( cmd.BookingId, cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price, TestData.GuestId ) ); }
public async Task Store <T>(T aggregate) where T : Aggregate { Ensure.NotNull(aggregate, nameof(aggregate)); if (aggregate.Changes.Count == 0) { return; } var stream = StreamName.For <T>(aggregate.GetId()); var expectedVersion = new ExpectedStreamVersion(aggregate.OriginalVersion); await _eventStore.AppendEvents(stream, expectedVersion, aggregate.Changes.Select(ToStreamEvent).ToArray()); StreamEvent ToStreamEvent(object evt) => new(TypeMap.GetTypeName(evt), _serializer.Serialize(evt), null, _serializer.ContentType); }
public DayArchiverProcessManager( IColdStorage coldStorage, IArchivableDaysRepository archivableDaysRepository, ICommandStore commandStore, TimeSpan threshold, IEventStore eventStore, Func <Guid> idGenerator ) { _commandStore = commandStore; _idGenerator = idGenerator; When <DayScheduled>(async(e, m) => { await archivableDaysRepository.Add(new ArchivableDay(e.DayId, e.Date)); }); When <CalendarDayStarted>(async(e, m) => { var archivableDays = await archivableDaysRepository.FindAll(e.Date.Add(threshold)); foreach (var day in archivableDays) { await SendCommand(day.Id, m.CorrelationId, m.CausationId); } }); When <DayScheduleArchived>(async(e, m) => { var streamName = StreamName.For <Day>(e.DayId); var events = await eventStore.LoadEvents(streamName); coldStorage.SaveAll(events); var lastVersion = await eventStore.GetLastVersion(streamName); if (lastVersion.HasValue) { await eventStore.TruncateStream(streamName, lastVersion.Value); } }); }
public async Task Store <T>(T aggregate) where T : Aggregate { if (aggregate == null) { throw new ArgumentNullException(nameof(aggregate)); } if (aggregate.Changes.Count == 0) { return; } var stream = StreamName.For <T>(aggregate.GetId()); var expectedVersion = new ExpectedStreamVersion(aggregate.Version); await _eventStore.AppendEvents(stream, expectedVersion, aggregate.Changes.Select(ToStreamEvent).ToArray()); JsonStreamEvent ToStreamEvent(object evt) => new(TypeMap.GetTypeName(evt), _serializer.Serialize(evt)); }
public async Task StoreInitial() { var cmd = new Commands.BookRoom( Auto.Create <string>(), Auto.Create <string>(), LocalDate.FromDateTime(DateTime.Today), LocalDate.FromDateTime(DateTime.Today.AddDays(2)), Auto.Create <decimal>() ); var expected = new Change[] { new( new BookingEvents.RoomBooked( cmd.BookingId, cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price ), "RoomBooked" ) }; var result = await Service.Handle(cmd, default); result.Success.Should().BeTrue(); result.Changes.Should().BeEquivalentTo(expected); var evt = await EventStore.ReadEvents( StreamName.For <Booking>(cmd.BookingId), StreamReadPosition.Start, 1, CancellationToken.None ); evt[0].Payload.Should().BeEquivalentTo(result.Changes !.First().Event); }
public async Task StreamSubscriptionGetsDeletedEvents() { var service = new BookingService(Instance.AggregateStore); var categoryStream = new StreamName("$ce-Booking"); ulong?startPosition = null; try { var last = await Instance.Client.ReadStreamAsync( Direction.Backwards, categoryStream, StreamPosition.End, 1 ).ToArrayAsync(); startPosition = last[0].OriginalEventNumber; } catch (StreamNotFound) { } const int produceCount = 20; const int deleteCount = 5; var commands = Enumerable.Range(0, produceCount) .Select(_ => DomainFixture.CreateImportBooking()) .ToArray(); await Task.WhenAll( commands.Select(x => service.Handle(x, CancellationToken.None)) ); var delete = Enumerable.Range(5, deleteCount).Select(x => commands[x]).ToList(); await Task.WhenAll( delete .Select( x => Instance.EventStore.DeleteStream( StreamName.For <Booking>(x.BookingId), ExpectedStreamVersion.Any, CancellationToken.None ) ) ); var handler = new TestHandler(); const string subscriptionId = "TestSub"; var subscription = new StreamSubscription( Instance.Client, new StreamSubscriptionOptions { StreamName = categoryStream, SubscriptionId = subscriptionId, ResolveLinkTos = true, ThrowOnError = true }, new NoOpCheckpointStore(startPosition), new ConsumePipe() .AddFilterLast(new MessageFilter(x => !x.MessageType.StartsWith("$"))) .AddDefaultConsumer(handler) ); var log = _loggerFactory.CreateLogger("Test"); await subscription.SubscribeWithLog(log); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(200)); while (handler.Count < produceCount - deleteCount && !cts.IsCancellationRequested) { await Task.Delay(100); } await subscription.UnsubscribeWithLog(log); log.LogInformation("Received {Count} events", handler.Count); var actual = handler.Processed .Select(x => (x.Message as BookingEvents.BookingImported) !.BookingId) .ToList(); log.LogInformation("Actual contains {Count} events", actual.Count); actual .Should() .BeEquivalentTo(commands.Except(delete).Select(x => x.BookingId)); }