diff --git a/Source/Projections/Builder/KeySelectorBuilder.cs b/Source/Projections/Builder/KeySelectorBuilder.cs index 1d82c3f6..a9f9e46d 100644 --- a/Source/Projections/Builder/KeySelectorBuilder.cs +++ b/Source/Projections/Builder/KeySelectorBuilder.cs @@ -14,13 +14,13 @@ public class KeySelectorBuilder /// Select projection key from the . /// /// A . - public static KeySelector KeyFromEventSource() => KeySelector.EventSource; + public static KeySelector KeyFromEventSource => KeySelector.EventSource; /// /// Select projection key from the . /// /// A . - public static KeySelector KeyFromPartitionId() => KeySelector.Partition; + public static KeySelector KeyFromPartitionId => KeySelector.Partition; /// /// Select projection key from a property of the event. @@ -46,4 +46,21 @@ public static KeySelector StaticKey(Key staticKey) /// A . public static KeySelector KeyFromEventOccurred(OccurredFormat occurredFormat) => KeySelector.Occurred(occurredFormat); + + /// + /// Select projection key from a property of the event. + /// + /// The property on the event. + /// The date time format. + /// A . + public static KeySelector KeyFromPropertyAndEventOccurred(KeySelectorExpression selectorExpression, OccurredFormat occurredFormat) + => KeySelector.PropertyAndOccured(selectorExpression, occurredFormat); + + /// + /// Select projection key from when an event occurred. + /// + /// The date time format. + /// A . + public static KeySelector KeyFromEventSourceIdAndOccurred(OccurredFormat occurredFormat) + => KeySelector.EventSourceAndOccured(occurredFormat); } diff --git a/Source/Projections/Builder/KeySelectorBuilder{TEvent}.cs b/Source/Projections/Builder/KeySelectorBuilder{TEvent}.cs index 0d477c34..1c95ff28 100644 --- a/Source/Projections/Builder/KeySelectorBuilder{TEvent}.cs +++ b/Source/Projections/Builder/KeySelectorBuilder{TEvent}.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; +using Dolittle.SDK.Events; namespace Dolittle.SDK.Projections.Builder; @@ -29,4 +30,18 @@ public KeySelector KeyFromProperty(Expression throw new KeySelectorExpressionWasNotAMemberExpression(); } + + /// + /// Select projection key from a property of the event. + /// + /// The property type. + /// The function for getting the projection key (id). + /// A . + /// Is thrown when the provided property expression is not a member expression. + public KeySelector KeyFromFunction(Func function) + { + if (function is null) throw new ArgumentNullException(nameof(function)); + + return KeySelector.ByFunction(function); + } } diff --git a/Source/Projections/IKeySelector.cs b/Source/Projections/IKeySelector.cs new file mode 100644 index 00000000..eb541efe --- /dev/null +++ b/Source/Projections/IKeySelector.cs @@ -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; + +/// +/// Use this interface to define projection key selectors. +/// Instances of this interface MUST be thread safe and stateless, as they are used as singletons. +/// +/// The mapped event type +public interface IKeySelector where TEvent : class +{ + /// + /// Map to a from an event and context. + /// + /// + /// + /// The projection key + Key Selector(TEvent @event, EventContext eventContext); +} diff --git a/Source/Projections/Internal/ProjectionsProcessor.cs b/Source/Projections/Internal/ProjectionsProcessor.cs index f918eba9..29649ccc 100644 --- a/Source/Projections/Internal/ProjectionsProcessor.cs +++ b/Source/Projections/Internal/ProjectionsProcessor.cs @@ -103,28 +103,4 @@ protected override RetryProcessingState GetRetryProcessingStateFromRequest(Handl /// protected override EventHandlerResponse CreateResponseFromFailure(ProcessorFailure failure) => new() { Failure = failure }; - - static ProjectionEventSelector CreateProjectionEventSelector(EventSelector eventSelector) - { - static ProjectionEventSelector WithEventType(EventSelector eventSelector, Action 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) - }; - } } diff --git a/Source/Projections/KeyFromEventOccurredAttribute.cs b/Source/Projections/KeyFromEventOccurredAttribute.cs index d8040033..5ef71c99 100644 --- a/Source/Projections/KeyFromEventOccurredAttribute.cs +++ b/Source/Projections/KeyFromEventOccurredAttribute.cs @@ -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; @@ -15,7 +14,7 @@ public class KeyFromEventOccurredAttribute : Attribute, IKeySelectorAttribute /// /// Initializes a new instance of the class. /// - /// The name of the property. + /// The date time format. public KeyFromEventOccurredAttribute(string occurredFormat) => OccurredFormat = occurredFormat; /// @@ -24,5 +23,5 @@ public class KeyFromEventOccurredAttribute : Attribute, IKeySelectorAttribute public OccurredFormat OccurredFormat { get; } /// - public KeySelector KeySelector => KeySelectorBuilder.KeyFromEventOccurred(OccurredFormat); + public KeySelector KeySelector => KeySelector.Occurred(OccurredFormat); } diff --git a/Source/Projections/KeyFromEventSourceAndOccurredAttribute.cs b/Source/Projections/KeyFromEventSourceAndOccurredAttribute.cs new file mode 100644 index 00000000..66bef526 --- /dev/null +++ b/Source/Projections/KeyFromEventSourceAndOccurredAttribute.cs @@ -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; + +/// +/// Decorates a projection method with the . +/// +[AttributeUsage(AttributeTargets.Method)] +public class KeyFromEventSourceAndOccurredAttribute : Attribute, IKeySelectorAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The date time format. + public KeyFromEventSourceAndOccurredAttribute(string occurredFormat) + { + OccurredFormat = occurredFormat; + } + + /// + /// Gets the . + /// + public OccurredFormat OccurredFormat { get; } + + /// + public KeySelector KeySelector => KeySelector.EventSourceAndOccured(OccurredFormat); +} diff --git a/Source/Projections/KeyFromEventSourceAttribute.cs b/Source/Projections/KeyFromEventSourceAttribute.cs index d281a591..b5ba216b 100644 --- a/Source/Projections/KeyFromEventSourceAttribute.cs +++ b/Source/Projections/KeyFromEventSourceAttribute.cs @@ -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; @@ -13,5 +12,5 @@ namespace Dolittle.SDK.Projections; public class KeyFromEventSourceAttribute : Attribute, IKeySelectorAttribute { /// - public KeySelector KeySelector { get; } = KeySelectorBuilder.KeyFromEventSource(); -} \ No newline at end of file + public KeySelector KeySelector => KeySelector.EventSource; +} diff --git a/Source/Projections/KeyFromFunctionAttribute.cs b/Source/Projections/KeyFromFunctionAttribute.cs new file mode 100644 index 00000000..07062c5c --- /dev/null +++ b/Source/Projections/KeyFromFunctionAttribute.cs @@ -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 where TKeySelector : IKeySelector, new() where TEvent : class +{ + public static TKeySelector Mapper { get; } = new(); + public static KeySelector Instance { get; } = KeySelector.ByFunction(Mapper.Selector); +} + +/// +/// Decorates a projection method with the . +/// +[AttributeUsage(AttributeTargets.Method)] +public class KeyFromFunctionAttribute : Attribute, IKeySelectorAttribute + where TKeySelector : IKeySelector, new() where TEvent : class +{ + /// + public KeySelector KeySelector => KeySelectorInstance.Instance; +} diff --git a/Source/Projections/KeyFromPartitionAttribute.cs b/Source/Projections/KeyFromPartitionAttribute.cs index 8f98fe85..b33ed1cb 100644 --- a/Source/Projections/KeyFromPartitionAttribute.cs +++ b/Source/Projections/KeyFromPartitionAttribute.cs @@ -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; @@ -13,5 +12,5 @@ namespace Dolittle.SDK.Projections; public class KeyFromPartitionAttribute : Attribute, IKeySelectorAttribute { /// - public KeySelector KeySelector { get; } = KeySelectorBuilder.KeyFromPartitionId(); -} \ No newline at end of file + public KeySelector KeySelector => KeySelector.Partition; +} diff --git a/Source/Projections/KeyFromPropertyAndOccurredAttribute.cs b/Source/Projections/KeyFromPropertyAndOccurredAttribute.cs new file mode 100644 index 00000000..1b01fc79 --- /dev/null +++ b/Source/Projections/KeyFromPropertyAndOccurredAttribute.cs @@ -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; + +/// +/// Decorates a projection method with the . +/// +[AttributeUsage(AttributeTargets.Method)] +public class KeyFromPropertyAndOccurredAttribute : Attribute, IKeySelectorAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The date time format. + public KeyFromPropertyAndOccurredAttribute(string propertyName, string occurredFormat) + { + Expression = propertyName; + OccurredFormat = occurredFormat; + } + + /// + /// Gets the . + /// + public KeySelectorExpression Expression { get; } + + /// + /// Gets the . + /// + public OccurredFormat OccurredFormat { get; } + + /// + public KeySelector KeySelector => KeySelector.PropertyAndOccured(Expression, OccurredFormat); +} diff --git a/Source/Projections/KeyFromPropertyAttribute.cs b/Source/Projections/KeyFromPropertyAttribute.cs index 16905e67..c7635a99 100644 --- a/Source/Projections/KeyFromPropertyAttribute.cs +++ b/Source/Projections/KeyFromPropertyAttribute.cs @@ -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; @@ -24,5 +23,5 @@ public class KeyFromPropertyAttribute : Attribute, IKeySelectorAttribute public KeySelectorExpression Expression { get; } /// - public KeySelector KeySelector => KeySelectorBuilder.KeyFromProperty(Expression); + public KeySelector KeySelector => KeySelector.Property(Expression); } diff --git a/Source/Projections/KeySelector.cs b/Source/Projections/KeySelector.cs index 43c4191c..47a3717d 100644 --- a/Source/Projections/KeySelector.cs +++ b/Source/Projections/KeySelector.cs @@ -12,46 +12,71 @@ namespace Dolittle.SDK.Projections; /// public class KeySelector { - KeySelector(KeySelectorType type, KeySelectorExpression expression, Key staticKey, OccurredFormat occurredFormat) + KeySelector(KeySelectorType type, KeySelectorExpression expression, Key staticKey, OccurredFormat occurredFormat, Func? function) { Type = type; Expression = expression; StaticKey = staticKey; OccurredFormat = occurredFormat; + Function = function; } + public Func? Function { get; } + /// /// Creates a . /// /// The . - public static KeySelector Partition { get; } = new(KeySelectorType.PartitionId, "", "", ""); + public static KeySelector Partition { get; } = new(KeySelectorType.PartitionId, "", "", "", null); /// /// Creates a . /// /// The . - public static KeySelector EventSource { get; } = new(KeySelectorType.EventSourceId, "", "", ""); + public static KeySelector EventSource { get; } = new(KeySelectorType.EventSourceId, "", "", "", null); + + public static KeySelector ByFunction(Func function) where TEvent : class + { + return new KeySelector(KeySelectorType.Function, "", "", "", (evt, eventContext) => function((TEvent)evt, eventContext)); + } /// /// Creates a . /// /// The . /// The . - public static KeySelector Property(KeySelectorExpression expression) => new(KeySelectorType.Property, expression, "", ""); + public static KeySelector Property(KeySelectorExpression expression) => new(KeySelectorType.Property, expression, "", "", null); /// /// Creates a . /// /// The static . /// The . - public static KeySelector Static(Key key) => new(KeySelectorType.Static, "", key, ""); + public static KeySelector Static(Key key) => new(KeySelectorType.Static, "", key, "", null); /// - /// Creates a . + /// Creates a . + /// + /// The . + /// The . + public static KeySelector Occurred(OccurredFormat occurredFormat) => new(KeySelectorType.EventOccurred, "", "", occurredFormat, null); + + /// + /// Creates a . /// + /// The . /// The . /// The . - public static KeySelector Occurred(OccurredFormat occurredFormat) => new(KeySelectorType.EventOccurred, "", "", occurredFormat); + public static KeySelector PropertyAndOccured(KeySelectorExpression expression, OccurredFormat occurredFormat) => + new(KeySelectorType.PropertyAndEventOccurred, expression, "", occurredFormat, null); + + /// + /// Creates a . + /// + /// The . + /// The . + public static KeySelector EventSourceAndOccured(OccurredFormat occurredFormat) => + new(KeySelectorType.EventSourceIdAndOccurred, "", "", occurredFormat, null); /// /// Gets the . @@ -73,18 +98,21 @@ public class KeySelector /// 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) { diff --git a/Source/Projections/KeySelectorType.cs b/Source/Projections/KeySelectorType.cs index 93a2958d..e8350d64 100644 --- a/Source/Projections/KeySelectorType.cs +++ b/Source/Projections/KeySelectorType.cs @@ -29,7 +29,22 @@ public enum KeySelectorType Static, /// - /// Gets the key the event occurred metadata. + /// Gets the key from the event occurred metadata. /// EventOccurred, + + /// + /// Gets the key from a named property on the event content and the event occurred metadata. + /// + PropertyAndEventOccurred, + + /// + /// Gets the key Gets the key from the event source id and the event occurred metadata. + /// + EventSourceIdAndOccurred, + + /// + /// The key is returned as a function of the event and the event context. + /// + Function } diff --git a/Source/Projections/StaticKeyAttribute.cs b/Source/Projections/StaticKeyAttribute.cs index 4f2f0d22..b9e0e66d 100644 --- a/Source/Projections/StaticKeyAttribute.cs +++ b/Source/Projections/StaticKeyAttribute.cs @@ -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; @@ -24,5 +23,5 @@ public class StaticKeyAttribute : Attribute, IKeySelectorAttribute public Key StaticKey { get; } /// - public KeySelector KeySelector => KeySelectorBuilder.StaticKey(StaticKey); + public KeySelector KeySelector => KeySelector.Static(StaticKey); } diff --git a/Tests/ProjectionsTests/KeyFromEventSourceAndOccurredTests.cs b/Tests/ProjectionsTests/KeyFromEventSourceAndOccurredTests.cs new file mode 100644 index 00000000..d0d8a9be --- /dev/null +++ b/Tests/ProjectionsTests/KeyFromEventSourceAndOccurredTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using Dolittle.SDK.Projections.Occurred; +using Dolittle.SDK.Testing.Projections; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace Dolittle.SDK.Projections; + +public class KeyFromEventSourceAndOccurredTests : ProjectionTests +{ + [Fact] + public void ShouldAggregateByDayAndStore() + { + WithEvent("store1", new ProductSold("store1", "product1", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent("store1", new ProductSold("store1", "product2", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent("store1", new ProductSold("store1", "product1", 10, 10), DateTimeOffset.Parse("2024-01-02", NumberFormatInfo.InvariantInfo)); + WithEvent("store2", new ProductSold("store2", "product1", 5, 10), DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + + using var _ = new AssertionScope(); + + AssertThat.HasReadModel("store1_2024-01-01").Where( + it => it.TotalSales.Should().Be(2000), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + + AssertThat.HasReadModel("store1_2024-01-02").Where( + it => it.TotalSales.Should().Be(100), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 2)) + ); + + AssertThat.HasReadModel("store2_2024-01-01").Where( + it => it.TotalSales.Should().Be(50), + it => it.Store.Should().Be("store2"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + } +} diff --git a/Tests/ProjectionsTests/KeyFromFunctionTests.cs b/Tests/ProjectionsTests/KeyFromFunctionTests.cs new file mode 100644 index 00000000..e2bb78c6 --- /dev/null +++ b/Tests/ProjectionsTests/KeyFromFunctionTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using Dolittle.SDK.Projections.Occurred; +using Dolittle.SDK.Testing.Projections; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace Dolittle.SDK.Projections; + +public class KeyFromFunctionTests : ProjectionTests +{ + [Fact] + public void ShouldAggregateByDayAndStore() + { + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product1", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product2", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product1", 10, 10), DateTimeOffset.Parse("2024-01-02", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store2", "product1", 5, 10), DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + + using var _ = new AssertionScope(); + + AssertThat.HasReadModel("store1_2024-01-01").Where( + it => it.TotalSales.Should().Be(2000), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + + AssertThat.HasReadModel("store1_2024-01-02").Where( + it => it.TotalSales.Should().Be(100), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 2)) + ); + + AssertThat.HasReadModel("store2_2024-01-01").Where( + it => it.TotalSales.Should().Be(50), + it => it.Store.Should().Be("store2"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + } +} diff --git a/Tests/ProjectionsTests/KeyFromPropertyAndOccurredTests.cs b/Tests/ProjectionsTests/KeyFromPropertyAndOccurredTests.cs new file mode 100644 index 00000000..a8bb5d5b --- /dev/null +++ b/Tests/ProjectionsTests/KeyFromPropertyAndOccurredTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using Dolittle.SDK.Projections.Occurred; +using Dolittle.SDK.Testing.Projections; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +namespace Dolittle.SDK.Projections; + +public class KeyFromPropertyAndOccurredTests : ProjectionTests +{ + [Fact] + public void ShouldAggregateByDayAndStore() + { + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product1", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product2", 10, 100), + DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store1", "product1", 10, 10), DateTimeOffset.Parse("2024-01-02", NumberFormatInfo.InvariantInfo)); + WithEvent(Guid.NewGuid().ToString(), new ProductSold("store2", "product1", 5, 10), DateTimeOffset.Parse("2024-01-01", NumberFormatInfo.InvariantInfo)); + + using var _ = new AssertionScope(); + + AssertThat.HasReadModel("store1_2024-01-01").Where( + it => it.TotalSales.Should().Be(2000), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + + AssertThat.HasReadModel("store1_2024-01-02").Where( + it => it.TotalSales.Should().Be(100), + it => it.Store.Should().Be("store1"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 2)) + ); + + AssertThat.HasReadModel("store2_2024-01-01").Where( + it => it.TotalSales.Should().Be(50), + it => it.Store.Should().Be("store2"), + it => it.Date.Should().Be(new DateOnly(2024, 1, 1)) + ); + } +} diff --git a/Tests/ProjectionsTests/Occurred/SalesPerDay.cs b/Tests/ProjectionsTests/Occurred/SalesPerDay.cs new file mode 100644 index 00000000..639ae7aa --- /dev/null +++ b/Tests/ProjectionsTests/Occurred/SalesPerDay.cs @@ -0,0 +1,80 @@ +// 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.Occurred; + +[EventType("37c19042-3ac8-4ec0-8de1-9ecb4b8c56b0")] +public record ProductSold(string Store, string Product, decimal Quantity, decimal Price); + +[Projection("3dce944f-279e-4150-bef3-dd9e113220c6")] +public class SalesPerDayByStoreProperty : ReadModel +{ + public string Store { get; private set; } + public DateOnly Date { get; private set; } + + public decimal TotalSales { get; private set; } + + [KeyFromPropertyAndOccurred(nameof(ProductSold.Store), "yyyy-MM-dd")] + public void On(ProductSold evt, EventContext ctx) + { + if (string.IsNullOrEmpty(Store)) + { + Store = evt.Store; + Date = new DateOnly(ctx.Occurred.Year, ctx.Occurred.Month, ctx.Occurred.Day); + } + + TotalSales += evt.Quantity * evt.Price; + } +} + +[Projection("3dce944f-279e-4150-bef3-dd9e113220c6")] +public class SalesPerDayTotalByEventSource : ReadModel +{ + public string Store { get; private set; } + public DateOnly Date { get; private set; } + + public decimal TotalSales { get; private set; } + + [KeyFromEventSourceAndOccurred("yyyy-MM-dd")] + public void On(ProductSold evt, EventContext ctx) + { + if (string.IsNullOrEmpty(Store)) + { + Store = evt.Store; + Date = new DateOnly(ctx.Occurred.Year, ctx.Occurred.Month, ctx.Occurred.Day); + } + + TotalSales += evt.Quantity * evt.Price; + } +} + +class ProductKeySelector: IKeySelector +{ + public Key Selector(ProductSold evt, EventContext ctx) => $"{evt.Store}_{ctx.Occurred.Year}-{ctx.Occurred.Month:D2}-{ctx.Occurred.Day:D2}"; +} + +[Projection("3dce944f-279e-4150-bef3-dd9e113220c6")] +public class SalesPerDayTotalByFunction : ReadModel +{ + public string Store { get; private set; } + public DateOnly Date { get; private set; } + + public decimal TotalSales { get; private set; } + + [KeyFromFunction] + public void On(ProductSold evt, EventContext ctx) + { + if (string.IsNullOrEmpty(Store)) + { + Store = evt.Store; + Date = new DateOnly(ctx.Occurred.Year, ctx.Occurred.Month, ctx.Occurred.Day); + } + + TotalSales += evt.Quantity * evt.Price; + } + + public static string GetKey(ProductSold evt, EventContext ctx) => $"{evt.Store}_{ctx.Occurred.Year}-{ctx.Occurred.Month:D2}-{ctx.Occurred.Day:D2}"; +} +