Skip to content

DataSeeder

The DataSeeder is the cornerstone of the entire data seeding feature, the central component that is responsible for handling the entire process.

Implementing a data seeder can be as simple as implementing the IDataSeeder interface and the required SeedAsync method, where your seeding logic should be placed. Our current approach encourages creating a separate data seeder for each required entity/message, in order to keep a clean separation of concerns.

For example, we may implement a data seeder that uses a repository for creating the required data:

C#
public class MyDataSeeder : IDataSeeder
{
    private readonly IRepository<Guid, MyEntity> repository;

    public MyDataSeeder(IRepository<Guid, MyEntity repository>)
    {
        this.repository = repository;
    }

    public Task SeedAsync(CancellationToken ct = default)
    {
        // Your seeding logic goes here.
    }
}

Message-based seeding

While seeding data for a particular service might seem good enough, it has a major flaw when used in the context of a microservices architecture: the fact that the data is created directly against the repository does not allow other services to be aware of the events that took place in our current service. This makes repository-based seeding an anti-pattern for the majority of scenarios and should be implemented only in those particular cases where the data should not be made public to the ecosystem (and will never be).

Having this in mind, it makes sense for us to perform our seeding operations as close as possible to how a business operation would take place, making profit of the messages and consumers we already have in place to handle the entire task. A MessagingDataSeeder would act as a message publisher with the required data we expect to seed, leveraging the creation logic to the actual service responsible of handling it.

Publish only

Messaging data-seeding can be performed in two ways. The first and most natural one is to publish the messages with our data and assume all services that know how to handle such message to perform the required changes to their internal state. As messages are, by default, published to a fanout exchange we can be certain that each service expecting the message will eventually receive a copy of it.

In order to implement a publish data seeder we can implement the base class provided by the module:

C#
1
2
3
4
5
6
7
[TransientDependency(typeof(IDataSeeder))]
public class MyPublishSeeder : MessagingDataSeeder<MyMessage>
{
    public MyPublishSeeder(IDataSource<MyMessage> dataSource, IPublishEndpoint publishEndpoint) : base(dataSource, publishEndpoint)
    {
    }
}

Note that the data seeder expects an IDataSource<MyMessage> to be registered, so be aware of that. No further configuration is required for this to work.

Request/Response

Another strategy for message-based data seeding is publishing a message but expecting for a specific response. In this case, any service capable of handling the request can respond to it. We only wait for the first response as a successful operation confirmation.

C#
1
2
3
4
5
6
7
[TransientDependency(typeof(IDataSeeder))]
public class MyRequestResponseSeeder : MessagingDataSeeder<MyMessage, MyResponse>
{
    public MyRequestResponseSeeder(IRequestClient<MyMessage> requestClient, IDataSource<MyMessage> dataSource) : base(requestClient, dataSource)
    {
    }
}

Important

The usage of publish or request/response seeders is closely related to the nature of the message being sent. As a general rule (and, of course, this has exceptions), a command should be expected to succeed since we are explicitly telling the system to do something. In the other hand, an event can be seen as a change that already took place in the system and about which we are only notifying. As we cannot ensure that every service listening to an event has succeeded in updating its internal state, it makes sense for us to simply publish the event and hope for the best.

DataSeeder lifetime

It is important to note that each possible implementation of a data seeder (and its execution) has a different lifetime and ultimately has a dependency over infrastructure concerns, and ultimately, over deployment stages.

A repository-based data seeder only requires the database to be up and running in order to perform its tasks. This allows for an early execution: it might be expected even before service startup (in fact, the DataSeedingModule allows to configure this behavior).

A message-based seeder, on the other hand, is inherently dependant on the bus to be ready so we can start publishing messages:

  • For a publish-only seeder, being connected to the bus is fair enough since it is the broker who is responsible for delivering the message to the consumers.
  • A request/response seeder also requires that at least one consumer is alive and available to perform the required operation and respond with the expected message.

This dependency over the bus state means we need a different way to attach our data seeder to the ecosystem. This can be as simple as calling the seeder from a bus observer or even include it in our DataSeeder service composition in case we don't want to rely on a specific service.