Read model stores¶
In order to create query handlers that perform and enable them search across multiple fields, read models or projections are used.
To get started you can use the built-in in-memory read model store, but EventFlow supports a few others as well.
Creating read models¶
Read models are a flattened view of a subset or all aggregate domain events created specifically for efficient queries.
Here’s a simple example of how a read model for doing searches for
usernames could look. The read model handles the UserCreated
domain
event to get the username and user ID.
public class UserReadModel : IReadModel,
IAmReadModelFor<UserAggregate, UserId, UserCreated>
{
public string UserId { get; set; }
public string Username { get; set; }
public void Apply(
IReadModelContext context,
IDomainEvent<UserAggregate, UserId, UserCreated> domainEvent)
{
UserId = domainEvent.AggregateIdentity.Value;
Username = domainEvent.AggregateEvent.Username.Value;
}
}
The read model applies all UserCreated
events and thereby merely saves
the latest value instead of the entire history, which makes it much easier to
store in an efficient manner.
Read model locators¶
Typically the ID of read models are the aggregate identity, but sometimes this isn’t the case. Here are some examples.
- Items from a collection on the aggregate root
- Deterministic ID created from event data
- Entity within the aggregate
To create read models in these cases, use the EventFlow concept of read model locators, which is basically a mapping from a domain event to a read model ID.
As an example, consider if we could add several nicknames to a user. We
might have a domain event called UserNicknameAdded
similar to this.
public class UserNicknameAdded : AggregateEvent<UserAggregate, UserId>
{
public Nickname Nickname { get; set; }
}
We could then create a read model locator that would return the ID for each nickname we add via the event like this.
public class UserNicknameReadModelLocator : IReadModelLocator
{
public IEnumerable<string> GetReadModelIds(IDomainEvent domainEvent)
{
var userNicknameAdded = domainEvent as
IDomainEvent<UserAggregate, UserId, UserNicknameAdded>;
if (userNicknameAdded == null)
{
yield break;
}
yield return userNicknameAdded.Nickname.Id;
}
}
And then use a read model similar to this that represents each nickname.
public class UserNicknameReadModel : IReadModel,
IAmReadModelFor<UserAggregate, UserId, UserNicknameAdded>
{
public string UserId { get; set; }
public string Nickname { get; set; }
public void Apply(
IReadModelContext context,
IDomainEvent<UserAggregate, UserId, UserNicknameAdded> domainEvent)
{
UserId = domainEvent.AggregateIdentity.Value;
Nickname = domainEvent.AggregateEvent.Nickname.Value;
}
}
You may need to assign the id of your readmodel from a batch of nicknames assigned on the creation event of the username. You would then read the assigned readmodel id acquired from the locator using the ‘context’ field:
public class UserNicknameReadModel : IReadModel,
IAmReadModelFor<UserAggregate, UserId, UserCreatedEvent>
{
public string Id { get; set; }
public string UserId { get; set; }
public string Nickname { get; set; }
public void Apply(
IReadModelContext context,
IDomainEvent<UserAggregate, UserId, UserCreatedEvent> domainEvent)
{
var id = context.ReadModelId;
UserId = domainEvent.AggregateIdentity.Value;
var nickname = domainEvent.AggregateEvent.Nicknames.Single(n => n.Id == id);
Id = nickname.Id;
Nickname = nickname.Nickname;
}
}
We could then use this nickname read model to query all the nicknames
for a given user by search for read models that have a specific
UserId
.
Read store implementations¶
EventFlow has built-in support for several different read model stores.
In-memory¶
The in-memory read store is easy to use and easy to configure. All read models are stored in-memory, so if EventFlow is restarted all read models are lost.
To configure the in-memory read model store, simply call
UseInMemoryReadStoreFor<>
or UseInMemoryReadStoreFor<,>
with
your read model as the generic argument.
var resolver = EventFlowOptions.New
...
.UseInMemoryReadStoreFor<UserReadModel>()
.UseInMemoryReadStoreFor<UserNicknameReadModel,UserNicknameReadModelLocator>()
...
.CreateResolver();
Microsoft SQL Server¶
To configure the MSSQL read model store, simply call
UseMssqlReadModel<>
or UseMssqlReadModel<,>
with your read model
as the generic argument.
var resolver = EventFlowOptions.New
...
.UseMssqlReadModel<UserReadModel>()
.UseMssqlReadModel<UserNicknameReadModel,UserNicknameReadModelLocator>()
...
.CreateResolver();
By convention, EventFlow uses the table named ReadModel-[CLASS NAME]
as the table to store the read model rows in. If you need to change
this, use the Table
from the
System.ComponentModel.DataAnnotations.Schema
namespace. So in the
above example, the read model UserReadModel
would be stored in a
table called ReadModel-UserReadModel
unless stated otherwise.
To allow EventFlow to find the read models stored, a single column is
required to have the MsSqlReadModelIdentityColumn
attribute. This
will be used to store the read model ID.
You should also create an int
column that has the
MsSqlReadModelVersionColumn
attribute to tell EventFlow which column
the read model version is stored in.
Important
EventFlow expects the read model to exist, and thus any maintenance of the database schema for the read models must be handled before EventFlow is initialized. Or, at least before the read models are used in EventFlow.
Elasticsearch¶
To configure the
Elasticsearch read
model store, simply call UseElasticsearchReadModel<>
or
UseElasticsearchReadModel<,>
with your read model as the generic
argument.
var resolver = EventFlowOptions.New
...
.ConfigureElasticsearch(new Uri("http://localhost:9200/"))
...
.UseElasticsearchReadModel<UserReadModel>()
.UseElasticsearchReadModel<UserNicknameReadModel,UserNicknameReadModelLocator>()
...
.CreateResolver();
Overloads of ConfigureElasticsearch(...)
are available for
alternative Elasticsearch configurations.
Important
Make sure to create any mapping the read model requires in Elasticsearch before using the read model in EventFlow.
If EventFlow receives a request to purge a specific read model, it does it by deleting the index. This means that a separate index should be created for each read model.
If you want to control the index a specific read model is stored in,
create an implementation of IReadModelDescriptionProvider
and
register it in the EventFlow IoC.
Mongo DB¶
To configure the Mongo DB read model store, call UseMongoDbReadModel<>
or
UseMongoDbReadModel<,>
with your read model as the generic
argument.
var resolver = EventFlowOptions.New
...
.UseMongoDbReadModel<UserReadModel>()
.UseMongoDbReadModel<UserNicknameReadModel,UserNicknameReadModelLocator>()
...
.CreateResolver();