Skip to content

Commit

Permalink
Time-keyed projections (#239)
Browse files Browse the repository at this point in the history
* Enable property & eventsource to be used with occurred in projections, allowing useful aggregations.
Also added support for specifying a function to create the key directly, with even more flexibility.

* Projection Key by function via KeyFromFunctionAttribute
  • Loading branch information
mhelleborg authored Mar 14, 2024
1 parent a10c9b4 commit 9216f63
Show file tree
Hide file tree
Showing 18 changed files with 425 additions and 51 deletions.
21 changes: 19 additions & 2 deletions Source/Projections/Builder/KeySelectorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ public class KeySelectorBuilder
/// Select projection key from the <see cref="EventSourceId"/>.
/// </summary>
/// <returns>A <see cref="KeySelector"/>.</returns>
public static KeySelector KeyFromEventSource() => KeySelector.EventSource;
public static KeySelector KeyFromEventSource => KeySelector.EventSource;

/// <summary>
/// Select projection key from the <see cref="PartitionId"/>.
/// </summary>
/// <returns>A <see cref="KeySelector"/>.</returns>
public static KeySelector KeyFromPartitionId() => KeySelector.Partition;
public static KeySelector KeyFromPartitionId => KeySelector.Partition;

/// <summary>
/// Select projection key from a property of the event.
Expand All @@ -46,4 +46,21 @@ public static KeySelector StaticKey(Key staticKey)
/// <returns>A <see cref="KeySelector"/>.</returns>
public static KeySelector KeyFromEventOccurred(OccurredFormat occurredFormat)
=> KeySelector.Occurred(occurredFormat);

/// <summary>
/// Select projection key from a property of the event.
/// </summary>
/// <param name="selectorExpression">The property on the event.</param>
/// <param name="occurredFormat">The date time format.</param>
/// <returns>A <see cref="KeySelector"/>.</returns>
public static KeySelector KeyFromPropertyAndEventOccurred(KeySelectorExpression selectorExpression, OccurredFormat occurredFormat)
=> KeySelector.PropertyAndOccured(selectorExpression, occurredFormat);

/// <summary>
/// Select projection key from when an event occurred.
/// </summary>
/// <param name="occurredFormat">The date time format.</param>
/// <returns>A <see cref="KeySelector"/>.</returns>
public static KeySelector KeyFromEventSourceIdAndOccurred(OccurredFormat occurredFormat)
=> KeySelector.EventSourceAndOccured(occurredFormat);
}
15 changes: 15 additions & 0 deletions Source/Projections/Builder/KeySelectorBuilder{TEvent}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Linq.Expressions;
using Dolittle.SDK.Events;

namespace Dolittle.SDK.Projections.Builder;

Expand All @@ -29,4 +30,18 @@ public KeySelector KeyFromProperty<TProperty>(Expression<Func<TEvent, TProperty>

throw new KeySelectorExpressionWasNotAMemberExpression();
}

/// <summary>
/// Select projection key from a property of the event.
/// </summary>
/// <typeparam name="TProperty">The property type.</typeparam>
/// <param name="function">The function for getting the projection key (id).</param>
/// <returns>A <see cref="KeySelector"/>.</returns>
/// <exception cref="KeySelectorExpressionWasNotAMemberExpression">Is thrown when the provided property expression is not a member expression.</exception>
public KeySelector KeyFromFunction(Func<TEvent, EventContext, Key> function)
{
if (function is null) throw new ArgumentNullException(nameof(function));

return KeySelector.ByFunction(function);
}
}
22 changes: 22 additions & 0 deletions Source/Projections/IKeySelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Dolittle.SDK.Events;

namespace Dolittle.SDK.Projections;

