From fff7967fb211dd6da6d03a96e8d3bc58294e3af1 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sun, 6 Aug 2017 13:22:19 +0200 Subject: [PATCH 01/26] Changed ViewProjection to allow asynchronous applyProjections handling --- .../Events/Projections/ViewProjection.cs | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/Marten/Events/Projections/ViewProjection.cs b/src/Marten/Events/Projections/ViewProjection.cs index 8852747b3a..18250b327d 100644 --- a/src/Marten/Events/Projections/ViewProjection.cs +++ b/src/Marten/Events/Projections/ViewProjection.cs @@ -39,13 +39,13 @@ private class EventHandler { public Func IdSelector { get; } public Func> IdsSelector { get; } - public Action Handler { get; } + public Func Handler { get; } public ProjectionEventType Type { get; set; } public EventHandler( Func idSelector, Func> idsSelector, - Action handler, + Func handler, ProjectionEventType type) { IdSelector = idSelector; @@ -58,7 +58,7 @@ public EventHandler( private class EventProjection { public TId ViewId { get; } - public Action ProjectTo { get; } + public Func ProjectTo { get; } public ProjectionEventType Type { get; set; } public EventProjection(EventHandler eventHandler, TId viewId, IEvent @event, object projectionEvent) @@ -117,27 +117,54 @@ public ViewProjection DeleteEvent(Func ProjectEvent(Action handler) where TEvent : class - => projectEvent((session, @event, streamId) => convertToTId(streamId), null, handler); + => projectEvent((session, @event, streamId) => convertToTId(streamId), null, (TView view, TEvent @event) => { handler(view, @event); return Task.CompletedTask; }); public ViewProjection ProjectEvent(Func viewIdSelector, Action handler) where TEvent : class { if (viewIdSelector == null) throw new ArgumentNullException(nameof(viewIdSelector)); - return projectEvent((session, @event, streamId) => viewIdSelector(session, @event as TEvent), null, handler); + return projectEvent((session, @event, streamId) => viewIdSelector(session, @event as TEvent), null, (TView view, TEvent @event) => { handler(view, @event); return Task.CompletedTask; }); } public ViewProjection ProjectEvent(Func viewIdSelector, Action handler) where TEvent : class { if (viewIdSelector == null) throw new ArgumentNullException(nameof(viewIdSelector)); - return projectEvent((session, @event, streamId) => viewIdSelector(@event as TEvent), null, handler); + return projectEvent((session, @event, streamId) => viewIdSelector(@event as TEvent), null, (TView view, TEvent @event) => { handler(view, @event); return Task.CompletedTask; }); } public ViewProjection ProjectEvent(Func> viewIdsSelector, Action handler) where TEvent : class { if (viewIdsSelector == null) throw new ArgumentNullException(nameof(viewIdsSelector)); - return projectEvent(null, (session, @event, streamId) => viewIdsSelector(session, @event as TEvent), handler); + return projectEvent(null, (session, @event, streamId) => viewIdsSelector(session, @event as TEvent), (TView view, TEvent @event) => { handler(view, @event); return Task.CompletedTask; }); } public ViewProjection ProjectEvent(Func> viewIdsSelector, Action handler) where TEvent : class + { + if (viewIdsSelector == null) throw new ArgumentNullException(nameof(viewIdsSelector)); + return projectEvent(null, (session, @event, streamId) => viewIdsSelector(@event as TEvent), (TView view, TEvent @event) => { handler(view, @event); return Task.CompletedTask; }); + } + + public ViewProjection ProjectEventAsync(Func handler) where TEvent : class + => projectEvent((session, @event, streamId) => convertToTId(streamId), null, handler); + + public ViewProjection ProjectEventAsync(Func viewIdSelector, Func handler) where TEvent : class + { + if (viewIdSelector == null) throw new ArgumentNullException(nameof(viewIdSelector)); + return projectEvent((session, @event, streamId) => viewIdSelector(session, @event as TEvent), null, handler); + } + + public ViewProjection ProjectEventAsync(Func viewIdSelector, Func handler) where TEvent : class + { + if (viewIdSelector == null) throw new ArgumentNullException(nameof(viewIdSelector)); + return projectEvent((session, @event, streamId) => viewIdSelector(@event as TEvent), null, handler); + } + + public ViewProjection ProjectEventAsync(Func> viewIdsSelector, Func handler) where TEvent : class + { + if (viewIdsSelector == null) throw new ArgumentNullException(nameof(viewIdsSelector)); + return projectEvent(null, (session, @event, streamId) => viewIdsSelector(session, @event as TEvent), handler); + } + + public ViewProjection ProjectEventAsync(Func> viewIdsSelector, Func handler) where TEvent : class { if (viewIdsSelector == null) throw new ArgumentNullException(nameof(viewIdsSelector)); return projectEvent(null, (session, @event, streamId) => viewIdsSelector(@event as TEvent), handler); @@ -146,7 +173,7 @@ public ViewProjection ProjectEvent(Func> v private ViewProjection projectEvent( Func viewIdSelector, Func> viewIdsSelector, - Action handler, + Func handler, ProjectionEventType type = ProjectionEventType.Modify) where TEvent : class { if (viewIdSelector == null && viewIdsSelector == null) throw new ArgumentException($"{nameof(viewIdSelector)} or {nameof(viewIdsSelector)} must be provided."); @@ -155,7 +182,7 @@ private ViewProjection projectEvent( EventHandler eventHandler; if (type == ProjectionEventType.Modify) { - eventHandler = new EventHandler(viewIdSelector, viewIdsSelector, (view, @event) => handler(view, @event as TEvent), type); + eventHandler = new EventHandler(viewIdSelector, viewIdsSelector, (view, @event) => { handler(view, @event as TEvent); return Task.CompletedTask; } , type); } else { @@ -193,7 +220,7 @@ void IProjection.Apply(IDocumentSession session, EventPage page) } } - Task IProjection.ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + async Task IProjection.ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) { var projections = getEventProjections(session, page); @@ -203,10 +230,8 @@ Task IProjection.ApplyAsync(IDocumentSession session, EventPage page, Cancellati { var views = _sessionLoadMany((DocumentSession)session, viewIds); - applyProjections(session, projections, views); + await applyProjectionsAsync(session, projections, views); } - - return Task.CompletedTask; } private void applyProjections(IDocumentSession session, ICollection projections, IEnumerable views) @@ -223,7 +248,26 @@ private void applyProjections(IDocumentSession session, ICollection projections, IEnumerable views) + { + var viewMap = createViewMap(session, projections, views); + + foreach (var eventProjection in projections) + { + var view = viewMap[eventProjection.ViewId]; + + if (eventProjection.Type == ProjectionEventType.Delete) + { + session.Delete(view); + } + else + { + await eventProjection.ProjectTo(view); } } } From 56f3969c510e373d6b1ec1729f8e6853082f8902 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sun, 6 Aug 2017 13:53:47 +0200 Subject: [PATCH 02/26] Added tests for async event projection handling for ViewProjection --- .../custom_async_transformation_of_events.cs | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/Marten.Testing/Events/Projections/custom_async_transformation_of_events.cs diff --git a/src/Marten.Testing/Events/Projections/custom_async_transformation_of_events.cs b/src/Marten.Testing/Events/Projections/custom_async_transformation_of_events.cs new file mode 100644 index 0000000000..916c23b2a0 --- /dev/null +++ b/src/Marten.Testing/Events/Projections/custom_async_transformation_of_events.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using Marten.Events.Projections; +using Marten.Services; +using Shouldly; +using Xunit; +using System.Threading.Tasks; + +namespace Marten.Testing.Events.Projections +{ + public class project_events_async_from_multiple_streams_into_view : DocumentSessionFixture + { + static readonly Guid streamId = Guid.NewGuid(); + static readonly Guid streamId2 = Guid.NewGuid(); + + QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; + QuestStarted started2 = new QuestStarted { Id = streamId2, Name = "Find the Orb 2.0" }; + MonsterQuestsAdded monsterQuestsAdded = new MonsterQuestsAdded { QuestIds = new List { streamId, streamId2 }, Name = "Dragon" }; + MonsterQuestsRemoved monsterQuestsRemoved = new MonsterQuestsRemoved { QuestIds = new List { streamId, streamId2 }, Name = "Dragon" }; + QuestEnded ended = new QuestEnded { Id = streamId, Name = "Find the Orb" }; + MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; + MonsterSlayed slayed1 = new MonsterSlayed { QuestId = streamId, Name = "Troll" }; + MonsterSlayed slayed2 = new MonsterSlayed { QuestId = streamId, Name = "Dragon" }; + MonsterDestroyed destroyed = new MonsterDestroyed { QuestId = streamId, Name = "Troll" }; + MembersDeparted departed = new MembersDeparted { QuestId = streamId, Day = 5, Location = "Sendaria", Members = new[] { "Silk", "Barak" } }; + MembersJoined joined2 = new MembersJoined { QuestId = streamId, Day = 5, Location = "Sendaria", Members = new[] { "Silk", "Barak" } }; + + [Fact] + public void from_configuration() + { + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.ProjectView() + .ProjectEventAsync((view, @event) => { view.Events.Add(@event); return Task.CompletedTask; }) + .ProjectEventAsync(e => e.QuestId, (view, @event) => { view.Events.Add(@event); return Task.CompletedTask; }) + .ProjectEventAsync(e => e.QuestId, (view, @event) => { view.Events.Add(@event); return Task.CompletedTask; }) + .DeleteEvent() + .DeleteEvent(e => e.QuestId) + .DeleteEvent((session, e) => session.Load(e.QuestId).Id); + }); + + theSession.Events.StartStream(streamId, started, joined); + theSession.SaveChanges(); + + theSession.Events.StartStream(slayed1, slayed2); + theSession.SaveChanges(); + + theSession.Events.Append(streamId, joined2); + theSession.SaveChanges(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(5); + document.Events.ShouldHaveTheSameElementsAs(started, joined, slayed1, slayed2, joined2); + + theSession.Events.Append(streamId, ended); + theSession.SaveChanges(); + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + + // Add document back to so we can delete it by selector + theSession.Events.Append(streamId, started); + theSession.SaveChanges(); + var document2 = theSession.Load(streamId); + document2.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, departed); + theSession.SaveChanges(); + var nullDocument2 = theSession.Load(streamId); + nullDocument2.ShouldBeNull(); + + // Add document back to so we can delete it by other selector type + theSession.Events.Append(streamId, started); + theSession.SaveChanges(); + var document3 = theSession.Load(streamId); + document3.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, destroyed); + theSession.SaveChanges(); + var nullDocument3 = theSession.Load(streamId); + nullDocument3.ShouldBeNull(); + } + + [Fact] + public async void from_configuration_async() + { + // SAMPLE: viewprojection-from-configuration + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.ProjectView() + .ProjectEventAsync((view, @event) => { view.Events.Add(@event); return Task.CompletedTask; }) + .ProjectEventAsync(e => e.QuestId, (view, @event) => { view.Events.Add(@event); return Task.CompletedTask; }) + .ProjectEventAsync>(e => e.Data.QuestId, (view, @event) => { view.Events.Add(@event.Data); return Task.CompletedTask; }) + .DeleteEvent() + .DeleteEvent(e => e.QuestId) + .DeleteEvent((session, e) => session.Load(e.QuestId).Id); + }); + // ENDSAMPLE + + theSession.Events.StartStream(streamId, started, joined); + await theSession.SaveChangesAsync(); + + theSession.Events.StartStream(slayed1, slayed2); + await theSession.SaveChangesAsync(); + + theSession.Events.Append(streamId, joined2); + await theSession.SaveChangesAsync(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(5); + document.Events.ShouldHaveTheSameElementsAs(started, joined, slayed1, slayed2, joined2); + + theSession.Events.Append(streamId, ended); + await theSession.SaveChangesAsync(); + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + + // Add document back to so we can delete it by selector + theSession.Events.Append(streamId, started); + await theSession.SaveChangesAsync(); + var document2 = theSession.Load(streamId); + document2.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, departed); + await theSession.SaveChangesAsync(); + var nullDocument2 = theSession.Load(streamId); + nullDocument2.ShouldBeNull(); + + // Add document back to so we can delete it by other selector type + theSession.Events.Append(streamId, started); + await theSession.SaveChangesAsync(); + var document3 = theSession.Load(streamId); + document3.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, destroyed); + await theSession.SaveChangesAsync(); + var nullDocument3 = theSession.Load(streamId); + nullDocument3.ShouldBeNull(); + } + + [Fact] + public void from_projection() + { + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.InlineProjections.Add(new PersistAsyncViewProjection()); + }); + + theSession.Events.StartStream(streamId, started, joined); + theSession.SaveChanges(); + + theSession.Events.StartStream(slayed1, slayed2); + theSession.SaveChanges(); + + theSession.Events.Append(streamId, joined2); + theSession.SaveChanges(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(5); + document.Events.ShouldHaveTheSameElementsAs(started, joined, slayed1, slayed2, joined2); + + theSession.Events.Append(streamId, ended); + theSession.SaveChanges(); + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + + // Add document back to so we can delete it by selector + theSession.Events.Append(streamId, started); + theSession.SaveChanges(); + var document2 = theSession.Load(streamId); + document2.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, departed); + theSession.SaveChanges(); + var nullDocument2 = theSession.Load(streamId); + nullDocument2.ShouldBeNull(); + + // Add document back to so we can delete it by other selector type + theSession.Events.Append(streamId, started); + theSession.SaveChanges(); + var document3 = theSession.Load(streamId); + document3.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, destroyed); + theSession.SaveChanges(); + var nullDocument3 = theSession.Load(streamId); + nullDocument3.ShouldBeNull(); + } + + [Fact] + public async void from_projection_async() + { + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.InlineProjections.Add(new PersistAsyncViewProjection()); + }); + + theSession.Events.StartStream(streamId, started, joined); + await theSession.SaveChangesAsync(); + + theSession.Events.StartStream(slayed1, slayed2); + await theSession.SaveChangesAsync(); + + theSession.Events.Append(streamId, joined2); + await theSession.SaveChangesAsync(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(5); + document.Events.ShouldHaveTheSameElementsAs(started, joined, slayed1, slayed2, joined2); + + theSession.Events.Append(streamId, ended); + await theSession.SaveChangesAsync(); + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + + // Add document back to so we can delete it by selector + theSession.Events.Append(streamId, started); + await theSession.SaveChangesAsync(); + var document2 = theSession.Load(streamId); + document2.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, departed); + await theSession.SaveChangesAsync(); + var nullDocument2 = theSession.Load(streamId); + nullDocument2.ShouldBeNull(); + + // Add document back to so we can delete it by other selector type + theSession.Events.Append(streamId, started); + await theSession.SaveChangesAsync(); + var document3 = theSession.Load(streamId); + document3.Events.Count.ShouldBe(1); + + theSession.Events.Append(streamId, destroyed); + await theSession.SaveChangesAsync(); + var nullDocument3 = theSession.Load(streamId); + nullDocument3.ShouldBeNull(); + } + + [Fact] + public void using_collection_of_ids() + { + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.ProjectView() + .ProjectEventAsync((view, @event) => { view.Name = @event.Name; return Task.CompletedTask; }) + .ProjectEventAsync(e => e.QuestIds, (view, @event) => { view.Name = view.Name.Insert(0, $"{@event.Name}: "); return Task.CompletedTask; }) + .DeleteEvent(e => e.QuestIds); + }); + + theSession.Events.StartStream(streamId, started); + theSession.Events.StartStream(streamId2, started2); + theSession.SaveChanges(); + + theSession.Events.StartStream(monsterQuestsAdded); + theSession.SaveChanges(); + + var document = theSession.Load(streamId); + document.Name.ShouldStartWith(monsterQuestsAdded.Name); + var document2 = theSession.Load(streamId2); + document2.Name.ShouldStartWith(monsterQuestsAdded.Name); + + theSession.Events.StartStream(monsterQuestsRemoved); + theSession.SaveChanges(); + + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + var nullDocument2 = theSession.Load(streamId2); + nullDocument2.ShouldBeNull(); + } + + [Fact] + public async void using_collection_of_ids_async() + { + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.ProjectView() + .ProjectEventAsync((view, @event) => { view.Name = @event.Name; return Task.CompletedTask; } ) + .ProjectEventAsync(e => e.QuestIds, (view, @event) => { view.Name = view.Name.Insert(0, $"{@event.Name}: "); return Task.CompletedTask; }) + .DeleteEvent(e => e.QuestIds); + }); + + theSession.Events.StartStream(streamId, started); + theSession.Events.StartStream(streamId2, started2); + await theSession.SaveChangesAsync(); + + theSession.Events.StartStream(monsterQuestsAdded); + await theSession.SaveChangesAsync(); + + var document = theSession.Load(streamId); + document.Name.ShouldStartWith(monsterQuestsAdded.Name); + var document2 = theSession.Load(streamId2); + document2.Name.ShouldStartWith(monsterQuestsAdded.Name); + + theSession.Events.StartStream(monsterQuestsRemoved); + await theSession.SaveChangesAsync(); + + var nullDocument = theSession.Load(streamId); + nullDocument.ShouldBeNull(); + var nullDocument2 = theSession.Load(streamId2); + nullDocument2.ShouldBeNull(); + } + } + + // SAMPLE: viewprojection-from-class + public class PersistAsyncViewProjection : ViewProjection + { + public PersistAsyncViewProjection() + { + ProjectEventAsync(PersistAsync); + ProjectEventAsync(e => e.QuestId, PersistAsync); + ProjectEventAsync((session, e) => session.Load(e.QuestId).Id, PersistAsync); + DeleteEvent(); + DeleteEvent(e => e.QuestId); + DeleteEvent((session, e) => session.Load(e.QuestId).Id); + } + + private Task PersistAsync(PersistedView view, T @event) + { + view.Events.Add(@event); + return Task.CompletedTask; + } + } + // ENDSAMPLE +} \ No newline at end of file From e5d7a7984eafb588a32d4e9b80d4aa707fe57f4d Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 16 Aug 2017 20:31:50 +0200 Subject: [PATCH 03/26] Added NoSynchronizationContextScope for running async methods synchronously without deadlocks. Used it in ViewProjection applyProjections method --- .../Events/Projections/ViewProjection.cs | 5 +++- .../Util/NoSynchronizationContextScope.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/Marten/Util/NoSynchronizationContextScope.cs diff --git a/src/Marten/Events/Projections/ViewProjection.cs b/src/Marten/Events/Projections/ViewProjection.cs index 18250b327d..eeefc637f0 100644 --- a/src/Marten/Events/Projections/ViewProjection.cs +++ b/src/Marten/Events/Projections/ViewProjection.cs @@ -248,7 +248,10 @@ private void applyProjections(IDocumentSession session, ICollection + SynchronizationContext.SetSynchronizationContext(_synchronizationContext); + } + } +} From e5cc5fd07fa65651e74b5376cb1eac3f80802b62 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:33:03 +0100 Subject: [PATCH 04/26] Added Possibility To Inject Classes To Projection --- .../Projections/lazy_loaded_projection.cs | 87 +++++++++++++++++++ .../Projections/LazyLoadedProjection.cs | 42 +++++++++ .../Projections/ProjectionCollection.cs | 21 ++++- 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs create mode 100644 src/Marten/Events/Projections/LazyLoadedProjection.cs diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs new file mode 100644 index 0000000000..ed51c133f1 --- /dev/null +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Marten.Services; +using Shouldly; +using Xunit; + +namespace Marten.Testing.Events.Projections +{ + public class lazy_loaded_projection : DocumentSessionFixture + { + public class Logger + { + public List Logs { get; } = new List(); + + public void Log(string message) + { + Logs.Add(message); + } + } + + public class QuestPaused + { + public string Name { get; set; } + public Guid Id { get; set; } + + public override string ToString() + { + return $"Quest {Name} paused"; + } + } + + public class PersistViewProjectionWithInjection : PersistViewProjection + { + private readonly Logger logger; + + public PersistViewProjectionWithInjection() : base() + { + ProjectEvent(@event => @event.Id, LogAndPersist); + } + + public PersistViewProjectionWithInjection(Logger logger) + { + } + + private void LogAndPersist(PersistedView view, T @event) + { + logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}"); + view.Events.Add(@event); + } + } + + private static readonly Guid streamId = Guid.NewGuid(); + + private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; + private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; + private QuestPaused paused = new QuestPaused { Id = streamId, Name = "Find the Orb" }; + + [Fact] + public async void from_projection() + { + var logger = new Logger(); + + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger)); + }); + + theSession.Events.StartStream(streamId, started, joined); + theSession.SaveChanges(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(2); + logger.Logs.Count.ShouldBe(0); + + //check injection + theSession.Events.Append(streamId, paused); + theSession.SaveChanges(); + + var document2 = theSession.Load(streamId); + document.Events.Count.ShouldBe(3); + + logger.Logs.Count.ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/Marten/Events/Projections/LazyLoadedProjection.cs b/src/Marten/Events/Projections/LazyLoadedProjection.cs new file mode 100644 index 0000000000..0998122954 --- /dev/null +++ b/src/Marten/Events/Projections/LazyLoadedProjection.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Marten.Events.Projections.Async; +using Marten.Storage; + +namespace Marten.Events.Projections +{ + public class LazyLoadedProjection : IProjection + where T : IProjection, new() + { + private readonly Func factory; + + public LazyLoadedProjection(Func factory) + { + this.factory = factory; + var definition = new T(); + + Consumes = definition.Consumes; + AsyncOptions = definition.AsyncOptions; + } + + public Type[] Consumes { get; } + + public AsyncOptions AsyncOptions { get; } + + public void Apply(IDocumentSession session, EventPage page) + { + factory().Apply(session, page); + } + + public async Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + { + await factory().ApplyAsync(session, page, token); + } + + public void EnsureStorageExists(ITenant tenant) + { + factory().EnsureStorageExists(tenant); + } + } +} \ No newline at end of file diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index d86baafd5b..786bcf63e2 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -26,10 +26,9 @@ IEnumerator IEnumerable.GetEnumerator() } public AggregationProjection AggregateStreamsWith() where T : class, new() - { + { var aggregator = _options.Events.AggregateFor(); - IAggregationFinder finder = _options.Events.StreamIdentity == StreamIdentity.AsGuid ? (IAggregationFinder)new AggregateFinder() : new StringIdentifiedAggregateFinder(); @@ -56,11 +55,25 @@ public void Add(IProjection projection) if (projection is IDocumentProjection) { _options.Storage.MappingFor(projection.ProjectedType()); - } - + } + _projections.Add(projection); } + public void Add(Func projectionFactory) where T : IProjection, new() + { + var lazyLoadedProjection = new LazyLoadedProjection(projectionFactory); + + if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); + + if (typeof(T) is IDocumentProjection) + { + _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); + } + + _projections.Add(lazyLoadedProjection); + } + public IProjection ForView(Type viewType) { return _projections.FirstOrDefault(x => x.ProjectedType() == viewType); From d43ff1be333d4fbd172fa03ca31cf1f524ff44ec Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:55:32 +0100 Subject: [PATCH 05/26] Fixed failing test --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index ed51c133f1..a7457a722a 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -79,7 +79,7 @@ public async void from_projection() theSession.SaveChanges(); var document2 = theSession.Load(streamId); - document.Events.Count.ShouldBe(3); + document2.Events.Count.ShouldBe(3); logger.Logs.Count.ShouldBe(1); } From 4cb0a4e6f189f714b3a35aede0e06896baf40f11 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:59:23 +0100 Subject: [PATCH 06/26] Fixed failing test --- .../Events/Projections/lazy_loaded_projection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index a7457a722a..121f8acf62 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -21,7 +21,7 @@ public void Log(string message) public class QuestPaused { public string Name { get; set; } - public Guid Id { get; set; } + public Guid QuestId { get; set; } public override string ToString() { @@ -35,7 +35,7 @@ public class PersistViewProjectionWithInjection : PersistViewProjection public PersistViewProjectionWithInjection() : base() { - ProjectEvent(@event => @event.Id, LogAndPersist); + ProjectEvent(@event => @event.QuestId, LogAndPersist); } public PersistViewProjectionWithInjection(Logger logger) @@ -53,7 +53,7 @@ private void LogAndPersist(PersistedView view, T @event) private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; - private QuestPaused paused = new QuestPaused { Id = streamId, Name = "Find the Orb" }; + private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" }; [Fact] public async void from_projection() From f89bf5a360ee492770ce3f6f0a1a9b485ae819ca Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:09:48 +0100 Subject: [PATCH 07/26] Fixed failing test --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 121f8acf62..9f6c3a19cb 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -40,6 +40,7 @@ public PersistViewProjectionWithInjection() : base() public PersistViewProjectionWithInjection(Logger logger) { + this.logger = logger; } private void LogAndPersist(PersistedView view, T @event) From 63be32459423296c97a2720cda87fa5aa382ba49 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:15:28 +0100 Subject: [PATCH 08/26] Updated check if wrapped Projection is assignable to IDocumentProjection --- src/Marten/Events/Projections/ProjectionCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 786bcf63e2..8e70c030a8 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -66,7 +66,7 @@ public void Add(IProjection projection) if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); - if (typeof(T) is IDocumentProjection) + if (typeof(T).IsAssignableFrom(typeof(IDocumentProjection))) { _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); } From fa52bd209303d2f4f6326d339ea18fad3afc6f43 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:18:00 +0100 Subject: [PATCH 09/26] Updated async usage --- src/Marten/Events/Projections/LazyLoadedProjection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Marten/Events/Projections/LazyLoadedProjection.cs b/src/Marten/Events/Projections/LazyLoadedProjection.cs index 0998122954..ee35d91d85 100644 --- a/src/Marten/Events/Projections/LazyLoadedProjection.cs +++ b/src/Marten/Events/Projections/LazyLoadedProjection.cs @@ -29,9 +29,9 @@ public void Apply(IDocumentSession session, EventPage page) factory().Apply(session, page); } - public async Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + public Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) { - await factory().ApplyAsync(session, page, token); + return factory().ApplyAsync(session, page, token); } public void EnsureStorageExists(ITenant tenant) From 5c17b00489e4482ad3fa0ec523629f3564bece59 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:58:15 +0100 Subject: [PATCH 10/26] Updated checking assignment to document projection, fixed test --- .../Events/Projections/lazy_loaded_projection.cs | 2 +- src/Marten/Events/Projections/ProjectionCollection.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 9f6c3a19cb..467eb59479 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -38,7 +38,7 @@ public PersistViewProjectionWithInjection() : base() ProjectEvent(@event => @event.QuestId, LogAndPersist); } - public PersistViewProjectionWithInjection(Logger logger) + public PersistViewProjectionWithInjection(Logger logger) : this() { this.logger = logger; } diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 8e70c030a8..0271d9cf8e 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; - + +using System.Reflection; + namespace Marten.Events.Projections { public class ProjectionCollection : IEnumerable @@ -66,7 +68,7 @@ public void Add(IProjection projection) if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); - if (typeof(T).IsAssignableFrom(typeof(IDocumentProjection))) + if (typeof(T).GetTypeInfo().IsAssignableFrom(typeof(IDocumentProjection).GetTypeInfo())) { _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); } From bb14ba1b7eb36fa3e39efc6506e9fd1eeb6a37d1 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 23:02:39 +0100 Subject: [PATCH 11/26] Fixed compiler warning --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 467eb59479..b3118e3c71 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -57,7 +57,7 @@ private void LogAndPersist(PersistedView view, T @event) private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" }; [Fact] - public async void from_projection() + public void from_projection() { var logger = new Logger(); From 70071b9c625c2b1e402e6b180074ef11ec92260b Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 23:16:08 +0100 Subject: [PATCH 12/26] Empty commit to trigger build From ffa07a7888783e0f7ab4bdb2183b3f4bbcf3fc9b Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 23:45:49 +0100 Subject: [PATCH 13/26] Empty commit to trigger build From de8192930016418c4a2b67f69b644a4216e13449 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Mon, 8 Jan 2018 12:29:05 +0100 Subject: [PATCH 14/26] Empty commit to trigger build From 4e7e5790830f4d9e2a429f28bed11bc9e0a629be Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 17 Jan 2018 21:01:51 +0100 Subject: [PATCH 15/26] Added genric Add method to ProjectionCollection --- src/Marten/Events/Projections/ProjectionCollection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 0271d9cf8e..418ff841e9 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -62,6 +62,11 @@ public void Add(IProjection projection) _projections.Add(projection); } + public void Add() where T : IProjection, new() + { + Add(new T()); + } + public void Add(Func projectionFactory) where T : IProjection, new() { var lazyLoadedProjection = new LazyLoadedProjection(projectionFactory); From c1c38b4bb22bc8ffb4d023e6bd2bc7841e58eaf2 Mon Sep 17 00:00:00 2001 From: "oskar.dudycz@gmail.com" Date: Mon, 12 Feb 2018 20:40:01 +0100 Subject: [PATCH 16/26] Added documentation for Lazy Loaded Projection --- .../documentation/events/projections/custom.md | 11 ++++++++++- .../Events/Projections/lazy_loaded_projection.cs | 16 ++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/documentation/documentation/events/projections/custom.md b/documentation/documentation/events/projections/custom.md index e6ceec84f8..45134e3a10 100644 --- a/documentation/documentation/events/projections/custom.md +++ b/documentation/documentation/events/projections/custom.md @@ -14,4 +14,13 @@ or through a class like: `ProjectEvent` and `DeleteEvent` can operate on events that need a single or multiple Ids operated on. With `ProjectEvent` if a `List` is passed, the handler method will be called for each Id in the collection. With `DeleteEvent` if a `List` is passed, then each document tied to the Id in the collection will be removed. Each of these methods take various overloads that allow selecting the Id field implicitly, through a property or through two different Funcs `Func` and `Func`. -If additional Marten event details are needed, then events can use the `ProjectionEvent<>` generic when setting them up with `ProjectEvent`. `ProjectionEvent` exposes the Marten Id, Version, Timestamp and Data. \ No newline at end of file +If additional Marten event details are needed, then events can use the `ProjectionEvent<>` generic when setting them up with `ProjectEvent`. `ProjectionEvent` exposes the Marten Id, Version, Timestamp and Data. + +Projections are created during the DocumentStore creation by default. Marten gives also possible to register them with factory method. With such registration projections are created on runtime during the events application. Thanks to that it's possible to setup custom creation logic or event connect dependency injection mechanism. + +<[sample:viewprojection-from-class-with-injection-configuration]> + +By convention it's needed to provide the default constructor with projections definition and other with code injection (that calls the default constructor). + +<[sample:viewprojection-from-class-with-injection]> + diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index b3118e3c71..cba4304c94 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -29,6 +29,7 @@ public override string ToString() } } + // SAMPLE: viewprojection-from-class-with-injection public class PersistViewProjectionWithInjection : PersistViewProjection { private readonly Logger logger; @@ -48,8 +49,9 @@ private void LogAndPersist(PersistedView view, T @event) logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}"); view.Events.Add(@event); } - } - + } + // ENDSAMPLE + private static readonly Guid streamId = Guid.NewGuid(); private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; @@ -59,15 +61,17 @@ private void LogAndPersist(PersistedView view, T @event) [Fact] public void from_projection() { - var logger = new Logger(); - + var logger = new Logger(); + + // SAMPLE: viewprojection-from-class-with-injection-configuration StoreOptions(_ => { _.AutoCreateSchemaObjects = AutoCreate.All; _.Events.InlineProjections.AggregateStreamsWith(); _.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger)); - }); - + }); + // ENDSAMPLE + theSession.Events.StartStream(streamId, started, joined); theSession.SaveChanges(); From df5841b852954d1e4d1290b7f7f57bc2564bbfde Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:33:03 +0100 Subject: [PATCH 17/26] Added Possibility To Inject Classes To Projection --- .../Projections/lazy_loaded_projection.cs | 87 +++++++++++++++++++ .../Projections/LazyLoadedProjection.cs | 42 +++++++++ .../Projections/ProjectionCollection.cs | 21 ++++- 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs create mode 100644 src/Marten/Events/Projections/LazyLoadedProjection.cs diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs new file mode 100644 index 0000000000..ed51c133f1 --- /dev/null +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Marten.Services; +using Shouldly; +using Xunit; + +namespace Marten.Testing.Events.Projections +{ + public class lazy_loaded_projection : DocumentSessionFixture + { + public class Logger + { + public List Logs { get; } = new List(); + + public void Log(string message) + { + Logs.Add(message); + } + } + + public class QuestPaused + { + public string Name { get; set; } + public Guid Id { get; set; } + + public override string ToString() + { + return $"Quest {Name} paused"; + } + } + + public class PersistViewProjectionWithInjection : PersistViewProjection + { + private readonly Logger logger; + + public PersistViewProjectionWithInjection() : base() + { + ProjectEvent(@event => @event.Id, LogAndPersist); + } + + public PersistViewProjectionWithInjection(Logger logger) + { + } + + private void LogAndPersist(PersistedView view, T @event) + { + logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}"); + view.Events.Add(@event); + } + } + + private static readonly Guid streamId = Guid.NewGuid(); + + private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; + private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; + private QuestPaused paused = new QuestPaused { Id = streamId, Name = "Find the Orb" }; + + [Fact] + public async void from_projection() + { + var logger = new Logger(); + + StoreOptions(_ => + { + _.AutoCreateSchemaObjects = AutoCreate.All; + _.Events.InlineProjections.AggregateStreamsWith(); + _.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger)); + }); + + theSession.Events.StartStream(streamId, started, joined); + theSession.SaveChanges(); + + var document = theSession.Load(streamId); + document.Events.Count.ShouldBe(2); + logger.Logs.Count.ShouldBe(0); + + //check injection + theSession.Events.Append(streamId, paused); + theSession.SaveChanges(); + + var document2 = theSession.Load(streamId); + document.Events.Count.ShouldBe(3); + + logger.Logs.Count.ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/Marten/Events/Projections/LazyLoadedProjection.cs b/src/Marten/Events/Projections/LazyLoadedProjection.cs new file mode 100644 index 0000000000..0998122954 --- /dev/null +++ b/src/Marten/Events/Projections/LazyLoadedProjection.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Marten.Events.Projections.Async; +using Marten.Storage; + +namespace Marten.Events.Projections +{ + public class LazyLoadedProjection : IProjection + where T : IProjection, new() + { + private readonly Func factory; + + public LazyLoadedProjection(Func factory) + { + this.factory = factory; + var definition = new T(); + + Consumes = definition.Consumes; + AsyncOptions = definition.AsyncOptions; + } + + public Type[] Consumes { get; } + + public AsyncOptions AsyncOptions { get; } + + public void Apply(IDocumentSession session, EventPage page) + { + factory().Apply(session, page); + } + + public async Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + { + await factory().ApplyAsync(session, page, token); + } + + public void EnsureStorageExists(ITenant tenant) + { + factory().EnsureStorageExists(tenant); + } + } +} \ No newline at end of file diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index d86baafd5b..786bcf63e2 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -26,10 +26,9 @@ IEnumerator IEnumerable.GetEnumerator() } public AggregationProjection AggregateStreamsWith() where T : class, new() - { + { var aggregator = _options.Events.AggregateFor(); - IAggregationFinder finder = _options.Events.StreamIdentity == StreamIdentity.AsGuid ? (IAggregationFinder)new AggregateFinder() : new StringIdentifiedAggregateFinder(); @@ -56,11 +55,25 @@ public void Add(IProjection projection) if (projection is IDocumentProjection) { _options.Storage.MappingFor(projection.ProjectedType()); - } - + } + _projections.Add(projection); } + public void Add(Func projectionFactory) where T : IProjection, new() + { + var lazyLoadedProjection = new LazyLoadedProjection(projectionFactory); + + if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); + + if (typeof(T) is IDocumentProjection) + { + _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); + } + + _projections.Add(lazyLoadedProjection); + } + public IProjection ForView(Type viewType) { return _projections.FirstOrDefault(x => x.ProjectedType() == viewType); From 5e8d369a54bb6f6414fbbe79ee4f76b95ecfc711 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:55:32 +0100 Subject: [PATCH 18/26] Fixed failing test --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index ed51c133f1..a7457a722a 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -79,7 +79,7 @@ public async void from_projection() theSession.SaveChanges(); var document2 = theSession.Load(streamId); - document.Events.Count.ShouldBe(3); + document2.Events.Count.ShouldBe(3); logger.Logs.Count.ShouldBe(1); } From 4224fdceb62ca08d1d38ec902f495b49b5bf583f Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 21:59:23 +0100 Subject: [PATCH 19/26] Fixed failing test --- .../Events/Projections/lazy_loaded_projection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index a7457a722a..121f8acf62 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -21,7 +21,7 @@ public void Log(string message) public class QuestPaused { public string Name { get; set; } - public Guid Id { get; set; } + public Guid QuestId { get; set; } public override string ToString() { @@ -35,7 +35,7 @@ public class PersistViewProjectionWithInjection : PersistViewProjection public PersistViewProjectionWithInjection() : base() { - ProjectEvent(@event => @event.Id, LogAndPersist); + ProjectEvent(@event => @event.QuestId, LogAndPersist); } public PersistViewProjectionWithInjection(Logger logger) @@ -53,7 +53,7 @@ private void LogAndPersist(PersistedView view, T @event) private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; - private QuestPaused paused = new QuestPaused { Id = streamId, Name = "Find the Orb" }; + private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" }; [Fact] public async void from_projection() From 2cb40784875bdde8b97168b7f293ed893e5ad9b1 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:09:48 +0100 Subject: [PATCH 20/26] Fixed failing test --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 121f8acf62..9f6c3a19cb 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -40,6 +40,7 @@ public PersistViewProjectionWithInjection() : base() public PersistViewProjectionWithInjection(Logger logger) { + this.logger = logger; } private void LogAndPersist(PersistedView view, T @event) From 7d5bf53787e060ed97237bbce58e8f0d3970b28b Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:15:28 +0100 Subject: [PATCH 21/26] Updated check if wrapped Projection is assignable to IDocumentProjection --- src/Marten/Events/Projections/ProjectionCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 786bcf63e2..8e70c030a8 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -66,7 +66,7 @@ public void Add(IProjection projection) if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); - if (typeof(T) is IDocumentProjection) + if (typeof(T).IsAssignableFrom(typeof(IDocumentProjection))) { _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); } From 25f0e7f725d34d0d3c3ae5fbb3353e26014b4047 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:18:00 +0100 Subject: [PATCH 22/26] Updated async usage --- src/Marten/Events/Projections/LazyLoadedProjection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Marten/Events/Projections/LazyLoadedProjection.cs b/src/Marten/Events/Projections/LazyLoadedProjection.cs index 0998122954..ee35d91d85 100644 --- a/src/Marten/Events/Projections/LazyLoadedProjection.cs +++ b/src/Marten/Events/Projections/LazyLoadedProjection.cs @@ -29,9 +29,9 @@ public void Apply(IDocumentSession session, EventPage page) factory().Apply(session, page); } - public async Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + public Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) { - await factory().ApplyAsync(session, page, token); + return factory().ApplyAsync(session, page, token); } public void EnsureStorageExists(ITenant tenant) From ab92da2b13965791ad0957bdf318dca993715821 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 22:58:15 +0100 Subject: [PATCH 23/26] Updated checking assignment to document projection, fixed test --- .../Events/Projections/lazy_loaded_projection.cs | 2 +- src/Marten/Events/Projections/ProjectionCollection.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 9f6c3a19cb..467eb59479 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -38,7 +38,7 @@ public PersistViewProjectionWithInjection() : base() ProjectEvent(@event => @event.QuestId, LogAndPersist); } - public PersistViewProjectionWithInjection(Logger logger) + public PersistViewProjectionWithInjection(Logger logger) : this() { this.logger = logger; } diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 8e70c030a8..0271d9cf8e 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; - + +using System.Reflection; + namespace Marten.Events.Projections { public class ProjectionCollection : IEnumerable @@ -66,7 +68,7 @@ public void Add(IProjection projection) if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); - if (typeof(T).IsAssignableFrom(typeof(IDocumentProjection))) + if (typeof(T).GetTypeInfo().IsAssignableFrom(typeof(IDocumentProjection).GetTypeInfo())) { _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); } From c9c83ba60f912cae5252f510be1cfe3ad0c62028 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 6 Jan 2018 23:02:39 +0100 Subject: [PATCH 24/26] Fixed compiler warning --- src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index 467eb59479..b3118e3c71 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -57,7 +57,7 @@ private void LogAndPersist(PersistedView view, T @event) private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" }; [Fact] - public async void from_projection() + public void from_projection() { var logger = new Logger(); From c94b8446ebb10dfbe5ebe8f41f867ed3a984a786 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 17 Jan 2018 21:01:51 +0100 Subject: [PATCH 25/26] Added genric Add method to ProjectionCollection --- src/Marten/Events/Projections/ProjectionCollection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Marten/Events/Projections/ProjectionCollection.cs b/src/Marten/Events/Projections/ProjectionCollection.cs index 0271d9cf8e..418ff841e9 100644 --- a/src/Marten/Events/Projections/ProjectionCollection.cs +++ b/src/Marten/Events/Projections/ProjectionCollection.cs @@ -62,6 +62,11 @@ public void Add(IProjection projection) _projections.Add(projection); } + public void Add() where T : IProjection, new() + { + Add(new T()); + } + public void Add(Func projectionFactory) where T : IProjection, new() { var lazyLoadedProjection = new LazyLoadedProjection(projectionFactory); From ddcdd6b19d4d149be6318404cb504ff5209f6b29 Mon Sep 17 00:00:00 2001 From: "oskar.dudycz@gmail.com" Date: Mon, 12 Feb 2018 20:40:01 +0100 Subject: [PATCH 26/26] Added documentation for Lazy Loaded Projection --- .../documentation/events/projections/custom.md | 11 ++++++++++- .../Events/Projections/lazy_loaded_projection.cs | 16 ++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/documentation/documentation/events/projections/custom.md b/documentation/documentation/events/projections/custom.md index e6ceec84f8..45134e3a10 100644 --- a/documentation/documentation/events/projections/custom.md +++ b/documentation/documentation/events/projections/custom.md @@ -14,4 +14,13 @@ or through a class like: `ProjectEvent` and `DeleteEvent` can operate on events that need a single or multiple Ids operated on. With `ProjectEvent` if a `List` is passed, the handler method will be called for each Id in the collection. With `DeleteEvent` if a `List` is passed, then each document tied to the Id in the collection will be removed. Each of these methods take various overloads that allow selecting the Id field implicitly, through a property or through two different Funcs `Func` and `Func`. -If additional Marten event details are needed, then events can use the `ProjectionEvent<>` generic when setting them up with `ProjectEvent`. `ProjectionEvent` exposes the Marten Id, Version, Timestamp and Data. \ No newline at end of file +If additional Marten event details are needed, then events can use the `ProjectionEvent<>` generic when setting them up with `ProjectEvent`. `ProjectionEvent` exposes the Marten Id, Version, Timestamp and Data. + +Projections are created during the DocumentStore creation by default. Marten gives also possible to register them with factory method. With such registration projections are created on runtime during the events application. Thanks to that it's possible to setup custom creation logic or event connect dependency injection mechanism. + +<[sample:viewprojection-from-class-with-injection-configuration]> + +By convention it's needed to provide the default constructor with projections definition and other with code injection (that calls the default constructor). + +<[sample:viewprojection-from-class-with-injection]> + diff --git a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs index b3118e3c71..cba4304c94 100644 --- a/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -29,6 +29,7 @@ public override string ToString() } } + // SAMPLE: viewprojection-from-class-with-injection public class PersistViewProjectionWithInjection : PersistViewProjection { private readonly Logger logger; @@ -48,8 +49,9 @@ private void LogAndPersist(PersistedView view, T @event) logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}"); view.Events.Add(@event); } - } - + } + // ENDSAMPLE + private static readonly Guid streamId = Guid.NewGuid(); private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" }; @@ -59,15 +61,17 @@ private void LogAndPersist(PersistedView view, T @event) [Fact] public void from_projection() { - var logger = new Logger(); - + var logger = new Logger(); + + // SAMPLE: viewprojection-from-class-with-injection-configuration StoreOptions(_ => { _.AutoCreateSchemaObjects = AutoCreate.All; _.Events.InlineProjections.AggregateStreamsWith(); _.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger)); - }); - + }); + // ENDSAMPLE + theSession.Events.StartStream(streamId, started, joined); theSession.SaveChanges();