Skip to content

Latest commit

 

History

History
194 lines (147 loc) · 8.43 KB

File metadata and controls

194 lines (147 loc) · 8.43 KB

Storage

Storage is a persistence layer for the StreamStore library.

Create your own storage implementation

Implement your own storage from scratch

To create your own storage implementation, you need to implement the following interfaces:

Single tenant mode

Multitenant mode

Storage class diagram

However, there is much easier way to do so.

Use base implementations

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);
        }
    }

Considerations

  • 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 by ACID compliant DBMS.

  • Get and Delete operations must be implemented as idempotent by their nature.