/// <summary>
/// Use this interface to define projection key selectors.
/// Instances of this interface MUST be thread safe and stateless, as they are used as singletons.
/// </summary>
/// <typeparam name="TEvent">The mapped event type</typeparam>
public interface IKeySelector<TEvent> where TEvent : class
{
/// <summary>
/// Map to a <see cref="Key"/> from an event and context.
/// </summary>
/// <param name="event"></param>
/// <param name="eventContext"></param>
/// <returns>The projection key</returns>
Key Selector(TEvent @event, EventContext eventContext);
}
24 changes: 0 additions & 24 deletions Source/Projections/Internal/ProjectionsProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,28 +103,4 @@ protected override RetryProcessingState GetRetryProcessingStateFromRequest(Handl
/// <inheritdoc/>
protected override EventHandlerResponse CreateResponseFromFailure(ProcessorFailure failure)
=> new() { Failure = failure };

static ProjectionEventSelector CreateProjectionEventSelector(EventSelector eventSelector)
{
static ProjectionEventSelector WithEventType(EventSelector eventSelector, Action<ProjectionEventSelector> callback)
{
var message = new ProjectionEventSelector();
callback(message);
message.EventType = eventSelector.EventType.ToProtobuf();
return message;
}

return eventSelector.KeySelector.Type switch
{
KeySelectorType.EventSourceId => WithEventType(eventSelector, _ => _.EventSourceKeySelector = new EventSourceIdKeySelector()),
KeySelectorType.PartitionId => WithEventType(eventSelector, _ => _.PartitionKeySelector = new PartitionIdKeySelector()),
KeySelectorType.Property => WithEventType(eventSelector,
_ => _.EventPropertyKeySelector = new EventPropertyKeySelector { PropertyName = eventSelector.KeySelector.Expression ?? string.Empty }),
KeySelectorType.Static => WithEventType(eventSelector,
_ => _.StaticKeySelector = new StaticKeySelector { StaticKey = eventSelector.KeySelector.StaticKey ?? string.Empty }),
KeySelectorType.EventOccurred => WithEventType(eventSelector,
_ => _.EventOccurredKeySelector = new EventOccurredKeySelector { Format = eventSelector.KeySelector.OccurredFormat ?? string.Empty }),
_ => throw new UnknownKeySelectorType(eventSelector.KeySelector.Type)
};
}
}
5 changes: 2 additions & 3 deletions Source/Projections/KeyFromEventOccurredAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Dolittle.SDK.Projections.Builder;

namespace Dolittle.SDK.Projections;

Expand All @@ -15,7 +14,7 @@ public class KeyFromEventOccurredAttribute : Attribute, IKeySelectorAttribute
/// <summary>
/// Initializes a new instance of the <see cref="KeyFromEventOccurredAttribute"/> class.
/// </summary>
/// <param name="occurredFormat">The name of the property.</param>
/// <param name="occurredFormat">The date time format.</param>
public KeyFromEventOccurredAttribute(string occurredFormat) => OccurredFormat = occurredFormat;

/// <summary>
Expand All @@ -24,5 +23,5 @@ public class KeyFromEventOccurredAttribute : Attribute, IKeySelectorAttribute
public OccurredFormat OccurredFormat { get; }

/// <inheritdoc/>
public KeySelector KeySelector => KeySelectorBuilder.KeyFromEventOccurred(OccurredFormat);
public KeySelector KeySelector => KeySelector.Occurred(OccurredFormat);
}
31 changes: 31 additions & 0 deletions Source/Projections/KeyFromEventSourceAndOccurredAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace Dolittle.SDK.Projections;

/// <summary>
/// Decorates a projection method with the <see cref="KeySelectorType.Property" />.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class KeyFromEventSourceAndOccurredAttribute : Attribute, IKeySelectorAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="KeyFromEventSourceAndOccurredAttribute"/> class.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
/// <param name="occurredFormat">The date time format.</param>
public KeyFromEventSourceAndOccurredAttribute(string occurredFormat)
{
OccurredFormat = occurredFormat;
}

/// <summary>
/// Gets the <see cref="OccurredFormat" />.
/// </summary>
public OccurredFormat OccurredFormat { get; }

/// <inheritdoc/>
public KeySelector KeySelector => KeySelector.EventSourceAndOccured(OccurredFormat);
}
5 changes: 2 additions & 3 deletions Source/Projections/KeyFromEventSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Dolittle.SDK.Projections.Builder;

namespace Dolittle.SDK.Projections;

Expand All @@ -13,5 +12,5 @@ namespace Dolittle.SDK.Projections;
public class KeyFromEventSourceAttribute : Attribute, IKeySelectorAttribute
{
/// <inheritdoc/>
public KeySelector KeySelector { get; } = KeySelectorBuilder.KeyFromEventSource();
}
public KeySelector KeySelector => KeySelector.EventSource;
}
23 changes: 23 additions & 0 deletions Source/Projections/KeyFromFunctionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace Dolittle.SDK.Projections;

