Event Store Projections – Counting events of a specific type
Our first Event Store Projections example simply counts the number of events of a specific type. Pretty mundane, I know, but it’s a good place to start.
In this example, the projection will take all MeasurementRead events from all streams and increase a counter in its global state for each one. The end result is a global state containing the number of MeasurementRead events.
JavaScript implementation
The projection logic is implemented in a self-revealing module. This pattern allows us to decide which methods of a module should be accessible from outside, much like creating a class with public and private methods. The pattern is probably overkill for this projection, since it only has public methods, but it allows us to create unit tests for its logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var eventCounter = function () { var init = function () { return { count: }; }; var increase = function (state, eventEnvelope) { state.count += 1; return state; }; return { init: init, increase: increase }; }; |
The init method initializes the state for the first time by setting the count property to 0. The increase method increases the count property by 1 when called.
This module is instantiated once globally and then referenced from the projection’s definition:
1 2 3 4 5 6 7 8 9 10 11 | var counter = eventCounter(); options({ producesResults: true }); fromAll() .when({ $init: counter.init, MeasurementRead: counter.increase }); |
The fromAll definition tells the engine that the projection’s events come from all streams. Inside, we delegate $init and MeasurementRead to our counter module implementation. The $init field defines the method that initializes the projection’s state, and the MeasurementRead field defines that all MeasurementRead events are to be handled by the counter.increase method.
producesResults is set to true so that the projection’s state is stored in a stream. This allows us to subscribe to projection state updates without polling and use the output of the projection as input for other projections, as I’ll demonstrate later. In the future, we can replace this with the outputState method.
Testing the projection
I used Jasmine to test this logic. Jasmine is a really nice BDD framework for testing JavaScript code that encourages developers to write readable test scenarios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /// /// describe("when counting measurement reads", function () { var defaultEvent = { body: {} }; var counter; beforeEach(function () { counter = eventCounter(); }); it("should initialize new state with count 0", function () { var initState = counter.init(); expect(initState.count).toEqual(); }); it("should count the number of events", function () { var state = counter.init(); state = counter.increase(state, defaultEvent); state = counter.increase(state, defaultEvent); state = counter.increase(state, defaultEvent); expect(state.count).toEqual(3); }); }); |
The test contains two cases: the first one verifies the initial state of the projection, and the second one verifies the count property is increased by 1 for each call of the increase method. The first two lines define the referenced JavaScript files, which enables Intellisense in Visual Studio and allows R# to run the tests from your IDE.
Querying the results with the Client API
To read the value of the projection, we use the getState method of the ProjectionsManager class.
Since we specified that the output of the state should be stored in a stream, we can subscribe to event updates without polling:
1 2 3 4 5 6 | _connection.SubscribeToStream( "$projections-MeasurementReadCounter-result", false, (subscription, resolvedEvent) => // do something with the state updates, (subscription, subscriptionDropReason, exception) => // do something when subscription is dropped, EventStoreCredentials.Default); |
Source code
A working project for this example can be found on github: https://github.com/tim-cools/EventStore-Examples
Event Store Projections by Example
This post is part of a series:
Leave a Reply
Want to join the discussion?Feel free to contribute!