Skip to content

Commit 4082695

Browse files
committed
Merge branch 'oskardudycz-feature/AddPossibilityToInjectClassesToProjection'
2 parents a100a46 + 2cc98b2 commit 4082695

4 files changed

Lines changed: 169 additions & 6 deletions

File tree

documentation/documentation/events/projections/custom.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,13 @@ or through a class like:
1414

1515
`ProjectEvent` and `DeleteEvent` can operate on events that need a single or multiple Ids operated on. With `ProjectEvent` if a `List<TId>` is passed, the handler method will be called for each Id in the collection. With `DeleteEvent` if a `List<TId>` 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<IDocumentSession, TEvent, TId>` and `Func<TEvent, TId>`.
1616

17-
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.
17+
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.
18+
19+
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.
20+
21+
<[sample:viewprojection-from-class-with-injection-configuration]>
22+
23+
By convention it's needed to provide the default constructor with projections definition and other with code injection (that calls the default constructor).
24+
25+
<[sample:viewprojection-from-class-with-injection]>
26+
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Marten.Services;
4+
using Shouldly;
5+
using Xunit;
6+
7+
namespace Marten.Testing.Events.Projections
8+
{
9+
public class lazy_loaded_projection : DocumentSessionFixture<IdentityMap>
10+
{
11+
public class Logger
12+
{
13+
public List<string> Logs { get; } = new List<string>();
14+
15+
public void Log(string message)
16+
{
17+
Logs.Add(message);
18+
}
19+
}
20+
21+
public class QuestPaused
22+
{
23+
public string Name { get; set; }
24+
public Guid QuestId { get; set; }
25+
26+
public override string ToString()
27+
{
28+
return $"Quest {Name} paused";
29+
}
30+
}
31+
32+
// SAMPLE: viewprojection-from-class-with-injection
33+
public class PersistViewProjectionWithInjection : PersistViewProjection
34+
{
35+
private readonly Logger logger;
36+
37+
public PersistViewProjectionWithInjection() : base()
38+
{
39+
ProjectEvent<QuestPaused>(@event => @event.QuestId, LogAndPersist);
40+
}
41+
42+
public PersistViewProjectionWithInjection(Logger logger) : this()
43+
{
44+
this.logger = logger;
45+
}
46+
47+
private void LogAndPersist<T>(PersistedView view, T @event)
48+
{
49+
logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}");
50+
view.Events.Add(@event);
51+
}
52+
}
53+
// ENDSAMPLE
54+
55+
private static readonly Guid streamId = Guid.NewGuid();
56+
57+
private QuestStarted started = new QuestStarted { Id = streamId, Name = "Find the Orb" };
58+
private MembersJoined joined = new MembersJoined { QuestId = streamId, Day = 2, Location = "Faldor's Farm", Members = new[] { "Garion", "Polgara", "Belgarath" } };
59+
private QuestPaused paused = new QuestPaused { QuestId = streamId, Name = "Find the Orb" };
60+
61+
[Fact]
62+
public void from_projection()
63+
{
64+
var logger = new Logger();
65+
66+
// SAMPLE: viewprojection-from-class-with-injection-configuration
67+
StoreOptions(_ =>
68+
{
69+
_.AutoCreateSchemaObjects = AutoCreate.All;
70+
_.Events.InlineProjections.AggregateStreamsWith<QuestParty>();
71+
_.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger));
72+
});
73+
// ENDSAMPLE
74+
75+
theSession.Events.StartStream<QuestParty>(streamId, started, joined);
76+
theSession.SaveChanges();
77+
78+
var document = theSession.Load<PersistedView>(streamId);
79+
document.Events.Count.ShouldBe(2);
80+
logger.Logs.Count.ShouldBe(0);
81+
82+
//check injection
83+
theSession.Events.Append(streamId, paused);
84+
theSession.SaveChanges();
85+
86+
var document2 = theSession.Load<PersistedView>(streamId);
87+
document2.Events.Count.ShouldBe(3);
88+
89+
logger.Logs.Count.ShouldBe(1);
90+
}
91+
}
92+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Marten.Events.Projections.Async;
5+
using Marten.Storage;
6+
7+
namespace Marten.Events.Projections
8+
{
9+
public class LazyLoadedProjection<T> : IProjection
10+
where T : IProjection, new()
11+
{
12+
private readonly Func<T> factory;
13+
14+
public LazyLoadedProjection(Func<T> factory)
15+
{
16+
this.factory = factory;
17+
var definition = new T();
18+
19+
Consumes = definition.Consumes;
20+
AsyncOptions = definition.AsyncOptions;
21+
}
22+
23+
public Type[] Consumes { get; }
24+
25+
public AsyncOptions AsyncOptions { get; }
26+
27+
public void Apply(IDocumentSession session, EventPage page)
28+
{
29+
factory().Apply(session, page);
30+
}
31+
32+
public Task ApplyAsync(IDocumentSession session, EventPage page, CancellationToken token)
33+
{
34+
return factory().ApplyAsync(session, page, token);
35+
}
36+
37+
public void EnsureStorageExists(ITenant tenant)
38+
{
39+
factory().EnsureStorageExists(tenant);
40+
}
41+
}
42+
}

src/Marten/Events/Projections/ProjectionCollection.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5-
5+
6+
using System.Reflection;
7+
68
namespace Marten.Events.Projections
79
{
810
public class ProjectionCollection : IEnumerable<IProjection>
@@ -26,10 +28,9 @@ IEnumerator IEnumerable.GetEnumerator()
2628
}
2729

2830
public AggregationProjection<T> AggregateStreamsWith<T>() where T : class, new()
29-
{
31+
{
3032
var aggregator = _options.Events.AggregateFor<T>();
3133

32-
3334
IAggregationFinder<T> finder = _options.Events.StreamIdentity == StreamIdentity.AsGuid
3435
? (IAggregationFinder<T>)new AggregateFinder<T>()
3536
: new StringIdentifiedAggregateFinder<T>();
@@ -56,11 +57,30 @@ public void Add(IProjection projection)
5657
if (projection is IDocumentProjection)
5758
{
5859
_options.Storage.MappingFor(projection.ProjectedType());
59-
}
60-
60+
}
61+
6162
_projections.Add(projection);
6263
}
6364

65+
public void Add<T>() where T : IProjection, new()
66+
{
67+
Add(new T());
68+
}
69+
70+
public void Add<T>(Func<T> projectionFactory) where T : IProjection, new()
71+
{
72+
var lazyLoadedProjection = new LazyLoadedProjection<T>(projectionFactory);
73+
74+
if (lazyLoadedProjection == null) throw new ArgumentNullException(nameof(lazyLoadedProjection));
75+
76+
if (typeof(T).GetTypeInfo().IsAssignableFrom(typeof(IDocumentProjection).GetTypeInfo()))
77+
{
78+
_options.Storage.MappingFor(lazyLoadedProjection.ProjectedType());
79+
}
80+
81+
_projections.Add(lazyLoadedProjection);
82+
}
83+
6484
public IProjection ForView(Type viewType)
6585
{
6686
return _projections.FirstOrDefault(x => x.ProjectedType() == viewType);

0 commit comments

Comments
 (0)