static class KeySelectorInstance<TKeySelector, TEvent> where TKeySelector : IKeySelector<TEvent>, new() where TEvent : class
{
public static TKeySelector Mapper { get; } = new();
public static KeySelector Instance { get; } = KeySelector.ByFunction<TEvent>(Mapper.Selector);
}

/// <summary>
/// Decorates a projection method with the <see cref="KeySelectorType.Function" />.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class KeyFromFunctionAttribute<TKeySelector, TEvent> : Attribute, IKeySelectorAttribute
where TKeySelector : IKeySelector<TEvent>, new() where TEvent : class
{
/// <inheritdoc/>
public KeySelector KeySelector => KeySelectorInstance<TKeySelector, TEvent>.Instance;
}
5 changes: 2 additions & 3 deletions Source/Projections/KeyFromPartitionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Dolittle.SDK.Projections.Builder;

namespace Dolittle.SDK.Projections;

Expand All @@ -13,5 +12,5 @@ namespace Dolittle.SDK.Projections;
public class KeyFromPartitionAttribute : Attribute, IKeySelectorAttribute
{
/// <inheritdoc/>
public KeySelector KeySelector { get; } = KeySelectorBuilder.KeyFromPartitionId();
}
public KeySelector KeySelector => KeySelector.Partition;
}
37 changes: 37 additions & 0 deletions Source/Projections/KeyFromPropertyAndOccurredAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace Dolittle.SDK.Projections;

/// <summary>
/// Decorates a projection method with the <see cref="KeySelectorType.Property" />.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class KeyFromPropertyAndOccurredAttribute : Attribute, IKeySelectorAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="KeyFromPropertyAttribute"/> class.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
/// <param name="occurredFormat">The date time format.</param>
public KeyFromPropertyAndOccurredAttribute(string propertyName, string occurredFormat)
{
Expression = propertyName;
OccurredFormat = occurredFormat;
}

/// <summary>
/// Gets the <see cref="KeySelector" />.
/// </summary>
public KeySelectorExpression Expression { get; }

/// <summary>
/// Gets the <see cref="OccurredFormat" />.
/// </summary>
public OccurredFormat OccurredFormat { get; }

/// <inheritdoc/>
public KeySelector KeySelector => KeySelector.PropertyAndOccured(Expression, OccurredFormat);
}
3 changes: 1 addition & 2 deletions Source/Projections/KeyFromPropertyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Dolittle.SDK.Projections.Builder;

namespace Dolittle.SDK.Projections;

Expand All @@ -24,5 +23,5 @@ public class KeyFromPropertyAttribute : Attribute, IKeySelectorAttribute
public KeySelectorExpression Expression { get; }

