Storage is a persistence layer for the StreamStore library.
To create your own storage implementation, you need to implement the following interfaces:
- IStreamStorage - provides methods for working with streams.
- ISchemaProvisioner - provides methods for provisioning storage schema.
- ITenantStreamStorageProvider interface, provider of IStreamStorage for particular tenant.
- ITenantSchemaProvisionerFactory interface, factory of ISchemaProvisioner for particular tenant.
However, there is much easier way to do so.
You can use base implementations of the interfaces provided by StreamStore.Storage package.
For to do so, you need to create implementation of the following abstract classes:
-
StorageConfiguratorBase - provides methods for configuring storage and registering it in DI container.
Example: SQLite storage configurator
// Example of SQLite storage configurator internal class StorageConfigurator: StorageConfiguratorBase { readonly SqlStorageConfiguration config = SqliteConfiguration.DefaultConfiguration; public StorageConfigurator() { } public StorageConfigurator(SqlStorageConfiguration configuration) { config = configuration.ThrowIfNull(nameof(configuration)); } protected override void ConfigureStorage(StorageDependencyRegistrator registrator) { registrator.RegisterStorage<SqlStreamStorage>(); } protected override void ConfigureSchemaProvisioner(SchemaProvisionerRegistrator registrator) { // Register the PostgreSQL schema provisioner registrator.RegisterSchemaProvisioner<SqlSchemaProvisioner>(); } protected override void ConfigureAdditionalDependencies(IServiceCollection services) { services.AddSingleton(config); services.AddSingleton<IDbConnectionFactory, SqliteDbConnectionFactory>(); services.AddSingleton<IDapperCommandFactory, DefaultDapperCommandFactory>(); services.AddSingleton<ISqlExceptionHandler, SqliteExceptionHandler>(); services.AddSingleton<ISqlQueryProvider, DefaultSqlQueryProvider>(); services.AddSingleton<IMigrator, SqliteMigrator>(); services.AddSingleton(new MigrationConfiguration { MigrationAssembly = typeof(SqliteMigrator).Assembly }); } }
-
MultitenancyConfiguratorBase - provides methods for configuring multitenant aspect of storage and registering it in DI container.
Example: SQLite multitenancy configurator
internal class MultitenancyConfigurator : MultitenancyConfiguratorBase { readonly Action<SqlMultitenancyConfigurator> configure; public MultitenancyConfigurator(Action<SqlMultitenancyConfigurator> configure) { this.configure = configure.ThrowIfNull(nameof(configure)); } protected override void ConfigureStorageProvider(StorageProviderRegistrator registrator) { registrator.RegisterStorageProvider(serviceProvider => serviceProvider.GetRequiredService<SqliteTenantStorageProvider>().GetStorage); } protected override void ConfigureSchemaProvisionerFactory(SchemaProvisionerFactoryRegistrator registrator) { registrator.RegisterSchemaProvisioningFactory(provider => provider.GetRequiredService<SqliteSchemaProvisionerFactory>().Create); } protected override void ConfigureAdditionalDependencies(IServiceCollection services) { services.ThrowIfNull(nameof(services)); services.AddSingleton<ISqlTenantStorageConfigurationProvider,SqlTenantStorageConfigurationProvider>(); services.AddSingleton<SqliteTenantStorageProvider>(); configure(new SqlMultitenancyConfigurator(services)); services.AddSingleton<SqliteSchemaProvisionerFactory>(); } }
After that you can register your storage implementation in the DI container by creating extension method for IServiceCollection and using existing extensions.
Example: SQLite storage service collection extensions
public static class ServiceCollectionExtension
{
public static IServiceCollection UseSqlite(this IServiceCollection services)
{
services.ConfigurePersistence(new StorageConfigurator(SqliteConfiguration.DefaultConfiguration));
return services;
}
public static IServiceCollection UseSqlite(this IServiceCollection services, IConfiguration configuration)
{
configuration.ThrowIfNull(nameof(configuration));
services.ConfigurePersistence(
new StorageConfigurator(
SqlStorageConfigurationBuilder.ReadFromConfig(
configuration,
SqliteConfiguration.ConfigurationSection,
SqliteConfiguration.DefaultConfiguration)));
return services;
}
public static IServiceCollection UseSqlite(this IServiceCollection services, Action<SqlStorageConfigurationBuilder> configure)
{
configure.ThrowIfNull(nameof(configure));
services.ConfigurePersistence(
new StorageConfigurator(
new SqlStorageConfigurationBuilder(
SqliteConfiguration.DefaultConfiguration,
configure).Build()));
return services;
}
public static IServiceCollection UseSqliteWithMultitenancy(this IServiceCollection services, SqlStorageConfiguration defaultConfig, Action<SqlMultitenancyConfigurator> configure)
{
configure.ThrowIfNull(nameof(defaultConfig));
configure.ThrowIfNull(nameof(configure));
services.ConfigurePersistenceMultitenancy(
new StorageConfigurator(defaultConfig),
new MultitenancyConfigurator(configure));
return services;
}
public static IServiceCollection UseSqliteWithMultitenancy(this IServiceCollection services, Action<SqlMultitenancyConfigurator> configure)
{
return services.UseSqliteWithMultitenancy(SqliteConfiguration.DefaultConfiguration, configure);
}
}-
To implement your own storage you do not need StreamStore package, all necessary interfaces are located in StreamStore.Storage.Contracts and base implementations in StreamStore.Storage package.
-
You can register your own storage implementation in the DI container using any kind of lifetime (i.e. Singleton, Transient, Scoped, etc.)
However, if you register it as a singleton, you should be aware that it should be thread-safe and preferably stateless. -
Solution already provides optimistic concurrency and event duplication control mechanisms, as a pre-check during stream opening.
However, if you need consistency guaranteed, you should implement your own mechanisms as a part of IStreamWriter implementation.
For instance, you can use a transaction mechanism supported byACID compliant DBMS. -
Get and Delete operations must be implemented as idempotent by their nature.
