From bf74eeff0c00d20b54b9534412a23d05b6c95e7e Mon Sep 17 00:00:00 2001 From: Kevin Well Date: Tue, 9 May 2023 11:41:55 +0300 Subject: [PATCH 1/8] Add ClickView.Goodstuff.Repositories.Snowflake --- ClickView.GoodStuff.sln | 19 ++- .../Snowflake/src/BaseSnowflakeRepository.cs | 124 ++++++++++++++++++ ...ew.GoodStuff.Repositories.Snowflake.csproj | 17 +++ .../src/ISnowflakeConnectionFactory.cs | 9 ++ .../src/SnowflakeConnectionFactory.cs | 44 +++++++ .../src/SnowflakeConnectionOptions.cs | 62 +++++++++ ...dStuff.Repositories.Snowflake.Tests.csproj | 26 ++++ .../test/SnowflakeConnectionOptionsTests.cs | 104 +++++++++++++++ 8 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs create mode 100644 src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj create mode 100644 src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs create mode 100644 src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs create mode 100644 src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs create mode 100644 src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj create mode 100644 src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs diff --git a/ClickView.GoodStuff.sln b/ClickView.GoodStuff.sln index 11561d12..83ecf4f1 100644 --- a/ClickView.GoodStuff.sln +++ b/ClickView.GoodStuff.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28917.181 @@ -103,6 +103,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Vault", "Vault", "{AFB5C6D0 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickView.GoodStuff.Configuration.Vault", "src\Configuration\Vault\src\ClickView.GoodStuff.Configuration.Vault.csproj", "{0CE2213A-4C95-4692-96BC-B16079C601BE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Snowflake", "Snowflake", "{FB39F90B-78CE-416D-99DF-ED74B3D5483E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickView.GoodStuff.Repositories.Snowflake", "src\Repositories\Snowflake\src\ClickView.GoodStuff.Repositories.Snowflake.csproj", "{AAC65E72-7744-42D6-94DB-D2041B8DC39C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickView.GoodStuff.Repositories.Snowflake.Tests", "src\Repositories\Snowflake\test\ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj", "{6C099EEC-28F1-4F65-B6AF-9410AC821B7C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -213,6 +219,14 @@ Global {0CE2213A-4C95-4692-96BC-B16079C601BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CE2213A-4C95-4692-96BC-B16079C601BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {0CE2213A-4C95-4692-96BC-B16079C601BE}.Release|Any CPU.Build.0 = Release|Any CPU + {AAC65E72-7744-42D6-94DB-D2041B8DC39C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAC65E72-7744-42D6-94DB-D2041B8DC39C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAC65E72-7744-42D6-94DB-D2041B8DC39C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAC65E72-7744-42D6-94DB-D2041B8DC39C}.Release|Any CPU.Build.0 = Release|Any CPU + {6C099EEC-28F1-4F65-B6AF-9410AC821B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C099EEC-28F1-4F65-B6AF-9410AC821B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C099EEC-28F1-4F65-B6AF-9410AC821B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C099EEC-28F1-4F65-B6AF-9410AC821B7C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -255,6 +269,9 @@ Global {51DC2CBC-4C90-4533-919F-7053F7E21FD8} = {41CD17D6-2B4C-4E27-9ABB-D21A6276BE7A} {AFB5C6D0-7ABE-4809-90C8-6A3EB2CE1A14} = {28B6F3FE-DB59-4FB9-A0F1-264E17CFA5C9} {0CE2213A-4C95-4692-96BC-B16079C601BE} = {AFB5C6D0-7ABE-4809-90C8-6A3EB2CE1A14} + {FB39F90B-78CE-416D-99DF-ED74B3D5483E} = {17A5F05F-B3EB-4011-A58C-BB1BB7995692} + {AAC65E72-7744-42D6-94DB-D2041B8DC39C} = {FB39F90B-78CE-416D-99DF-ED74B3D5483E} + {6C099EEC-28F1-4F65-B6AF-9410AC821B7C} = {FB39F90B-78CE-416D-99DF-ED74B3D5483E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F935033F-FAFD-48D4-843C-C7C8A9AE6562} diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs new file mode 100644 index 00000000..847a9629 --- /dev/null +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -0,0 +1,124 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Abstractions; + using Abstractions.Factories; + using Dapper; + using global::Snowflake.Data.Client; + + public class BaseSnowflakeRepository : BaseRepository + { + public BaseSnowflakeRepository(IConnectionFactory connectionFactory) : base( + connectionFactory) + { + } + + /// + /// Executes a write command + /// + /// + /// + /// + protected async Task ExecuteAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetWriteConnection(); +#else + using var conn = GetWriteConnection(); +#endif + + return await conn.ExecuteAsync(sql, param); + } + + /// + /// Executes a write command which selects a single value + /// + /// + /// + /// + /// + protected async Task ExecuteScalarAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetWriteConnection(); +#else + using var conn = GetWriteConnection(); +#endif + + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single value query + /// + /// + /// + /// + /// + protected async Task QueryScalarValueAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single row query + /// + /// + /// + /// + /// + protected async Task QueryFirstOrDefaultAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QueryFirstOrDefaultAsync(sql, param); + } + + /// + /// Executes a single row query and throws an exception if more than one record is found + /// + /// + /// + /// + /// + protected async Task QuerySingleOrDefaultAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QuerySingleOrDefaultAsync(sql, param); + } + + /// + /// Executes a multiple row query + /// + /// + /// + /// + /// + protected async Task> QueryAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QueryAsync(sql, param); + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj new file mode 100644 index 00000000..457df372 --- /dev/null +++ b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0;net6.0 + true + + + + + + + + + + + + diff --git a/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs new file mode 100644 index 00000000..53f6738a --- /dev/null +++ b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs @@ -0,0 +1,9 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using Abstractions.Factories; + using global::Snowflake.Data.Client; + + public interface ISnowflakeConnectionFactory : IConnectionFactory + { + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs new file mode 100644 index 00000000..742ba485 --- /dev/null +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs @@ -0,0 +1,44 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using System; + using Abstractions.Factories; + using global::Snowflake.Data.Client; + + public class SnowflakeConnectionFactory : SnowflakeConnectionFactory + { + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } + } + + public class SnowflakeConnectionFactory + : ConnectionFactory, ISnowflakeConnectionFactory + where TOptions : SnowflakeConnectionOptions + { + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } + + public override SnowflakeDbConnection GetReadConnection() + { + if (string.IsNullOrEmpty(ReadConnectionString)) + throw new InvalidOperationException("Read is not allowed. No read connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = ReadConnectionString }; + connection.Open(); + + return connection; + } + + public override SnowflakeDbConnection GetWriteConnection() + { + if (string.IsNullOrEmpty(WriteConnectionString)) + throw new InvalidOperationException("Write is not allowed. No write connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = WriteConnectionString }; + connection.Open(); + + return connection; + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs new file mode 100644 index 00000000..a181e61b --- /dev/null +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs @@ -0,0 +1,62 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using Abstractions; + + public class SnowflakeConnectionOptions : RepositoryConnectionOptions + { + /// + /// The full account name which might include additional segments that identify the region and + /// cloud platform where your account is hosted + /// + public string? Account + { + set => SetParameter("account", value); + get => GetParameter("account"); + } + + /// + /// The name of the warehouse to use + /// + public string? Warehouse + { + set => SetParameter("warehouse", value); + get => GetParameter("warehouse"); + } + + /// + /// The database to use + /// + public string? Database + { + set => SetParameter("db", value); + get => GetParameter("db"); + } + + /// + /// The schema to use + /// + public string? Schema + { + set => SetParameter("schema", value); + get => GetParameter("schema"); + } + + /// + /// The Snowflake user to use + /// + public string? User + { + set => SetParameter("user", value); + get => GetParameter("user"); + } + + /// + /// The password for the Snowflake user + /// + public string? Password + { + set => SetParameter("password", value); + get => GetParameter("password"); + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj new file mode 100644 index 00000000..34b99bcb --- /dev/null +++ b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs new file mode 100644 index 00000000..a5991fb1 --- /dev/null +++ b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs @@ -0,0 +1,104 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake.Tests +{ + using Xunit; + + public class SnowflakeConnectionOptionsTests + { + [Fact] + public void GetConnectionString_Default_Empty() + { + var options = new SnowflakeConnectionOptions(); + + var connString = options.GetConnectionString(); + + Assert.Equal(string.Empty, connString); + } + + [Fact] + public void GetConnectionString_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions + { + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; + + var connString = options.GetConnectionString(); + + Assert.Equal( + "host=host;" + + "account=acc;" + + "warehouse=wh;" + + "db=db;" + + "schema=sch;" + + "user=user;" + + "password=pass;", + connString); + } + + [Fact] + public void PropertiesSet_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions + { + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; + + Assert.Equal("host", options.Host); + Assert.Equal("acc", options.Account); + Assert.Equal("wh", options.Warehouse); + Assert.Equal("db", options.Database); + Assert.Equal("sch", options.Schema); + Assert.Equal("user", options.User); + Assert.Equal("pass", options.Password); + } + + [Fact] + public void PropertiesSet_Default_Empty() + { + var options = new SnowflakeConnectionOptions(); + + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); + } + + [Fact] + public void PropertiesSet_Null_Empty() + { + var options = new SnowflakeConnectionOptions + { + Host = null, + Account = null, + Warehouse = null, + Database = null, + Schema = null, + User = null, + Password = null + }; + + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); + } + } +} From 1bed9c8d78ec520336565c259933c3a3ed13edf1 Mon Sep 17 00:00:00 2001 From: Kevin Well Date: Wed, 10 May 2023 10:53:27 +0300 Subject: [PATCH 2/8] Use ISnowflakeConnectionFactory in DI --- src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs index 847a9629..63109af2 100644 --- a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -3,13 +3,12 @@ using System.Collections.Generic; using System.Threading.Tasks; using Abstractions; - using Abstractions.Factories; using Dapper; using global::Snowflake.Data.Client; public class BaseSnowflakeRepository : BaseRepository { - public BaseSnowflakeRepository(IConnectionFactory connectionFactory) : base( + public BaseSnowflakeRepository(ISnowflakeConnectionFactory connectionFactory) : base( connectionFactory) { } From 3fdbe579a1a058d83dd3db96a4f0d889219522c7 Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 06:42:45 +0100 Subject: [PATCH 3/8] Use file-scoped namespaces --- .../Snowflake/src/BaseSnowflakeRepository.cs | 18 ++- .../src/ISnowflakeConnectionFactory.cs | 13 +- .../src/SnowflakeConnectionFactory.cs | 72 ++++++----- .../src/SnowflakeConnectionOptions.cs | 115 +++++++++--------- 4 files changed, 105 insertions(+), 113 deletions(-) diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs index 63109af2..c1a6c903 100644 --- a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -1,13 +1,11 @@ -namespace ClickView.GoodStuff.Repositories.Snowflake -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using Abstractions; - using Dapper; - using global::Snowflake.Data.Client; +namespace ClickView.GoodStuff.Repositories.Snowflake; + +using Abstractions; +using Dapper; +using global::Snowflake.Data.Client; - public class BaseSnowflakeRepository : BaseRepository - { +public class BaseSnowflakeRepository : BaseRepository +{ public BaseSnowflakeRepository(ISnowflakeConnectionFactory connectionFactory) : base( connectionFactory) { @@ -120,4 +118,4 @@ protected async Task> QueryAsync(string sql, object? param = n return await conn.QueryAsync(sql, param); } } -} \ No newline at end of file +} diff --git a/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs index 53f6738a..69340ee2 100644 --- a/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs +++ b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs @@ -1,9 +1,6 @@ -namespace ClickView.GoodStuff.Repositories.Snowflake -{ - using Abstractions.Factories; - using global::Snowflake.Data.Client; +namespace ClickView.GoodStuff.Repositories.Snowflake; - public interface ISnowflakeConnectionFactory : IConnectionFactory - { - } -} \ No newline at end of file +using Abstractions.Factories; +using global::Snowflake.Data.Client; + +public interface ISnowflakeConnectionFactory : IConnectionFactory; diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs index 742ba485..9dd79f7d 100644 --- a/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs @@ -1,44 +1,42 @@ -namespace ClickView.GoodStuff.Repositories.Snowflake +namespace ClickView.GoodStuff.Repositories.Snowflake; + +using Abstractions.Factories; +using global::Snowflake.Data.Client; + +public class SnowflakeConnectionFactory : SnowflakeConnectionFactory +{ + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } +} + +public class SnowflakeConnectionFactory + : ConnectionFactory, ISnowflakeConnectionFactory + where TOptions : SnowflakeConnectionOptions { - using System; - using Abstractions.Factories; - using global::Snowflake.Data.Client; + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } - public class SnowflakeConnectionFactory : SnowflakeConnectionFactory + public override SnowflakeDbConnection GetReadConnection() { - public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) - { - } + if (string.IsNullOrEmpty(ReadConnectionString)) + throw new InvalidOperationException("Read is not allowed. No read connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = ReadConnectionString }; + connection.Open(); + + return connection; } - public class SnowflakeConnectionFactory - : ConnectionFactory, ISnowflakeConnectionFactory - where TOptions : SnowflakeConnectionOptions + public override SnowflakeDbConnection GetWriteConnection() { - public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) - { - } - - public override SnowflakeDbConnection GetReadConnection() - { - if (string.IsNullOrEmpty(ReadConnectionString)) - throw new InvalidOperationException("Read is not allowed. No read connection options defined"); - - var connection = new SnowflakeDbConnection { ConnectionString = ReadConnectionString }; - connection.Open(); - - return connection; - } - - public override SnowflakeDbConnection GetWriteConnection() - { - if (string.IsNullOrEmpty(WriteConnectionString)) - throw new InvalidOperationException("Write is not allowed. No write connection options defined"); - - var connection = new SnowflakeDbConnection { ConnectionString = WriteConnectionString }; - connection.Open(); - - return connection; - } + if (string.IsNullOrEmpty(WriteConnectionString)) + throw new InvalidOperationException("Write is not allowed. No write connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = WriteConnectionString }; + connection.Open(); + + return connection; } -} \ No newline at end of file +} diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs index a181e61b..1b7a0ae5 100644 --- a/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs @@ -1,62 +1,61 @@ -namespace ClickView.GoodStuff.Repositories.Snowflake +namespace ClickView.GoodStuff.Repositories.Snowflake; + +using Abstractions; + +public class SnowflakeConnectionOptions : RepositoryConnectionOptions { - using Abstractions; + /// + /// The full account name which might include additional segments that identify the region and + /// cloud platform where your account is hosted + /// + public string? Account + { + set => SetParameter("account", value); + get => GetParameter("account"); + } + + /// + /// The name of the warehouse to use + /// + public string? Warehouse + { + set => SetParameter("warehouse", value); + get => GetParameter("warehouse"); + } + + /// + /// The database to use + /// + public string? Database + { + set => SetParameter("db", value); + get => GetParameter("db"); + } + + /// + /// The schema to use + /// + public string? Schema + { + set => SetParameter("schema", value); + get => GetParameter("schema"); + } + + /// + /// The Snowflake user to use + /// + public string? User + { + set => SetParameter("user", value); + get => GetParameter("user"); + } - public class SnowflakeConnectionOptions : RepositoryConnectionOptions + /// + /// The password for the Snowflake user + /// + public string? Password { - /// - /// The full account name which might include additional segments that identify the region and - /// cloud platform where your account is hosted - /// - public string? Account - { - set => SetParameter("account", value); - get => GetParameter("account"); - } - - /// - /// The name of the warehouse to use - /// - public string? Warehouse - { - set => SetParameter("warehouse", value); - get => GetParameter("warehouse"); - } - - /// - /// The database to use - /// - public string? Database - { - set => SetParameter("db", value); - get => GetParameter("db"); - } - - /// - /// The schema to use - /// - public string? Schema - { - set => SetParameter("schema", value); - get => GetParameter("schema"); - } - - /// - /// The Snowflake user to use - /// - public string? User - { - set => SetParameter("user", value); - get => GetParameter("user"); - } - - /// - /// The password for the Snowflake user - /// - public string? Password - { - set => SetParameter("password", value); - get => GetParameter("password"); - } + set => SetParameter("password", value); + get => GetParameter("password"); } -} \ No newline at end of file +} From 68eb50a27d8f10ffc496be32bed91f4f1b23d328 Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 06:56:04 +0100 Subject: [PATCH 4/8] Add QueryAsync method with support for splitOn --- .../Snowflake/src/BaseSnowflakeRepository.cs | 173 +++++++++--------- 1 file changed, 90 insertions(+), 83 deletions(-) diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs index c1a6c903..ce4bfbf6 100644 --- a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -4,118 +4,125 @@ namespace ClickView.GoodStuff.Repositories.Snowflake; using Dapper; using global::Snowflake.Data.Client; -public class BaseSnowflakeRepository : BaseRepository +public abstract class BaseSnowflakeRepository(ISnowflakeConnectionFactory connectionFactory) + : BaseRepository(connectionFactory) { - public BaseSnowflakeRepository(ISnowflakeConnectionFactory connectionFactory) : base( - connectionFactory) - { - } - - /// - /// Executes a write command - /// - /// - /// - /// - protected async Task ExecuteAsync(string sql, object? param = null) - { + /// + /// Executes a write command + /// + /// + /// + /// + protected async Task ExecuteAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetWriteConnection(); + await using var conn = GetWriteConnection(); #else - using var conn = GetWriteConnection(); + using var conn = GetWriteConnection(); #endif - return await conn.ExecuteAsync(sql, param); - } + return await conn.ExecuteAsync(sql, param); + } - /// - /// Executes a write command which selects a single value - /// - /// - /// - /// - /// - protected async Task ExecuteScalarAsync(string sql, object? param = null) - { + /// + /// Executes a write command which selects a single value + /// + /// + /// + /// + /// + protected async Task ExecuteScalarAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetWriteConnection(); + await using var conn = GetWriteConnection(); #else using var conn = GetWriteConnection(); #endif - - return await conn.ExecuteScalarAsync(sql, param); - } - /// - /// Executes a single value query - /// - /// - /// - /// - /// - protected async Task QueryScalarValueAsync(string sql, object? param = null) - { + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single value query + /// + /// + /// + /// + /// + protected async Task QueryScalarValueAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); + await using var conn = GetReadConnection(); #else using var conn = GetReadConnection(); #endif - - return await conn.ExecuteScalarAsync(sql, param); - } - /// - /// Executes a single row query - /// - /// - /// - /// - /// - protected async Task QueryFirstOrDefaultAsync(string sql, object? param = null) - { + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single row query + /// + /// + /// + /// + /// + protected async Task QueryFirstOrDefaultAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); + await using var conn = GetReadConnection(); #else using var conn = GetReadConnection(); #endif - - return await conn.QueryFirstOrDefaultAsync(sql, param); - } - /// - /// Executes a single row query and throws an exception if more than one record is found - /// - /// - /// - /// - /// - protected async Task QuerySingleOrDefaultAsync(string sql, object? param = null) - { + return await conn.QueryFirstOrDefaultAsync(sql, param); + } + + /// + /// Executes a single row query and throws an exception if more than one record is found + /// + /// + /// + /// + /// + protected async Task QuerySingleOrDefaultAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); + await using var conn = GetReadConnection(); #else using var conn = GetReadConnection(); #endif - - return await conn.QuerySingleOrDefaultAsync(sql, param); - } - /// - /// Executes a multiple row query - /// - /// - /// - /// - /// - protected async Task> QueryAsync(string sql, object? param = null) - { + return await conn.QuerySingleOrDefaultAsync(sql, param); + } + + /// + /// Executes a multiple row query + /// + /// + /// + /// + /// + protected async Task> QueryAsync(string sql, object? param = null) + { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); + await using var conn = GetReadConnection(); #else using var conn = GetReadConnection(); #endif - - return await conn.QueryAsync(sql, param); - } + + return await conn.QueryAsync(sql, param); + } + + protected async Task> QueryAsync(string sql, + Func map, object? param = null, string splitOn = "Id") + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QueryAsync(sql, map, splitOn: splitOn, param: param); } } From 218bbe6672191a98ab3764f8c182466bd42a1c20 Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 06:56:12 +0100 Subject: [PATCH 5/8] Upgrade Snowflake.Data --- .../src/ClickView.GoodStuff.Repositories.Snowflake.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj index 457df372..e7ebd821 100644 --- a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj +++ b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj @@ -7,7 +7,7 @@ - + From 72010583bf2f2659af85509661240760b29f0c4f Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 07:09:03 +0100 Subject: [PATCH 6/8] Support cancellation tokens --- .../Snowflake/src/BaseSnowflakeRepository.cs | 117 ++++++++++-------- ...ew.GoodStuff.Repositories.Snowflake.csproj | 2 +- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs index ce4bfbf6..c3f30f5f 100644 --- a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -1,5 +1,6 @@ namespace ClickView.GoodStuff.Repositories.Snowflake; +using System.Threading; using Abstractions; using Dapper; using global::Snowflake.Data.Client; @@ -12,16 +13,15 @@ public abstract class BaseSnowflakeRepository(ISnowflakeConnectionFactory connec /// /// /// + /// /// - protected async Task ExecuteAsync(string sql, object? param = null) + protected Task ExecuteAsync(string sql, object? param = null, CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetWriteConnection(); -#else - using var conn = GetWriteConnection(); -#endif - - return await conn.ExecuteAsync(sql, param); + return WrapAsync((con, cd) => con.ExecuteAsync(cd), + write: true, + sql, + param, + cancellationToken); } /// @@ -30,16 +30,16 @@ protected async Task ExecuteAsync(string sql, object? param = null) /// /// /// + /// /// - protected async Task ExecuteScalarAsync(string sql, object? param = null) + protected Task ExecuteScalarAsync(string sql, object? param = null, + CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetWriteConnection(); -#else - using var conn = GetWriteConnection(); -#endif - - return await conn.ExecuteScalarAsync(sql, param); + return WrapAsync((con, cd) => con.ExecuteScalarAsync(cd), + write: true, + sql, + param, + cancellationToken); } /// @@ -48,16 +48,16 @@ protected async Task ExecuteAsync(string sql, object? param = null) /// /// /// + /// /// - protected async Task QueryScalarValueAsync(string sql, object? param = null) + protected Task QueryScalarValueAsync(string sql, object? param = null, + CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); -#else - using var conn = GetReadConnection(); -#endif - - return await conn.ExecuteScalarAsync(sql, param); + return WrapAsync((con, cd) => con.ExecuteScalarAsync(cd), + write: false, + sql, + param, + cancellationToken); } /// @@ -66,16 +66,16 @@ protected async Task ExecuteAsync(string sql, object? param = null) /// /// /// + /// /// - protected async Task QueryFirstOrDefaultAsync(string sql, object? param = null) + protected Task QueryFirstOrDefaultAsync(string sql, object? param = null, + CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); -#else - using var conn = GetReadConnection(); -#endif - - return await conn.QueryFirstOrDefaultAsync(sql, param); + return WrapAsync((con, cd) => con.QueryFirstOrDefaultAsync(cd), + write: false, + sql, + param, + cancellationToken); } /// @@ -84,16 +84,16 @@ protected async Task ExecuteAsync(string sql, object? param = null) /// /// /// + /// /// - protected async Task QuerySingleOrDefaultAsync(string sql, object? param = null) + protected Task QuerySingleOrDefaultAsync(string sql, object? param = null, + CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); -#else - using var conn = GetReadConnection(); -#endif - - return await conn.QuerySingleOrDefaultAsync(sql, param); + return WrapAsync((con, cd) => con.QuerySingleOrDefaultAsync(cd), + write: false, + sql, + param, + cancellationToken); } /// @@ -102,27 +102,40 @@ protected async Task ExecuteAsync(string sql, object? param = null) /// /// /// + /// /// - protected async Task> QueryAsync(string sql, object? param = null) + protected Task> QueryAsync(string sql, object? param = null, + CancellationToken cancellationToken = default) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); -#else - using var conn = GetReadConnection(); -#endif + return WrapAsync((con, cd) => con.QueryAsync(cd), + write: false, + sql, + param, + cancellationToken); + } - return await conn.QueryAsync(sql, param); + protected Task> QueryAsync(string sql, + Func map, object? param = null, string splitOn = "Id", + CancellationToken cancellationToken = default) + { + return WrapAsync((con, cd) => con.QueryAsync(cd, map, splitOn: splitOn), + write: false, + sql, + param, + cancellationToken); } - protected async Task> QueryAsync(string sql, - Func map, object? param = null, string splitOn = "Id") + private async Task WrapAsync(Func> func, + bool write, string sql, object? param, CancellationToken cancellationToken) { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await using var conn = GetReadConnection(); + await using var conn = write ? GetWriteConnection() : GetReadConnection(); #else - using var conn = GetReadConnection(); + using var conn = write ? GetWriteConnection() : GetReadConnection(); #endif - return await conn.QueryAsync(sql, map, splitOn: splitOn, param: param); + var command = new CommandDefinition(sql, param, cancellationToken: cancellationToken); + + return await func(conn, command); } } diff --git a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj index e7ebd821..6fb37271 100644 --- a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj +++ b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj @@ -6,7 +6,7 @@ - + From c4e858fb1e56b89289134d23efd85bd9347c472d Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 07:25:49 +0100 Subject: [PATCH 7/8] Update csproj files --- ...ew.GoodStuff.Repositories.Snowflake.csproj | 24 ++++++------- ...dStuff.Repositories.Snowflake.Tests.csproj | 36 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj index 6fb37271..3704ab68 100644 --- a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj +++ b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj @@ -1,17 +1,17 @@ - - netstandard2.0;net6.0 - true - + + $(FullTargetFrameworks) + true + - - - - - - - - + + + + + + + + diff --git a/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj index 34b99bcb..727f1784 100644 --- a/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj +++ b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj @@ -1,26 +1,22 @@ - - net6.0 - false - false - + + $(TestTargetFrameworks) + false + false + - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + From 308699994c1b69b96cde0526f0620b4fa8f15482 Mon Sep 17 00:00:00 2001 From: Kevin Wellalagodage Date: Wed, 11 Mar 2026 07:25:56 +0100 Subject: [PATCH 8/8] Use file-scoped namespace --- .../test/SnowflakeConnectionOptionsTests.cs | 171 +++++++++--------- 1 file changed, 85 insertions(+), 86 deletions(-) diff --git a/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs index a5991fb1..36f819e0 100644 --- a/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs +++ b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs @@ -1,104 +1,103 @@ -namespace ClickView.GoodStuff.Repositories.Snowflake.Tests -{ - using Xunit; +namespace ClickView.GoodStuff.Repositories.Snowflake.Tests; + +using Xunit; - public class SnowflakeConnectionOptionsTests +public class SnowflakeConnectionOptionsTests +{ + [Fact] + public void GetConnectionString_Default_Empty() { - [Fact] - public void GetConnectionString_Default_Empty() - { - var options = new SnowflakeConnectionOptions(); + var options = new SnowflakeConnectionOptions(); - var connString = options.GetConnectionString(); + var connString = options.GetConnectionString(); - Assert.Equal(string.Empty, connString); - } + Assert.Equal(string.Empty, connString); + } - [Fact] - public void GetConnectionString_AllOptionsSet_Valid() + [Fact] + public void GetConnectionString_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions { - var options = new SnowflakeConnectionOptions - { - Host = "host", - Account = "acc", - Warehouse = "wh", - Database = "db", - Schema = "sch", - User = "user", - Password = "pass" - }; + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; - var connString = options.GetConnectionString(); + var connString = options.GetConnectionString(); - Assert.Equal( - "host=host;" + - "account=acc;" + - "warehouse=wh;" + - "db=db;" + - "schema=sch;" + - "user=user;" + - "password=pass;", - connString); - } + Assert.Equal( + "host=host;" + + "account=acc;" + + "warehouse=wh;" + + "db=db;" + + "schema=sch;" + + "user=user;" + + "password=pass;", + connString); + } - [Fact] - public void PropertiesSet_AllOptionsSet_Valid() + [Fact] + public void PropertiesSet_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions { - var options = new SnowflakeConnectionOptions - { - Host = "host", - Account = "acc", - Warehouse = "wh", - Database = "db", - Schema = "sch", - User = "user", - Password = "pass" - }; + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; - Assert.Equal("host", options.Host); - Assert.Equal("acc", options.Account); - Assert.Equal("wh", options.Warehouse); - Assert.Equal("db", options.Database); - Assert.Equal("sch", options.Schema); - Assert.Equal("user", options.User); - Assert.Equal("pass", options.Password); - } + Assert.Equal("host", options.Host); + Assert.Equal("acc", options.Account); + Assert.Equal("wh", options.Warehouse); + Assert.Equal("db", options.Database); + Assert.Equal("sch", options.Schema); + Assert.Equal("user", options.User); + Assert.Equal("pass", options.Password); + } - [Fact] - public void PropertiesSet_Default_Empty() - { - var options = new SnowflakeConnectionOptions(); + [Fact] + public void PropertiesSet_Default_Empty() + { + var options = new SnowflakeConnectionOptions(); - Assert.Null(options.Host); - Assert.Null(options.Account); - Assert.Null(options.Warehouse); - Assert.Null(options.Database); - Assert.Null(options.Schema); - Assert.Null(options.User); - Assert.Null(options.Password); - } + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); + } - [Fact] - public void PropertiesSet_Null_Empty() + [Fact] + public void PropertiesSet_Null_Empty() + { + var options = new SnowflakeConnectionOptions { - var options = new SnowflakeConnectionOptions - { - Host = null, - Account = null, - Warehouse = null, - Database = null, - Schema = null, - User = null, - Password = null - }; + Host = null, + Account = null, + Warehouse = null, + Database = null, + Schema = null, + User = null, + Password = null + }; - Assert.Null(options.Host); - Assert.Null(options.Account); - Assert.Null(options.Warehouse); - Assert.Null(options.Database); - Assert.Null(options.Schema); - Assert.Null(options.User); - Assert.Null(options.Password); - } + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); } }