diff --git a/ClickHouse.Facades.sln b/ClickHouse.Facades.sln index ddc81ca..2a75823 100644 --- a/ClickHouse.Facades.sln +++ b/ClickHouse.Facades.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouse.Facades.Example" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouse.Facades.Tests", "src\ClickHouse.Facades.Tests\ClickHouse.Facades.Tests.csproj", "{EE903EEE-DD9E-4402-B758-0E2B13363138}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouse.Facades.Testing", "src\ClickHouse.Facades.Testing\ClickHouse.Facades.Testing.csproj", "{677931CB-3677-44A0-A358-0C32ADAC479A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {EE903EEE-DD9E-4402-B758-0E2B13363138}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE903EEE-DD9E-4402-B758-0E2B13363138}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE903EEE-DD9E-4402-B758-0E2B13363138}.Release|Any CPU.Build.0 = Release|Any CPU + {677931CB-3677-44A0-A358-0C32ADAC479A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {677931CB-3677-44A0-A358-0C32ADAC479A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {677931CB-3677-44A0-A358-0C32ADAC479A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {677931CB-3677-44A0-A358-0C32ADAC479A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/ClickHouse.Facades.Testing/ClickHouse.Facades.Testing.csproj b/src/ClickHouse.Facades.Testing/ClickHouse.Facades.Testing.csproj new file mode 100644 index 0000000..2936d26 --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouse.Facades.Testing.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.1 + enable + enable + default + + + + + + + + + + + diff --git a/src/ClickHouse.Facades.Testing/ClickHouseConnectionBrokerStub.cs b/src/ClickHouse.Facades.Testing/ClickHouseConnectionBrokerStub.cs new file mode 100644 index 0000000..24bc649 --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouseConnectionBrokerStub.cs @@ -0,0 +1,90 @@ +using System.Data; +using System.Data.Common; +using ClickHouse.Client.ADO; +using ClickHouse.Client.Copy; +using Microsoft.Extensions.DependencyInjection; + +namespace ClickHouse.Facades.Testing; + +internal class ClickHouseConnectionBrokerStub : ClickHouseConnectionBroker + where TContext : ClickHouseContext +{ + private readonly ClickHouseConnectionTracker _tracker; + private readonly ClickHouseConnectionResponseProducer _responseProducer; + + public ClickHouseConnectionBrokerStub( + IServiceProvider serviceProvider, + ClickHouseConnection connection) : base(connection) + { + _tracker = serviceProvider.GetRequiredService>(); + _responseProducer = serviceProvider.GetRequiredService>(); + } + + internal override string? ServerVersion => _responseProducer.ServerVersion; + + internal override string? ServerTimezone => _responseProducer.ServerTimezone; + + internal override ClickHouseCommand CreateCommand() + { + throw new NotImplementedException(); + } + + internal override Task ExecuteNonQueryAsync(string statement, CancellationToken cancellationToken) + { + var result = _responseProducer.TryGetResponse(TestQueryType.ExecuteNonQuery, statement, out var response) + ? (int) response! + : 0; + + _tracker.Add(new ClickHouseTestResponse(TestQueryType.ExecuteNonQuery, statement, result)); + + return Task.FromResult(result); + } + + internal override Task ExecuteScalarAsync(string query, CancellationToken cancellationToken) + { + var result = _responseProducer.TryGetResponse(TestQueryType.ExecuteScalar, query, out var response) + ? response! + : 0; + + _tracker.Add(new ClickHouseTestResponse(TestQueryType.ExecuteScalar, query, result)); + + return Task.FromResult(result); + } + + internal override Task ExecuteReaderAsync(string query, CancellationToken cancellationToken) + { + var result = _responseProducer.TryGetResponse(TestQueryType.ExecuteReader, query, out var response) + ? (DbDataReader) response! + : new DataTableReader(new DataTable()); + + _tracker.Add(new ClickHouseTestResponse(TestQueryType.ExecuteReader, query, result.HasRows)); + + return Task.FromResult(result); + } + + internal override DataTable ExecuteDataTable(string query, CancellationToken cancellationToken) + { + var responseMocked = _responseProducer + .TryGetResponse(TestQueryType.ExecuteReader, query, out var response); + + var result = new DataTable(); + + if (responseMocked) + { + result.Load((DbDataReader) response!); + } + + _tracker.Add(new ClickHouseTestResponse(TestQueryType.ExecuteReader, query, result)); + + return result; + } + + internal override Task BulkInsertAsync( + string destinationTable, + Func saveAction, + int batchSize, + int maxDegreeOfParallelism) + { + throw new NotImplementedException(); + } +} diff --git a/src/ClickHouse.Facades.Testing/ClickHouseConnectionResponseProducer.cs b/src/ClickHouse.Facades.Testing/ClickHouseConnectionResponseProducer.cs new file mode 100644 index 0000000..94c8288 --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouseConnectionResponseProducer.cs @@ -0,0 +1,56 @@ +namespace ClickHouse.Facades.Testing; + +internal class ClickHouseConnectionResponseProducer + where TContext : ClickHouseContext +{ + private readonly Dictionary, Func)>> _responseDictionary = new(); + + private Func? _serverVersionProvider; + private Func? _serverTimezoneProvider; + + internal void SetServerVersionProvider(Func serverVersionProvider) + { + _serverVersionProvider = serverVersionProvider; + } + + internal void SetServerTimezoneProvider(Func serverTimezoneProvider) + { + _serverTimezoneProvider = serverTimezoneProvider; + } + + internal string? ServerVersion => _serverVersionProvider?.Invoke(); + internal string? ServerTimezone => _serverTimezoneProvider?.Invoke(); + + internal void Add(TestQueryType queryType, Predicate sqlPredicate, Func result) + { + if (!_responseDictionary.ContainsKey(queryType)) + { + _responseDictionary.Add(queryType, new List<(Predicate, Func)>()); + } + + _responseDictionary[queryType].Add((sqlPredicate, result)); + } + + internal bool TryGetResponse(TestQueryType queryType, string sql, out object? response) + { + if (!_responseDictionary.ContainsKey(queryType)) + { + response = null; + return false; + } + + foreach (var match in _responseDictionary[queryType]) + { + if (match.Item1(sql)) + { + response = match.Item2(); + + return true; + } + } + + response = null; + + return false; + } +} diff --git a/src/ClickHouse.Facades.Testing/ClickHouseConnectionTracker.cs b/src/ClickHouse.Facades.Testing/ClickHouseConnectionTracker.cs new file mode 100644 index 0000000..b9a9ced --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouseConnectionTracker.cs @@ -0,0 +1,25 @@ +namespace ClickHouse.Facades.Testing; + +internal class ClickHouseConnectionTracker + where TContext : ClickHouseContext +{ + private readonly Dictionary _records = new(); + private int _recordsCount = 0; + + internal void Add(ClickHouseTestResponse record) + { + _records.Add(++_recordsCount, record); + } + + public IReadOnlyCollection GetAllRecords() + { + return _records.Select(r => r.Value).ToList(); + } + + public ClickHouseTestResponse GetRecord(int index) + { + return _records[index]; + } + + public int RecordsCount => _recordsCount; +} diff --git a/src/ClickHouse.Facades.Testing/ClickHouseFacadesTestsCore.cs b/src/ClickHouse.Facades.Testing/ClickHouseFacadesTestsCore.cs new file mode 100644 index 0000000..3e99b09 --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouseFacadesTestsCore.cs @@ -0,0 +1,126 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace ClickHouse.Facades.Testing; + +public class ClickHouseFacadesTestsCore +{ + private readonly IServiceCollection _serviceCollection; + private IServiceProvider _serviceProvider; + + + [SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")] + protected ClickHouseFacadesTestsCore() + { + _serviceCollection = new ServiceCollection(); + SetupServiceCollection(_serviceCollection); + _serviceProvider = _serviceCollection.BuildServiceProvider(); + } + + protected T GetService() where T : notnull + { + return _serviceProvider.GetRequiredService(); + } + + /// + /// Is called in constructor. Should never access class members. + /// + protected virtual void SetupServiceCollection(IServiceCollection services) + { + + } + + protected void UpdateServiceCollection(Action action) + { + action(_serviceCollection); + + _serviceProvider = _serviceCollection.BuildServiceProvider(); + } + + protected void MockExecuteNonQuery(Predicate sqlPredicate, Func resultProvider) + where TContext : ClickHouseContext + { + GetService>() + .Add(TestQueryType.ExecuteNonQuery, sqlPredicate, () => resultProvider()); + } + + protected void MockExecuteScalar(Predicate sqlPredicate, Func resultProvider) + where TContext : ClickHouseContext + { + GetService>() + .Add(TestQueryType.ExecuteScalar, sqlPredicate, resultProvider); + } + + protected void MockExecuteReader( + Predicate sqlPredicate, + IEnumerable rows, + params (string ColumnName, Type DataType, Func PropertySelector)[] columns) + where TContext : ClickHouseContext + { + var dataTable = new DataTable(); + + foreach (var column in columns) + { + dataTable.Columns.Add(column.ColumnName, column.DataType); + } + + foreach (var row in rows) + { + var dataRow = dataTable.NewRow(); + + foreach (var column in columns) + { + dataRow[column.ColumnName] = column.PropertySelector(row) ?? DBNull.Value; + } + + dataTable.Rows.Add(dataRow); + } + + var dataReader = dataTable.CreateDataReader(); + + GetService>() + .Add(TestQueryType.ExecuteReader, sqlPredicate, () => dataReader); + } + + protected IReadOnlyCollection GetClickHouseResponses() + where TContext : ClickHouseContext + { + return GetService>().GetAllRecords(); + } + + protected ClickHouseTestResponse GetClickHouseResponse(int index) + where TContext : ClickHouseContext + { + return GetService>().GetRecord(index); + } + + protected int GetClickHouseResponsesCount() + where TContext : ClickHouseContext + { + return GetService>().RecordsCount; + } + + protected void MockServerVersion(Func valueProvider) + where TContext : ClickHouseContext + { + GetService>().SetServerVersionProvider(valueProvider); + } + + protected void MockServerTimezone(Func valueProvider) + where TContext : ClickHouseContext + { + GetService>().SetServerTimezoneProvider(valueProvider); + } + + protected void MockFacadeAbstraction(TAbstraction mock) + where TAbstraction : class + { + UpdateServiceCollection(services => + { + services.RemoveAll(); + services.AddTransient(_ => mock); + }); + } +} diff --git a/src/ClickHouse.Facades.Testing/ClickHouseTestResponse.cs b/src/ClickHouse.Facades.Testing/ClickHouseTestResponse.cs new file mode 100644 index 0000000..b20bdfc --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ClickHouseTestResponse.cs @@ -0,0 +1,22 @@ +namespace ClickHouse.Facades.Testing; + +public enum TestQueryType +{ + ExecuteNonQuery = 1, + ExecuteScalar, + ExecuteReader, +} + +public class ClickHouseTestResponse +{ + public TestQueryType QueryType { get; } + public string Sql { get; } + public object? Result { get; } + + internal ClickHouseTestResponse(TestQueryType queryType, string sql, object? result) + { + QueryType = queryType; + Sql = sql; + Result = result; + } +} diff --git a/src/ClickHouse.Facades.Testing/ServiceCollectionExtensions.cs b/src/ClickHouse.Facades.Testing/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..79e197e --- /dev/null +++ b/src/ClickHouse.Facades.Testing/ServiceCollectionExtensions.cs @@ -0,0 +1,37 @@ +using ClickHouse.Facades.Utility; +using Microsoft.Extensions.DependencyInjection; + +namespace ClickHouse.Facades.Testing; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddClickHouseTestContext( + this IServiceCollection services, + Action> builderAction) + where TContext : ClickHouseContext, new() + where TContextFactory : ClickHouseContextFactory + { + ExceptionHelpers.ThrowIfNull(builderAction); + + var descriptor = new ServiceDescriptor( + typeof(IClickHouseContextFactory), + serviceProvider => ActivatorUtilities + .CreateInstance(serviceProvider) + .Setup( + serviceProvider.GetRequiredService>(), + connection => new ClickHouseConnectionBrokerStub(serviceProvider, connection)), + ServiceLifetime.Singleton); + + services.Add(descriptor); + + var builder = ClickHouseContextServiceBuilder.Create; + builderAction(builder); + builder.Build(services); + + services.AddSingleton>(); + services.AddSingleton>(); + services.AddSingleton>(); + + return services; + } +} diff --git a/src/ClickHouse.Facades.Tests/ClickHouse.Facades.Tests.csproj b/src/ClickHouse.Facades.Tests/ClickHouse.Facades.Tests.csproj index 533752b..b7f20b4 100644 --- a/src/ClickHouse.Facades.Tests/ClickHouse.Facades.Tests.csproj +++ b/src/ClickHouse.Facades.Tests/ClickHouse.Facades.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/ClickHouse.Facades.Tests/Migrations/ClickHouseMigratorTests.cs b/src/ClickHouse.Facades.Tests/Migrations/ClickHouseMigratorTests.cs new file mode 100644 index 0000000..e451c45 --- /dev/null +++ b/src/ClickHouse.Facades.Tests/Migrations/ClickHouseMigratorTests.cs @@ -0,0 +1,158 @@ +using ClickHouse.Facades.Migrations; +using ClickHouse.Facades.Testing; +using Moq; + +namespace ClickHouse.Facades.Tests; + +[TestClass] +public class ClickHouseMigratorTests : ClickHouseFacadesTestsCore +{ + [TestMethod] + public async Task NoMigrations_ApplyMigrations_MigrationsTableCreated() + { + var databaseName = "test"; + SetupMigrations(databaseName, rollbackOnMigrationFail: false); + SetupAppliedMigrations(databaseName); + + + await GetService().ApplyMigrationsAsync(); + + + Assert.AreEqual(3, GetClickHouseResponsesCount()); + + Assert.AreEqual( + $"create database if not exists {databaseName}\nengine = Atomic", + GetClickHouseResponse(1).Sql); + + Assert.IsTrue( + GetClickHouseResponse(2) + .Sql + .StartsWith($"create table if not exists {databaseName}.db_migrations_history")); + } + + [TestMethod] + public async Task OneMigrationToApply_ApplyMigrations_MigrationApplied() + { + var databaseName = "test"; + + Mock<_1_FirstMigration> migrationMock = new(); + migrationMock + .Setup(m => m.Up(It.IsAny())) + .Callback(b => b.AddRawSqlStatement("apply migration")); + + SetupMigrations(databaseName, rollbackOnMigrationFail: false, migrationMock.Object); + SetupAppliedMigrations(databaseName); + + + await GetService().ApplyMigrationsAsync(); + + + Assert.AreEqual(5, GetClickHouseResponsesCount()); + + Assert.AreEqual("apply migration", GetClickHouseResponse(4).Sql); + + Assert.AreEqual( + $"insert into {databaseName}.db_migrations_history values " + + $"({_1_FirstMigration.MigrationIndex}, '{_1_FirstMigration.MigrationName}', 0)", + GetClickHouseResponse(5).Sql); + } + + [TestMethod] + public async Task FailingMigrationToApply_RollbackEnabled_ApplyMigrations_MigrationRolledBack() + { + var databaseName = "test"; + + Mock<_1_FirstMigration> migrationMock = new(); + migrationMock + .Setup(m => m.Up(It.IsAny())) + .Callback(b => b.AddRawSqlStatement("apply migration")); + + migrationMock + .Setup(m => m.Down(It.IsAny())) + .Callback(b => b.AddRawSqlStatement("rollback migration")); + + SetupMigrations(databaseName, rollbackOnMigrationFail: true, migrationMock.Object); + SetupAppliedMigrations(databaseName); + + MockExecuteNonQuery( + sql => sql == "apply migration", + () => throw new Exception("test exception")); + + + await Assert.ThrowsExceptionAsync( + () => GetService().ApplyMigrationsAsync()); + + + Assert.AreEqual(5, GetClickHouseResponsesCount()); + + Assert.AreEqual("rollback migration", GetClickHouseResponse(4).Sql); + + Assert.AreEqual( + $"insert into {databaseName}.db_migrations_history values " + + $"({_1_FirstMigration.MigrationIndex}, '{_1_FirstMigration.MigrationName}', 1)", + GetClickHouseResponse(5).Sql); + } + + [TestMethod] + public async Task FailingMigrationToApply_RollbackDisabled_ApplyMigrations_Throws() + { + var databaseName = "test"; + + Mock<_1_FirstMigration> migrationMock = new(); + migrationMock + .Setup(m => m.Up(It.IsAny())) + .Callback(b => b.AddRawSqlStatement("apply migration")); + + migrationMock + .Setup(m => m.Down(It.IsAny())) + .Callback(b => b.AddRawSqlStatement("rollback migration")); + + SetupMigrations(databaseName, rollbackOnMigrationFail: false, migrationMock.Object); + SetupAppliedMigrations(databaseName); + + MockExecuteNonQuery( + sql => sql == "apply migration", + () => throw new Exception("test exception")); + + + await Assert.ThrowsExceptionAsync( + () => GetService().ApplyMigrationsAsync()); + + + Assert.AreEqual(3, GetClickHouseResponsesCount()); + } + + private void SetupMigrations( + string databaseName, + bool rollbackOnMigrationFail, + params ClickHouseMigration[] migrations) + { + Mock migrationInstructionsMock = new(); + migrationInstructionsMock + .Setup(m => m.GetConnectionString()) + .Returns("host=localhost;port=8123;database=test;"); + migrationInstructionsMock + .Setup(m => m.DatabaseName) + .Returns(databaseName); + migrationInstructionsMock + .Setup(m => m.RollbackOnMigrationFail) + .Returns(rollbackOnMigrationFail); + + Mock migrationsLocatorMock = new(); + migrationsLocatorMock + .Setup(m => m.GetMigrations()) + .Returns(migrations); + + UpdateServiceCollection(services => + services.AddClickHouseTestMigrations(migrationInstructionsMock.Object, migrationsLocatorMock.Object)); + } + + private void SetupAppliedMigrations(string databaseName, params AppliedMigration[] appliedMigrations) + { + MockExecuteReader( + sql => sql == $"select id, name from {databaseName}.db_migrations_history final", + appliedMigrations, + ("id", typeof(ulong), m => m.Id), + ("name", typeof(string), m => m.Name)); + } +} diff --git a/src/ClickHouse.Facades.Tests/ServiceCollectionExtensions.cs b/src/ClickHouse.Facades.Tests/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2883a94 --- /dev/null +++ b/src/ClickHouse.Facades.Tests/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using ClickHouse.Facades.Migrations; +using ClickHouse.Facades.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace ClickHouse.Facades.Tests; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddClickHouseTestMigrations( + this IServiceCollection services, + TInstructions instructions, + TLocator locator) + where TInstructions : class, IClickHouseMigrationInstructions + where TLocator : class, IClickHouseMigrationsLocator + { + return services + .AddSingleton(_ => instructions) + .AddSingleton(_ => locator) + .AddClickHouseTestContext( + builder => builder + .AddFacade()) + .AddSingleton(); + } +} diff --git a/src/ClickHouse.Facades/AssemblyInfo.cs b/src/ClickHouse.Facades/AssemblyInfo.cs index 337f62b..f0d8d57 100644 --- a/src/ClickHouse.Facades/AssemblyInfo.cs +++ b/src/ClickHouse.Facades/AssemblyInfo.cs @@ -1,3 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("ClickHouse.Facades.Tests")] +[assembly: InternalsVisibleTo("ClickHouse.Facades.Testing")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/ClickHouse.Facades/Context/ClickHouseConnectionBroker.cs b/src/ClickHouse.Facades/Context/ClickHouseConnectionBroker.cs new file mode 100644 index 0000000..f516275 --- /dev/null +++ b/src/ClickHouse.Facades/Context/ClickHouseConnectionBroker.cs @@ -0,0 +1,117 @@ +using System.Data; +using System.Data.Common; +using ClickHouse.Client.ADO; +using ClickHouse.Client.Copy; +using ClickHouse.Client.Utility; +using ClickHouse.Facades.Utility; + +namespace ClickHouse.Facades; + +internal class ClickHouseConnectionBroker +{ + private const string UseSessionConnectionStringParameter = "usesession"; + + private readonly ClickHouseConnection _connection; + private readonly bool _sessionEnabled; + + public ClickHouseConnectionBroker(ClickHouseConnection connection) + { + if (_connection != null) + { + throw new InvalidOperationException($"{GetType()} is already connected."); + } + + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + + _sessionEnabled = connection.ConnectionString + .GetConnectionStringParameters() + .Contains(new KeyValuePair(UseSessionConnectionStringParameter, true.ToString())); + } + + internal virtual string? ServerVersion => _connection.ServerVersion; + + internal virtual string? ServerTimezone => _connection.ServerTimezone; + + internal virtual ClickHouseCommand CreateCommand() + { + ThrowIfNotConnected(); + + return _connection.CreateCommand(); + } + + internal virtual Task ExecuteScalarAsync(string query, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + cancellationToken.ThrowIfCancellationRequested(); + + return _connection.ExecuteScalarAsync(query); + } + + internal virtual Task ExecuteNonQueryAsync(string statement, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + cancellationToken.ThrowIfCancellationRequested(); + + return _connection.ExecuteStatementAsync(statement); + } + + internal virtual Task ExecuteReaderAsync(string query, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + cancellationToken.ThrowIfCancellationRequested(); + + return _connection.ExecuteReaderAsync(query); + } + + internal virtual DataTable ExecuteDataTable(string query, CancellationToken cancellationToken) + { + ThrowIfNotConnected(); + cancellationToken.ThrowIfCancellationRequested(); + + return _connection.ExecuteDataTable(query); + } + + internal virtual async Task BulkInsertAsync( + string destinationTable, + Func saveAction, + int batchSize, + int maxDegreeOfParallelism) + { + ThrowIfNotConnected(); + + if (destinationTable.IsNullOrWhiteSpace()) + { + throw new ArgumentException($"{nameof(destinationTable)} is null or whitespace."); + } + + if (batchSize < 1) + { + throw new ArgumentException($"{nameof(batchSize)} is lower than 1."); + } + + switch (maxDegreeOfParallelism) + { + case < 1: + throw new ArgumentException($"{nameof(maxDegreeOfParallelism)} is lower than 1."); + case > 1 when _sessionEnabled: + throw new InvalidOperationException($"Sessions are not compatible with parallel insertion."); + } + + using var bulkCopyInterface = new ClickHouseBulkCopy(_connection); + bulkCopyInterface.DestinationTableName = destinationTable; + bulkCopyInterface.BatchSize = batchSize; + bulkCopyInterface.MaxDegreeOfParallelism = maxDegreeOfParallelism; + + await saveAction(bulkCopyInterface); + + return bulkCopyInterface.RowsWritten; + } + + private void ThrowIfNotConnected() + { + if (_connection == null) + { + throw new InvalidOperationException($"{GetType()} is not connected."); + } + } +} diff --git a/src/ClickHouse.Facades/Context/ClickHouseContext.cs b/src/ClickHouse.Facades/Context/ClickHouseContext.cs index 523c94e..2008a80 100644 --- a/src/ClickHouse.Facades/Context/ClickHouseContext.cs +++ b/src/ClickHouse.Facades/Context/ClickHouseContext.cs @@ -6,9 +6,10 @@ public abstract class ClickHouseContext : IDisposable, IAsyncDisposabl where TContext : ClickHouseContext { private bool _initialized = false; - private ClickHouseConnection? _connection; + private ClickHouseConnection? _connection = null; + private ClickHouseConnectionBroker _connectionBroker = null!; private ClickHouseFacadeFactory _facadeFactory = null!; - private readonly Dictionary> _facades = new(); + private readonly Dictionary _facades = new(); private bool _allowDatabaseChanges = false; @@ -28,7 +29,7 @@ public string? ServerVersion { ThrowIfNotInitialized(); - return _connection!.ServerVersion; + return _connectionBroker.ServerVersion; } } @@ -38,7 +39,7 @@ public string? ServerTimezone { ThrowIfNotInitialized(); - return _connection!.ServerTimezone; + return _connectionBroker.ServerTimezone; } } @@ -52,12 +53,28 @@ public TFacade GetFacade() return (TFacade) facade; } - var newFacade = _facadeFactory.CreateFacade(_connection!); + var newFacade = _facadeFactory.CreateFacade(_connectionBroker); _facades.Add(typeof(TFacade), newFacade); return newFacade; } + public TAbstraction GetFacadeAbstraction() + where TAbstraction : class + { + ThrowIfNotInitialized(); + + if (_facades.TryGetValue(typeof(TAbstraction), out var abstraction)) + { + return (TAbstraction) abstraction; + } + + var newFacade = _facadeFactory.CreateFacadeAbstraction(_connectionBroker); + _facades.Add(typeof(TAbstraction), newFacade); + + return newFacade; + } + public void ChangeDatabase(string databaseName) { ThrowIfNotInitialized(); @@ -75,6 +92,7 @@ internal void Initialize(ClickHouseContextOptions options) ThrowIfInitialized(); _connection = CreateConnection(options); + _connectionBroker = options.ConnectionBrokerProvider(_connection); _facadeFactory = options.FacadeFactory; _allowDatabaseChanges = options.AllowDatabaseChanges; diff --git a/src/ClickHouse.Facades/Context/ClickHouseContextFactory.cs b/src/ClickHouse.Facades/Context/ClickHouseContextFactory.cs index 49cbf6c..2a8f111 100644 --- a/src/ClickHouse.Facades/Context/ClickHouseContextFactory.cs +++ b/src/ClickHouse.Facades/Context/ClickHouseContextFactory.cs @@ -1,30 +1,33 @@ -namespace ClickHouse.Facades; +using ClickHouse.Client.ADO; + +namespace ClickHouse.Facades; public abstract class ClickHouseContextFactory : IClickHouseContextFactory where TContext : ClickHouseContext, new() { - private ClickHouseFacadeFactory? _facadeFactory; + private ClickHouseFacadeFactory _facadeFactory = null!; + private Func _connectionBrokerProvider = null!; - internal ClickHouseContextFactory Setup(ClickHouseFacadeFactory facadeFactory) + internal ClickHouseContextFactory Setup( + ClickHouseFacadeFactory facadeFactory, + Func connectionBrokerProvider) { _facadeFactory = facadeFactory ?? throw new ArgumentNullException(nameof(facadeFactory)); + _connectionBrokerProvider = connectionBrokerProvider + ?? throw new ArgumentNullException(nameof(connectionBrokerProvider)); return this; } public TContext CreateContext() { - if (_facadeFactory == null) - { - throw new InvalidOperationException($"{GetType()} has no facade registry."); - } - var builder = ClickHouseContextOptionsBuilder.Create; SetupContextOptions(builder); var contextOptions = builder .WithFacadeFactory(_facadeFactory) + .WithConnectionBrokerProvider(_connectionBrokerProvider) .Build(); var context = new TContext(); diff --git a/src/ClickHouse.Facades/Context/ClickHouseFacade.cs b/src/ClickHouse.Facades/Context/ClickHouseFacade.cs index da1e93d..4d6b7e0 100644 --- a/src/ClickHouse.Facades/Context/ClickHouseFacade.cs +++ b/src/ClickHouse.Facades/Context/ClickHouseFacade.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using ClickHouse.Client.ADO; using ClickHouse.Client.Copy; -using ClickHouse.Client.Utility; using ClickHouse.Facades.Utility; namespace ClickHouse.Facades; @@ -11,42 +10,26 @@ namespace ClickHouse.Facades; public abstract class ClickHouseFacade where TContext : ClickHouseContext { - private const string UseSessionConnectionStringParameter = "usesession"; + private ClickHouseConnectionBroker _connectionBroker = null!; - private ClickHouseConnection? _connection = null; - private bool _sessionEnabled = false; - - internal ClickHouseFacade SetConnection(ClickHouseConnection connection) + internal void SetConnectionBroker(ClickHouseConnectionBroker connectionBroker) { - ExceptionHelpers.ThrowIfNull(connection); - - if (_connection != null) + if (_connectionBroker != null) { - throw new InvalidOperationException($"{GetType()} is already connected."); + throw new InvalidOperationException("Connection broker is already set."); } - _connection = connection; - - _sessionEnabled = connection.ConnectionString - .GetConnectionStringParameters() - .Contains(new KeyValuePair(UseSessionConnectionStringParameter, true.ToString())); - - return this; + _connectionBroker = connectionBroker ?? throw new ArgumentNullException(nameof(connectionBroker)); } protected ClickHouseCommand CreateCommand() { - ThrowIfNotConnected(); - - return _connection!.CreateCommand(); + return _connectionBroker.CreateCommand(); } protected Task ExecuteScalarAsync(string query, CancellationToken cancellationToken = default) { - ThrowIfNotConnected(); - cancellationToken.ThrowIfCancellationRequested(); - - return _connection.ExecuteScalarAsync(query); + return _connectionBroker.ExecuteScalarAsync(query, cancellationToken); } /// Unable to cast object to type T. @@ -59,18 +42,12 @@ protected async Task ExecuteScalarAsync(string query, CancellationToken ca protected Task ExecuteNonQueryAsync(string statement, CancellationToken cancellationToken = default) { - ThrowIfNotConnected(); - cancellationToken.ThrowIfCancellationRequested(); - - return _connection.ExecuteStatementAsync(statement); + return _connectionBroker.ExecuteNonQueryAsync(statement, cancellationToken); } protected Task ExecuteReaderAsync(string query, CancellationToken cancellationToken = default) { - ThrowIfNotConnected(); - cancellationToken.ThrowIfCancellationRequested(); - - return _connection.ExecuteReaderAsync(query); + return _connectionBroker.ExecuteReaderAsync(query, cancellationToken); } protected async IAsyncEnumerable ExecuteQueryAsync( @@ -78,7 +55,7 @@ protected async IAsyncEnumerable ExecuteQueryAsync( Func rowSelector, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var reader = await ExecuteReaderAsync(query, cancellationToken); + await using var reader = await ExecuteReaderAsync(query, cancellationToken); if (!reader.HasRows) { @@ -93,10 +70,7 @@ protected async IAsyncEnumerable ExecuteQueryAsync( protected DataTable ExecuteDataTable(string query, CancellationToken cancellationToken = default) { - ThrowIfNotConnected(); - cancellationToken.ThrowIfCancellationRequested(); - - return _connection.ExecuteDataTable(query); + return _connectionBroker.ExecuteDataTable(query, cancellationToken); } protected Task BulkInsertAsync( @@ -146,47 +120,12 @@ protected Task BulkInsertAsync( maxDegreeOfParallelism); } - private async Task BulkInsertAsync( + private Task BulkInsertAsync( string destinationTable, Func saveAction, int batchSize, int maxDegreeOfParallelism) { - ThrowIfNotConnected(); - - if (destinationTable.IsNullOrWhiteSpace()) - { - throw new ArgumentException($"{nameof(destinationTable)} is null or whitespace."); - } - - if (batchSize < 1) - { - throw new ArgumentException($"{nameof(batchSize)} is lower than 1."); - } - - switch (maxDegreeOfParallelism) - { - case < 1: - throw new ArgumentException($"{nameof(maxDegreeOfParallelism)} is lower than 1."); - case > 1 when _sessionEnabled: - throw new InvalidOperationException($"Sessions are not compatible with parallel insertion."); - } - - using var bulkCopyInterface = new ClickHouseBulkCopy(_connection); - bulkCopyInterface.DestinationTableName = destinationTable; - bulkCopyInterface.BatchSize = batchSize; - bulkCopyInterface.MaxDegreeOfParallelism = maxDegreeOfParallelism; - - await saveAction(bulkCopyInterface); - - return bulkCopyInterface.RowsWritten; - } - - private void ThrowIfNotConnected() - { - if (_connection == null) - { - throw new InvalidOperationException($"{GetType()} is not connected."); - } + return _connectionBroker.BulkInsertAsync(destinationTable, saveAction, batchSize, maxDegreeOfParallelism); } } diff --git a/src/ClickHouse.Facades/Migrations/ClickHouseMigrationContext.cs b/src/ClickHouse.Facades/Migrations/ClickHouseMigrationContext.cs index a656584..b296e2b 100644 --- a/src/ClickHouse.Facades/Migrations/ClickHouseMigrationContext.cs +++ b/src/ClickHouse.Facades/Migrations/ClickHouseMigrationContext.cs @@ -2,5 +2,5 @@ internal class ClickHouseMigrationContext : ClickHouseContext { - public ClickHouseMigrationFacade MigrationFacade => GetFacade(); + public IClickHouseMigrationFacade MigrationFacade => GetFacadeAbstraction(); } diff --git a/src/ClickHouse.Facades/Migrations/ClickHouseMigrationFacade.cs b/src/ClickHouse.Facades/Migrations/ClickHouseMigrationFacade.cs index 8a4d8c6..bdee32b 100644 --- a/src/ClickHouse.Facades/Migrations/ClickHouseMigrationFacade.cs +++ b/src/ClickHouse.Facades/Migrations/ClickHouseMigrationFacade.cs @@ -3,7 +3,8 @@ namespace ClickHouse.Facades.Migrations; -internal sealed class ClickHouseMigrationFacade : ClickHouseFacade +internal sealed class ClickHouseMigrationFacade + : ClickHouseFacade, IClickHouseMigrationFacade { private const string MigrationsTable = "db_migrations_history"; @@ -23,7 +24,7 @@ public ClickHouseMigrationFacade(IClickHouseMigrationInstructions migrationInstr _dbName = _migrationInstructions.DatabaseName; } - internal async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancellationToken) + public async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancellationToken) { var builder = CreateTableSqlBuilder.Create .IfNotExists() @@ -52,8 +53,7 @@ internal async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancella await ExecuteNonQueryAsync(statement, cancellationToken); } - internal async Task EnsureDatabaseCreatedAsync( - CancellationToken cancellationToken) + public async Task EnsureDatabaseCreatedAsync(CancellationToken cancellationToken) { var builder = CreateDatabaseSqlBuilder.Create .IfNotExists() @@ -68,7 +68,7 @@ internal async Task EnsureDatabaseCreatedAsync( private string GetAppliedMigrationsSql => $"select id, name from {_dbName}.{MigrationsTable} final"; - internal async Task> GetAppliedMigrationsAsync(CancellationToken cancellationToken) + public async Task> GetAppliedMigrationsAsync(CancellationToken cancellationToken) { var migrations = await ExecuteQueryAsync( GetAppliedMigrationsSql, @@ -81,7 +81,7 @@ internal async Task> GetAppliedMigrationsAsync(Cancellati private const string AddAppliedMigrationSql = "insert into {0} values ({1}, '{2}', 0)"; - internal async Task ApplyMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken) + public async Task ApplyMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken) { ExceptionHelpers.ThrowIfNull(migration); @@ -122,7 +122,7 @@ internal async Task ApplyMigrationAsync(ClickHouseMigration migration, Cancellat private const string AddRolledBackMigrationSql = "insert into {0} values ({1}, '{2}', 1)"; - internal async Task RollbackMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken) + public async Task RollbackMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken) { ExceptionHelpers.ThrowIfNull(migration); diff --git a/src/ClickHouse.Facades/Migrations/ClickHouseMigrator.cs b/src/ClickHouse.Facades/Migrations/ClickHouseMigrator.cs index 1716baa..40f41ca 100644 --- a/src/ClickHouse.Facades/Migrations/ClickHouseMigrator.cs +++ b/src/ClickHouse.Facades/Migrations/ClickHouseMigrator.cs @@ -1,6 +1,4 @@ -using ClickHouse.Facades.Utility; - -namespace ClickHouse.Facades.Migrations; +namespace ClickHouse.Facades.Migrations; internal class ClickHouseMigrator : IClickHouseMigrator { @@ -9,16 +7,13 @@ internal class ClickHouseMigrator : IClickHouseMigrator public ClickHouseMigrator( IClickHouseContextFactory migrationContextFactory, - IClickHouseMigrationsLocator migrationsLocator, - IClickHouseMigrationInstructions instructions) + IClickHouseMigrationsLocator migrationsLocator) { _migrationContextFactory = migrationContextFactory ?? throw new ArgumentNullException(nameof(migrationContextFactory)); _migrationsLocator = migrationsLocator ?? throw new ArgumentNullException(nameof(migrationsLocator)); - - ExceptionHelpers.ThrowIfNull(instructions); } public async Task ApplyMigrationsAsync(CancellationToken cancellationToken = default) diff --git a/src/ClickHouse.Facades/Migrations/IClickHouseMigrationFacade.cs b/src/ClickHouse.Facades/Migrations/IClickHouseMigrationFacade.cs new file mode 100644 index 0000000..98b0c34 --- /dev/null +++ b/src/ClickHouse.Facades/Migrations/IClickHouseMigrationFacade.cs @@ -0,0 +1,14 @@ +namespace ClickHouse.Facades.Migrations; + +internal interface IClickHouseMigrationFacade +{ + Task EnsureMigrationsTableCreatedAsync(CancellationToken cancellationToken); + + Task EnsureDatabaseCreatedAsync(CancellationToken cancellationToken); + + Task> GetAppliedMigrationsAsync(CancellationToken cancellationToken); + + Task ApplyMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken); + + Task RollbackMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken); +} diff --git a/src/ClickHouse.Facades/Setup/ClickHouseContextOptions.cs b/src/ClickHouse.Facades/Setup/ClickHouseContextOptions.cs index f586481..ab90ae8 100644 --- a/src/ClickHouse.Facades/Setup/ClickHouseContextOptions.cs +++ b/src/ClickHouse.Facades/Setup/ClickHouseContextOptions.cs @@ -1,4 +1,6 @@ -namespace ClickHouse.Facades; +using ClickHouse.Client.ADO; + +namespace ClickHouse.Facades; public sealed class ClickHouseContextOptions where TContext : ClickHouseContext @@ -6,9 +8,10 @@ public sealed class ClickHouseContextOptions internal string ConnectionString { get; set; } = ""; internal bool AllowDatabaseChanges { get; set; } = false; - internal ClickHouseFacadeFactory FacadeFactory { get; set; } = null!; - internal HttpClient? HttpClient { get; set; } internal IHttpClientFactory? HttpClientFactory { get; set; } internal string? HttpClientName { get; set; } + + internal ClickHouseFacadeFactory FacadeFactory { get; set; } = null!; + internal Func ConnectionBrokerProvider { get; set; } = null!; } diff --git a/src/ClickHouse.Facades/Setup/ClickHouseContextOptionsBuilder.cs b/src/ClickHouse.Facades/Setup/ClickHouseContextOptionsBuilder.cs index cd2bf96..1dec059 100644 --- a/src/ClickHouse.Facades/Setup/ClickHouseContextOptionsBuilder.cs +++ b/src/ClickHouse.Facades/Setup/ClickHouseContextOptionsBuilder.cs @@ -1,4 +1,5 @@ -using ClickHouse.Facades.Utility; +using ClickHouse.Client.ADO; +using ClickHouse.Facades.Utility; namespace ClickHouse.Facades; @@ -10,12 +11,15 @@ public sealed class ClickHouseContextOptionsBuilder private OptionalValue _connectionString; private OptionalValue _forceSession; - private OptionalValue> _facadeFactory; private OptionalValue _allowDatabaseChanges; + private OptionalValue _httpClient; private OptionalValue _httpClientFactory; private OptionalValue _httpClientName; + private OptionalValue> _facadeFactory; + private OptionalValue> _connectionBrokerProvider; + public ClickHouseContextOptionsBuilder WithHttpClientFactory( IHttpClientFactory httpClientFactory, string httpClientName) @@ -64,17 +68,6 @@ public ClickHouseContextOptionsBuilder AllowDatabaseChanges() true); } - internal ClickHouseContextOptionsBuilder WithFacadeFactory( - ClickHouseFacadeFactory facadeFactory) - { - ExceptionHelpers.ThrowIfNull(facadeFactory); - - return WithPropertyValue( - builder => builder._facadeFactory, - (builder, value) => builder._facadeFactory = value, - facadeFactory); - } - public ClickHouseContextOptionsBuilder ForceSessions() { return WithPropertyValue( @@ -96,6 +89,28 @@ public ClickHouseContextOptionsBuilder WithConnectionString(string con connectionString); } + internal ClickHouseContextOptionsBuilder WithFacadeFactory( + ClickHouseFacadeFactory facadeFactory) + { + ExceptionHelpers.ThrowIfNull(facadeFactory); + + return WithPropertyValue( + builder => builder._facadeFactory, + (builder, value) => builder._facadeFactory = value, + facadeFactory); + } + + internal ClickHouseContextOptionsBuilder WithConnectionBrokerProvider( + Func connectionBrokerProvider) + { + ExceptionHelpers.ThrowIfNull(connectionBrokerProvider); + + return WithPropertyValue( + builder => builder._connectionBrokerProvider, + (builder, value) => builder._connectionBrokerProvider = value, + connectionBrokerProvider); + } + protected override ClickHouseContextOptions BuildCore() { var connectionString = _connectionString.NotNullOrThrow(); @@ -108,11 +123,12 @@ protected override ClickHouseContextOptions BuildCore() return new ClickHouseContextOptions { ConnectionString = connectionString, - FacadeFactory = _facadeFactory.NotNullOrThrow(), AllowDatabaseChanges = _allowDatabaseChanges.OrElseValue(false), HttpClient = _httpClient.OrDefault(), HttpClientFactory = _httpClientFactory.OrDefault(), HttpClientName = _httpClientName.OrDefault(), + FacadeFactory = _facadeFactory.NotNullOrThrow(), + ConnectionBrokerProvider = _connectionBrokerProvider.NotNullOrThrow(), }; } diff --git a/src/ClickHouse.Facades/Setup/ClickHouseContextServiceBuilder.cs b/src/ClickHouse.Facades/Setup/ClickHouseContextServiceBuilder.cs index cc2a80d..1eb9464 100644 --- a/src/ClickHouse.Facades/Setup/ClickHouseContextServiceBuilder.cs +++ b/src/ClickHouse.Facades/Setup/ClickHouseContextServiceBuilder.cs @@ -23,6 +23,15 @@ public ClickHouseContextServiceBuilder AddFacade() return this; } + public ClickHouseContextServiceBuilder AddFacade() + where TFacade : ClickHouseFacade, TAbstraction + where TAbstraction : class + { + _facadeRegistry.AddFacade(); + + return this; + } + internal void Build(IServiceCollection serviceCollection) { ExceptionHelpers.ThrowIfNull(serviceCollection); diff --git a/src/ClickHouse.Facades/Setup/ClickHouseFacadeFactory.cs b/src/ClickHouse.Facades/Setup/ClickHouseFacadeFactory.cs index e8c12c2..f8ee55a 100644 --- a/src/ClickHouse.Facades/Setup/ClickHouseFacadeFactory.cs +++ b/src/ClickHouse.Facades/Setup/ClickHouseFacadeFactory.cs @@ -1,5 +1,4 @@ -using ClickHouse.Client.ADO; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; namespace ClickHouse.Facades; @@ -15,14 +14,35 @@ public ClickHouseFacadeFactory(ClickHouseFacadeRegistry registry, ISer _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); } - internal TFacade CreateFacade(ClickHouseConnection connection) + internal TFacade CreateFacade(ClickHouseConnectionBroker connectionBroker) where TFacade : ClickHouseFacade { if (_registry.Contains()) { - return (_serviceProvider.GetRequiredService().SetConnection(connection) as TFacade)!; + var facade = _serviceProvider.GetRequiredService(); + facade.SetConnectionBroker(connectionBroker); + + return facade; } throw new InvalidOperationException($"Facade of type {typeof(TFacade)} was not found."); } + + internal TAbstraction CreateFacadeAbstraction(ClickHouseConnectionBroker connectionBroker) + where TAbstraction : class + { + if (_registry.ContainsAbstraction()) + { + var abstraction = _serviceProvider.GetRequiredService(); + + if (abstraction is ClickHouseFacade facade) + { + facade.SetConnectionBroker(connectionBroker); + } + + return abstraction; + } + + throw new InvalidOperationException($"Facade abstraction of type {typeof(TAbstraction)} was not found."); + } } diff --git a/src/ClickHouse.Facades/Setup/ClickHouseFacadeRegistry.cs b/src/ClickHouse.Facades/Setup/ClickHouseFacadeRegistry.cs index b48e089..ebff8d0 100644 --- a/src/ClickHouse.Facades/Setup/ClickHouseFacadeRegistry.cs +++ b/src/ClickHouse.Facades/Setup/ClickHouseFacadeRegistry.cs @@ -5,12 +5,30 @@ namespace ClickHouse.Facades; internal sealed class ClickHouseFacadeRegistry where TContext : ClickHouseContext { - private readonly List _facades = new(); + private readonly HashSet _facades = new(); + private readonly Dictionary _facadeAbstractions = new(); internal void AddFacade() where TFacade : ClickHouseFacade { - _facades.Add(typeof(TFacade)); + if (!_facades.Add(typeof(TFacade))) + { + throw new InvalidOperationException($"Facade of type {typeof(TFacade)} is already registered."); + } + } + + internal void AddFacade() + where TFacade : ClickHouseFacade, TAbstraction + where TAbstraction : class + { + var implementationType = typeof(TFacade); + var serviceType = typeof(TAbstraction); + + if (!_facadeAbstractions.TryAdd(serviceType, implementationType)) + { + throw new InvalidOperationException( + $"Facade of abstraction type {serviceType} is already registered."); + } } internal bool Contains() @@ -19,11 +37,22 @@ internal bool Contains() return _facades.Contains(typeof(TFacade)); } + internal bool ContainsAbstraction() + where TAbstraction : class + { + return _facadeAbstractions.ContainsKey(typeof(TAbstraction)); + } + internal void RegisterFacades(IServiceCollection serviceCollection) { foreach (var facade in _facades) { serviceCollection.AddTransient(facade); } + + foreach (var (serviceType, implementationType) in _facadeAbstractions) + { + serviceCollection.AddTransient(serviceType, implementationType); + } } } diff --git a/src/ClickHouse.Facades/Setup/ServiceCollectionExtensions.cs b/src/ClickHouse.Facades/Setup/ServiceCollectionExtensions.cs index 07d49e6..bc779bf 100644 --- a/src/ClickHouse.Facades/Setup/ServiceCollectionExtensions.cs +++ b/src/ClickHouse.Facades/Setup/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ public static IServiceCollection AddClickHouseMigrations() .AddClickHouseContext( builder => builder - .AddFacade(), + .AddFacade(), ServiceLifetime.Transient) .AddTransient(); } @@ -34,7 +34,9 @@ public static IServiceCollection AddClickHouseContext typeof(IClickHouseContextFactory), serviceProvider => ActivatorUtilities .CreateInstance(serviceProvider) - .Setup(serviceProvider.GetRequiredService>()), + .Setup( + serviceProvider.GetRequiredService>(), + connection => new ClickHouseConnectionBroker(connection)), factoryLifetime); services.Add(descriptor);