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 new file mode 100644 index 0000000000..cba4304c94 --- /dev/null +++ b/src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs @@ -0,0 +1,92 @@ +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 QuestId { get; set; } + + public override string ToString() + { + return $"Quest {Name} paused"; + } + } + + // SAMPLE: viewprojection-from-class-with-injection + public class PersistViewProjectionWithInjection : PersistViewProjection + { + private readonly Logger logger; + + public PersistViewProjectionWithInjection() : base() + { + ProjectEvent(@event => @event.QuestId, LogAndPersist); + } + + public PersistViewProjectionWithInjection(Logger logger) : this() + { + this.logger = logger; + } + + 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" }; + private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } }; + private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" }; + + [Fact] + public void from_projection() + { + 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(); + + 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); + document2.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..ee35d91d85 --- /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 Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token) + { + return 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..418ff841e9 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 @@ -26,10 +28,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 +57,30 @@ public void Add(IProjection projection) if (projection is IDocumentProjection) { _options.Storage.MappingFor(projection.ProjectedType()); - } - + } + _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); + + if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection)); + + if (typeof(T).GetTypeInfo().IsAssignableFrom(typeof(IDocumentProjection).GetTypeInfo())) + { + _options.Storage.MappingFor(lazyLoadedProjection.ProjectedType()); + } + + _projections.Add(lazyLoadedProjection); + } + public IProjection ForView(Type viewType) { return _projections.FirstOrDefault(x => x.ProjectedType() == viewType);