/// <inheritdoc/>
public KeySelector KeySelector => KeySelectorBuilder.KeyFromProperty(Expression);
public KeySelector KeySelector => KeySelector.Property(Expression);
}
50 changes: 39 additions & 11 deletions Source/Projections/KeySelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,71 @@ namespace Dolittle.SDK.Projections;
/// </summary>
public class KeySelector
{
KeySelector(KeySelectorType type, KeySelectorExpression expression, Key staticKey, OccurredFormat occurredFormat)
KeySelector(KeySelectorType type, KeySelectorExpression expression, Key staticKey, OccurredFormat occurredFormat, Func<object, EventContext, Key>? function)
{
Type = type;
Expression = expression;
StaticKey = staticKey;
OccurredFormat = occurredFormat;
Function = function;
}

public Func<object, EventContext, Key>? Function { get; }

/// <summary>
/// Creates a <see cref="KeySelectorType.PartitionId"/> <see cref="KeySelector"/>.
/// </summary>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector Partition { get; } = new(KeySelectorType.PartitionId, "", "", "");
public static KeySelector Partition { get; } = new(KeySelectorType.PartitionId, "", "", "", null);

/// <summary>
/// Creates a <see cref="KeySelectorType.EventSourceId"/> <see cref="KeySelector"/>.
/// </summary>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector EventSource { get; } = new(KeySelectorType.EventSourceId, "", "", "");
public static KeySelector EventSource { get; } = new(KeySelectorType.EventSourceId, "", "", "", null);

public static KeySelector ByFunction<TEvent>(Func<TEvent, EventContext, Key> function) where TEvent : class
{
return new KeySelector(KeySelectorType.Function, "", "", "", (evt, eventContext) => function((TEvent)evt, eventContext));
}

/// <summary>
/// Creates a <see cref="KeySelectorType.Property"/> <see cref="KeySelector"/>.
/// </summary>
/// <param name="expression">The <see cref="KeySelectorExpression"/>.</param>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector Property(KeySelectorExpression expression) => new(KeySelectorType.Property, expression, "", "");
public static KeySelector Property(KeySelectorExpression expression) => new(KeySelectorType.Property, expression, "", "", null);

/// <summary>
/// Creates a <see cref="KeySelectorType.Property"/> <see cref="KeySelector"/>.
/// </summary>
/// <param name="key">The static <see cref="Key"/>.</param>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector Static(Key key) => new(KeySelectorType.Static, "", key, "");
public static KeySelector Static(Key key) => new(KeySelectorType.Static, "", key, "", null);

/// <summary>
/// Creates a <see cref="KeySelectorType.Property"/> <see cref="KeySelector"/>.
/// Creates a <see cref="KeySelectorType.EventOccurred"/> <see cref="KeySelector"/>.
/// </summary>
/// <param name="occurredFormat">The <see cref="OccurredFormat"/>.</param>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector Occurred(OccurredFormat occurredFormat) => new(KeySelectorType.EventOccurred, "", "", occurredFormat, null);

/// <summary>
/// Creates a <see cref="KeySelectorType.PropertyAndEventOccurred"/> <see cref="KeySelector"/>.
/// </summary>
/// <param name="expression">The <see cref="KeySelectorExpression"/>.</param>
/// <param name="occurredFormat">The <see cref="OccurredFormat"/>.</param>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector Occurred(OccurredFormat occurredFormat) => new(KeySelectorType.EventOccurred, "", "", occurredFormat);
public static KeySelector PropertyAndOccured(KeySelectorExpression expression, OccurredFormat occurredFormat) =>
new(KeySelectorType.PropertyAndEventOccurred, expression, "", occurredFormat, null);

/// <summary>
/// Creates a <see cref="KeySelectorType.EventSourceIdAndOccurred"/> <see cref="KeySelector"/>.
/// </summary>
/// <param name="occurredFormat">The <see cref="OccurredFormat"/>.</param>
/// <returns>The <see cref="KeySelector"/>.</returns>
public static KeySelector EventSourceAndOccured(OccurredFormat occurredFormat) =>
new(KeySelectorType.EventSourceIdAndOccurred, "", "", occurredFormat, null);

/// <summary>
/// Gets the <see cref="KeySelectorType" />.
Expand All @@ -73,18 +98,21 @@ public class KeySelector
/// </summary>
public OccurredFormat OccurredFormat { get; }

public Key GetKey(object evt, EventContext eventContext)
{
return Type switch
public Key GetKey(object evt, EventContext eventContext) =>
Type switch
{
KeySelectorType.PartitionId => eventContext.EventSourceId.Value,
KeySelectorType.EventSourceId => eventContext.EventSourceId.Value,
KeySelectorType.Static => StaticKey,
KeySelectorType.EventOccurred => eventContext.Occurred.ToString(OccurredFormat.Value, CultureInfo.InvariantCulture),
KeySelectorType.Property => GetProperty(evt, Expression),
KeySelectorType.Function => Function!.Invoke(evt, eventContext),
KeySelectorType.EventSourceIdAndOccurred =>
$"{eventContext.EventSourceId.Value}_{eventContext.Occurred.ToString(OccurredFormat.Value, CultureInfo.InvariantCulture)}",
KeySelectorType.PropertyAndEventOccurred =>
$"{GetProperty(evt, Expression)}_{eventContext.Occurred.ToString(OccurredFormat.Value, CultureInfo.InvariantCulture)}",
_ => eventContext.EventSourceId.Value
};
}

static string GetProperty(object evt, KeySelectorExpression expression)
{
Expand Down
Loading

0 comments on commit 9216f63

Please sign in to comment.