Skip to content

Commit

Permalink
Merge branch 'oskardudycz-feature/AddPossibilityToInjectClassesToProj…
Browse files Browse the repository at this point in the history
…ection'
  • Loading branch information
jeremydmiller committed Mar 6, 2018
2 parents a100a46 + 2cc98b2 commit 4082695
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 6 deletions.
11 changes: 10 additions & 1 deletion documentation/documentation/events/projections/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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>`.

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.
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]>

92 changes: 92 additions & 0 deletions src/Marten.Testing/Events/Projections/lazy_loaded_projection.cs
Original file line number Diff line number Diff line change
@@ -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<IdentityMap>
{
public class Logger
{
public List<string> Logs { get; } = new List<string>();

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<QuestPaused>(@event => @event.QuestId, LogAndPersist);
}

public PersistViewProjectionWithInjection(Logger logger) : this()
{
this.logger = logger;
}

private void LogAndPersist<T>(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<QuestParty>();
_.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger));
});
// ENDSAMPLE

theSession.Events.StartStream<QuestParty>(streamId, started, joined);
theSession.SaveChanges();

var document = theSession.Load<PersistedView>(streamId);
document.Events.Count.ShouldBe(2);
logger.Logs.Count.ShouldBe(0);

//check injection
theSession.Events.Append(streamId, paused);
theSession.SaveChanges();

var document2 = theSession.Load<PersistedView>(streamId);
document2.Events.Count.ShouldBe(3);

logger.Logs.Count.ShouldBe(1);
}
}
}
42 changes: 42 additions & 0 deletions src/Marten/Events/Projections/LazyLoadedProjection.cs
Original file line number Diff line number Diff line change
@@ -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<T> : IProjection
where T : IProjection, new()
{
private readonly Func<T> factory;

public LazyLoadedProjection(Func<T> 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);
}
}
}
30 changes: 25 additions & 5 deletions src/Marten/Events/Projections/ProjectionCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IProjection>
Expand All @@ -26,10 +28,9 @@ IEnumerator IEnumerable.GetEnumerator()
}

public AggregationProjection<T> AggregateStreamsWith<T>() where T : class, new()
{
{
var aggregator = _options.Events.AggregateFor<T>();


IAggregationFinder<T> finder = _options.Events.StreamIdentity == StreamIdentity.AsGuid
? (IAggregationFinder<T>)new AggregateFinder<T>()
: new StringIdentifiedAggregateFinder<T>();
Expand All @@ -56,11 +57,30 @@ public void Add(IProjection projection)
if (projection is IDocumentProjection)
{
_options.Storage.MappingFor(projection.ProjectedType());
}
}

_projections.Add(projection);
}

public void Add<T>() where T : IProjection, new()
{
Add(new T());
}

public void Add<T>(Func<T> projectionFactory) where T : IProjection, new()
{
var lazyLoadedProjection = new LazyLoadedProjection<T>(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);
Expand Down

0 comments on commit 4082695

Please sign in to comment.