From 851810c7541e732c6e8dd7b4c8d34e5e4b1299b0 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 20:56:02 +0200 Subject: [PATCH 01/24] Update description --- .../AdvancedSystems.Connector.Abstractions.csproj | 2 +- AdvancedSystems.Connector/AdvancedSystems.Connector.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AdvancedSystems.Connector.Abstractions/AdvancedSystems.Connector.Abstractions.csproj b/AdvancedSystems.Connector.Abstractions/AdvancedSystems.Connector.Abstractions.csproj index e746157..02e1401 100644 --- a/AdvancedSystems.Connector.Abstractions/AdvancedSystems.Connector.Abstractions.csproj +++ b/AdvancedSystems.Connector.Abstractions/AdvancedSystems.Connector.Abstractions.csproj @@ -3,7 +3,7 @@ net8.0 0.0.0-alpha - TODO + Abstractions for AdvancedSystems.Connector. AdvancedSystems.Connector.Abstractions AdvancedSystems.Connector.Abstractions Advanced Systems Connector Abstractions Library diff --git a/AdvancedSystems.Connector/AdvancedSystems.Connector.csproj b/AdvancedSystems.Connector/AdvancedSystems.Connector.csproj index b6394a4..6920219 100644 --- a/AdvancedSystems.Connector/AdvancedSystems.Connector.csproj +++ b/AdvancedSystems.Connector/AdvancedSystems.Connector.csproj @@ -3,7 +3,7 @@ net8.0 0.0.0-alpha - TODO + Provides a database access layer to streamline database connections. AdvancedSystems.Connector AdvancedSystems.Connector Advanced Systems Connector Library From 989948cc265cec6aa7b7e21fcbe770fe126bfad8 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 20:56:24 +0200 Subject: [PATCH 02/24] Remove placeholder text in readme and add status badges --- readme.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index c372877..799bbdc 100644 --- a/readme.md +++ b/readme.md @@ -6,10 +6,51 @@

Advanced Systems Connector

+[![Unit Tests](https://github.com/Advanced-Systems/connector/actions/workflows/dotnet-tests.yml/badge.svg)](https://github.com/Advanced-Systems/connector/actions/workflows/dotnet-tests.yml) +[![CodeQL](https://github.com/Advanced-Systems/connector/actions/workflows/codeql.yml/badge.svg)](https://github.com/Advanced-Systems/connector/actions/workflows/codeql.yml) +[![Docs](https://github.com/Advanced-Systems/connector/actions/workflows/docs.yml/badge.svg)](https://github.com/Advanced-Systems/connector/actions/workflows/docs.yml) + ## About -TODO +Provides a database access layer to streamline database connections. This package can be installed +from the public [NuGet Gallery](https://www.nuget.org/packages/AdvancedSystems.Connector): + +```powershell +dotnet add package AdvancedSystems.Connector +``` + +The changelog for this package are available [here](https://advanced-systems.github.io/connector/docs/changelog.html). + +Package consumers can also use the symbols published to nuget.org symbol server by adding +to their symbol sources in Visual Studio, which allows stepping into package code in the Visual Studio debugger. See +[Specify symbol (.pdb) and source files in the Visual Studio debugger](https://learn.microsoft.com/en-us/visualstudio/debugger/specify-symbol-dot-pdb-and-source-files-in-the-visual-studio-debugger) +for details on that process. + +Additionally, this project also supports [source link technology](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/sourcelink) +for debugging .NET assemblies. ## Developer Notes -TODO +Run test suite: + +```powershell +dotnet test .\AdvancedSystems.Connector.Tests\ --no-logo +``` + +In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with + +```powershell +dotnet tool restore --configfile nuget.config +``` + +Run stryker locally: + +```powershell +dotnet stryker +``` + +Build and serve documentation locally (`http://localhost:8080`): + +```powershell +docfx .\docs\docfx.json --serve +``` From a6b0b78c4c69b78a6276edffff48c26cb5a1db20 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 21:03:15 +0200 Subject: [PATCH 03/24] Add automatically generated documentation --- docs/docfx.json | 54 ++++++++++++++++++++++++++++++++++ docs/docs/changelog.md | 3 ++ docs/docs/getting-started.md | 3 ++ docs/docs/introduction.md | 3 ++ docs/docs/toc.yml | 6 ++++ docs/images/adv-logo-brand.svg | 1 + docs/images/favicon.svg | 1 + docs/index.md | 7 +++++ docs/toc.yml | 4 +++ 9 files changed, 82 insertions(+) create mode 100644 docs/docfx.json create mode 100644 docs/docs/changelog.md create mode 100644 docs/docs/getting-started.md create mode 100644 docs/docs/introduction.md create mode 100644 docs/docs/toc.yml create mode 100644 docs/images/adv-logo-brand.svg create mode 100644 docs/images/favicon.svg create mode 100644 docs/index.md create mode 100644 docs/toc.yml diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..5c64254 --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,54 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../AdvancedSystems.Connector", + "files": [ + "**/*.csproj" + ] + } + ], + "dest": "api", + "disableGitFeatures": false, + "disableDefaultFilter": false + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "AdvancedSystems.Connector", + "_appTitle": "AdvancedSystems.Connnector", + "_appFaviconPath": "images/favicon.svg", + "_appLogoPath": "images/adv-logo-brand.svg", + "_appFooter": "Copyright © Advanced Systems 2024", + "_disableContribution": false, + "_gitContribute": { + "repo": "https://github.com/Advanced-Systems/connector" + }, + "_enableSearch": true, + "pdf": false + } + } +} diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md new file mode 100644 index 0000000..9a593cf --- /dev/null +++ b/docs/docs/changelog.md @@ -0,0 +1,3 @@ +# Changelog + +TODO diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md new file mode 100644 index 0000000..d5eca4a --- /dev/null +++ b/docs/docs/getting-started.md @@ -0,0 +1,3 @@ +# Getting Started + +TODO diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md new file mode 100644 index 0000000..cdd72da --- /dev/null +++ b/docs/docs/introduction.md @@ -0,0 +1,3 @@ +# Introduction + +TODO diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml new file mode 100644 index 0000000..47aa192 --- /dev/null +++ b/docs/docs/toc.yml @@ -0,0 +1,6 @@ +- name: Introduction + href: introduction.md +- name: Getting Started + href: getting-started.md +- name: Changelog + href: changelog.md diff --git a/docs/images/adv-logo-brand.svg b/docs/images/adv-logo-brand.svg new file mode 100644 index 0000000..1d4d04f --- /dev/null +++ b/docs/images/adv-logo-brand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/favicon.svg b/docs/images/favicon.svg new file mode 100644 index 0000000..efbed0e --- /dev/null +++ b/docs/images/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e66bdef --- /dev/null +++ b/docs/index.md @@ -0,0 +1,7 @@ +--- +_layout: landing +--- + +# Advanced Systems Connector + +Provides a database access layer to streamline database connections. diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..061acc6 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,4 @@ +- name: Docs + href: docs/ +- name: API + href: api/ \ No newline at end of file From 4543d92fc801853632d148d4e50fdf965cd1b80f Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 21:39:25 +0200 Subject: [PATCH 04/24] Set default values for MsSqlServerSettings --- .../Options/MsSqlServerSettings.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs b/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs index c5c8596..fbbe598 100644 --- a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs +++ b/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs @@ -11,21 +11,21 @@ public sealed class MsSqlServerSettings : DatabaseOptions /// a command and generating an error. The default is 30 seconds. /// [DisplayName("Command Timeout")] - public int CommandTimeout { get; set; } + public int CommandTimeout { get; set; } = 30; /// /// Gets or sets the length of time (in seconds) to wait for a connection to the server /// before terminating the attempt and generating an error. /// [DisplayName("Connect Timeout")] - public int ConnectTimeout { get; set; } + public int ConnectTimeout { get; set; } = 15; /// /// Gets or sets a value that indicates whether /// TLS encryption is required for all data sent between the client and server. /// [DisplayName("Encrypt")] - public SqlConnectionEncryptOption? Encrypt { get; set; } + public SqlConnectionEncryptOption? Encrypt { get; set; } = SqlConnectionEncryptOption.Optional; /// /// Gets or sets a Boolean value that indicates whether User ID and Password are @@ -33,33 +33,33 @@ public sealed class MsSqlServerSettings : DatabaseOptions /// credentials are used for authentication (when true). /// [DisplayName("Integrated Security")] - public bool IntegratedSecurity { get; set; } + public bool IntegratedSecurity { get; set; } = false; /// /// Gets or sets the maximum number of connections allowed in the connection pool for this specific /// connection string. /// [DisplayName("Max Pool Size")] - public int MaxPoolSize { get; set; } + public int MaxPoolSize { get; set; } = 100; /// /// Gets or sets the minimum number of connections allowed in the connection pool for this specific /// connection string. /// [DisplayName("Min Pool Size")] - public int MinPoolSize { get; set; } + public int MinPoolSize { get; set; } = 0; /// /// Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly /// opened every time that the connection is requested. /// [DisplayName("Pooling")] - public bool Pooling { get; set; } + public bool Pooling { get; set; } = true; /// /// Gets or sets a value that indicates whether the channel will be encrypted while bypassing /// walking the certificate chain to validate trust. /// [DisplayName("Trust Server Certificate")] - public bool TrustServerCertificate { get; set; } + public bool TrustServerCertificate { get; set; } = false; } From 341cb61824161298ae7f08164da3207df1d39b41 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 21:39:55 +0200 Subject: [PATCH 05/24] Add null check for connection string builder helper method --- AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs b/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs index fc286ca..2ea1102 100644 --- a/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs +++ b/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs @@ -14,6 +14,8 @@ internal static class MsSqlServerExtensions internal static string CreateConnectionString(this MsSqlServerSettings settings) { + ArgumentNullException.ThrowIfNull(settings, nameof(settings)); + var builder = new SqlConnectionStringBuilder { // Database Options From 7cfa68b522efcfdd8db2f13cd2ebd22e878f9ddd Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 10 Aug 2024 21:40:11 +0200 Subject: [PATCH 06/24] Start working on unit test project --- ...cedSystems - Backup.Connector.Tests.csproj | 29 +++++++++++ .../AdvancedSystems.Connector.Tests.csproj | 43 +++++++++++++++++ .../MsSqlServerConnectionServiceFixture.cs | 42 ++++++++++++++++ .../MsSqlServerConnectionServiceTests.cs | 48 +++++++++++++++++++ AdvancedSystems.Connector.sln | 6 +++ 5 files changed, 168 insertions(+) create mode 100644 AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj create mode 100644 AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj create mode 100644 AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs create mode 100644 AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj new file mode 100644 index 0000000..c9b6f41 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + Unit test project for AdvancedSystems.Connector. + AdvancedSystems.Security.Tests + AdvancedSystems.Security.Tests + true + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj new file mode 100644 index 0000000..f7c037a --- /dev/null +++ b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + Unit test project for AdvancedSystems.Connector. + AdvancedSystems.Security.Tests + AdvancedSystems.Security.Tests + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs new file mode 100644 index 0000000..abd7da4 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs @@ -0,0 +1,42 @@ +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Common; +using AdvancedSystems.Connector.Options; +using AdvancedSystems.Connector.Services; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Moq; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public class MsSqlServerConnectionServiceFixture +{ + public MsSqlServerConnectionServiceFixture() + { + this.Logger = new Mock>(); + this.Options = new Mock>(); + this.Options.Setup(x => x.Value).Returns(new MsSqlServerSettings + { + ApplicationName = "Advanced Systems Connector Test Suite", + DataSource = new DataSource("localhost", 1433), + InitialCatalog = "TestDatabase", + UserID = "stgr", + Password = "REDACTED", + TrustServerCertificate = true, + Pooling = true, + IntegratedSecurity = false, + }); + this.DatabaseConnectionService = new MsSqlServerConnectionService(this.Logger.Object, this.Options.Object); + } + + #region Properties + + public Mock> Logger { get; private set; } + + public Mock> Options { get; private set; } + + public IDatabaseConnectionService DatabaseConnectionService { get; private set; } + + #endregion +} diff --git a/AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs new file mode 100644 index 0000000..db86df9 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; + +using AdvancedSystems.Security.Tests.Fixtures; + +namespace AdvancedSystems.Security.Tests.Services; + +public class MsSqlServerConnectionServiceTests : IClassFixture +{ + private readonly MsSqlServerConnectionServiceFixture _sut; + + public MsSqlServerConnectionServiceTests(MsSqlServerConnectionServiceFixture fixture) + { + this._sut = fixture; + } + + #region Tests + + [Fact] + public void TestExecuteQuery() + { + // Arrange + var connectionService = this._sut.DatabaseConnectionService; + + // Act + + // Assert + } + + [Fact] + public async Task ExecuteQueryAsync() + { + + } + + [Fact] + public void TestExecuteNonQuery() + { + + } + + [Fact] + public async Task ExecuteNonQueryAsync() + { + + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.sln b/AdvancedSystems.Connector.sln index e076f01..3031cd4 100644 --- a/AdvancedSystems.Connector.sln +++ b/AdvancedSystems.Connector.sln @@ -12,6 +12,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedSystems.Connector.A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedSystems.Connector", "AdvancedSystems.Connector\AdvancedSystems.Connector.csproj", "{3D582FC1-A594-4438-872D-415154846C13}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedSystems.Connector.Tests", "AdvancedSystems.Connector.Tests\AdvancedSystems.Connector.Tests.csproj", "{247636DB-37C4-466C-B53C-7A313AD7DD6A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {3D582FC1-A594-4438-872D-415154846C13}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D582FC1-A594-4438-872D-415154846C13}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D582FC1-A594-4438-872D-415154846C13}.Release|Any CPU.Build.0 = Release|Any CPU + {247636DB-37C4-466C-B53C-7A313AD7DD6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {247636DB-37C4-466C-B53C-7A313AD7DD6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {247636DB-37C4-466C-B53C-7A313AD7DD6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {247636DB-37C4-466C-B53C-7A313AD7DD6A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 20d2a0d513fd6c079f00f245ed56fec171e05bca Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Mon, 12 Aug 2024 17:45:36 +0200 Subject: [PATCH 07/24] Uncouple provider-specific object instatiations from service, and refactor code --- .../DatabaseCommandType.cs | 16 ----- .../IDatabaseCommand.cs | 23 +------ .../IDatabaseConnectionServiceFactory.cs | 24 +++++++ .../IDatabaseParameter.cs | 2 +- ...tionFactory.cs => IDbConnectionFactory.cs} | 14 ++-- .../IDbDataAdapterFactory.cs | 28 ++++++++ .../Common/DatabaseCommand.cs | 53 ++++++++------- .../Common/DatabaseParameter.cs | 6 +- .../ServiceCollectionExtensions.cs | 24 +++++-- .../Internals/DbConnectionFactory.cs | 20 ++++++ .../Internals/DbDataAdapterFactory.cs | 20 ++++++ .../MsSqlServerExtensions.cs | 40 ++++++------ ...cs => DatabaseConnectionServiceFactory.cs} | 4 +- .../Services/MsSqlServerConnectionService.cs | 64 ++++++------------- 14 files changed, 203 insertions(+), 135 deletions(-) delete mode 100644 AdvancedSystems.Connector.Abstractions/DatabaseCommandType.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs rename AdvancedSystems.Connector.Abstractions/{IDatabaseConnectionFactory.cs => IDbConnectionFactory.cs} (57%) create mode 100644 AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs create mode 100644 AdvancedSystems.Connector/Internals/DbConnectionFactory.cs create mode 100644 AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs rename AdvancedSystems.Connector/{Converters => Internals}/MsSqlServerExtensions.cs (59%) rename AdvancedSystems.Connector/Services/{DatabaseConnectionFactory.cs => DatabaseConnectionServiceFactory.cs} (77%) diff --git a/AdvancedSystems.Connector.Abstractions/DatabaseCommandType.cs b/AdvancedSystems.Connector.Abstractions/DatabaseCommandType.cs deleted file mode 100644 index 6cbee71..0000000 --- a/AdvancedSystems.Connector.Abstractions/DatabaseCommandType.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace AdvancedSystems.Connector.Abstractions; - -/// -/// Specifies how a command string is interpreted. -/// -public enum DatabaseCommandType -{ - /// - /// An SQL command. - /// - Text, - /// - /// The name of a stored procedure. - /// - StoredProcedure -} diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs index 141bed7..22c2145 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Data; namespace AdvancedSystems.Connector.Abstractions; @@ -19,30 +20,12 @@ public interface IDatabaseCommand /// Gets or sets a value indicating how the /// property is to be interpreted. /// - DatabaseCommandType CommandType { get; set; } + CommandType CommandType { get; set; } /// /// Gets a collection of parameters. /// - List Parameters { get; } - - #endregion - - #region Methods - - /// - /// Adds a to . - /// - /// The parameter to add. - void AddParameter(IDatabaseParameter parameter); - - /// - /// Adds a to . - /// - /// The type of the parameter to add. - /// The name of the parameter to add. - /// The value of the parameter to add. - void AddParameter(string name, T value); + IReadOnlyList Parameters { get; } #endregion } diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs new file mode 100644 index 0000000..31fa094 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs @@ -0,0 +1,24 @@ +using System; + +namespace AdvancedSystems.Connector.Abstractions; + +/// +/// Defines a factory for creating instances of +/// based on a specified provider. +/// +public interface IDatabaseConnectionServiceFactory +{ + /// + /// Creates an instance of for the specified provider. + /// + /// + /// The database provider for which the connection service is to be created. + /// + /// + /// An instance of configured for the specified provider. + /// + /// + /// Thrown when the specified provider is not supported by the factory. + /// + IDatabaseConnectionService Create(Provider provider); +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs index ef6f365..b3afbca 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs @@ -8,7 +8,7 @@ public interface IDatabaseParameter string ParameterName { get; set; } - SqlDbType SqlDbType { get; set; } + DbType DbType { get; set; } object? Value { get; set; } diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs similarity index 57% rename from AdvancedSystems.Connector.Abstractions/IDatabaseConnectionFactory.cs rename to AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs index 8a95485..d1251a8 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs @@ -1,25 +1,29 @@ using System; +using System.Data.Common; namespace AdvancedSystems.Connector.Abstractions; /// -/// Defines a factory for creating instances of . +/// Defines a factory for creating instances of . /// -public interface IDatabaseConnectionFactory +public interface IDbConnectionFactory { /// - /// Creates a new instance of based on the + /// Creates a new instance of based on the /// specified provider. /// /// /// The database provider for which the connection service should be created. /// + /// + /// The connection string used to establish the connection with the database. + /// /// - /// An instance of configured for the + /// An instance of configured for the /// specified provider. /// /// /// Thrown when the specified is not supported by the factory. /// - IDatabaseConnectionService Create(Provider provider); + DbConnection Create(Provider provider, string connectionString); } diff --git a/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs new file mode 100644 index 0000000..75553b1 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Data.Common; + +namespace AdvancedSystems.Connector.Abstractions; + +/// +/// Defines a factory for creating instances of based on the +/// specified . +/// +public interface IDbDataAdapterFactory +{ + /// + /// Creates an instance of based on the specific implementation + /// derived from . + /// + /// + /// The that will be used to determine the type of + /// to create. + /// + /// + /// An instance of that is configured to work with the type of + /// provided. + /// + /// + /// Thrown when the specified provider is not supported by the factory. + /// + DbDataAdapter Create(DbCommand dbCommand); +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DatabaseCommand.cs b/AdvancedSystems.Connector/Common/DatabaseCommand.cs index 4477e88..fd799d8 100644 --- a/AdvancedSystems.Connector/Common/DatabaseCommand.cs +++ b/AdvancedSystems.Connector/Common/DatabaseCommand.cs @@ -7,59 +7,68 @@ using System.Text; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Converters; +using AdvancedSystems.Connector.Internals; namespace AdvancedSystems.Connector.Common; [DebuggerDisplay("{CommandText}")] public sealed class DatabaseCommand : IDatabaseCommand { + private readonly List _parameters = []; + #region Properties [DisplayName("Command Text")] public required string CommandText { get; set; } [DisplayName("Command Type")] - public required DatabaseCommandType CommandType { get; set; } + public required CommandType CommandType { get; set; } [DisplayName("Parameters")] - public List Parameters { get; } = []; + public IReadOnlyList Parameters => this._parameters; #endregion #region Methods - public void AddParameter(IDatabaseParameter parameter) + public void AddParameter(string parameterName, T value) { - this.Parameters.Add(parameter); + var databaseParameter = new DatabaseParameter + { + ParameterName = parameterName, + DbType = typeof(T).DeriveFrom(), + Value = (object?)value ?? DBNull.Value, + }; + + this._parameters.Add(databaseParameter); } - public void AddParameter(string name, T value) + public void AddParameter(string parameterName, object value, DbType type) { - var parameter = new DatabaseParameter + var databaseParameter = new DatabaseParameter { - ParameterName = name, - SqlDbType = typeof(T).Cast(), - Value = (object?)value ?? DBNull.Value + ParameterName = parameterName, + DbType = type, + Value = value }; - this.Parameters.Add(parameter); + this._parameters.Add(databaseParameter); } private static string? FormatValue(IDatabaseParameter parameter) { - return parameter.SqlDbType switch + return parameter.DbType switch { - SqlDbType.VarChar or SqlDbType.Char => parameter.Value?.ToString(), - SqlDbType.Bit => ((bool?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.TinyInt => ((byte?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.SmallInt => ((short?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.Int => ((int?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.BigInt => ((long?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.Real => ((float?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.Float => ((double?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.Decimal => ((decimal?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - SqlDbType.DateTime => $"'{((DateTime?)parameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture)}'", + DbType.String=> parameter.Value?.ToString(), + DbType.Boolean => ((bool?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Byte => ((byte?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Int16 => ((short?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Int32 => ((int?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Int64 => ((long?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Single => ((float?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Double => ((double?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.Decimal => ((decimal?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), + DbType.DateTime => ((DateTime?)parameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), _ => parameter.Value?.ToString(), }; diff --git a/AdvancedSystems.Connector/Common/DatabaseParameter.cs b/AdvancedSystems.Connector/Common/DatabaseParameter.cs index d9e0301..11445b0 100644 --- a/AdvancedSystems.Connector/Common/DatabaseParameter.cs +++ b/AdvancedSystems.Connector/Common/DatabaseParameter.cs @@ -2,6 +2,8 @@ using AdvancedSystems.Connector.Abstractions; +using Microsoft.Data.SqlClient; + namespace AdvancedSystems.Connector.Common; public sealed class DatabaseParameter : IDatabaseParameter @@ -10,9 +12,9 @@ public sealed class DatabaseParameter : IDatabaseParameter public required string ParameterName { get; set; } - public required SqlDbType SqlDbType { get; set; } + public required DbType DbType { get; set; } public required object? Value { get; set; } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index f328a45..9b67b94 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Internals; using AdvancedSystems.Connector.Options; using AdvancedSystems.Connector.Services; @@ -14,15 +15,30 @@ public static class ServiceCollectionExtensions { public delegate string DecryptPassword(string cipher); - #region DbConnectionService + #region Internals - private static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDatabaseConnectionService + internal static IServiceCollection AddDbConnectionFactory(this IServiceCollection services) { + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + + internal static IServiceCollection AddDbDataAdapterFactory(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + + internal static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDatabaseConnectionService + { + services.AddDbConnectionFactory(); + services.AddDbDataAdapterFactory(); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } - private static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions + internal static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions { services.AddOptions() .Configure(setupAction) @@ -38,7 +54,7 @@ private static IServiceCollection AddDbConnectionService(this IServiceColl return services; } - private static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions + internal static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions { services.AddOptions() .Bind(configuration.GetRequiredSection(Sections.DATABASE)) diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs new file mode 100644 index 0000000..be24168 --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Data.Common; + +using AdvancedSystems.Connector.Abstractions; + +using Microsoft.Data.SqlClient; + +namespace AdvancedSystems.Connector.Internals; + +internal sealed class DbConnectionFactory : IDbConnectionFactory +{ + public DbConnection Create(Provider provider, string connectionString) + { + return provider switch + { + Provider.MsSql => new SqlConnection(connectionString), + _ => throw new NotSupportedException(Enum.GetName(provider)), + }; + } +} diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs new file mode 100644 index 0000000..92f9020 --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Data.Common; + +using AdvancedSystems.Connector.Abstractions; + +using Microsoft.Data.SqlClient; + +namespace AdvancedSystems.Connector.Internals; + +internal sealed class DbDataAdapterFactory : IDbDataAdapterFactory +{ + public DbDataAdapter Create(DbCommand dbCommand) + { + return dbCommand switch + { + SqlCommand sqlCommand => new SqlDataAdapter(sqlCommand), + _ => throw new NotSupportedException() + }; + } +} diff --git a/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs similarity index 59% rename from AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs rename to AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs index 2ea1102..a3be0b6 100644 --- a/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs +++ b/AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs @@ -1,12 +1,13 @@ using System; using System.Data; +using System.Data.Common; using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; -namespace AdvancedSystems.Connector.Converters; +namespace AdvancedSystems.Connector.Internals; internal static class MsSqlServerExtensions { @@ -42,33 +43,36 @@ internal static string CreateConnectionString(this MsSqlServerSettings settings) #region Converters - internal static CommandType Cast(this DatabaseCommandType commandType) + internal static DbParameter DeriveFrom(this IDatabaseParameter databaseParameter, DbCommand dbCommand) { - return commandType switch + return dbCommand switch { - DatabaseCommandType.Text => CommandType.Text, - DatabaseCommandType.StoredProcedure => CommandType.StoredProcedure, - _ => throw new NotImplementedException(Enum.GetName(commandType)), + SqlCommand _ => new SqlParameter + { + ParameterName = databaseParameter.ParameterName, + DbType = databaseParameter.DbType, + Value = databaseParameter.Value, + }, + _ => throw new NotSupportedException(), }; } - internal static SqlDbType Cast(this Type type) + internal static DbType DeriveFrom(this Type type) { var typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); return typeCode switch { - TypeCode.String => SqlDbType.VarChar, - TypeCode.Char => SqlDbType.Char, - TypeCode.Boolean => SqlDbType.Bit, - TypeCode.Byte => SqlDbType.TinyInt, - TypeCode.SByte or TypeCode.Int16 => SqlDbType.SmallInt, - TypeCode.Int32 => SqlDbType.Int, - TypeCode.Int64 => SqlDbType.BigInt, - TypeCode.Single => SqlDbType.Real, - TypeCode.Double => SqlDbType.Float, - TypeCode.Decimal => SqlDbType.Decimal, - TypeCode.DateTime => SqlDbType.DateTime, + TypeCode.String or TypeCode.Char => DbType.String, + TypeCode.Boolean => DbType.Boolean, + TypeCode.Byte => DbType.Byte, + TypeCode.SByte or TypeCode.Int16 => DbType.Int16, + TypeCode.Int32 => DbType.Int32, + TypeCode.Int64 => DbType.Int64, + TypeCode.Single => DbType.Single, + TypeCode.Double => DbType.Double, + TypeCode.Decimal => DbType.Decimal, + TypeCode.DateTime => DbType.DateTime, _ => throw new ArgumentException($"Failed to infer type from {typeCode}."), }; } diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs similarity index 77% rename from AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs rename to AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs index cc0d8b8..08af932 100644 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs +++ b/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs @@ -6,11 +6,11 @@ namespace AdvancedSystems.Connector.Services; -public sealed class DatabaseConnectionFactory : IDatabaseConnectionFactory +public sealed class DatabaseConnectionServiceFactory : IDatabaseConnectionServiceFactory { private readonly IServiceProvider _serviceProvider; - public DatabaseConnectionFactory(IServiceProvider serviceProvider) + public DatabaseConnectionServiceFactory(IServiceProvider serviceProvider) { this._serviceProvider = serviceProvider; } diff --git a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs b/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs index 11b8d93..00e74f1 100644 --- a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs +++ b/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs @@ -1,13 +1,12 @@ using System; using System.Data; using System.Data.Common; -using System.Linq; using System.Threading; using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; -using AdvancedSystems.Connector.Converters; +using AdvancedSystems.Connector.Internals; using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; @@ -20,11 +19,15 @@ public sealed class MsSqlServerConnectionService : IDatabaseConnectionService { private readonly ILogger _logger; private readonly MsSqlServerSettings _settings; + private readonly IDbConnectionFactory _databaseConnectionFactory; + private readonly IDbDataAdapterFactory _dataAdapterFactory; - public MsSqlServerConnectionService(ILogger logger, IOptions options) + public MsSqlServerConnectionService(ILogger logger, IOptions options, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) { this._logger = logger; this._settings = options.Value; + this._databaseConnectionFactory = databaseConnectionFactory; + this._dataAdapterFactory = dbDataAdapterFactory; this.ConnectionString = this._settings.CreateConnectionString(); } @@ -44,31 +47,6 @@ private void ConnectionStateHandler(object sender, StateChangeEventArgs e) this.ConnectionState = e.CurrentState; } - private void InfoMessageHandler(object sender, SqlInfoMessageEventArgs e) - { - if (e.Errors.Count == 0) return; - - byte warningThreshold = 10; - byte errorThreshold = 20; - SqlError lastError = e.Errors[^1]; - - if (lastError.Class <= warningThreshold) - { - this._logger.LogWarning("{Server} issued the following warning: {Message}.", lastError.Server, lastError.Message); - } - else if (lastError.Class > warningThreshold && lastError.Class <= errorThreshold) - { - this._logger.LogError("{Server} raised an error on line {Line}: {Message} (State={State}).", lastError.Server, lastError.LineNumber, lastError.Message, lastError.State); - } - else - { - // A severity over 20 causes the connection to close - string reason = "Connection was closed"; - this._logger.LogCritical("{Reason} ({Message}).", reason, lastError.Message); - throw new DbConnectionException($"{reason} ({lastError.Message})."); - } - } - private void InvokeExceptionHandler(Action action, IDatabaseCommand databaseCommand) { try @@ -121,21 +99,20 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { - using var connection = new SqlConnection(this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); connection.StateChange += ConnectionStateHandler; - connection.InfoMessage += InfoMessageHandler; connection.Open(); using var sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType.Cast(); + sqlCommand.CommandType = databaseCommand.CommandType; foreach (var parameter in databaseCommand.Parameters) { - sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value); + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } - using var adapter = new SqlDataAdapter(sqlCommand); + using var adapter = this._dataAdapterFactory.Create(sqlCommand); adapter.Fill(result); }, databaseCommand); @@ -148,18 +125,17 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = new SqlConnection(this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); connection.StateChange += ConnectionStateHandler; - connection.InfoMessage += InfoMessageHandler; await connection.OpenAsync(cancellationToken); using var sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType.Cast(); + sqlCommand.CommandType = databaseCommand.CommandType; foreach (var parameter in databaseCommand.Parameters) { - sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value); + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } using var reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); @@ -175,18 +151,17 @@ public int ExecuteNonQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { - using var connection = new SqlConnection(this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); connection.StateChange += ConnectionStateHandler; - connection.InfoMessage += InfoMessageHandler; connection.Open(); using var sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType.Cast(); + sqlCommand.CommandType = databaseCommand.CommandType; foreach (var parameter in databaseCommand.Parameters) { - sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value); + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } rowsAffected = sqlCommand.ExecuteNonQuery(); @@ -201,18 +176,17 @@ public async ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseComman await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = new SqlConnection(this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); connection.StateChange += ConnectionStateHandler; - connection.InfoMessage += InfoMessageHandler; await connection.OpenAsync(cancellationToken); using var sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType.Cast(); + sqlCommand.CommandType = databaseCommand.CommandType; foreach (var parameter in databaseCommand.Parameters) { - sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value); + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } rowsAffected = await sqlCommand.ExecuteNonQueryAsync(cancellationToken); From f4b111ef5d813030954d0fb904dba38f92dcd8dc Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Mon, 26 Aug 2024 10:35:48 +0200 Subject: [PATCH 08/24] Continue refactoring and uncoupling of implementation details --- .../IDatabaseConnectionService.cs | 5 ++ .../IDatabaseConnectionServiceFactory.cs | 5 ++ .../IDatabaseOptions.cs | 38 +++++++++++ .../IDbConnectionFactory.cs | 4 ++ .../IDbConnectionStringFactory.cs | 10 +++ .../IDbOptionsFactory.cs | 6 ++ .../DatabaseConnectionServiceFixture.cs | 55 ++++++++++++++++ .../MsSqlServerConnectionServiceFixture.cs | 42 ------------- ...s.cs => DatabaseConnectionServiceTests.cs} | 6 +- .../ServiceCollectionExtensions.cs | 20 +++++- .../Internals/DbConnectionFactory.cs | 6 +- .../Internals/DbConnectionStringFactory.cs | 63 +++++++++++++++++++ .../Internals/DbDataAdapterFactory.cs | 6 +- .../Internals/DbOptionsFactory.cs | 34 ++++++++++ ...ServerExtensions.cs => MsSqlExtensions.cs} | 38 +---------- .../Options/DatabaseOptions.cs | 29 ++++----- ...MsSqlServerSettings.cs => MsSqlOptions.cs} | 2 +- ...ervice.cs => DatabaseConnectionService.cs} | 46 ++++++++------ .../DatabaseConnectionServiceFactory.cs | 6 +- 19 files changed, 297 insertions(+), 124 deletions(-) create mode 100644 AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs create mode 100644 AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs delete mode 100644 AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs rename AdvancedSystems.Connector.Tests/Services/{MsSqlServerConnectionServiceTests.cs => DatabaseConnectionServiceTests.cs} (70%) create mode 100644 AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs create mode 100644 AdvancedSystems.Connector/Internals/DbOptionsFactory.cs rename AdvancedSystems.Connector/Internals/{MsSqlServerExtensions.cs => MsSqlExtensions.cs} (50%) rename AdvancedSystems.Connector/Options/{MsSqlServerSettings.cs => MsSqlOptions.cs} (97%) rename AdvancedSystems.Connector/Services/{MsSqlServerConnectionService.cs => DatabaseConnectionService.cs} (85%) diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs index 249785b..e555b4a 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs @@ -27,6 +27,11 @@ public interface IDatabaseConnectionService /// ConnectionState ConnectionState { get; } + /// + /// Gets the active database provider. + /// + Provider Provider { get; } + #endregion #region Methods diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs index 31fa094..06c7b1b 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs @@ -8,6 +8,9 @@ namespace AdvancedSystems.Connector.Abstractions; /// public interface IDatabaseConnectionServiceFactory { + + #region Methods + /// /// Creates an instance of for the specified provider. /// @@ -21,4 +24,6 @@ public interface IDatabaseConnectionServiceFactory /// Thrown when the specified provider is not supported by the factory. /// IDatabaseConnectionService Create(Provider provider); + + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs new file mode 100644 index 0000000..961f874 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs @@ -0,0 +1,38 @@ +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDatabaseOptions +{ + #region Properties + + /// + /// Gets or sets the name of the application associated with the connection string. + /// + string ApplicationName { get; set; } + + /// + /// Gets or sets the name or network address of the instance of SQL Server to connect to. + /// + string DataSource { get; set; } + + /// + /// Gets or sets the name of the database associated with the connection. + /// + string InitialCatalog { get; set; } + + /// + /// Gets or sets the password for the SQL Server account. + /// + string Password { get; set; } + + /// + /// Gets or sets the targeted database provider. + /// + Provider Provider { get; set; } + + /// + /// Gets or sets the user ID to be used when connecting to SQL Server. + /// + string UserID { get; set; } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs index d1251a8..a3d78db 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs @@ -8,6 +8,8 @@ namespace AdvancedSystems.Connector.Abstractions; /// public interface IDbConnectionFactory { + #region Methods + /// /// Creates a new instance of based on the /// specified provider. @@ -26,4 +28,6 @@ public interface IDbConnectionFactory /// Thrown when the specified is not supported by the factory. /// DbConnection Create(Provider provider, string connectionString); + + #endregion } diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs new file mode 100644 index 0000000..6bd834d --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -0,0 +1,10 @@ +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDbConnectionStringFactory +{ + #region Methods + + public string Create(Provider provider); + + #endregion +} diff --git a/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs new file mode 100644 index 0000000..1ddffb2 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs @@ -0,0 +1,6 @@ +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDbOptionsFactory +{ + IDatabaseOptions Create(); +} diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs new file mode 100644 index 0000000..18c0019 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs @@ -0,0 +1,55 @@ +using AdvancedSystems.Connector; +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Common; +using AdvancedSystems.Connector.Options; +using AdvancedSystems.Connector.Services; + +using Microsoft.Extensions.Logging; + +using Moq; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public class DatabaseConnectionServiceFixture +{ + public DatabaseConnectionServiceFixture() + { + this.DbOptionsFactory.Setup(x => x.Create()) + .Returns(new MsSqlOptions + { + ApplicationName = "Advanced Systems Connector Test Suite", + DataSource = new DataSource("localhost", 1433), + InitialCatalog = "TestDatabase", + UserID = "stgr", + Password = "REDACTED", + Provider = Provider.MsSql, + TrustServerCertificate = true, + Pooling = true, + IntegratedSecurity = false, + }); + + this.DatabaseConnectionService = new DatabaseConnectionService( + this.Logger.Object, + this.DbOptionsFactory.Object, + this.DbConnectionStringFactory.Object, + this.DbConnectionFactory.Object, + this.DbDataAdapterFactory.Object + ); + } + + #region Properties + + public Mock> Logger { get; private set; } = new(); + + public Mock DbOptionsFactory { get; private set; } = new(); + + public Mock DbConnectionStringFactory { get; private set; } = new(); + + public Mock DbConnectionFactory { get; private set; } = new(); + + public Mock DbDataAdapterFactory { get; private set; } = new(); + + public IDatabaseConnectionService DatabaseConnectionService { get; private set; } + + #endregion +} diff --git a/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs deleted file mode 100644 index abd7da4..0000000 --- a/AdvancedSystems.Connector.Tests/Fixtures/MsSqlServerConnectionServiceFixture.cs +++ /dev/null @@ -1,42 +0,0 @@ -using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Common; -using AdvancedSystems.Connector.Options; -using AdvancedSystems.Connector.Services; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Moq; - -namespace AdvancedSystems.Security.Tests.Fixtures; - -public class MsSqlServerConnectionServiceFixture -{ - public MsSqlServerConnectionServiceFixture() - { - this.Logger = new Mock>(); - this.Options = new Mock>(); - this.Options.Setup(x => x.Value).Returns(new MsSqlServerSettings - { - ApplicationName = "Advanced Systems Connector Test Suite", - DataSource = new DataSource("localhost", 1433), - InitialCatalog = "TestDatabase", - UserID = "stgr", - Password = "REDACTED", - TrustServerCertificate = true, - Pooling = true, - IntegratedSecurity = false, - }); - this.DatabaseConnectionService = new MsSqlServerConnectionService(this.Logger.Object, this.Options.Object); - } - - #region Properties - - public Mock> Logger { get; private set; } - - public Mock> Options { get; private set; } - - public IDatabaseConnectionService DatabaseConnectionService { get; private set; } - - #endregion -} diff --git a/AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs similarity index 70% rename from AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs rename to AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs index db86df9..b30fade 100644 --- a/AdvancedSystems.Connector.Tests/Services/MsSqlServerConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs @@ -4,11 +4,11 @@ namespace AdvancedSystems.Security.Tests.Services; -public class MsSqlServerConnectionServiceTests : IClassFixture +public class DatabaseConnectionServiceTests : IClassFixture { - private readonly MsSqlServerConnectionServiceFixture _sut; + private readonly DatabaseConnectionServiceFixture _sut; - public MsSqlServerConnectionServiceTests(MsSqlServerConnectionServiceFixture fixture) + public DatabaseConnectionServiceTests(DatabaseConnectionServiceFixture fixture) { this._sut = fixture; } diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 9b67b94..106b716 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -17,6 +17,18 @@ public static class ServiceCollectionExtensions #region Internals + internal static IServiceCollection AddDbOptionsFactory(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + + internal static IServiceCollection AddDbConnectionStringFactory(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + internal static IServiceCollection AddDbConnectionFactory(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Transient()); @@ -31,6 +43,8 @@ internal static IServiceCollection AddDbDataAdapterFactory(this IServiceCollecti internal static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDatabaseConnectionService { + services.AddDbOptionsFactory(); + services.AddDbConnectionStringFactory(); services.AddDbConnectionFactory(); services.AddDbDataAdapterFactory(); @@ -76,15 +90,15 @@ internal static IServiceCollection AddDbConnectionService(this IServiceCol #region Microsoft SQL Server - public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) + public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) { - services.AddDbConnectionService(setupAction, decryptPassword); + services.AddDbConnectionService(setupAction, decryptPassword); return services; } public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) { - services.AddDbConnectionService(configuration, decryptPassword); + services.AddDbConnectionService(configuration, decryptPassword); return services; } diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index be24168..2727114 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -7,8 +7,10 @@ namespace AdvancedSystems.Connector.Internals; -internal sealed class DbConnectionFactory : IDbConnectionFactory +public sealed class DbConnectionFactory : IDbConnectionFactory { + #region Methods + public DbConnection Create(Provider provider, string connectionString) { return provider switch @@ -17,4 +19,6 @@ public DbConnection Create(Provider provider, string connectionString) _ => throw new NotSupportedException(Enum.GetName(provider)), }; } + + #endregion } diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs new file mode 100644 index 0000000..0270318 --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -0,0 +1,63 @@ +using System; + +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Options; + +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace AdvancedSystems.Connector.Internals; + +public sealed class DbConnectionStringFactory : IDbConnectionStringFactory +{ + private readonly IServiceProvider _serviceProvider; + + public DbConnectionStringFactory(IServiceProvider serviceProvider) + { + this._serviceProvider = serviceProvider; + } + + #region Helpers + + private string CreateMsSqlConnectionString(MsSqlOptions? settings) + { + ArgumentNullException.ThrowIfNull(settings, nameof(settings)); + + var builder = new SqlConnectionStringBuilder + { + // Database Options + ApplicationName = settings.ApplicationName, + DataSource = settings.DataSource, + InitialCatalog = settings.InitialCatalog, + Password = settings.Password, + UserID = settings.UserID, + // Microsoft SQL Server Options + CommandTimeout = settings.CommandTimeout, + ConnectTimeout = settings.ConnectTimeout, + Encrypt = settings.Encrypt, + IntegratedSecurity = settings.IntegratedSecurity, + MaxPoolSize = settings.MaxPoolSize, + MinPoolSize = settings.MinPoolSize, + Pooling = settings.Pooling, + TrustServerCertificate = settings.TrustServerCertificate, + }; + + return builder.ToString(); + } + + #endregion + + #region Methods + + public string Create(Provider provider) + { + return provider switch + { + Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), + _ => throw new NotSupportedException(), + }; + } + + #endregion +} diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index 92f9020..e60ed04 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -7,8 +7,10 @@ namespace AdvancedSystems.Connector.Internals; -internal sealed class DbDataAdapterFactory : IDbDataAdapterFactory +public sealed class DbDataAdapterFactory : IDbDataAdapterFactory { + #region Methods + public DbDataAdapter Create(DbCommand dbCommand) { return dbCommand switch @@ -17,4 +19,6 @@ public DbDataAdapter Create(DbCommand dbCommand) _ => throw new NotSupportedException() }; } + + #endregion } diff --git a/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs b/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs new file mode 100644 index 0000000..87bdff9 --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs @@ -0,0 +1,34 @@ +using System; + +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Options; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace AdvancedSystems.Connector.Internals; + +public class DbOptionsFactory : IDbOptionsFactory +{ + private readonly IServiceProvider _serviceProvider; + + public DbOptionsFactory(IServiceProvider serviceProvider) + { + this._serviceProvider = serviceProvider; + } + + #region Methods + + public IDatabaseOptions Create() + { + var databaseOptions = this._serviceProvider.GetRequiredService>(); + + return databaseOptions.Value.Provider switch + { + Provider.MsSql => this._serviceProvider.GetRequiredService>().Value, + _ => throw new NotSupportedException(), + }; + } + + #endregion +} diff --git a/AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs similarity index 50% rename from AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs rename to AdvancedSystems.Connector/Internals/MsSqlExtensions.cs index a3be0b6..34797eb 100644 --- a/AdvancedSystems.Connector/Internals/MsSqlServerExtensions.cs +++ b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs @@ -3,47 +3,16 @@ using System.Data.Common; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; namespace AdvancedSystems.Connector.Internals; -internal static class MsSqlServerExtensions +public static class MsSqlExtensions { - #region Helpers - - internal static string CreateConnectionString(this MsSqlServerSettings settings) - { - ArgumentNullException.ThrowIfNull(settings, nameof(settings)); - - var builder = new SqlConnectionStringBuilder - { - // Database Options - ApplicationName = settings.ApplicationName, - DataSource = settings.DataSource, - InitialCatalog = settings.InitialCatalog, - Password = settings.Password, - UserID = settings.UserID, - // Microsoft SQL Server Options - CommandTimeout = settings.CommandTimeout, - ConnectTimeout = settings.ConnectTimeout, - Encrypt = settings.Encrypt, - IntegratedSecurity = settings.IntegratedSecurity, - MaxPoolSize = settings.MaxPoolSize, - MinPoolSize = settings.MinPoolSize, - Pooling = settings.Pooling, - TrustServerCertificate = settings.TrustServerCertificate, - }; - - return builder.ToString(); - } - - #endregion - #region Converters - internal static DbParameter DeriveFrom(this IDatabaseParameter databaseParameter, DbCommand dbCommand) + public static DbParameter DeriveFrom(this IDatabaseParameter databaseParameter, DbCommand dbCommand) { return dbCommand switch { @@ -56,8 +25,7 @@ internal static DbParameter DeriveFrom(this IDatabaseParameter databaseParameter _ => throw new NotSupportedException(), }; } - - internal static DbType DeriveFrom(this Type type) + public static DbType DeriveFrom(this Type type) { var typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); diff --git a/AdvancedSystems.Connector/Options/DatabaseOptions.cs b/AdvancedSystems.Connector/Options/DatabaseOptions.cs index b5321e6..b1bbbfd 100644 --- a/AdvancedSystems.Connector/Options/DatabaseOptions.cs +++ b/AdvancedSystems.Connector/Options/DatabaseOptions.cs @@ -1,43 +1,38 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; -using Microsoft.Data.SqlClient; +using AdvancedSystems.Connector.Abstractions; namespace AdvancedSystems.Connector.Options; -public class DatabaseOptions +public class DatabaseOptions : IDatabaseOptions { - /// - /// Gets or sets the name of the application associated with the connection string. - /// + #region Properties + [DisplayName("Application Name")] public required string ApplicationName { get; set; } - /// - /// Gets or sets the name or network address of the instance of SQL Server to connect to. - /// [Required] [DisplayName("Data Source")] public required string DataSource { get; set; } - /// - /// Gets or sets the name of the database associated with the connection. - /// [Required] [DisplayName("Initial Catalog")] public required string InitialCatalog { get; set; } - /// - /// Gets or sets the password for the SQL Server account. - /// [Required] [DisplayName("Password")] public required string Password { get; set; } - /// - /// Gets or sets the user ID to be used when connecting to SQL Server. - /// + [Required] + [DisplayName("Provider")] + [JsonConverter(typeof(Provider))] + public required Provider Provider { get; set; } + [Required] [DisplayName("User ID")] public required string UserID { get; set; } + + #endregion } diff --git a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs similarity index 97% rename from AdvancedSystems.Connector/Options/MsSqlServerSettings.cs rename to AdvancedSystems.Connector/Options/MsSqlOptions.cs index fbbe598..8546126 100644 --- a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs +++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs @@ -4,7 +4,7 @@ namespace AdvancedSystems.Connector.Options; -public sealed class MsSqlServerSettings : DatabaseOptions +public sealed class MsSqlOptions : DatabaseOptions { /// /// Gets or sets the default wait time (in seconds) before terminating the attempt to execute diff --git a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs similarity index 85% rename from AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs rename to AdvancedSystems.Connector/Services/DatabaseConnectionService.cs index 00e74f1..95f87b6 100644 --- a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs @@ -7,39 +7,35 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; using AdvancedSystems.Connector.Internals; -using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace AdvancedSystems.Connector.Services; -public sealed class MsSqlServerConnectionService : IDatabaseConnectionService +public sealed class DatabaseConnectionService : IDatabaseConnectionService { - private readonly ILogger _logger; - private readonly MsSqlServerSettings _settings; + private readonly ILogger _logger; + private readonly IDbOptionsFactory _dbOptionsFactory; + private readonly IDbConnectionStringFactory _dbConnectionStringFactory; private readonly IDbConnectionFactory _databaseConnectionFactory; private readonly IDbDataAdapterFactory _dataAdapterFactory; - public MsSqlServerConnectionService(ILogger logger, IOptions options, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) + private readonly IDatabaseOptions _databaseOptions; + + public DatabaseConnectionService(ILogger logger, IDbOptionsFactory dbOptionsFactory, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) { this._logger = logger; - this._settings = options.Value; + this._dbOptionsFactory = dbOptionsFactory; + this._dbConnectionStringFactory = dbConnectionStringFactory; this._databaseConnectionFactory = databaseConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - this.ConnectionString = this._settings.CreateConnectionString(); + this._databaseOptions = this._dbOptionsFactory.Create(); + this.Provider = this._databaseOptions.Provider; + this.ConnectionString = this._dbConnectionStringFactory.Create(this.Provider); } - #region Properties - - public string ConnectionString { get; private set; } - - public ConnectionState ConnectionState { get; private set; } - - #endregion - #region Helpers private void ConnectionStateHandler(object sender, StateChangeEventArgs e) @@ -91,6 +87,16 @@ private async ValueTask InvokeExceptionHandlerAsync(Func { - using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); connection.StateChange += ConnectionStateHandler; connection.Open(); @@ -125,7 +131,7 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); connection.StateChange += ConnectionStateHandler; await connection.OpenAsync(cancellationToken); @@ -151,7 +157,7 @@ public int ExecuteNonQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { - using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); connection.StateChange += ConnectionStateHandler; connection.Open(); @@ -176,7 +182,7 @@ public async ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseComman await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = this._databaseConnectionFactory.Create(Provider.MsSql, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); connection.StateChange += ConnectionStateHandler; await connection.OpenAsync(cancellationToken); diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs index 08af932..3b9f850 100644 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs @@ -15,12 +15,16 @@ public DatabaseConnectionServiceFactory(IServiceProvider serviceProvider) this._serviceProvider = serviceProvider; } + #region Methods + public IDatabaseConnectionService Create(Provider provider) { return provider switch { - Provider.MsSql => this._serviceProvider.GetRequiredService(), + Provider.MsSql => this._serviceProvider.GetRequiredService(), _ => throw new NotSupportedException(Enum.GetName(provider)), }; } + + #endregion } From 66ba76ff66a91e96002103a000d75a4b2151dd22 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 13 Sep 2024 22:21:16 +0200 Subject: [PATCH 09/24] Refactor code * rename region names * add doc strings to DatabaseOptions * remove IDatabaseConnectionServiceFactory and DatabaseConnectionServiceFactory * add IOptions to DatabaseConnectionService * update unit tests --- .../IDatabaseConnectionServiceFactory.cs | 29 ----- .../IDbOptionsFactory.cs | 6 - .../DatabaseConnectionServiceFixture.cs | 7 +- .../ServiceCollectionExtensions.cs | 7 -- .../Internals/DbConnectionFactory.cs | 2 +- .../Internals/DbConnectionStringFactory.cs | 28 ++--- .../Internals/DbDataAdapterFactory.cs | 2 +- .../Internals/DbOptionsFactory.cs | 34 ----- .../Options/DatabaseOptions.cs | 18 +++ .../Services/DatabaseConnectionService.cs | 119 +++++++++--------- .../DatabaseConnectionServiceFactory.cs | 30 ----- 11 files changed, 97 insertions(+), 185 deletions(-) delete mode 100644 AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs delete mode 100644 AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs delete mode 100644 AdvancedSystems.Connector/Internals/DbOptionsFactory.cs delete mode 100644 AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs deleted file mode 100644 index 06c7b1b..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionServiceFactory.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace AdvancedSystems.Connector.Abstractions; - -/// -/// Defines a factory for creating instances of -/// based on a specified provider. -/// -public interface IDatabaseConnectionServiceFactory -{ - - #region Methods - - /// - /// Creates an instance of for the specified provider. - /// - /// - /// The database provider for which the connection service is to be created. - /// - /// - /// An instance of configured for the specified provider. - /// - /// - /// Thrown when the specified provider is not supported by the factory. - /// - IDatabaseConnectionService Create(Provider provider); - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs deleted file mode 100644 index 1ddffb2..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDbOptionsFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AdvancedSystems.Connector.Abstractions; - -public interface IDbOptionsFactory -{ - IDatabaseOptions Create(); -} diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs index 18c0019..2999d05 100644 --- a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs +++ b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs @@ -5,6 +5,7 @@ using AdvancedSystems.Connector.Services; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; @@ -14,7 +15,7 @@ public class DatabaseConnectionServiceFixture { public DatabaseConnectionServiceFixture() { - this.DbOptionsFactory.Setup(x => x.Create()) + this.Options.Setup(x => x.Value) .Returns(new MsSqlOptions { ApplicationName = "Advanced Systems Connector Test Suite", @@ -30,7 +31,7 @@ public DatabaseConnectionServiceFixture() this.DatabaseConnectionService = new DatabaseConnectionService( this.Logger.Object, - this.DbOptionsFactory.Object, + this.Options.Object, this.DbConnectionStringFactory.Object, this.DbConnectionFactory.Object, this.DbDataAdapterFactory.Object @@ -41,7 +42,7 @@ public DatabaseConnectionServiceFixture() public Mock> Logger { get; private set; } = new(); - public Mock DbOptionsFactory { get; private set; } = new(); + public Mock> Options { get; private set; } = new(); public Mock DbConnectionStringFactory { get; private set; } = new(); diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 106b716..9bf7f28 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -17,12 +17,6 @@ public static class ServiceCollectionExtensions #region Internals - internal static IServiceCollection AddDbOptionsFactory(this IServiceCollection services) - { - services.TryAdd(ServiceDescriptor.Transient()); - return services; - } - internal static IServiceCollection AddDbConnectionStringFactory(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Transient()); @@ -43,7 +37,6 @@ internal static IServiceCollection AddDbDataAdapterFactory(this IServiceCollecti internal static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDatabaseConnectionService { - services.AddDbOptionsFactory(); services.AddDbConnectionStringFactory(); services.AddDbConnectionFactory(); services.AddDbDataAdapterFactory(); diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index 2727114..2eaef65 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -9,7 +9,7 @@ namespace AdvancedSystems.Connector.Internals; public sealed class DbConnectionFactory : IDbConnectionFactory { - #region Methods + #region Public Methods public DbConnection Create(Provider provider, string connectionString) { diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 0270318..7b21caa 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -18,7 +18,20 @@ public DbConnectionStringFactory(IServiceProvider serviceProvider) this._serviceProvider = serviceProvider; } - #region Helpers + #region Public Methods + + public string Create(Provider provider) + { + return provider switch + { + Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), + _ => throw new NotSupportedException(), + }; + } + + #endregion + + #region Private Methods private string CreateMsSqlConnectionString(MsSqlOptions? settings) { @@ -47,17 +60,4 @@ private string CreateMsSqlConnectionString(MsSqlOptions? settings) } #endregion - - #region Methods - - public string Create(Provider provider) - { - return provider switch - { - Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), - _ => throw new NotSupportedException(), - }; - } - - #endregion } diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index e60ed04..2034b73 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -9,7 +9,7 @@ namespace AdvancedSystems.Connector.Internals; public sealed class DbDataAdapterFactory : IDbDataAdapterFactory { - #region Methods + #region Public Methods public DbDataAdapter Create(DbCommand dbCommand) { diff --git a/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs b/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs deleted file mode 100644 index 87bdff9..0000000 --- a/AdvancedSystems.Connector/Internals/DbOptionsFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Options; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace AdvancedSystems.Connector.Internals; - -public class DbOptionsFactory : IDbOptionsFactory -{ - private readonly IServiceProvider _serviceProvider; - - public DbOptionsFactory(IServiceProvider serviceProvider) - { - this._serviceProvider = serviceProvider; - } - - #region Methods - - public IDatabaseOptions Create() - { - var databaseOptions = this._serviceProvider.GetRequiredService>(); - - return databaseOptions.Value.Provider switch - { - Provider.MsSql => this._serviceProvider.GetRequiredService>().Value, - _ => throw new NotSupportedException(), - }; - } - - #endregion -} diff --git a/AdvancedSystems.Connector/Options/DatabaseOptions.cs b/AdvancedSystems.Connector/Options/DatabaseOptions.cs index b1bbbfd..e04e6a4 100644 --- a/AdvancedSystems.Connector/Options/DatabaseOptions.cs +++ b/AdvancedSystems.Connector/Options/DatabaseOptions.cs @@ -10,26 +10,44 @@ public class DatabaseOptions : IDatabaseOptions { #region Properties + /// + /// Gets or sets the name of the application associated with the connection string. + /// [DisplayName("Application Name")] public required string ApplicationName { get; set; } + /// + /// Gets or sets the name or network address of the instance of SQL Server to connect to. + /// [Required] [DisplayName("Data Source")] public required string DataSource { get; set; } + /// + /// Gets or sets the name of the database associated with the connection. + /// [Required] [DisplayName("Initial Catalog")] public required string InitialCatalog { get; set; } + /// + /// Gets or sets the password for the SQL Server account. + /// [Required] [DisplayName("Password")] public required string Password { get; set; } + /// + /// Gets or sets a provider. + /// [Required] [DisplayName("Provider")] [JsonConverter(typeof(Provider))] public required Provider Provider { get; set; } + /// + /// Gets or sets the user ID to be used when connecting to SQL Server. + /// [Required] [DisplayName("User ID")] public required string UserID { get; set; } diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs index 95f87b6..a766c94 100644 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs @@ -7,86 +7,34 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; using AdvancedSystems.Connector.Internals; +using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace AdvancedSystems.Connector.Services; public sealed class DatabaseConnectionService : IDatabaseConnectionService { private readonly ILogger _logger; - private readonly IDbOptionsFactory _dbOptionsFactory; + private readonly DatabaseOptions _dbOptions; private readonly IDbConnectionStringFactory _dbConnectionStringFactory; private readonly IDbConnectionFactory _databaseConnectionFactory; private readonly IDbDataAdapterFactory _dataAdapterFactory; - private readonly IDatabaseOptions _databaseOptions; - - public DatabaseConnectionService(ILogger logger, IDbOptionsFactory dbOptionsFactory, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) + public DatabaseConnectionService(ILogger logger, IOptions dbOptions, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) { this._logger = logger; - this._dbOptionsFactory = dbOptionsFactory; + this._dbOptions = dbOptions.Value; this._dbConnectionStringFactory = dbConnectionStringFactory; this._databaseConnectionFactory = databaseConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - - this._databaseOptions = this._dbOptionsFactory.Create(); - this.Provider = this._databaseOptions.Provider; + + this.Provider = this._dbOptions.Provider; this.ConnectionString = this._dbConnectionStringFactory.Create(this.Provider); } - #region Helpers - - private void ConnectionStateHandler(object sender, StateChangeEventArgs e) - { - this.ConnectionState = e.CurrentState; - } - - private void InvokeExceptionHandler(Action action, IDatabaseCommand databaseCommand) - { - try - { - action(databaseCommand); - this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); - } - catch (SqlException exception) - { - string reason = $"Database failed to execute command {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); - throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); - } - catch (DbException exception) - { - string reason = $"Communication to database failed during the execution of {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); - throw new DbConnectionException($"{reason} ({exception.Message}).", exception); - } - } - - private async ValueTask InvokeExceptionHandlerAsync(Func action, IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default) - { - try - { - await action(databaseCommand, cancellationToken); - this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); - } - catch (SqlException exception) - { - string reason = $"Database failed to execute command {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); - throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); - } - catch (DbException exception) - { - string reason = $"Communication with the database failed or was interrupted during the execution off {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); - throw new DbConnectionException($"{reason} ({exception.Message}).", exception); - } - } - - #endregion - #region Properties public string ConnectionString { get; private set; } @@ -97,7 +45,7 @@ private async ValueTask InvokeExceptionHandlerAsync(Func action, IDatabaseCommand databaseCommand) + { + try + { + action(databaseCommand); + this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); + } + catch (SqlException exception) + { + string reason = $"Database failed to execute command {databaseCommand}"; + this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); + throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); + } + catch (DbException exception) + { + string reason = $"Communication to database failed during the execution of {databaseCommand}"; + this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); + throw new DbConnectionException($"{reason} ({exception.Message}).", exception); + } + } + + private async ValueTask InvokeExceptionHandlerAsync(Func action, IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default) + { + try + { + await action(databaseCommand, cancellationToken); + this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); + } + catch (SqlException exception) + { + string reason = $"Database failed to execute command {databaseCommand}"; + this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); + throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); + } + catch (DbException exception) + { + string reason = $"Communication with the database failed or was interrupted during the execution off {databaseCommand}"; + this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); + throw new DbConnectionException($"{reason} ({exception.Message}).", exception); + } + } + + #endregion } diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs deleted file mode 100644 index 3b9f850..0000000 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionServiceFactory.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -using AdvancedSystems.Connector.Abstractions; - -using Microsoft.Extensions.DependencyInjection; - -namespace AdvancedSystems.Connector.Services; - -public sealed class DatabaseConnectionServiceFactory : IDatabaseConnectionServiceFactory -{ - private readonly IServiceProvider _serviceProvider; - - public DatabaseConnectionServiceFactory(IServiceProvider serviceProvider) - { - this._serviceProvider = serviceProvider; - } - - #region Methods - - public IDatabaseConnectionService Create(Provider provider) - { - return provider switch - { - Provider.MsSql => this._serviceProvider.GetRequiredService(), - _ => throw new NotSupportedException(Enum.GetName(provider)), - }; - } - - #endregion -} From 8b5cdac03bf95fb081a1f952e4d4589b78ab4bd8 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 27 Oct 2024 20:06:47 +0100 Subject: [PATCH 10/24] Format project and enable warning for missing this qualifier --- .editorconfig | 8 ++++---- .../Exceptions/DbCommandExecutionException.cs | 2 +- .../Exceptions/DbConnectionException.cs | 2 +- .../IDatabaseCommand.cs | 2 +- .../IDatabaseConnectionService.cs | 4 ++-- .../IDatabaseParameter.cs | 2 +- .../IDbConnectionFactory.cs | 2 +- .../IDbConnectionStringFactory.cs | 2 +- AdvancedSystems.Connector.Abstractions/Provider.cs | 2 +- .../Fixtures/DatabaseConnectionServiceFixture.cs | 2 +- AdvancedSystems.Connector/Common/DataSource.cs | 2 +- AdvancedSystems.Connector/Common/DatabaseCommand.cs | 6 +++--- .../ServiceCollectionExtensions.cs | 2 +- .../Internals/DbConnectionFactory.cs | 2 +- .../Internals/DbConnectionStringFactory.cs | 4 ++-- .../Internals/DbDataAdapterFactory.cs | 2 +- .../Internals/MsSqlExtensions.cs | 2 +- AdvancedSystems.Connector/Options/DatabaseOptions.cs | 2 +- AdvancedSystems.Connector/Options/MsSqlOptions.cs | 2 +- AdvancedSystems.Connector/Options/Sections.cs | 2 +- .../Services/DatabaseConnectionService.cs | 12 ++++++------ 21 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.editorconfig b/.editorconfig index 57c8b2e..2dc062b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,10 +30,10 @@ dotnet_sort_system_directives_first = true file_header_template = unset # this. and Me. preferences -dotnet_style_qualification_for_event = false:silent -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_event = true:warning +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_property = true:warning # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs index 5d1cdca..45a233e 100644 --- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs +++ b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs @@ -42,4 +42,4 @@ public DbCommandExecutionException(string message, Exception inner) : base(messa { } -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs index e5111cc..0af0a78 100644 --- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs +++ b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs @@ -42,4 +42,4 @@ public DbConnectionException(string message, Exception inner) : base(message, in { } -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs index 22c2145..c630e7b 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs @@ -28,4 +28,4 @@ public interface IDatabaseCommand IReadOnlyList Parameters { get; } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs index e555b4a..5fd1200 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs @@ -1,6 +1,6 @@ using System.Data; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions.Exceptions; @@ -128,4 +128,4 @@ public interface IDatabaseConnectionService ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default); #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs index b3afbca..e2eab8e 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs @@ -13,4 +13,4 @@ public interface IDatabaseParameter object? Value { get; set; } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs index a3d78db..0cb12a1 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs @@ -30,4 +30,4 @@ public interface IDbConnectionFactory DbConnection Create(Provider provider, string connectionString); #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index 6bd834d..48dd9a2 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -7,4 +7,4 @@ public interface IDbConnectionStringFactory public string Create(Provider provider); #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/Provider.cs b/AdvancedSystems.Connector.Abstractions/Provider.cs index ae0b0dc..86e0fec 100644 --- a/AdvancedSystems.Connector.Abstractions/Provider.cs +++ b/AdvancedSystems.Connector.Abstractions/Provider.cs @@ -9,4 +9,4 @@ public enum Provider /// Microsoft SQL Server. /// MsSql, -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs index 2999d05..86f907e 100644 --- a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs +++ b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs @@ -53,4 +53,4 @@ public DatabaseConnectionServiceFixture() public IDatabaseConnectionService DatabaseConnectionService { get; private set; } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DataSource.cs b/AdvancedSystems.Connector/Common/DataSource.cs index 49da794..991f0ca 100644 --- a/AdvancedSystems.Connector/Common/DataSource.cs +++ b/AdvancedSystems.Connector/Common/DataSource.cs @@ -33,4 +33,4 @@ public static implicit operator string(DataSource dataSource) } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DatabaseCommand.cs b/AdvancedSystems.Connector/Common/DatabaseCommand.cs index fd799d8..8c12c47 100644 --- a/AdvancedSystems.Connector/Common/DatabaseCommand.cs +++ b/AdvancedSystems.Connector/Common/DatabaseCommand.cs @@ -19,7 +19,7 @@ public sealed class DatabaseCommand : IDatabaseCommand #region Properties [DisplayName("Command Text")] - public required string CommandText { get; set; } + public required string CommandText { get; set; } [DisplayName("Command Type")] public required CommandType CommandType { get; set; } @@ -59,7 +59,7 @@ public void AddParameter(string parameterName, object value, DbType type) { return parameter.DbType switch { - DbType.String=> parameter.Value?.ToString(), + DbType.String => parameter.Value?.ToString(), DbType.Boolean => ((bool?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), DbType.Byte => ((byte?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), DbType.Int16 => ((short?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), @@ -89,4 +89,4 @@ public override string ToString() } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 9bf7f28..436f3f6 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -96,4 +96,4 @@ public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCo } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index 2eaef65..542eee5 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -21,4 +21,4 @@ public DbConnection Create(Provider provider, string connectionString) } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 7b21caa..6c31fb8 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -24,7 +24,7 @@ public string Create(Provider provider) { return provider switch { - Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), + Provider.MsSql => this.CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), _ => throw new NotSupportedException(), }; } @@ -60,4 +60,4 @@ private string CreateMsSqlConnectionString(MsSqlOptions? settings) } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index 2034b73..6c3af97 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -21,4 +21,4 @@ public DbDataAdapter Create(DbCommand dbCommand) } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs index 34797eb..d92cb0c 100644 --- a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs +++ b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs @@ -46,4 +46,4 @@ public static DbType DeriveFrom(this Type type) } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/DatabaseOptions.cs b/AdvancedSystems.Connector/Options/DatabaseOptions.cs index e04e6a4..9c1fe37 100644 --- a/AdvancedSystems.Connector/Options/DatabaseOptions.cs +++ b/AdvancedSystems.Connector/Options/DatabaseOptions.cs @@ -53,4 +53,4 @@ public class DatabaseOptions : IDatabaseOptions public required string UserID { get; set; } #endregion -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/MsSqlOptions.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs index 8546126..f1a0b00 100644 --- a/AdvancedSystems.Connector/Options/MsSqlOptions.cs +++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs @@ -62,4 +62,4 @@ public sealed class MsSqlOptions : DatabaseOptions /// [DisplayName("Trust Server Certificate")] public bool TrustServerCertificate { get; set; } = false; -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/Sections.cs b/AdvancedSystems.Connector/Options/Sections.cs index 22cc1f8..205ccfa 100644 --- a/AdvancedSystems.Connector/Options/Sections.cs +++ b/AdvancedSystems.Connector/Options/Sections.cs @@ -3,4 +3,4 @@ public readonly record struct Sections { public const string DATABASE = "Database"; -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs index a766c94..5056f8c 100644 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs @@ -30,7 +30,7 @@ public DatabaseConnectionService(ILogger logger, IOpt this._dbConnectionStringFactory = dbConnectionStringFactory; this._databaseConnectionFactory = databaseConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - + this.Provider = this._dbOptions.Provider; this.ConnectionString = this._dbConnectionStringFactory.Create(this.Provider); } @@ -54,7 +54,7 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); - connection.StateChange += ConnectionStateHandler; + connection.StateChange += this.ConnectionStateHandler; connection.Open(); using var sqlCommand = connection.CreateCommand(); @@ -80,7 +80,7 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); - connection.StateChange += ConnectionStateHandler; + connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); using var sqlCommand = connection.CreateCommand(); @@ -106,7 +106,7 @@ public int ExecuteNonQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); - connection.StateChange += ConnectionStateHandler; + connection.StateChange += this.ConnectionStateHandler; connection.Open(); using var sqlCommand = connection.CreateCommand(); @@ -131,7 +131,7 @@ public async ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseComman await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); - connection.StateChange += ConnectionStateHandler; + connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); using var sqlCommand = connection.CreateCommand(); @@ -201,4 +201,4 @@ private async ValueTask InvokeExceptionHandlerAsync(Func Date: Sat, 9 Nov 2024 15:03:53 +0100 Subject: [PATCH 11/24] Setup pre-commit and pre-push hooks --- .config/dotnet-tools.json | 13 +++++++ .husky/pre-commit | 5 +++ .husky/pre-push | 5 +++ .husky/task-runner.json | 39 +++++++++++++++++++ .../DatabaseConnectionServiceTests.cs | 4 +- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .husky/pre-commit create mode 100644 .husky/pre-push create mode 100644 .husky/task-runner.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..e6df774 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "husky": { + "version": "0.7.1", + "commands": [ + "husky" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..c8c2dea --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# lint project based on editorconfig rules +dotnet husky run --group pre-commit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..63f2594 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# build project and run unit tests +dotnet husky run --group pre-push diff --git a/.husky/task-runner.json b/.husky/task-runner.json new file mode 100644 index 0000000..57978a0 --- /dev/null +++ b/.husky/task-runner.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "tasks": [ + { + "name": "dotnet-format", + "group": "pre-commit", + "command": "dotnet", + "args": [ + "format", + "--include", + "${staged}", + "--verbosity", + "diagnostic" + ], + "include": [ + "**/*.cs", + "**/*.ps1" + ] + }, + { + "name": "dotnet-build", + "group": "pre-push", + "command": "dotnet", + "args": [ + "build", + "/warnaserror" + ] + }, + { + "name": "dotnet-test", + "group": "pre-push", + "command": "dotnet", + "args": [ + "test", + "--nologo" + ] + } + ] +} diff --git a/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs index b30fade..ff45bbd 100644 --- a/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs @@ -27,7 +27,7 @@ public void TestExecuteQuery() } [Fact] - public async Task ExecuteQueryAsync() + public void ExecuteQueryAsync() { } @@ -39,7 +39,7 @@ public void TestExecuteNonQuery() } [Fact] - public async Task ExecuteNonQueryAsync() + public void ExecuteNonQueryAsync() { } From 2c74a7c04fb61277a0ad1d34464c80c77d060d25 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 22 Feb 2025 15:43:50 +0100 Subject: [PATCH 12/24] Refactor DbConnectionService and add boilerplate code for unit tests --- .../IDatabaseCommand.cs | 4 +- ...tionService.cs => IDbConnectionService.cs} | 17 ++- .../IDbConnectionServiceFactory.cs | 46 ++++++++ .../IDbConnectionSettings.cs | 29 +++++ .../IDbFactorySettings.cs | 22 ++++ ...{IDatabaseParameter.cs => IDbParameter.cs} | 2 +- .../{IDatabaseOptions.cs => IDbSettings.cs} | 10 +- .../ServiceCollectionExtensionsTests.cs | 8 ++ .../DatabaseConnectionServiceFixture.cs | 56 --------- .../DatabaseConnectionServiceTests.cs | 48 -------- .../Services/DbConnectionServiceTests.cs | 38 ++++++ .../Common/DatabaseCommand.cs | 10 +- .../{DatabaseParameter.cs => DbParameter.cs} | 2 +- .../ServiceCollectionExtensions.cs | 109 +++++++++--------- .../Internals/MsSqlExtensions.cs | 2 +- .../Options/MsSqlOptions.cs | 44 ++++++- AdvancedSystems.Connector/Options/Sections.cs | 2 +- .../{DatabaseOptions.cs => UserOptions.cs} | 26 ++--- ...ctionService.cs => DbConnectionService.cs} | 33 +++--- 19 files changed, 283 insertions(+), 225 deletions(-) rename AdvancedSystems.Connector.Abstractions/{IDatabaseConnectionService.cs => IDbConnectionService.cs} (92%) create mode 100644 AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs rename AdvancedSystems.Connector.Abstractions/{IDatabaseParameter.cs => IDbParameter.cs} (86%) rename AdvancedSystems.Connector.Abstractions/{IDatabaseOptions.cs => IDbSettings.cs} (84%) create mode 100644 AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs delete mode 100644 AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs delete mode 100644 AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs create mode 100644 AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs rename AdvancedSystems.Connector/Common/{DatabaseParameter.cs => DbParameter.cs} (85%) rename AdvancedSystems.Connector/Options/{DatabaseOptions.cs => UserOptions.cs} (54%) rename AdvancedSystems.Connector/Services/{DatabaseConnectionService.cs => DbConnectionService.cs} (82%) diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs index c630e7b..89cc847 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs +++ b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs @@ -23,9 +23,9 @@ public interface IDatabaseCommand CommandType CommandType { get; set; } /// - /// Gets a collection of parameters. + /// Gets a collection of parameters. /// - IReadOnlyList Parameters { get; } + IReadOnlyList Parameters { get; } #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs similarity index 92% rename from AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs rename to AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs index 5fd1200..a4618ce 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs @@ -12,25 +12,22 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// The connection is managed internally and will be closed after the query execution is complete. /// -public interface IDatabaseConnectionService +/// +/// A user defined option . +/// +public interface IDbConnectionService where T : class, IDbConnectionSettings, new() { #region Properties /// - /// Gets or sets the string used to open a SQL Server database. - /// - string ConnectionString { get; } - - /// - /// Indicates the state of the connection during the most recent network - /// operation performed on the connection. + /// /// ConnectionState ConnectionState { get; } /// - /// Gets the active database provider. + /// The connection options. /// - Provider Provider { get; } + T Options { get; } #endregion diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs new file mode 100644 index 0000000..948d6fd --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs @@ -0,0 +1,46 @@ +using System; + +using AdvancedSystems.Connector.Abstractions.Exceptions; + +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDbConnectionServiceFactory : IDisposable where T : class, IDbFactorySettings, new() where U : class, IDbConnectionSettings, new() +{ + #region Properties + + /// + /// Gets the name of the configured authentication user. + /// + /// + /// Thrown if no authenticated user is configured in the application settings. + /// + string AdminUser { get; } + + /// + /// Gets the name of the configured admin user. + /// + /// + /// Thrown if no admin user is configured in the application settings. + /// + string AuthenticationUser { get; } + + #endregion + + #region Methods + + /// + /// Retrieves a connection service configured for the specified database user. + /// + /// + /// The username of the database user for whom the connection service is to be retrieved. + /// + /// + /// Returns a connection service configured for . + /// + /// + /// Raised if the specified could not be located in the factory settings. + /// + IDbConnectionService GetConnection(string dbUser); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs new file mode 100644 index 0000000..bc57991 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs @@ -0,0 +1,29 @@ +namespace AdvancedSystems.Connector.Abstractions; + +/// +/// Defines a contract for configuring a connection string for +/// . +/// +public interface IDbConnectionSettings : IDbSettings +{ + #region Properties + + /// + /// Indicates whether the configured + /// is able to connect to the database with administrative privileges. + /// + public bool? IsAdmin { get; init; } + + /// + /// Indicates whether the configured + /// is authenticated with the minimum required privileges. + /// + public bool? IsAuthenticator { get; init; } + + /// + /// Gets or sets the targeted database provider. + /// + Provider Provider { get; set; } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs b/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs new file mode 100644 index 0000000..4fab337 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace AdvancedSystems.Connector.Abstractions; + +/// +/// Defines a contract for multiple configuring connection strings through +/// . +/// +/// +/// A user defined option . +/// +public interface IDbFactorySettings : IDbConnectionSettings where T : class, IDbConnectionSettings, new() +{ + #region Properties + + /// + /// Contains a list of connection options consumed by this factory. + /// + List Options { get; } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs b/AdvancedSystems.Connector.Abstractions/IDbParameter.cs similarity index 86% rename from AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs rename to AdvancedSystems.Connector.Abstractions/IDbParameter.cs index e2eab8e..5d8704f 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbParameter.cs @@ -2,7 +2,7 @@ namespace AdvancedSystems.Connector.Abstractions; -public interface IDatabaseParameter +public interface IDbParameter { #region Properties diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs similarity index 84% rename from AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs rename to AdvancedSystems.Connector.Abstractions/IDbSettings.cs index 961f874..3d552cd 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseOptions.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs @@ -1,6 +1,9 @@ namespace AdvancedSystems.Connector.Abstractions; -public interface IDatabaseOptions +/// +/// Defines common properties for creating connection strings. +/// +public interface IDbSettings { #region Properties @@ -24,11 +27,6 @@ public interface IDatabaseOptions /// string Password { get; set; } - /// - /// Gets or sets the targeted database provider. - /// - Provider Provider { get; set; } - /// /// Gets or sets the user ID to be used when connecting to SQL Server. /// diff --git a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..8146881 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,8 @@ +namespace AdvancedSystems.Security.Tests.DependencyInjection; + +public sealed class ServiceCollectionExtensionsTests +{ + #region Tests + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs deleted file mode 100644 index 86f907e..0000000 --- a/AdvancedSystems.Connector.Tests/Fixtures/DatabaseConnectionServiceFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using AdvancedSystems.Connector; -using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Common; -using AdvancedSystems.Connector.Options; -using AdvancedSystems.Connector.Services; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Moq; - -namespace AdvancedSystems.Security.Tests.Fixtures; - -public class DatabaseConnectionServiceFixture -{ - public DatabaseConnectionServiceFixture() - { - this.Options.Setup(x => x.Value) - .Returns(new MsSqlOptions - { - ApplicationName = "Advanced Systems Connector Test Suite", - DataSource = new DataSource("localhost", 1433), - InitialCatalog = "TestDatabase", - UserID = "stgr", - Password = "REDACTED", - Provider = Provider.MsSql, - TrustServerCertificate = true, - Pooling = true, - IntegratedSecurity = false, - }); - - this.DatabaseConnectionService = new DatabaseConnectionService( - this.Logger.Object, - this.Options.Object, - this.DbConnectionStringFactory.Object, - this.DbConnectionFactory.Object, - this.DbDataAdapterFactory.Object - ); - } - - #region Properties - - public Mock> Logger { get; private set; } = new(); - - public Mock> Options { get; private set; } = new(); - - public Mock DbConnectionStringFactory { get; private set; } = new(); - - public Mock DbConnectionFactory { get; private set; } = new(); - - public Mock DbDataAdapterFactory { get; private set; } = new(); - - public IDatabaseConnectionService DatabaseConnectionService { get; private set; } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs deleted file mode 100644 index ff45bbd..0000000 --- a/AdvancedSystems.Connector.Tests/Services/DatabaseConnectionServiceTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; - -using AdvancedSystems.Security.Tests.Fixtures; - -namespace AdvancedSystems.Security.Tests.Services; - -public class DatabaseConnectionServiceTests : IClassFixture -{ - private readonly DatabaseConnectionServiceFixture _sut; - - public DatabaseConnectionServiceTests(DatabaseConnectionServiceFixture fixture) - { - this._sut = fixture; - } - - #region Tests - - [Fact] - public void TestExecuteQuery() - { - // Arrange - var connectionService = this._sut.DatabaseConnectionService; - - // Act - - // Assert - } - - [Fact] - public void ExecuteQueryAsync() - { - - } - - [Fact] - public void TestExecuteNonQuery() - { - - } - - [Fact] - public void ExecuteNonQueryAsync() - { - - } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs new file mode 100644 index 0000000..b9aa37e --- /dev/null +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -0,0 +1,38 @@ +namespace AdvancedSystems.Security.Tests.Services; + +public sealed class DbConnectionServiceTests +{ + + public DbConnectionServiceTests() + { + + } + + #region Tests + + [Fact(Skip = "TODO")] + public void TestExecuteQuery() + { + + } + + [Fact(Skip = "TODO")] + public void ExecuteQueryAsync() + { + + } + + [Fact(Skip = "TODO")] + public void TestExecuteNonQuery() + { + + } + + [Fact(Skip = "TODO")] + public void ExecuteNonQueryAsync() + { + + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DatabaseCommand.cs b/AdvancedSystems.Connector/Common/DatabaseCommand.cs index 8c12c47..60c5c2b 100644 --- a/AdvancedSystems.Connector/Common/DatabaseCommand.cs +++ b/AdvancedSystems.Connector/Common/DatabaseCommand.cs @@ -14,7 +14,7 @@ namespace AdvancedSystems.Connector.Common; [DebuggerDisplay("{CommandText}")] public sealed class DatabaseCommand : IDatabaseCommand { - private readonly List _parameters = []; + private readonly List _parameters = []; #region Properties @@ -25,7 +25,7 @@ public sealed class DatabaseCommand : IDatabaseCommand public required CommandType CommandType { get; set; } [DisplayName("Parameters")] - public IReadOnlyList Parameters => this._parameters; + public IReadOnlyList Parameters => this._parameters; #endregion @@ -33,7 +33,7 @@ public sealed class DatabaseCommand : IDatabaseCommand public void AddParameter(string parameterName, T value) { - var databaseParameter = new DatabaseParameter + var databaseParameter = new DbParameter { ParameterName = parameterName, DbType = typeof(T).DeriveFrom(), @@ -45,7 +45,7 @@ public void AddParameter(string parameterName, T value) public void AddParameter(string parameterName, object value, DbType type) { - var databaseParameter = new DatabaseParameter + var databaseParameter = new DbParameter { ParameterName = parameterName, DbType = type, @@ -55,7 +55,7 @@ public void AddParameter(string parameterName, object value, DbType type) this._parameters.Add(databaseParameter); } - private static string? FormatValue(IDatabaseParameter parameter) + private static string? FormatValue(IDbParameter parameter) { return parameter.DbType switch { diff --git a/AdvancedSystems.Connector/Common/DatabaseParameter.cs b/AdvancedSystems.Connector/Common/DbParameter.cs similarity index 85% rename from AdvancedSystems.Connector/Common/DatabaseParameter.cs rename to AdvancedSystems.Connector/Common/DbParameter.cs index 11445b0..1f1f201 100644 --- a/AdvancedSystems.Connector/Common/DatabaseParameter.cs +++ b/AdvancedSystems.Connector/Common/DbParameter.cs @@ -6,7 +6,7 @@ namespace AdvancedSystems.Connector.Common; -public sealed class DatabaseParameter : IDatabaseParameter +public sealed class DbParameter : IDbParameter { #region Properties diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 436f3f6..c376548 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ using System; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Internals; -using AdvancedSystems.Connector.Options; using AdvancedSystems.Connector.Services; using Microsoft.Extensions.Configuration; @@ -13,43 +11,48 @@ namespace AdvancedSystems.Connector.DependencyInjection; public static class ServiceCollectionExtensions { + /// + /// Decrypts an encrypted password. + /// + /// + /// The encrypted password to be decrypted. + /// + /// + /// The decrypted plain-text password. + /// public delegate string DecryptPassword(string cipher); - #region Internals + #region AddDbConnectionService - internal static IServiceCollection AddDbConnectionStringFactory(this IServiceCollection services) + private static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDbConnectionSettings, new() { - services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService), typeof(DbConnectionService))); return services; } - internal static IServiceCollection AddDbConnectionFactory(this IServiceCollection services) + /// + /// Adds the default implementation of to . + /// + /// + /// The type of option to use for the connection service. + /// + /// + /// The service collection containing the service. + /// + /// + /// A configuration section targeting . + /// + /// + /// Defines a function handler for optional password decryption. + /// + /// + /// The value of . + /// + public static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfigurationSection configuration, DecryptPassword? decryptPassword = null) where T : class, IDbConnectionSettings, new() { - services.TryAdd(ServiceDescriptor.Transient()); - return services; - } - - internal static IServiceCollection AddDbDataAdapterFactory(this IServiceCollection services) - { - services.TryAdd(ServiceDescriptor.Transient()); - return services; - } - - internal static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDatabaseConnectionService - { - services.AddDbConnectionStringFactory(); - services.AddDbConnectionFactory(); - services.AddDbDataAdapterFactory(); - - services.TryAdd(ServiceDescriptor.Singleton()); - return services; - } - - internal static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions - { - services.AddOptions() - .Configure(setupAction) - .PostConfigure(options => + services.AddOptions() + .Bind(configuration) + .PostConfigure(options => { if (decryptPassword != null) { @@ -61,39 +64,39 @@ internal static IServiceCollection AddDbConnectionService(this IServiceCol return services; } - internal static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions + /// + /// Adds the default implementation of to . + /// + /// + /// The type of option to use for the connection service. + /// + /// + /// The service collection containing the service. + /// + /// + /// An action used to configure . + /// + /// + /// Defines a function handler for optional password decryption. + /// + /// + /// The value of . + /// + public static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDbConnectionSettings, new() { - services.AddOptions() - .Bind(configuration.GetRequiredSection(Sections.DATABASE)) + services.AddOptions() + .Configure(setupAction) .PostConfigure(options => { if (decryptPassword != null) { options.Password = decryptPassword(options.Password); } - }) - .ValidateDataAnnotations() - .ValidateOnStart(); + }); services.AddDbConnectionService(); return services; } #endregion - - #region Microsoft SQL Server - - public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) - { - services.AddDbConnectionService(setupAction, decryptPassword); - return services; - } - - public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) - { - services.AddDbConnectionService(configuration, decryptPassword); - return services; - } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs index d92cb0c..b93c018 100644 --- a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs +++ b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs @@ -12,7 +12,7 @@ public static class MsSqlExtensions { #region Converters - public static DbParameter DeriveFrom(this IDatabaseParameter databaseParameter, DbCommand dbCommand) + public static DbParameter DeriveFrom(this IDbParameter databaseParameter, DbCommand dbCommand) { return dbCommand switch { diff --git a/AdvancedSystems.Connector/Options/MsSqlOptions.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs index f1a0b00..eaedef2 100644 --- a/AdvancedSystems.Connector/Options/MsSqlOptions.cs +++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs @@ -4,12 +4,17 @@ namespace AdvancedSystems.Connector.Options; -public sealed class MsSqlOptions : DatabaseOptions +public sealed record MsSqlOptions : UserOptions { /// /// Gets or sets the default wait time (in seconds) before terminating the attempt to execute - /// a command and generating an error. The default is 30 seconds. + /// a command and generating an error. /// + /// + /// + /// Defaults to 30. + /// + /// [DisplayName("Command Timeout")] public int CommandTimeout { get; set; } = 30; @@ -17,6 +22,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets the length of time (in seconds) to wait for a connection to the server /// before terminating the attempt and generating an error. /// + /// + /// + /// Defaults to 15. + /// + /// [DisplayName("Connect Timeout")] public int ConnectTimeout { get; set; } = 15; @@ -24,6 +34,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets a value that indicates whether /// TLS encryption is required for all data sent between the client and server. /// + /// + /// + /// Defaults to . + /// + /// [DisplayName("Encrypt")] public SqlConnectionEncryptOption? Encrypt { get; set; } = SqlConnectionEncryptOption.Optional; @@ -32,6 +47,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// specified in the connection (when false) or whether the current Windows account /// credentials are used for authentication (when true). /// + /// + /// + /// Defaults to . + /// + /// [DisplayName("Integrated Security")] public bool IntegratedSecurity { get; set; } = false; @@ -39,6 +59,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets the maximum number of connections allowed in the connection pool for this specific /// connection string. /// + /// + /// + /// Defaults to 100. + /// + /// [DisplayName("Max Pool Size")] public int MaxPoolSize { get; set; } = 100; @@ -46,6 +71,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets the minimum number of connections allowed in the connection pool for this specific /// connection string. /// + /// + /// + /// Defaults to 0. + /// + /// [DisplayName("Min Pool Size")] public int MinPoolSize { get; set; } = 0; @@ -53,6 +83,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly /// opened every time that the connection is requested. /// + /// + /// + /// Defaults to . + /// + /// [DisplayName("Pooling")] public bool Pooling { get; set; } = true; @@ -60,6 +95,11 @@ public sealed class MsSqlOptions : DatabaseOptions /// Gets or sets a value that indicates whether the channel will be encrypted while bypassing /// walking the certificate chain to validate trust. /// + /// + /// + /// Defaults to . + /// + /// [DisplayName("Trust Server Certificate")] public bool TrustServerCertificate { get; set; } = false; } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/Sections.cs b/AdvancedSystems.Connector/Options/Sections.cs index 205ccfa..76e7cd3 100644 --- a/AdvancedSystems.Connector/Options/Sections.cs +++ b/AdvancedSystems.Connector/Options/Sections.cs @@ -1,6 +1,6 @@ namespace AdvancedSystems.Connector.Options; -public readonly record struct Sections +public static class Sections { public const string DATABASE = "Database"; } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/DatabaseOptions.cs b/AdvancedSystems.Connector/Options/UserOptions.cs similarity index 54% rename from AdvancedSystems.Connector/Options/DatabaseOptions.cs rename to AdvancedSystems.Connector/Options/UserOptions.cs index 9c1fe37..2646073 100644 --- a/AdvancedSystems.Connector/Options/DatabaseOptions.cs +++ b/AdvancedSystems.Connector/Options/UserOptions.cs @@ -6,48 +6,36 @@ namespace AdvancedSystems.Connector.Options; -public class DatabaseOptions : IDatabaseOptions +public record UserOptions : IDbConnectionSettings { #region Properties - /// - /// Gets or sets the name of the application associated with the connection string. - /// [DisplayName("Application Name")] public required string ApplicationName { get; set; } - /// - /// Gets or sets the name or network address of the instance of SQL Server to connect to. - /// [Required] [DisplayName("Data Source")] public required string DataSource { get; set; } - /// - /// Gets or sets the name of the database associated with the connection. - /// [Required] [DisplayName("Initial Catalog")] public required string InitialCatalog { get; set; } - /// - /// Gets or sets the password for the SQL Server account. - /// + [DisplayName("Is Admin")] + public bool? IsAdmin { get; init; } + + [DisplayName("Is Authenticator")] + public bool? IsAuthenticator { get; init; } + [Required] [DisplayName("Password")] public required string Password { get; set; } - /// - /// Gets or sets a provider. - /// [Required] [DisplayName("Provider")] [JsonConverter(typeof(Provider))] public required Provider Provider { get; set; } - /// - /// Gets or sets the user ID to be used when connecting to SQL Server. - /// [Required] [DisplayName("User ID")] public required string UserID { get; set; } diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs similarity index 82% rename from AdvancedSystems.Connector/Services/DatabaseConnectionService.cs rename to AdvancedSystems.Connector/Services/DbConnectionService.cs index 5056f8c..b988436 100644 --- a/AdvancedSystems.Connector/Services/DatabaseConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -7,7 +7,6 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; using AdvancedSystems.Connector.Internals; -using AdvancedSystems.Connector.Options; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -15,33 +14,31 @@ namespace AdvancedSystems.Connector.Services; -public sealed class DatabaseConnectionService : IDatabaseConnectionService +public sealed class DbConnectionService : IDbConnectionService where T : class, IDbConnectionSettings, new() { - private readonly ILogger _logger; - private readonly DatabaseOptions _dbOptions; + private readonly ILogger> _logger; private readonly IDbConnectionStringFactory _dbConnectionStringFactory; private readonly IDbConnectionFactory _databaseConnectionFactory; private readonly IDbDataAdapterFactory _dataAdapterFactory; + private readonly string _connectionString; - public DatabaseConnectionService(ILogger logger, IOptions dbOptions, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory databaseConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) + public DbConnectionService(ILogger> logger, IOptions options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) { this._logger = logger; - this._dbOptions = dbOptions.Value; + this.Options = options.Value; this._dbConnectionStringFactory = dbConnectionStringFactory; - this._databaseConnectionFactory = databaseConnectionFactory; + this._databaseConnectionFactory = dbConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - this.Provider = this._dbOptions.Provider; - this.ConnectionString = this._dbConnectionStringFactory.Create(this.Provider); + + this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider); } #region Properties - public string ConnectionString { get; private set; } - public ConnectionState ConnectionState { get; private set; } - public Provider Provider { get; private set; } + public T Options { get; private set; } #endregion @@ -53,7 +50,7 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { - using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; connection.Open(); @@ -79,7 +76,7 @@ public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); @@ -105,7 +102,7 @@ public int ExecuteNonQuery(IDatabaseCommand databaseCommand) this.InvokeExceptionHandler((databaseCommand) => { - using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; connection.Open(); @@ -130,7 +127,7 @@ public async ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseComman await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => { - using var connection = this._databaseConnectionFactory.Create(this.Provider, this.ConnectionString); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); @@ -168,13 +165,11 @@ private void InvokeExceptionHandler(Action action, IDatabaseCo catch (SqlException exception) { string reason = $"Database failed to execute command {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); } catch (DbException exception) { string reason = $"Communication to database failed during the execution of {databaseCommand}"; - this._logger.LogError("{Reason} ({Message}).", reason, exception.Message); throw new DbConnectionException($"{reason} ({exception.Message}).", exception); } } @@ -189,13 +184,11 @@ private async ValueTask InvokeExceptionHandlerAsync(Func Date: Fri, 14 Mar 2025 20:05:35 +0100 Subject: [PATCH 13/24] Continue development of DbConnectionService and DbConnectionServiceFactory --- .../Exceptions/DbCommandExecutionException.cs | 45 ----- .../Exceptions/DbConnectionException.cs | 45 ----- .../{IDatabaseCommand.cs => IDbCommand.cs} | 2 +- .../IDbConnectionService.cs | 24 ++- .../IDbConnectionServiceFactory.cs | 48 +++-- .../IDbConnectionSettings.cs | 29 --- .../IDbConnectionStringFactory.cs | 2 +- .../IDbFactorySettings.cs | 6 +- .../IDbSettings.cs | 7 +- .../ServiceCollectionExtensionsTests.cs | 63 ++++++- .../Services/DbConnectionServiceTests.cs | 16 +- .../Common/DataSource.cs | 12 +- .../{DatabaseCommand.cs => DbCommand.cs} | 6 +- .../Common/DbParameter.cs | 2 - .../{Options => Common}/Sections.cs | 2 + .../ServiceCollectionExtensions.cs | 6 +- .../Options/{UserOptions.cs => DbOptions.cs} | 10 +- .../Options/MsSqlOptions.cs | 2 +- .../Services/DbConnectionService.cs | 165 ++++++------------ .../Services/DbConnectionServiceFactory.cs | 87 +++++++++ 20 files changed, 280 insertions(+), 299 deletions(-) delete mode 100644 AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs delete mode 100644 AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs rename AdvancedSystems.Connector.Abstractions/{IDatabaseCommand.cs => IDbCommand.cs} (96%) delete mode 100644 AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs rename AdvancedSystems.Connector/Common/{DatabaseCommand.cs => DbCommand.cs} (93%) rename AdvancedSystems.Connector/{Options => Common}/Sections.cs (75%) rename AdvancedSystems.Connector/Options/{UserOptions.cs => DbOptions.cs} (75%) create mode 100644 AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs deleted file mode 100644 index 45a233e..0000000 --- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace AdvancedSystems.Connector.Abstractions.Exceptions; - -/// -/// Represents an error that occurs during the execution of a database command. -/// -public class DbCommandExecutionException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public DbCommandExecutionException() - { - - } - - /// - /// Initializes a new instance of the class - /// with a specified error . - /// - /// - /// The error message that explains the reason for the exception. - /// - public DbCommandExecutionException(string message) : base(message) - { - - } - - /// - /// Initializes a new instance of the class - /// with a specified error a reference to the - /// exception that is the cause of this exception. - /// - /// - /// The error message that explains the reason for the exception. - /// - /// - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - /// - public DbCommandExecutionException(string message, Exception inner) : base(message, inner) - { - - } -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs deleted file mode 100644 index 0af0a78..0000000 --- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace AdvancedSystems.Connector.Abstractions.Exceptions; - -/// -/// Represents an error that occurs during the communication with a database. -/// -public class DbConnectionException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public DbConnectionException() - { - - } - - /// - /// Initializes a new instance of the class - /// with a specified error . - /// - /// - /// The error message that explains the reason for the exception. - /// - public DbConnectionException(string message) : base(message) - { - - } - - /// - /// Initializes a new instance of the class - /// with a specified error a reference to the - /// exception that is the cause of this exception. - /// - /// - /// The error message that explains the reason for the exception. - /// - /// - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - /// - public DbConnectionException(string message, Exception inner) : base(message, inner) - { - - } -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs similarity index 96% rename from AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs rename to AdvancedSystems.Connector.Abstractions/IDbCommand.cs index 89cc847..22dd260 100644 --- a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs @@ -6,7 +6,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// Defines an SQL statement or stored procedure to execute against a data source. /// -public interface IDatabaseCommand +public interface IDbCommand { #region Properties diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs index a4618ce..a95f50e 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs @@ -2,20 +2,18 @@ using System.Threading; using System.Threading.Tasks; -using AdvancedSystems.Connector.Abstractions.Exceptions; - namespace AdvancedSystems.Connector.Abstractions; /// -/// Defines the contract for a service responsible for managing database connections. +/// Defines a contract for a service responsible for managing database connections. /// /// /// The connection is managed internally and will be closed after the query execution is complete. /// /// -/// A user defined option . +/// The connection options. /// -public interface IDbConnectionService where T : class, IDbConnectionSettings, new() +public interface IDbConnectionService where T : class, IDbSettings, new() { #region Properties @@ -34,7 +32,7 @@ namespace AdvancedSystems.Connector.Abstractions; #region Methods /// - /// Executes a query defined by the specified synchronously. + /// Executes a query defined by the specified synchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -52,10 +50,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - DataSet ExecuteQuery(IDatabaseCommand databaseCommand); + DataSet ExecuteQuery(IDbCommand databaseCommand); /// - /// Executes a query defined by the specified asynchronously. + /// Executes a query defined by the specified asynchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -76,10 +74,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default); /// - /// Executes a non-query command defined by the specified synchronously. + /// Executes a non-query command defined by the specified synchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -97,10 +95,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - int ExecuteNonQuery(IDatabaseCommand databaseCommand); + int ExecuteNonQuery(IDbCommand databaseCommand); /// - /// Executes a non-query command defined by the specified asynchronously. + /// Executes a non-query command defined by the specified asynchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -122,7 +120,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs index 948d6fd..4b82c52 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs @@ -1,46 +1,56 @@ using System; - -using AdvancedSystems.Connector.Abstractions.Exceptions; +using System.Data; namespace AdvancedSystems.Connector.Abstractions; -public interface IDbConnectionServiceFactory : IDisposable where T : class, IDbFactorySettings, new() where U : class, IDbConnectionSettings, new() +/// +/// Defines a contract for creating and managing multiple connection instances. +/// +/// +/// The factory options. +/// +/// +/// The connection options. +/// +public interface IDbConnectionServiceFactory : IDisposable where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() { #region Properties /// - /// Gets the name of the configured authentication user. + /// /// - /// - /// Thrown if no authenticated user is configured in the application settings. - /// - string AdminUser { get; } + ConnectionState ConnectionState { get; } /// - /// Gets the name of the configured admin user. + /// The factory options. /// - /// - /// Thrown if no admin user is configured in the application settings. - /// - string AuthenticationUser { get; } + T Options { get; } #endregion #region Methods /// - /// Retrieves a connection service configured for the specified database user. + /// Retrieves a connection service configured for the specified . /// /// /// The username of the database user for whom the connection service is to be retrieved. /// /// - /// Returns a connection service configured for . + /// Returns a connection service. + /// + IDbConnectionService? GetConnection(string dbUser); + + /// + /// Retrieves a connection service from the factory pool determined by the . + /// + /// + /// The filter to apply for retrieving a connection. + /// + /// + /// Returns a connection service. /// - /// - /// Raised if the specified could not be located in the factory settings. - /// - IDbConnectionService GetConnection(string dbUser); + IDbConnectionService? GetConnection(Predicate predicate); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs deleted file mode 100644 index bc57991..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionSettings.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace AdvancedSystems.Connector.Abstractions; - -/// -/// Defines a contract for configuring a connection string for -/// . -/// -public interface IDbConnectionSettings : IDbSettings -{ - #region Properties - - /// - /// Indicates whether the configured - /// is able to connect to the database with administrative privileges. - /// - public bool? IsAdmin { get; init; } - - /// - /// Indicates whether the configured - /// is authenticated with the minimum required privileges. - /// - public bool? IsAuthenticator { get; init; } - - /// - /// Gets or sets the targeted database provider. - /// - Provider Provider { get; set; } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index 48dd9a2..1d609f3 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -4,7 +4,7 @@ public interface IDbConnectionStringFactory { #region Methods - public string Create(Provider provider); + string Create(Provider provider); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs b/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs index 4fab337..b5a565c 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs @@ -4,12 +4,12 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// Defines a contract for multiple configuring connection strings through -/// . +/// . /// /// -/// A user defined option . +/// The factory options. /// -public interface IDbFactorySettings : IDbConnectionSettings where T : class, IDbConnectionSettings, new() +public interface IDbFactorySettings : IDbSettings where T : class, IDbSettings, new() { #region Properties diff --git a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs index 3d552cd..214b755 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs @@ -10,7 +10,7 @@ public interface IDbSettings /// /// Gets or sets the name of the application associated with the connection string. /// - string ApplicationName { get; set; } + string? ApplicationName { get; set; } /// /// Gets or sets the name or network address of the instance of SQL Server to connect to. @@ -27,6 +27,11 @@ public interface IDbSettings /// string Password { get; set; } + /// + /// + /// + Provider Provider { get; set; } + /// /// Gets or sets the user ID to be used when connecting to SQL Server. /// diff --git a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 8146881..2a429b6 100644 --- a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -1,8 +1,67 @@ -namespace AdvancedSystems.Security.Tests.DependencyInjection; +using AdvancedSystems.Connector.Abstractions; +using Microsoft.Extensions.Options; + +namespace AdvancedSystems.Security.Tests.DependencyInjection; + +/// +/// Tests the public methods in . +/// public sealed class ServiceCollectionExtensionsTests { - #region Tests + #region AddDbConnectionService + + /// + /// Tests that can be initialized through + /// dependency injection from . + /// + [Fact(Skip = "TODO")] + public void TestAddDbConnectionService_FromOptions() + { + // Arrange + // Act + // Assert + } + + /// + /// Tests that can be initialized through + /// dependency injection from appsettings.json. + /// + [Fact(Skip = "TODO")] + public void TestAddDbConnectionService_FromAppSettings() + { + // Arrange + // Act + // Assert + } + + #endregion + + #region AddDbConnectionServiceFactory + + /// + /// Tests that can be initialized through + /// dependency injection from and a collection of . + /// + [Fact(Skip = "TODO")] + public void TestAddDbConnectionServiceFactory_FromOptions() + { + // Arrange + // Act + // Assert + } + + /// + /// Tests that can be initialized through + /// dependency injection from appsettings.json. + /// + [Fact(Skip = "TODO")] + public void TestAddDbConnectionServiceFactory_FromAppSettings() + { + // Arrange + // Act + // Assert + } #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs index b9aa37e..4363be3 100644 --- a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -13,25 +13,33 @@ public DbConnectionServiceTests() [Fact(Skip = "TODO")] public void TestExecuteQuery() { - + // Arrange + // Act + // Assert } [Fact(Skip = "TODO")] public void ExecuteQueryAsync() { - + // Arrange + // Act + // Assert } [Fact(Skip = "TODO")] public void TestExecuteNonQuery() { - + // Arrange + // Act + // Assert } [Fact(Skip = "TODO")] public void ExecuteNonQueryAsync() { - + // Arrange + // Act + // Assert } #endregion diff --git a/AdvancedSystems.Connector/Common/DataSource.cs b/AdvancedSystems.Connector/Common/DataSource.cs index 991f0ca..09578cc 100644 --- a/AdvancedSystems.Connector/Common/DataSource.cs +++ b/AdvancedSystems.Connector/Common/DataSource.cs @@ -1,18 +1,12 @@ namespace AdvancedSystems.Connector.Common; -public sealed class DataSource +public sealed class DataSource(string host, int port) { - public DataSource(string host, int port) - { - this.Host = host; - this.Port = port; - } - #region Properties - public string Host { get; set; } + public string Host { get; set; } = host; - public int Port { get; set; } + public int Port { get; set; } = port; #endregion diff --git a/AdvancedSystems.Connector/Common/DatabaseCommand.cs b/AdvancedSystems.Connector/Common/DbCommand.cs similarity index 93% rename from AdvancedSystems.Connector/Common/DatabaseCommand.cs rename to AdvancedSystems.Connector/Common/DbCommand.cs index 60c5c2b..7ab54fc 100644 --- a/AdvancedSystems.Connector/Common/DatabaseCommand.cs +++ b/AdvancedSystems.Connector/Common/DbCommand.cs @@ -9,10 +9,12 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Internals; +using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; + namespace AdvancedSystems.Connector.Common; [DebuggerDisplay("{CommandText}")] -public sealed class DatabaseCommand : IDatabaseCommand +public sealed class DbCommand : IDbCommand { private readonly List _parameters = []; @@ -82,7 +84,7 @@ public override string ToString() foreach (var parameter in this.Parameters) { - commandBuilder.Replace(parameter.ParameterName, DatabaseCommand.FormatValue(parameter)); + commandBuilder.Replace(parameter.ParameterName, DbCommand.FormatValue(parameter)); } return commandBuilder.ToString(); diff --git a/AdvancedSystems.Connector/Common/DbParameter.cs b/AdvancedSystems.Connector/Common/DbParameter.cs index 1f1f201..a23315a 100644 --- a/AdvancedSystems.Connector/Common/DbParameter.cs +++ b/AdvancedSystems.Connector/Common/DbParameter.cs @@ -2,8 +2,6 @@ using AdvancedSystems.Connector.Abstractions; -using Microsoft.Data.SqlClient; - namespace AdvancedSystems.Connector.Common; public sealed class DbParameter : IDbParameter diff --git a/AdvancedSystems.Connector/Options/Sections.cs b/AdvancedSystems.Connector/Common/Sections.cs similarity index 75% rename from AdvancedSystems.Connector/Options/Sections.cs rename to AdvancedSystems.Connector/Common/Sections.cs index 76e7cd3..e473552 100644 --- a/AdvancedSystems.Connector/Options/Sections.cs +++ b/AdvancedSystems.Connector/Common/Sections.cs @@ -3,4 +3,6 @@ public static class Sections { public const string DATABASE = "Database"; + + public const string USERS = "Users"; } \ No newline at end of file diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index c376548..b9d54f6 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions #region AddDbConnectionService - private static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDbConnectionSettings, new() + private static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDbSettings, new() { services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService), typeof(DbConnectionService))); return services; @@ -48,7 +48,7 @@ public static class ServiceCollectionExtensions /// /// The value of . /// - public static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfigurationSection configuration, DecryptPassword? decryptPassword = null) where T : class, IDbConnectionSettings, new() + public static IServiceCollection AddDbConnectionService(this IServiceCollection services, IConfigurationSection configuration, DecryptPassword? decryptPassword = null) where T : class, IDbSettings, new() { services.AddOptions() .Bind(configuration) @@ -82,7 +82,7 @@ public static class ServiceCollectionExtensions /// /// The value of . /// - public static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDbConnectionSettings, new() + public static IServiceCollection AddDbConnectionService(this IServiceCollection services, Action setupAction, DecryptPassword? decryptPassword = null) where T : class, IDbSettings, new() { services.AddOptions() .Configure(setupAction) diff --git a/AdvancedSystems.Connector/Options/UserOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs similarity index 75% rename from AdvancedSystems.Connector/Options/UserOptions.cs rename to AdvancedSystems.Connector/Options/DbOptions.cs index 2646073..db8c2b6 100644 --- a/AdvancedSystems.Connector/Options/UserOptions.cs +++ b/AdvancedSystems.Connector/Options/DbOptions.cs @@ -6,12 +6,12 @@ namespace AdvancedSystems.Connector.Options; -public record UserOptions : IDbConnectionSettings +public record DbOptions : IDbSettings { #region Properties [DisplayName("Application Name")] - public required string ApplicationName { get; set; } + public string? ApplicationName { get; set; } [Required] [DisplayName("Data Source")] @@ -21,12 +21,6 @@ public record UserOptions : IDbConnectionSettings [DisplayName("Initial Catalog")] public required string InitialCatalog { get; set; } - [DisplayName("Is Admin")] - public bool? IsAdmin { get; init; } - - [DisplayName("Is Authenticator")] - public bool? IsAuthenticator { get; init; } - [Required] [DisplayName("Password")] public required string Password { get; set; } diff --git a/AdvancedSystems.Connector/Options/MsSqlOptions.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs index eaedef2..e43c713 100644 --- a/AdvancedSystems.Connector/Options/MsSqlOptions.cs +++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs @@ -4,7 +4,7 @@ namespace AdvancedSystems.Connector.Options; -public sealed record MsSqlOptions : UserOptions +public sealed record MsSqlOptions : DbOptions { /// /// Gets or sets the default wait time (in seconds) before terminating the attempt to execute diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index b988436..21b4bd3 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -1,20 +1,18 @@ -using System; -using System.Data; -using System.Data.Common; +using System.Data; using System.Threading; using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Abstractions.Exceptions; using AdvancedSystems.Connector.Internals; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; + namespace AdvancedSystems.Connector.Services; -public sealed class DbConnectionService : IDbConnectionService where T : class, IDbConnectionSettings, new() +public sealed class DbConnectionService : IDbConnectionService where T : class, IDbSettings, new() { private readonly ILogger> _logger; private readonly IDbConnectionStringFactory _dbConnectionStringFactory; @@ -30,7 +28,6 @@ public DbConnectionService(ILogger> logger, IOptions o this._databaseConnectionFactory = dbConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider); } @@ -44,104 +41,88 @@ public DbConnectionService(ILogger> logger, IOptions o #region Public Methods - public DataSet ExecuteQuery(IDatabaseCommand databaseCommand) + public DataSet ExecuteQuery(IDbCommand databaseCommand) { DataSet result = new(); - this.InvokeExceptionHandler((databaseCommand) => - { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - connection.Open(); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + connection.Open(); - using var sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using var sqlCommand = connection.CreateCommand(); + sqlCommand.CommandText = databaseCommand.CommandText; + sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) - { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); - } + foreach (var parameter in databaseCommand.Parameters) + { + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + } - using var adapter = this._dataAdapterFactory.Create(sqlCommand); - adapter.Fill(result); - }, databaseCommand); + using var adapter = this._dataAdapterFactory.Create(sqlCommand); + adapter.Fill(result); return result; } - public async ValueTask ExecuteQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default) + public async ValueTask ExecuteQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) { DataTable result = new(); - await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => - { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - await connection.OpenAsync(cancellationToken); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + await connection.OpenAsync(cancellationToken); - using var sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using var sqlCommand = connection.CreateCommand(); + sqlCommand.CommandText = databaseCommand.CommandText; + sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) - { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); - } + foreach (var parameter in databaseCommand.Parameters) + { + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + } - using var reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); - result.Load(reader); - }, databaseCommand, cancellationToken); + using var reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); + result.Load(reader); return result.DataSet; } - public int ExecuteNonQuery(IDatabaseCommand databaseCommand) + public int ExecuteNonQuery(IDbCommand databaseCommand) { - int rowsAffected = default; - - this.InvokeExceptionHandler((databaseCommand) => - { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - connection.Open(); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + connection.Open(); - using var sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using var sqlCommand = connection.CreateCommand(); + sqlCommand.CommandText = databaseCommand.CommandText; + sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) - { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); - } + foreach (var parameter in databaseCommand.Parameters) + { + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + } - rowsAffected = sqlCommand.ExecuteNonQuery(); - }, databaseCommand); + int rowsAffected = sqlCommand.ExecuteNonQuery(); return rowsAffected; } - public async ValueTask ExecuteNonQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default) + public async ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) { - int rowsAffected = default; - - await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) => - { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - await connection.OpenAsync(cancellationToken); + using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + await connection.OpenAsync(cancellationToken); - using var sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using var sqlCommand = connection.CreateCommand(); + sqlCommand.CommandText = databaseCommand.CommandText; + sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) - { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); - } + foreach (var parameter in databaseCommand.Parameters) + { + sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + } - rowsAffected = await sqlCommand.ExecuteNonQueryAsync(cancellationToken); - }, databaseCommand, cancellationToken); + int rowsAffected = await sqlCommand.ExecuteNonQueryAsync(cancellationToken); return rowsAffected; } @@ -155,43 +136,5 @@ private void ConnectionStateHandler(object sender, StateChangeEventArgs e) this.ConnectionState = e.CurrentState; } - private void InvokeExceptionHandler(Action action, IDatabaseCommand databaseCommand) - { - try - { - action(databaseCommand); - this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); - } - catch (SqlException exception) - { - string reason = $"Database failed to execute command {databaseCommand}"; - throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); - } - catch (DbException exception) - { - string reason = $"Communication to database failed during the execution of {databaseCommand}"; - throw new DbConnectionException($"{reason} ({exception.Message}).", exception); - } - } - - private async ValueTask InvokeExceptionHandlerAsync(Func action, IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default) - { - try - { - await action(databaseCommand, cancellationToken); - this._logger.LogTrace("Executed command '{Query}'.", databaseCommand); - } - catch (SqlException exception) - { - string reason = $"Database failed to execute command {databaseCommand}"; - throw new DbCommandExecutionException($"{reason} ({exception.Message}).", exception); - } - catch (DbException exception) - { - string reason = $"Communication with the database failed or was interrupted during the execution off {databaseCommand}"; - throw new DbConnectionException($"{reason} ({exception.Message}).", exception); - } - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs new file mode 100644 index 0000000..22cc48c --- /dev/null +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +using AdvancedSystems.Connector.Abstractions; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AdvancedSystems.Connector.Services; + +public sealed class DbConnectionServiceFactory : IDbConnectionServiceFactory where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() +{ + private bool _isDisposed = false; + + private readonly ILogger> _logger; + private readonly List _dbConnectionOptions; + private readonly ILoggerFactory _loggerFactory; + private readonly List> _dbConnectionServices; + + public DbConnectionServiceFactory(ILogger> logger, ILoggerFactory loggerFactory, IOptions factoryOptions) + { + this._logger = logger; + this._loggerFactory = loggerFactory; + this.Options = factoryOptions.Value; + this._dbConnectionOptions = this.Options.Options; + this._dbConnectionServices = [.. this.Create()]; + } + + #region Properties + + public ConnectionState ConnectionState { get; private set; } + + public T Options { get; private set; } + + #endregion + + #region Methods + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public IDbConnectionService? GetConnection(string dbUser) + { + throw new NotImplementedException(); + } + + public IDbConnectionService? GetConnection(Predicate predicate) + { + throw new NotImplementedException(); + } + + #endregion + + #region Helpers + + private IEnumerable> Create() + { + foreach (U dbOption in this._dbConnectionOptions) + { + U dbSettings = dbOption; + } + + throw new NotImplementedException(); + } + + private void Dispose(bool disposing) + { + if (this._isDisposed) return; + + if (disposing) + { + this._logger.LogTrace("Disposing {Service}.", nameof(DbConnectionServiceFactory)); + + this._dbConnectionOptions.Clear(); + this._dbConnectionServices.Clear(); + } + + this._isDisposed = true; + } + + #endregion +} \ No newline at end of file From 02a6d084633b07336ba58fcc060f3c47ff816df8 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Mar 2025 16:11:27 +0100 Subject: [PATCH 14/24] Refactor and lint code and start implementation of DbConnectionServiceFactory --- .editorconfig | 7 +- .../Exceptions/DbConnectionException.cs | 46 +++++++++++ .../IDbConnectionServiceFactory.cs | 18 ++-- .../IDbConnectionStringFactory.cs | 2 +- .../AdvancedSystems.Connector.Tests.csproj | 4 + AdvancedSystems.Connector/Common/DbCommand.cs | 33 ++++---- AdvancedSystems.Connector/Common/Sections.cs | 2 +- .../Internals/DbConnectionFactory.cs | 2 +- .../Internals/DbConnectionStringFactory.cs | 14 ++-- .../Internals/DbDataAdapterFactory.cs | 2 +- .../Internals/MsSqlExtensions.cs | 3 +- .../Services/DbConnectionService.cs | 82 ++++++++++++------- .../Services/DbConnectionServiceFactory.cs | 38 ++++++--- 13 files changed, 175 insertions(+), 78 deletions(-) create mode 100644 AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs diff --git a/.editorconfig b/.editorconfig index 2dc062b..23a9fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -78,9 +78,9 @@ dotnet_remove_unnecessary_suppression_exclusions = none [*.cs] # var preferences -csharp_style_var_elsewhere = false:silent -csharp_style_var_for_built_in_types = false:silent -csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent @@ -174,6 +174,7 @@ csharp_style_namespace_declarations = block_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion #### Naming styles #### [*.{cs,vb}] diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs new file mode 100644 index 0000000..fd01fe5 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs @@ -0,0 +1,46 @@ +using System; +using System.Data.Common; + +namespace AdvancedSystems.Connector.Abstractions.Exceptions; + +/// +/// Represents an exception that occurs when a database connection fails. +/// +public class DbConnectionException : DbException +{ + /// + /// Initializes a new instance of the class. + /// + public DbConnectionException() + { + + } + + /// + /// Initializes a new instance of the class + /// with the specified error . + /// + /// + /// The message to display for this exception. + /// + public DbConnectionException(string message) : base(message) + { + + } + + /// + /// Initializes a new instance of the class + /// with the specified error and a reference to the + /// that is cause of this exception. + /// + /// + /// The message to display for this exception. + /// + /// + /// The inner exception reference. + /// + public DbConnectionException(string message, Exception innerException) : base(message, innerException) + { + + } +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs index 4b82c52..97e8286 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs @@ -1,5 +1,6 @@ using System; -using System.Data; + +using AdvancedSystems.Connector.Abstractions.Exceptions; namespace AdvancedSystems.Connector.Abstractions; @@ -16,11 +17,6 @@ namespace AdvancedSystems.Connector.Abstractions; { #region Properties - /// - /// - /// - ConnectionState ConnectionState { get; } - /// /// The factory options. /// @@ -39,7 +35,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// Returns a connection service. /// - IDbConnectionService? GetConnection(string dbUser); + /// + /// Raised if the no connection matching the search criteria could be returned. + /// + IDbConnectionService GetConnection(string dbUser); /// /// Retrieves a connection service from the factory pool determined by the . @@ -50,7 +49,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// Returns a connection service. /// - IDbConnectionService? GetConnection(Predicate predicate); + /// + /// Raised if the no connection matching the search criteria could be returned. + /// + IDbConnectionService GetConnection(Predicate predicate); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index 1d609f3..da246c8 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -4,7 +4,7 @@ public interface IDbConnectionStringFactory { #region Methods - string Create(Provider provider); + string Create(Provider provider, bool maskPassword = false); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj index f7c037a..d6ca636 100644 --- a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj +++ b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj @@ -40,4 +40,8 @@ + + + + diff --git a/AdvancedSystems.Connector/Common/DbCommand.cs b/AdvancedSystems.Connector/Common/DbCommand.cs index 7ab54fc..2603993 100644 --- a/AdvancedSystems.Connector/Common/DbCommand.cs +++ b/AdvancedSystems.Connector/Common/DbCommand.cs @@ -57,6 +57,24 @@ public void AddParameter(string parameterName, object value, DbType type) this._parameters.Add(databaseParameter); } + public override string ToString() + { + if (this.Parameters.Count == 0) return this.CommandText; + + var commandBuilder = new StringBuilder(this.CommandText); + + foreach (IDbParameter parameter in this.Parameters) + { + commandBuilder.Replace(parameter.ParameterName, FormatValue(parameter)); + } + + return commandBuilder.ToString(); + } + + #endregion + + #region Helpers + private static string? FormatValue(IDbParameter parameter) { return parameter.DbType switch @@ -73,21 +91,6 @@ public void AddParameter(string parameterName, object value, DbType type) DbType.DateTime => ((DateTime?)parameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), _ => parameter.Value?.ToString(), }; - - } - - public override string ToString() - { - if (this.Parameters.Count == 0) return this.CommandText; - - var commandBuilder = new StringBuilder(this.CommandText); - - foreach (var parameter in this.Parameters) - { - commandBuilder.Replace(parameter.ParameterName, DbCommand.FormatValue(parameter)); - } - - return commandBuilder.ToString(); } #endregion diff --git a/AdvancedSystems.Connector/Common/Sections.cs b/AdvancedSystems.Connector/Common/Sections.cs index e473552..0e50aca 100644 --- a/AdvancedSystems.Connector/Common/Sections.cs +++ b/AdvancedSystems.Connector/Common/Sections.cs @@ -1,4 +1,4 @@ -namespace AdvancedSystems.Connector.Options; +namespace AdvancedSystems.Connector.Common; public static class Sections { diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index 542eee5..e228b44 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -9,7 +9,7 @@ namespace AdvancedSystems.Connector.Internals; public sealed class DbConnectionFactory : IDbConnectionFactory { - #region Public Methods + #region Methods public DbConnection Create(Provider provider, string connectionString) { diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 6c31fb8..060ae9f 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -18,32 +18,30 @@ public DbConnectionStringFactory(IServiceProvider serviceProvider) this._serviceProvider = serviceProvider; } - #region Public Methods + #region Methods - public string Create(Provider provider) + public string Create(Provider provider, bool maskPassword = false) { return provider switch { - Provider.MsSql => this.CreateMsSqlConnectionString(this._serviceProvider.GetService>()?.Value), + Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), _ => throw new NotSupportedException(), }; } #endregion - #region Private Methods + #region Helpers - private string CreateMsSqlConnectionString(MsSqlOptions? settings) + private static string CreateMsSqlConnectionString(MsSqlOptions settings, bool maskPassword) { - ArgumentNullException.ThrowIfNull(settings, nameof(settings)); - var builder = new SqlConnectionStringBuilder { // Database Options ApplicationName = settings.ApplicationName, DataSource = settings.DataSource, InitialCatalog = settings.InitialCatalog, - Password = settings.Password, + Password = maskPassword ? "******" : settings.Password, UserID = settings.UserID, // Microsoft SQL Server Options CommandTimeout = settings.CommandTimeout, diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index 6c3af97..0e48921 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -9,7 +9,7 @@ namespace AdvancedSystems.Connector.Internals; public sealed class DbDataAdapterFactory : IDbDataAdapterFactory { - #region Public Methods + #region Methods public DbDataAdapter Create(DbCommand dbCommand) { diff --git a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs index b93c018..9d5b922 100644 --- a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs +++ b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs @@ -25,9 +25,10 @@ public static DbParameter DeriveFrom(this IDbParameter databaseParameter, DbComm _ => throw new NotSupportedException(), }; } + public static DbType DeriveFrom(this Type type) { - var typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); + TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); return typeCode switch { diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 21b4bd3..fe66bb9 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -1,10 +1,14 @@ -using System.Data; +using System; +using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Abstractions.Exceptions; using AdvancedSystems.Connector.Internals; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -39,26 +43,23 @@ public DbConnectionService(ILogger> logger, IOptions o #endregion - #region Public Methods + #region Methods public DataSet ExecuteQuery(IDbCommand databaseCommand) { - DataSet result = new(); - - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - connection.Open(); + using DbConnection connection = this.InitializeConnection(); - using var sqlCommand = connection.CreateCommand(); + using DbCommand sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) + foreach (IDbParameter parameter in databaseCommand.Parameters) { sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } - using var adapter = this._dataAdapterFactory.Create(sqlCommand); + DataSet result = new(); + using DbDataAdapter adapter = this._dataAdapterFactory.Create(sqlCommand); adapter.Fill(result); return result; @@ -66,22 +67,19 @@ public DataSet ExecuteQuery(IDbCommand databaseCommand) public async ValueTask ExecuteQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) { - DataTable result = new(); - - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - await connection.OpenAsync(cancellationToken); + using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); - using var sqlCommand = connection.CreateCommand(); + using DbCommand sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) + foreach (IDbParameter parameter in databaseCommand.Parameters) { sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } - using var reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); + DataTable result = new(); + using DbDataReader reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); result.Load(reader); return result.DataSet; @@ -89,15 +87,13 @@ public DataSet ExecuteQuery(IDbCommand databaseCommand) public int ExecuteNonQuery(IDbCommand databaseCommand) { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - connection.Open(); + using DbConnection connection = this.InitializeConnection(); - using var sqlCommand = connection.CreateCommand(); + using DbCommand sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) + foreach (IDbParameter parameter in databaseCommand.Parameters) { sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } @@ -109,15 +105,13 @@ public int ExecuteNonQuery(IDbCommand databaseCommand) public async ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) { - using var connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - await connection.OpenAsync(cancellationToken); + using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); - using var sqlCommand = connection.CreateCommand(); + using DbCommand sqlCommand = connection.CreateCommand(); sqlCommand.CommandText = databaseCommand.CommandText; sqlCommand.CommandType = databaseCommand.CommandType; - foreach (var parameter in databaseCommand.Parameters) + foreach (IDbParameter parameter in databaseCommand.Parameters) { sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); } @@ -129,12 +123,42 @@ public async ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, Can #endregion - #region Private Methods + #region Helpers private void ConnectionStateHandler(object sender, StateChangeEventArgs e) { this.ConnectionState = e.CurrentState; } + private DbConnection InitializeConnection() + { + try + { + DbConnection connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + connection.Open(); + return connection; + } + catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) + { + throw new DbConnectionException($"Failed to initialize connection to database: {exception.Message}", exception); + } + } + + private async ValueTask InitializeConnectionAsync(CancellationToken cancellationToken) + { + try + { + DbConnection connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + connection.StateChange += this.ConnectionStateHandler; + await connection.OpenAsync(cancellationToken); + return connection; + } + catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) + { + throw new DbConnectionException($"Failed to initialize connection to database: {exception.Message}", exception); + } + } + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs index 22cc48c..a56f286 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; -using System.Data; using System.Linq; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Abstractions.Exceptions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -17,12 +18,14 @@ namespace AdvancedSystems.Connector.Services; private readonly ILogger> _logger; private readonly List _dbConnectionOptions; private readonly ILoggerFactory _loggerFactory; + private readonly IServiceProvider _serviceProvider; private readonly List> _dbConnectionServices; - public DbConnectionServiceFactory(ILogger> logger, ILoggerFactory loggerFactory, IOptions factoryOptions) + public DbConnectionServiceFactory(ILogger> logger, ILoggerFactory loggerFactory, IOptions factoryOptions, IServiceProvider serviceProvider) { this._logger = logger; this._loggerFactory = loggerFactory; + this._serviceProvider = serviceProvider; this.Options = factoryOptions.Value; this._dbConnectionOptions = this.Options.Options; this._dbConnectionServices = [.. this.Create()]; @@ -30,8 +33,6 @@ public DbConnectionServiceFactory(ILogger> logg #region Properties - public ConnectionState ConnectionState { get; private set; } - public T Options { get; private set; } #endregion @@ -44,14 +45,16 @@ public void Dispose() GC.SuppressFinalize(this); } - public IDbConnectionService? GetConnection(string dbUser) + public IDbConnectionService GetConnection(string dbUser) { - throw new NotImplementedException(); + IDbConnectionService? dbConnectionService = this._dbConnectionServices.FirstOrDefault(x => string.Equals(dbUser, x.Options.UserID, StringComparison.Ordinal)); + return dbConnectionService ?? throw new DbConnectionException($"Failed to retrieve connection from factory ({dbUser})."); } - public IDbConnectionService? GetConnection(Predicate predicate) + public IDbConnectionService GetConnection(Predicate predicate) { - throw new NotImplementedException(); + IDbConnectionService? dbConnectionService = this._dbConnectionServices.FirstOrDefault(x => predicate(x.Options)); + return dbConnectionService ?? throw new DbConnectionException("Failed to retrieve connection from factory (predicate)."); } #endregion @@ -60,12 +63,27 @@ public void Dispose() private IEnumerable> Create() { + ILogger> logger = this._loggerFactory.CreateLogger>(); + foreach (U dbOption in this._dbConnectionOptions) { U dbSettings = dbOption; - } + IOptions connectionOptions = Microsoft.Extensions.Options.Options.Create(dbSettings); - throw new NotImplementedException(); + IDbConnectionStringFactory dbConnectionStringFactory = this._serviceProvider.GetRequiredService(); + IDbConnectionFactory dbConnectionFactory = this._serviceProvider.GetRequiredService(); + IDbDataAdapterFactory dbDataAdapterFactory = this._serviceProvider.GetRequiredService(); + + this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.Provider, maskPassword: true)); + + yield return new DbConnectionService( + logger, + connectionOptions, + dbConnectionStringFactory, + dbConnectionFactory, + dbDataAdapterFactory + ); + } } private void Dispose(bool disposing) From 1e1631774158a4af53231c2276293c22c1999420 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Mar 2025 20:13:39 +0100 Subject: [PATCH 15/24] Rename Provider to DbProvider --- .../{Provider.cs => DbProvider.cs} | 4 ++-- .../IDbConnectionFactory.cs | 2 +- .../IDbConnectionStringFactory.cs | 2 +- AdvancedSystems.Connector.Abstractions/IDbSettings.cs | 4 ++-- AdvancedSystems.Connector/Internals/DbConnectionFactory.cs | 4 ++-- .../Internals/DbConnectionStringFactory.cs | 4 ++-- AdvancedSystems.Connector/Options/DbOptions.cs | 6 +++--- AdvancedSystems.Connector/Services/DbConnectionService.cs | 3 ++- .../Services/DbConnectionServiceFactory.cs | 1 + 9 files changed, 16 insertions(+), 14 deletions(-) rename AdvancedSystems.Connector.Abstractions/{Provider.cs => DbProvider.cs} (71%) diff --git a/AdvancedSystems.Connector.Abstractions/Provider.cs b/AdvancedSystems.Connector.Abstractions/DbProvider.cs similarity index 71% rename from AdvancedSystems.Connector.Abstractions/Provider.cs rename to AdvancedSystems.Connector.Abstractions/DbProvider.cs index 86e0fec..2f71d8d 100644 --- a/AdvancedSystems.Connector.Abstractions/Provider.cs +++ b/AdvancedSystems.Connector.Abstractions/DbProvider.cs @@ -1,9 +1,9 @@ -namespace AdvancedSystems.Connector; +namespace AdvancedSystems.Connector.Abstractions; /// /// Represents the different database providers supported by the system. /// -public enum Provider +public enum DbProvider { /// /// Microsoft SQL Server. diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs index 0cb12a1..972ccfd 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs @@ -27,7 +27,7 @@ public interface IDbConnectionFactory /// /// Thrown when the specified is not supported by the factory. /// - DbConnection Create(Provider provider, string connectionString); + DbConnection Create(DbProvider provider, string connectionString); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index da246c8..1537196 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -4,7 +4,7 @@ public interface IDbConnectionStringFactory { #region Methods - string Create(Provider provider, bool maskPassword = false); + string Create(DbProvider provider, bool maskPassword = false); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs index 214b755..c56f715 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs @@ -28,9 +28,9 @@ public interface IDbSettings string Password { get; set; } /// - /// + /// Gets or sets the database provider. /// - Provider Provider { get; set; } + DbProvider Provider { get; set; } /// /// Gets or sets the user ID to be used when connecting to SQL Server. diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index e228b44..6373112 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -11,11 +11,11 @@ public sealed class DbConnectionFactory : IDbConnectionFactory { #region Methods - public DbConnection Create(Provider provider, string connectionString) + public DbConnection Create(DbProvider provider, string connectionString) { return provider switch { - Provider.MsSql => new SqlConnection(connectionString), + DbProvider.MsSql => new SqlConnection(connectionString), _ => throw new NotSupportedException(Enum.GetName(provider)), }; } diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 060ae9f..d301410 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -20,11 +20,11 @@ public DbConnectionStringFactory(IServiceProvider serviceProvider) #region Methods - public string Create(Provider provider, bool maskPassword = false) + public string Create(DbProvider provider, bool maskPassword = false) { return provider switch { - Provider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), + DbProvider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), _ => throw new NotSupportedException(), }; } diff --git a/AdvancedSystems.Connector/Options/DbOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs index db8c2b6..0704c5d 100644 --- a/AdvancedSystems.Connector/Options/DbOptions.cs +++ b/AdvancedSystems.Connector/Options/DbOptions.cs @@ -26,9 +26,9 @@ public record DbOptions : IDbSettings public required string Password { get; set; } [Required] - [DisplayName("Provider")] - [JsonConverter(typeof(Provider))] - public required Provider Provider { get; set; } + [DisplayName("DB Provider")] + [JsonConverter(typeof(DbProvider))] + public required DbProvider Provider { get; set; } [Required] [DisplayName("User ID")] diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index fe66bb9..29d46fc 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -16,6 +16,7 @@ namespace AdvancedSystems.Connector.Services; +/// public sealed class DbConnectionService : IDbConnectionService where T : class, IDbSettings, new() { private readonly ILogger> _logger; @@ -32,7 +33,7 @@ public DbConnectionService(ILogger> logger, IOptions o this._databaseConnectionFactory = dbConnectionFactory; this._dataAdapterFactory = dbDataAdapterFactory; - this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider); + this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider, maskPassword: false); } #region Properties diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs index a56f286..e51e153 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -11,6 +11,7 @@ namespace AdvancedSystems.Connector.Services; +/// public sealed class DbConnectionServiceFactory : IDbConnectionServiceFactory where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() { private bool _isDisposed = false; From 6b3519d2dee1a30fc2745e14514765aeb1c267a8 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Mar 2025 21:53:00 +0100 Subject: [PATCH 16/24] Test AddDbConnectionService --- .../IDbSettings.cs | 8 +- .../ServiceCollectionExtensionsTests.cs | 141 +++++++++++++++++- .../ServiceCollectionExtensions.cs | 4 + .../Options/DbOptions.cs | 10 +- 4 files changed, 149 insertions(+), 14 deletions(-) diff --git a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs index c56f715..6abfb1e 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs @@ -15,17 +15,17 @@ public interface IDbSettings /// /// Gets or sets the name or network address of the instance of SQL Server to connect to. /// - string DataSource { get; set; } + string? DataSource { get; set; } /// /// Gets or sets the name of the database associated with the connection. /// - string InitialCatalog { get; set; } + string? InitialCatalog { get; set; } /// /// Gets or sets the password for the SQL Server account. /// - string Password { get; set; } + string? Password { get; set; } /// /// Gets or sets the database provider. @@ -35,7 +35,7 @@ public interface IDbSettings /// /// Gets or sets the user ID to be used when connecting to SQL Server. /// - string UserID { get; set; } + string? UserID { get; set; } #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 2a429b6..4ad591c 100644 --- a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -1,5 +1,17 @@ -using AdvancedSystems.Connector.Abstractions; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Common; +using AdvancedSystems.Connector.DependencyInjection; +using AdvancedSystems.Connector.Options; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; namespace AdvancedSystems.Security.Tests.DependencyInjection; @@ -9,30 +21,149 @@ namespace AdvancedSystems.Security.Tests.DependencyInjection; /// public sealed class ServiceCollectionExtensionsTests { + /// + /// Mock options for . + /// + private readonly MsSqlOptions _dbConnectionOptions; + + /// + /// Dummy decryption function to test the mechanics of calling + /// a delegate at option registration. + /// + /// + /// The password to decrypt. + /// + /// + /// The password in plaintext. + /// + private static string DecryptPassword(string password) => password.ToUpper(); + + public ServiceCollectionExtensionsTests() + { + this._dbConnectionOptions = new MsSqlOptions + { + DataSource = new DataSource("localhost", 80), + InitialCatalog = "ADVSYS", + MinPoolSize = 1, + MaxPoolSize = 3, + UserID = "admin", + Password = "admin", + ApplicationName = "Test", + }; + } + #region AddDbConnectionService /// /// Tests that can be initialized through /// dependency injection from . /// - [Fact(Skip = "TODO")] - public void TestAddDbConnectionService_FromOptions() + [Fact] + public async Task TestAddDbConnectionService_FromOptions() { // Arrange + using IHost? host = await new HostBuilder() + .ConfigureWebHostDefaults(builder => + { + builder.UseTestServer(); + + builder.ConfigureTestServices(services => + { + services.AddDbConnectionService(options => + { + options.DataSource = this._dbConnectionOptions.DataSource; + options.InitialCatalog = this._dbConnectionOptions.InitialCatalog; + options.MinPoolSize = this._dbConnectionOptions.MinPoolSize; + options.MaxPoolSize = this._dbConnectionOptions.MaxPoolSize; + options.UserID = this._dbConnectionOptions.UserID; + options.Password = this._dbConnectionOptions.Password; + options.ApplicationName = this._dbConnectionOptions.ApplicationName; + }, DecryptPassword); + }); + + builder.Configure(app => + { + + }); + }) + .StartAsync(); + // Act + IDbConnectionService? dbConnectionService = host.Services.GetService>(); + // Assert + Assert.Multiple(() => + { + Assert.NotNull(dbConnectionService); + Assert.True(dbConnectionService.Options.Password?.All(char.IsUpper)); + Assert.Equal(this._dbConnectionOptions.DataSource, dbConnectionService.Options.DataSource); + Assert.Equal(this._dbConnectionOptions.InitialCatalog, dbConnectionService.Options.InitialCatalog); + Assert.Equal(this._dbConnectionOptions.MinPoolSize, dbConnectionService.Options.MinPoolSize); + Assert.Equal(this._dbConnectionOptions.MaxPoolSize, dbConnectionService.Options.MaxPoolSize); + Assert.Equal(this._dbConnectionOptions.UserID, dbConnectionService.Options.UserID); + Assert.Equal(this._dbConnectionOptions.ApplicationName, dbConnectionService.Options.ApplicationName); + }); + + await host.StopAsync(); } /// /// Tests that can be initialized through /// dependency injection from appsettings.json. /// - [Fact(Skip = "TODO")] - public void TestAddDbConnectionService_FromAppSettings() + [Fact] + public async Task TestAddDbConnectionService_FromAppSettings() { // Arrange + var appSettings = new Dictionary() + { + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.DataSource)}", this._dbConnectionOptions.DataSource }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.InitialCatalog)}", this._dbConnectionOptions.InitialCatalog }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.MinPoolSize)}", this._dbConnectionOptions.MinPoolSize.ToString() }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.MaxPoolSize)}", this._dbConnectionOptions.MaxPoolSize.ToString() }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.UserID)}", this._dbConnectionOptions.UserID }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.Password)}", this._dbConnectionOptions.Password }, + { $"{Sections.DATABASE}:{nameof(MsSqlOptions.ApplicationName)}", this._dbConnectionOptions.ApplicationName }, + }; + + IConfigurationRoot configurationRoot = new ConfigurationBuilder() + .AddInMemoryCollection(appSettings) + .Build(); + + using IHost? host = await new HostBuilder() + .ConfigureWebHostDefaults(builder => + { + builder.UseTestServer(); + + builder.ConfigureTestServices(services => + { + services.AddDbConnectionService(configurationRoot.GetRequiredSection(Sections.DATABASE), DecryptPassword); + }); + + builder.Configure(app => + { + + }); + }) + .StartAsync(); + // Act + IDbConnectionService? dbConnectionService = host.Services.GetService>(); + // Assert + Assert.Multiple(() => + { + Assert.NotNull(dbConnectionService); + Assert.True(dbConnectionService.Options.Password?.All(char.IsUpper)); + Assert.Equal(this._dbConnectionOptions.DataSource, dbConnectionService.Options.DataSource); + Assert.Equal(this._dbConnectionOptions.InitialCatalog, dbConnectionService.Options.InitialCatalog); + Assert.Equal(this._dbConnectionOptions.MinPoolSize, dbConnectionService.Options.MinPoolSize); + Assert.Equal(this._dbConnectionOptions.MaxPoolSize, dbConnectionService.Options.MaxPoolSize); + Assert.Equal(this._dbConnectionOptions.UserID, dbConnectionService.Options.UserID); + Assert.Equal(this._dbConnectionOptions.ApplicationName, dbConnectionService.Options.ApplicationName); + }); + + await host.StopAsync(); } #endregion diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index b9d54f6..7239879 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Internals; using AdvancedSystems.Connector.Services; using Microsoft.Extensions.Configuration; @@ -26,6 +27,9 @@ public static class ServiceCollectionExtensions private static IServiceCollection AddDbConnectionService(this IServiceCollection services) where T : class, IDbSettings, new() { + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService), typeof(DbConnectionService))); return services; } diff --git a/AdvancedSystems.Connector/Options/DbOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs index 0704c5d..3b259d6 100644 --- a/AdvancedSystems.Connector/Options/DbOptions.cs +++ b/AdvancedSystems.Connector/Options/DbOptions.cs @@ -15,24 +15,24 @@ public record DbOptions : IDbSettings [Required] [DisplayName("Data Source")] - public required string DataSource { get; set; } + public string? DataSource { get; set; } [Required] [DisplayName("Initial Catalog")] - public required string InitialCatalog { get; set; } + public string? InitialCatalog { get; set; } [Required] [DisplayName("Password")] - public required string Password { get; set; } + public string? Password { get; set; } [Required] [DisplayName("DB Provider")] [JsonConverter(typeof(DbProvider))] - public required DbProvider Provider { get; set; } + public DbProvider Provider { get; set; } [Required] [DisplayName("User ID")] - public required string UserID { get; set; } + public string? UserID { get; set; } #endregion } \ No newline at end of file From 7f4b61263b89e4905a85af912863973c035f297d Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Mar 2025 22:07:12 +0100 Subject: [PATCH 17/24] Add null check prior to DecryptPassword --- .../DependencyInjection/ServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 7239879..3a8af49 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -60,6 +60,7 @@ public static class ServiceCollectionExtensions { if (decryptPassword != null) { + ArgumentNullException.ThrowIfNull(options.Password); options.Password = decryptPassword(options.Password); } }); @@ -94,6 +95,7 @@ public static class ServiceCollectionExtensions { if (decryptPassword != null) { + ArgumentNullException.ThrowIfNull(options.Password); options.Password = decryptPassword(options.Password); } }); From cf6a1b7f2b16537262c67f2e0266578852b38f3a Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Tue, 1 Apr 2025 21:28:07 +0200 Subject: [PATCH 18/24] Restructure and refactor code --- .../IDbCommand.cs | 10 +++ .../Common/DataSource.cs | 28 +++++-- AdvancedSystems.Connector/Common/DbCommand.cs | 39 ++-------- .../Extensions/InternalExtensions.cs | 75 +++++++++++++++++++ .../Internals/MsSqlExtensions.cs | 50 ------------- .../Options/DbOptions.cs | 3 + .../Services/DbConnectionService.cs | 2 +- 7 files changed, 120 insertions(+), 87 deletions(-) create mode 100644 AdvancedSystems.Connector/Extensions/InternalExtensions.cs delete mode 100644 AdvancedSystems.Connector/Internals/MsSqlExtensions.cs diff --git a/AdvancedSystems.Connector.Abstractions/IDbCommand.cs b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs index 22dd260..641443d 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbCommand.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs @@ -28,4 +28,14 @@ public interface IDbCommand IReadOnlyList Parameters { get; } #endregion + + #region Methods + + void AddParameter(string parameterName, object value, DbType type); + + void AddParameter(string parameterName, T value); + + string ToString(); + + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DataSource.cs b/AdvancedSystems.Connector/Common/DataSource.cs index 09578cc..f058636 100644 --- a/AdvancedSystems.Connector/Common/DataSource.cs +++ b/AdvancedSystems.Connector/Common/DataSource.cs @@ -1,12 +1,30 @@ -namespace AdvancedSystems.Connector.Common; - -public sealed class DataSource(string host, int port) +using AdvancedSystems.Connector.Options; + +namespace AdvancedSystems.Connector.Common; + +/// +/// Represents a data source with a host and port. +/// +/// +/// The host of the data source. +/// +/// +/// The port of the data source. +/// +/// +public sealed record DataSource(string Host, int Port) { #region Properties - public string Host { get; set; } = host; + /// + /// Gets or sets the host of the data source. + /// + public string Host { get; set; } = Host; - public int Port { get; set; } = port; + /// + /// Gets or sets the port of the data source. + /// + public int Port { get; set; } = Port; #endregion diff --git a/AdvancedSystems.Connector/Common/DbCommand.cs b/AdvancedSystems.Connector/Common/DbCommand.cs index 2603993..e08fa6b 100644 --- a/AdvancedSystems.Connector/Common/DbCommand.cs +++ b/AdvancedSystems.Connector/Common/DbCommand.cs @@ -3,11 +3,10 @@ using System.ComponentModel; using System.Data; using System.Diagnostics; -using System.Globalization; using System.Text; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Internals; +using AdvancedSystems.Connector.Extensions; using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; @@ -33,25 +32,25 @@ public sealed class DbCommand : IDbCommand #region Methods - public void AddParameter(string parameterName, T value) + public void AddParameter(string parameterName, object value, DbType type) { var databaseParameter = new DbParameter { ParameterName = parameterName, - DbType = typeof(T).DeriveFrom(), - Value = (object?)value ?? DBNull.Value, + DbType = type, + Value = value }; this._parameters.Add(databaseParameter); } - public void AddParameter(string parameterName, object value, DbType type) + public void AddParameter(string parameterName, T value) { var databaseParameter = new DbParameter { ParameterName = parameterName, - DbType = type, - Value = value + DbType = typeof(T).DeriveFrom(), + Value = (object?)value ?? DBNull.Value, }; this._parameters.Add(databaseParameter); @@ -65,33 +64,11 @@ public override string ToString() foreach (IDbParameter parameter in this.Parameters) { - commandBuilder.Replace(parameter.ParameterName, FormatValue(parameter)); + commandBuilder.Replace(parameter.ParameterName, parameter.FormatValue()); } return commandBuilder.ToString(); } #endregion - - #region Helpers - - private static string? FormatValue(IDbParameter parameter) - { - return parameter.DbType switch - { - DbType.String => parameter.Value?.ToString(), - DbType.Boolean => ((bool?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Byte => ((byte?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Int16 => ((short?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Int32 => ((int?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Int64 => ((long?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Single => ((float?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Double => ((double?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.Decimal => ((decimal?)parameter.Value)?.ToString(CultureInfo.InvariantCulture), - DbType.DateTime => ((DateTime?)parameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), - _ => parameter.Value?.ToString(), - }; - } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/InternalExtensions.cs b/AdvancedSystems.Connector/Extensions/InternalExtensions.cs new file mode 100644 index 0000000..0ee4e77 --- /dev/null +++ b/AdvancedSystems.Connector/Extensions/InternalExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Globalization; + +using AdvancedSystems.Connector.Abstractions; + +using Microsoft.Data.SqlClient; + +namespace AdvancedSystems.Connector.Extensions; + +internal static class InternalExtensions +{ + #region IDbParameter + + internal static string? FormatValue(this IDbParameter dbParameter) + { + CultureInfo cultureInfo = CultureInfo.InvariantCulture; + + return dbParameter.DbType switch + { + DbType.String => dbParameter.Value?.ToString(), + DbType.Boolean => ((bool?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Byte => ((byte?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Int16 => ((short?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Int32 => ((int?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Int64 => ((long?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Single => ((float?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Double => ((double?)dbParameter.Value)?.ToString(cultureInfo), + DbType.Decimal => ((decimal?)dbParameter.Value)?.ToString(cultureInfo), + DbType.DateTime => ((DateTime?)dbParameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", cultureInfo), + _ => throw new NotImplementedException(), + }; + } + + internal static DbParameter DeriveFrom(this IDbParameter dbParameter, DbCommand dbCommand) + { + return dbCommand switch + { + SqlCommand _ => new SqlParameter + { + ParameterName = dbParameter.ParameterName, + DbType = dbParameter.DbType, + Value = dbParameter.Value, + }, + _ => throw new NotImplementedException(), + }; + } + + #endregion + + #region Type + + internal static DbType DeriveFrom(this Type type) + { + TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); + + return typeCode switch + { + TypeCode.String or TypeCode.Char => DbType.String, + TypeCode.Boolean => DbType.Boolean, + TypeCode.Byte => DbType.Byte, + TypeCode.SByte or TypeCode.Int16 => DbType.Int16, + TypeCode.Int32 => DbType.Int32, + TypeCode.Int64 => DbType.Int64, + TypeCode.Single => DbType.Single, + TypeCode.Double => DbType.Double, + TypeCode.Decimal => DbType.Decimal, + TypeCode.DateTime => DbType.DateTime, + _ => throw new NotImplementedException($"Failed to infer type from {typeCode}."), + }; + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs b/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs deleted file mode 100644 index 9d5b922..0000000 --- a/AdvancedSystems.Connector/Internals/MsSqlExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; - -using AdvancedSystems.Connector.Abstractions; - -using Microsoft.Data.SqlClient; - -namespace AdvancedSystems.Connector.Internals; - -public static class MsSqlExtensions -{ - #region Converters - - public static DbParameter DeriveFrom(this IDbParameter databaseParameter, DbCommand dbCommand) - { - return dbCommand switch - { - SqlCommand _ => new SqlParameter - { - ParameterName = databaseParameter.ParameterName, - DbType = databaseParameter.DbType, - Value = databaseParameter.Value, - }, - _ => throw new NotSupportedException(), - }; - } - - public static DbType DeriveFrom(this Type type) - { - TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); - - return typeCode switch - { - TypeCode.String or TypeCode.Char => DbType.String, - TypeCode.Boolean => DbType.Boolean, - TypeCode.Byte => DbType.Byte, - TypeCode.SByte or TypeCode.Int16 => DbType.Int16, - TypeCode.Int32 => DbType.Int32, - TypeCode.Int64 => DbType.Int64, - TypeCode.Single => DbType.Single, - TypeCode.Double => DbType.Double, - TypeCode.Decimal => DbType.Decimal, - TypeCode.DateTime => DbType.DateTime, - _ => throw new ArgumentException($"Failed to infer type from {typeCode}."), - }; - } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Options/DbOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs index 3b259d6..7a8bd3b 100644 --- a/AdvancedSystems.Connector/Options/DbOptions.cs +++ b/AdvancedSystems.Connector/Options/DbOptions.cs @@ -6,6 +6,9 @@ namespace AdvancedSystems.Connector.Options; +/// +/// +/// public record DbOptions : IDbSettings { #region Properties diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 29d46fc..067ef79 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -6,7 +6,7 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; -using AdvancedSystems.Connector.Internals; +using AdvancedSystems.Connector.Extensions; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; From 3115e2108d9b4330c4b8ac39e50898d6ec28dab1 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Tue, 1 Apr 2025 23:46:11 +0200 Subject: [PATCH 19/24] Improve testability of DbConnectionService code * create DbCommandFactory (because dbConnection.CreateCommand cannot be mocked without this additional abstraction) * implement first unit test as proof of concept (ExecuteQuery) * add DbProvider.Generic (WIP) * add some more documentation via doc strings --- .../DbProvider.cs | 7 +- .../IDbCommandFactory.cs | 9 ++ .../IDbConnectionService.cs | 18 ++-- .../IDbConnectionStringFactory.cs | 22 +++- .../AdvancedSystems.Connector.Tests.csproj | 6 +- .../Fixtures/DbConnectionServiceFixture.cs | 101 ++++++++++++++++++ .../Services/DbConnectionServiceTests.cs | 40 ++++++- .../TestCategory.cs | 9 ++ .../ServiceCollectionExtensions.cs | 1 + .../Internals/DbCommandFactory.cs | 51 +++++++++ .../Internals/DbConnectionStringFactory.cs | 49 ++++++--- .../Services/DbConnectionService.cs | 91 +++++++++------- .../Services/DbConnectionServiceFactory.cs | 2 + readme.md | 2 +- 14 files changed, 337 insertions(+), 71 deletions(-) create mode 100644 AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs create mode 100644 AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs create mode 100644 AdvancedSystems.Connector.Tests/TestCategory.cs create mode 100644 AdvancedSystems.Connector/Internals/DbCommandFactory.cs diff --git a/AdvancedSystems.Connector.Abstractions/DbProvider.cs b/AdvancedSystems.Connector.Abstractions/DbProvider.cs index 2f71d8d..7ff8f9f 100644 --- a/AdvancedSystems.Connector.Abstractions/DbProvider.cs +++ b/AdvancedSystems.Connector.Abstractions/DbProvider.cs @@ -5,8 +5,13 @@ /// public enum DbProvider { + /// + /// Generic SQL Server. + /// + Generic = 0, + /// /// Microsoft SQL Server. /// - MsSql, + MsSql = 1, } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs new file mode 100644 index 0000000..a834c1b --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs @@ -0,0 +1,9 @@ +using System.Data; +using System.Data.Common; + +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDbCommandFactory +{ + DbCommand Create(DbProvider dbProvider, DbConnection dbConnection, string commandText, CommandType commandType); +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs index a95f50e..8dcafa9 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs @@ -2,6 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using AdvancedSystems.Connector.Abstractions.Exceptions; + namespace AdvancedSystems.Connector.Abstractions; /// @@ -34,7 +36,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// Executes a query defined by the specified synchronously. /// - /// + /// /// The command that defines the query to be executed. It must contain the SQL command /// text and any required parameters. /// @@ -50,12 +52,12 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - DataSet ExecuteQuery(IDbCommand databaseCommand); + DataSet ExecuteQuery(IDbCommand dbCommand); /// /// Executes a query defined by the specified asynchronously. /// - /// + /// /// The command that defines the query to be executed. It must contain the SQL command /// text and any required parameters. /// @@ -74,12 +76,12 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default); /// /// Executes a non-query command defined by the specified synchronously. /// - /// + /// /// The command that defines the query to be executed. It must contain the SQL command /// text and any required parameters. /// @@ -95,12 +97,12 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - int ExecuteNonQuery(IDbCommand databaseCommand); + int ExecuteNonQuery(IDbCommand dbCommand); /// /// Executes a non-query command defined by the specified asynchronously. /// - /// + /// /// The command that defines the query to be executed. It must contain the SQL command /// text and any required parameters. /// @@ -120,7 +122,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteNonQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index 1537196..cdf71d6 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -1,9 +1,29 @@ -namespace AdvancedSystems.Connector.Abstractions; +using System; +namespace AdvancedSystems.Connector.Abstractions; + +/// +/// Defines a factory for creating connection strings for various . +/// public interface IDbConnectionStringFactory { #region Methods + /// + /// Creates a database connection string based on the specified provider. + /// + /// + /// A that specifies the type of database to connect to. + /// + /// + /// A boolean value indicating whether the password in the connection string should be masked. + /// + /// + /// A database connection string. + /// + /// + /// Thrown when the specified is not implemented yet. + /// string Create(DbProvider provider, bool maskPassword = false); #endregion diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj index d6ca636..75ebabf 100644 --- a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj +++ b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -40,8 +40,4 @@ - - - - diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs new file mode 100644 index 0000000..4fb9c3e --- /dev/null +++ b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs @@ -0,0 +1,101 @@ +using System.Data; +using System.Data.Common; + +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Common; +using AdvancedSystems.Connector.Services; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Moq; + +using DbCommand = System.Data.Common.DbCommand; + +namespace AdvancedSystems.Connector.Tests.Fixtures; + +public sealed class DbConnectionServiceFixture where T : class, IDbSettings, new() +{ + private readonly Mock _dbConnection = new(); + private readonly Mock _dbDataAdapter = new(); + + private readonly Mock>> _logger = new(); + private readonly Mock> _options = new(); + private readonly Mock _dbConnectionFactory = new(); + private readonly Mock _dbConnectionStringFactory = new(); + private readonly Mock _dbCommandFactory = new(); + private readonly Mock _dbDataAdapterFactory = new(); + + public DbConnectionServiceFixture() + { + this.Options.Setup(x => x.Value) + .Returns(new T + { + ApplicationName = nameof(DbConnectionServiceFixture), + UserID = "admin", + Password = "password", + DataSource = new DataSource("localhost", 1433), + InitialCatalog = "TEST", + Provider = It.IsAny(), + }); + + this.DbConnectionStringFactory.Setup(x => x.Create(It.IsAny(), true)) + .Returns(It.IsAny()); + + this.DbConnectionFactory.Setup(x => x.Create(It.IsAny(), It.IsAny())) + .Returns(this._dbConnection.Object); + + this._dbConnection.SetupAdd(x => x.StateChange += It.IsAny()) + .Callback((StateChangeEventHandler handler) => + { + handler(this._dbConnection.Object, new StateChangeEventArgs(ConnectionState.Closed, ConnectionState.Open)); + }); + + //this._dbConnection.Setup(x => x.Open()); + + // mock DbCommand.Create + + this.DbDataAdapterFactory.Setup(x => x.Create(It.IsAny())) + .Returns(this._dbDataAdapter.Object); + + this.DbConnectionService = new DbConnectionService( + this.Logger.Object, + this.Options.Object, + this.DbConnectionStringFactory.Object, + this.DbConnectionFactory.Object, + this.DbCommandFactory.Object, + this.DbDataAdapterFactory.Object + ); + } + + #region Properties + + public Mock>> Logger => this._logger; + + public Mock> Options => this._options; + + public Mock DbConnectionFactory => this._dbConnectionFactory; + + public Mock DbConnectionStringFactory => this._dbConnectionStringFactory; + + public Mock DbCommandFactory => this._dbCommandFactory; + + public Mock DbDataAdapterFactory => this._dbDataAdapterFactory; + + public IDbConnectionService DbConnectionService { get; set; } + + #endregion + + #region Methods + + public void ClearInvocations() + { + this.Logger.Invocations.Clear(); + this.Options.Invocations.Clear(); + this.DbConnectionFactory.Invocations.Clear(); + this.DbConnectionStringFactory.Invocations.Clear(); + this.DbDataAdapterFactory.Invocations.Clear(); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs index 4363be3..2d65841 100644 --- a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -1,21 +1,55 @@ +using System.ComponentModel; +using System.Data; +using System.Data.Common; + +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Common; +using AdvancedSystems.Connector.Options; +using AdvancedSystems.Connector.Tests.Fixtures; + +using Microsoft.Data.SqlClient; + +using Moq; + +using DbCommand = AdvancedSystems.Connector.Common.DbCommand; + namespace AdvancedSystems.Security.Tests.Services; -public sealed class DbConnectionServiceTests +[Category(TestCategory.UNIT)] +public sealed class DbConnectionServiceTests : IClassFixture> { + private readonly DbConnectionServiceFixture _sut; - public DbConnectionServiceTests() + public DbConnectionServiceTests(DbConnectionServiceFixture sut) { + this._sut = sut; + //this._sut.Options.Setup(x => x.Value) + // .Returns(this._sut.Options.Object.Value with { Provider = DbProvider.MsSql }); } #region Tests - [Fact(Skip = "TODO")] + [Fact] public void TestExecuteQuery() { // Arrange + var dbCommand = new DbCommand + { + CommandText = "SELECT @value", + CommandType = CommandType.Text, + }; + + dbCommand.AddParameter("@value", 1); + + this._sut.DbCommandFactory.Setup(x => x.Create(It.IsAny(), It.IsAny(), dbCommand.CommandText, dbCommand.CommandType)) + .Returns(new SqlCommand { CommandText = dbCommand.CommandText, CommandType = dbCommand.CommandType }); + // Act + DataSet response = this._sut.DbConnectionService.ExecuteQuery(dbCommand); + // Assert + Assert.NotNull(response); } [Fact(Skip = "TODO")] diff --git a/AdvancedSystems.Connector.Tests/TestCategory.cs b/AdvancedSystems.Connector.Tests/TestCategory.cs new file mode 100644 index 0000000..b8c35b4 --- /dev/null +++ b/AdvancedSystems.Connector.Tests/TestCategory.cs @@ -0,0 +1,9 @@ +namespace AdvancedSystems.Security.Tests; + +internal static class TestCategory +{ + /// + /// Denotes a unit test category. + /// + internal const string UNIT = "UNIT"; +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 3a8af49..5b8aec5 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions { services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService), typeof(DbConnectionService))); return services; diff --git a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs new file mode 100644 index 0000000..3438860 --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs @@ -0,0 +1,51 @@ +using System; +using System.Data; +using System.Data.Common; + +using AdvancedSystems.Connector.Abstractions; + +using Microsoft.Data.SqlClient; + +namespace AdvancedSystems.Connector.Internals; + +public sealed class DbCommandFactory : IDbCommandFactory +{ + #region Methods + + public DbCommand Create(DbProvider dbProvider, DbConnection dbConnection, string commandText, CommandType commandType) + { + return dbProvider switch + { + DbProvider.Generic => CreateGenericCommand(dbConnection, commandText, commandType), + DbProvider.MsSql => CreateSqlCommand(dbConnection, commandText, commandType), + _ => throw new NotImplementedException(), + }; + } + + #endregion + + #region Helpers + + private static DbCommand CreateGenericCommand(DbConnection dbConnection, string commandText, CommandType commandType) + { + DbCommand dbCommand = dbConnection.CreateCommand(); + dbCommand.CommandText = commandText; + dbCommand.CommandType = commandType; + return dbCommand; + } + + private static DbCommand CreateSqlCommand(DbConnection dbConnection, string commandText, CommandType commandType) + { + if (dbConnection is SqlConnection sqlConnection) + { + SqlCommand sqlCommand = sqlConnection.CreateCommand(); + sqlCommand.CommandText = commandText; + sqlCommand.CommandType = commandType; + return sqlCommand; + } + + throw new Exception(); // TODO: Create DbCommandException + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index d301410..85fe405 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Options; @@ -24,8 +25,9 @@ public string Create(DbProvider provider, bool maskPassword = false) { return provider switch { + DbProvider.Generic => CreateGenericConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), DbProvider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), - _ => throw new NotSupportedException(), + _ => throw new NotImplementedException(), }; } @@ -33,25 +35,42 @@ public string Create(DbProvider provider, bool maskPassword = false) #region Helpers - private static string CreateMsSqlConnectionString(MsSqlOptions settings, bool maskPassword) + private static string CreateGenericConnectionString(DbOptions options, bool maskPassword) + { + var builder = new DbConnectionStringBuilder(); + + if (options.ApplicationName is not null) + { + builder.Add("Application Name", options.ApplicationName); + } + + builder.Add("Data Source", options.DataSource ?? throw new ArgumentNullException(nameof(options))); + builder.Add("Initial Catalog", options.InitialCatalog ?? throw new ArgumentNullException(nameof(options))); + builder.Add("Password", options.Password ?? throw new ArgumentNullException(nameof(options))); + builder.Add("User ID", options.UserID ?? throw new ArgumentNullException(nameof(options))); + + return builder.ConnectionString; + } + + private static string CreateMsSqlConnectionString(MsSqlOptions options, bool maskPassword) { var builder = new SqlConnectionStringBuilder { // Database Options - ApplicationName = settings.ApplicationName, - DataSource = settings.DataSource, - InitialCatalog = settings.InitialCatalog, - Password = maskPassword ? "******" : settings.Password, - UserID = settings.UserID, + ApplicationName = options.ApplicationName, + DataSource = options.DataSource, + InitialCatalog = options.InitialCatalog, + Password = maskPassword ? "******" : options.Password, + UserID = options.UserID, // Microsoft SQL Server Options - CommandTimeout = settings.CommandTimeout, - ConnectTimeout = settings.ConnectTimeout, - Encrypt = settings.Encrypt, - IntegratedSecurity = settings.IntegratedSecurity, - MaxPoolSize = settings.MaxPoolSize, - MinPoolSize = settings.MinPoolSize, - Pooling = settings.Pooling, - TrustServerCertificate = settings.TrustServerCertificate, + CommandTimeout = options.CommandTimeout, + ConnectTimeout = options.ConnectTimeout, + Encrypt = options.Encrypt, + IntegratedSecurity = options.IntegratedSecurity, + MaxPoolSize = options.MaxPoolSize, + MinPoolSize = options.MinPoolSize, + Pooling = options.Pooling, + TrustServerCertificate = options.TrustServerCertificate, }; return builder.ToString(); diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 067ef79..8943ce6 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -12,6 +12,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using DbCommand = System.Data.Common.DbCommand; +using DbParameter = System.Data.Common.DbParameter; using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; namespace AdvancedSystems.Connector.Services; @@ -21,16 +23,18 @@ namespace AdvancedSystems.Connector.Services; { private readonly ILogger> _logger; private readonly IDbConnectionStringFactory _dbConnectionStringFactory; - private readonly IDbConnectionFactory _databaseConnectionFactory; + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly IDbCommandFactory _dbCommandFactory; private readonly IDbDataAdapterFactory _dataAdapterFactory; private readonly string _connectionString; - public DbConnectionService(ILogger> logger, IOptions options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) + public DbConnectionService(ILogger> logger, IOptions options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbCommandFactory dbCommandFactory, IDbDataAdapterFactory dbDataAdapterFactory) { this._logger = logger; this.Options = options.Value; this._dbConnectionStringFactory = dbConnectionStringFactory; - this._databaseConnectionFactory = dbConnectionFactory; + this._dbConnectionFactory = dbConnectionFactory; + this._dbCommandFactory = dbCommandFactory; this._dataAdapterFactory = dbDataAdapterFactory; this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider, maskPassword: false); @@ -46,60 +50,72 @@ public DbConnectionService(ILogger> logger, IOptions o #region Methods - public DataSet ExecuteQuery(IDbCommand databaseCommand) + public DataSet ExecuteQuery(IDbCommand dbCommand) { - using DbConnection connection = this.InitializeConnection(); + using DbConnection dbConnection = this.InitializeConnection(); - using DbCommand sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using DbCommand command = this._dbCommandFactory.Create( + this.Options.Provider, + dbConnection, + dbCommand.CommandText, + dbCommand.CommandType + ); - foreach (IDbParameter parameter in databaseCommand.Parameters) + foreach (IDbParameter dbParameter in dbCommand.Parameters) { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + DbParameter parameter = dbParameter.DeriveFrom(command); + command.Parameters.Add(parameter); } DataSet result = new(); - using DbDataAdapter adapter = this._dataAdapterFactory.Create(sqlCommand); + using DbDataAdapter adapter = this._dataAdapterFactory.Create(command); adapter.Fill(result); return result; } - public async ValueTask ExecuteQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) + public async ValueTask ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default) { - using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); + using DbConnection dbConnection = await this.InitializeConnectionAsync(cancellationToken); - using DbCommand sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using DbCommand command = this._dbCommandFactory.Create( + this.Options.Provider, + dbConnection, + dbCommand.CommandText, + dbCommand.CommandType + ); - foreach (IDbParameter parameter in databaseCommand.Parameters) + foreach (IDbParameter dbParameter in dbCommand.Parameters) { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + DbParameter parameter = dbParameter.DeriveFrom(command); + command.Parameters.Add(parameter); } DataTable result = new(); - using DbDataReader reader = await sqlCommand.ExecuteReaderAsync(cancellationToken); + using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); result.Load(reader); return result.DataSet; } - public int ExecuteNonQuery(IDbCommand databaseCommand) + public int ExecuteNonQuery(IDbCommand dbCommand) { - using DbConnection connection = this.InitializeConnection(); + using DbConnection dbConnection = this.InitializeConnection(); - using DbCommand sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using DbCommand command = this._dbCommandFactory.Create( + this.Options.Provider, + dbConnection, + dbCommand.CommandText, + dbCommand.CommandType + ); - foreach (IDbParameter parameter in databaseCommand.Parameters) + foreach (IDbParameter dbParameter in dbCommand.Parameters) { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + DbParameter parameter = dbParameter.DeriveFrom(command); + command.Parameters.Add(parameter); } - int rowsAffected = sqlCommand.ExecuteNonQuery(); + int rowsAffected = command.ExecuteNonQuery(); return rowsAffected; } @@ -108,16 +124,17 @@ public async ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, Can { using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); - using DbCommand sqlCommand = connection.CreateCommand(); - sqlCommand.CommandText = databaseCommand.CommandText; - sqlCommand.CommandType = databaseCommand.CommandType; + using DbCommand command = connection.CreateCommand(); + command.CommandText = databaseCommand.CommandText; + command.CommandType = databaseCommand.CommandType; - foreach (IDbParameter parameter in databaseCommand.Parameters) + foreach (IDbParameter dbParameter in databaseCommand.Parameters) { - sqlCommand.Parameters.Add(parameter.DeriveFrom(sqlCommand)); + DbParameter parameter = dbParameter.DeriveFrom(command); + command.Parameters.Add(parameter); } - int rowsAffected = await sqlCommand.ExecuteNonQueryAsync(cancellationToken); + int rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); return rowsAffected; } @@ -135,14 +152,14 @@ private DbConnection InitializeConnection() { try { - DbConnection connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + DbConnection connection = this._dbConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; connection.Open(); return connection; } catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) { - throw new DbConnectionException($"Failed to initialize connection to database: {exception.Message}", exception); + throw new DbConnectionException($"Failed to initialize dbConnection to database: {exception.Message}", exception); } } @@ -150,14 +167,14 @@ private async ValueTask InitializeConnectionAsync(CancellationToke { try { - DbConnection connection = this._databaseConnectionFactory.Create(this.Options.Provider, this._connectionString); + DbConnection connection = this._dbConnectionFactory.Create(this.Options.Provider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); return connection; } catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) { - throw new DbConnectionException($"Failed to initialize connection to database: {exception.Message}", exception); + throw new DbConnectionException($"Failed to initialize dbConnection to database: {exception.Message}", exception); } } diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs index e51e153..12e46fb 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -73,6 +73,7 @@ private IEnumerable> Create() IDbConnectionStringFactory dbConnectionStringFactory = this._serviceProvider.GetRequiredService(); IDbConnectionFactory dbConnectionFactory = this._serviceProvider.GetRequiredService(); + IDbCommandFactory dbCommandFactory = this._serviceProvider.GetRequiredService(); IDbDataAdapterFactory dbDataAdapterFactory = this._serviceProvider.GetRequiredService(); this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.Provider, maskPassword: true)); @@ -82,6 +83,7 @@ private IEnumerable> Create() connectionOptions, dbConnectionStringFactory, dbConnectionFactory, + dbCommandFactory, dbDataAdapterFactory ); } diff --git a/readme.md b/readme.md index 799bbdc..dd78181 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ for debugging .NET assemblies. Run test suite: ```powershell -dotnet test .\AdvancedSystems.Connector.Tests\ --no-logo +dotnet test .\AdvancedSystems.Connector.Tests\ --nologo ``` In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with From cf15a15ec9ae4485e6703ae1836003f7db122e9c Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 2 Apr 2025 00:16:03 +0200 Subject: [PATCH 20/24] Set default DbProvider in option records --- .../IDbSettings.cs | 2 +- .../Fixtures/DbConnectionServiceFixture.cs | 1 - .../Services/DbConnectionServiceTests.cs | 2 +- AdvancedSystems.Connector/Options/DbOptions.cs | 9 +++++++-- AdvancedSystems.Connector/Options/MsSqlOptions.cs | 14 +++++++++++++- .../Services/DbConnectionService.cs | 12 ++++++------ .../Services/DbConnectionServiceFactory.cs | 2 +- 7 files changed, 29 insertions(+), 13 deletions(-) diff --git a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs index 6abfb1e..9bc4e18 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs @@ -30,7 +30,7 @@ public interface IDbSettings /// /// Gets or sets the database provider. /// - DbProvider Provider { get; set; } + DbProvider DbProvider { get; } /// /// Gets or sets the user ID to be used when connecting to SQL Server. diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs index 4fb9c3e..fe42a9e 100644 --- a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs +++ b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs @@ -36,7 +36,6 @@ public DbConnectionServiceFixture() Password = "password", DataSource = new DataSource("localhost", 1433), InitialCatalog = "TEST", - Provider = It.IsAny(), }); this.DbConnectionStringFactory.Setup(x => x.Create(It.IsAny(), true)) diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs index 2d65841..f68a9bc 100644 --- a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -25,7 +25,7 @@ public DbConnectionServiceTests(DbConnectionServiceFixture sut) this._sut = sut; //this._sut.Options.Setup(x => x.Value) - // .Returns(this._sut.Options.Object.Value with { Provider = DbProvider.MsSql }); + // .Returns(this._sut.Options.Object.Value with { DbProvider = DbProvider.MsSql }); } #region Tests diff --git a/AdvancedSystems.Connector/Options/DbOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs index 7a8bd3b..24b53bd 100644 --- a/AdvancedSystems.Connector/Options/DbOptions.cs +++ b/AdvancedSystems.Connector/Options/DbOptions.cs @@ -11,6 +11,11 @@ namespace AdvancedSystems.Connector.Options; /// public record DbOptions : IDbSettings { + public DbOptions(DbProvider dbProvider = DbProvider.Generic) + { + this.DbProvider = dbProvider; + } + #region Properties [DisplayName("Application Name")] @@ -29,9 +34,9 @@ public record DbOptions : IDbSettings public string? Password { get; set; } [Required] - [DisplayName("DB Provider")] + [DisplayName("DB DbProvider")] [JsonConverter(typeof(DbProvider))] - public DbProvider Provider { get; set; } + public DbProvider DbProvider { get; private set; } [Required] [DisplayName("User ID")] diff --git a/AdvancedSystems.Connector/Options/MsSqlOptions.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs index e43c713..e80d1bd 100644 --- a/AdvancedSystems.Connector/Options/MsSqlOptions.cs +++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs @@ -1,10 +1,12 @@ using System.ComponentModel; +using AdvancedSystems.Connector.Abstractions; + using Microsoft.Data.SqlClient; namespace AdvancedSystems.Connector.Options; -public sealed record MsSqlOptions : DbOptions +public sealed record MsSqlOptions() : DbOptions(DbProvider.MsSql) { /// /// Gets or sets the default wait time (in seconds) before terminating the attempt to execute @@ -102,4 +104,14 @@ public sealed record MsSqlOptions : DbOptions /// [DisplayName("Trust Server Certificate")] public bool TrustServerCertificate { get; set; } = false; + + /// + /// + /// + /// + /// + /// Configured as . + /// + /// + public new DbProvider DbProvider { get; private set; } } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 8943ce6..84915e4 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -37,7 +37,7 @@ public DbConnectionService(ILogger> logger, IOptions o this._dbCommandFactory = dbCommandFactory; this._dataAdapterFactory = dbDataAdapterFactory; - this._connectionString = this._dbConnectionStringFactory.Create(this.Options.Provider, maskPassword: false); + this._connectionString = this._dbConnectionStringFactory.Create(this.Options.DbProvider, maskPassword: false); } #region Properties @@ -55,7 +55,7 @@ public DataSet ExecuteQuery(IDbCommand dbCommand) using DbConnection dbConnection = this.InitializeConnection(); using DbCommand command = this._dbCommandFactory.Create( - this.Options.Provider, + this.Options.DbProvider, dbConnection, dbCommand.CommandText, dbCommand.CommandType @@ -79,7 +79,7 @@ public DataSet ExecuteQuery(IDbCommand dbCommand) using DbConnection dbConnection = await this.InitializeConnectionAsync(cancellationToken); using DbCommand command = this._dbCommandFactory.Create( - this.Options.Provider, + this.Options.DbProvider, dbConnection, dbCommand.CommandText, dbCommand.CommandType @@ -103,7 +103,7 @@ public int ExecuteNonQuery(IDbCommand dbCommand) using DbConnection dbConnection = this.InitializeConnection(); using DbCommand command = this._dbCommandFactory.Create( - this.Options.Provider, + this.Options.DbProvider, dbConnection, dbCommand.CommandText, dbCommand.CommandType @@ -152,7 +152,7 @@ private DbConnection InitializeConnection() { try { - DbConnection connection = this._dbConnectionFactory.Create(this.Options.Provider, this._connectionString); + DbConnection connection = this._dbConnectionFactory.Create(this.Options.DbProvider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; connection.Open(); return connection; @@ -167,7 +167,7 @@ private async ValueTask InitializeConnectionAsync(CancellationToke { try { - DbConnection connection = this._dbConnectionFactory.Create(this.Options.Provider, this._connectionString); + DbConnection connection = this._dbConnectionFactory.Create(this.Options.DbProvider, this._connectionString); connection.StateChange += this.ConnectionStateHandler; await connection.OpenAsync(cancellationToken); return connection; diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs index 12e46fb..7a9315f 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -76,7 +76,7 @@ private IEnumerable> Create() IDbCommandFactory dbCommandFactory = this._serviceProvider.GetRequiredService(); IDbDataAdapterFactory dbDataAdapterFactory = this._serviceProvider.GetRequiredService(); - this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.Provider, maskPassword: true)); + this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.DbProvider, maskPassword: true)); yield return new DbConnectionService( logger, From d30b75cc5e67dd24084f2ffe39f16d8b2bcc7343 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 2 Apr 2025 21:14:06 +0200 Subject: [PATCH 21/24] Split InternalExtensions class into multiple files * implement CreateDbDataAdapter method for DbProvider.Generic (DbCommand) in DbDataAdapterFactory * remove backup project file that was added in error in the test project --- .../Extensions/DbCommandExtensions.cs | 20 +++++++ ...Extensions.cs => DbParameterExtensions.cs} | 34 ++---------- .../Extensions/OptionsExtensions.cs | 52 +++++++++++++++++++ .../Extensions/TypeExtensions.cs | 27 ++++++++++ .../Internals/DbConnectionStringFactory.cs | 52 ++----------------- .../Internals/DbDataAdapterFactory.cs | 4 ++ 6 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 AdvancedSystems.Connector/Extensions/DbCommandExtensions.cs rename AdvancedSystems.Connector/Extensions/{InternalExtensions.cs => DbParameterExtensions.cs} (63%) create mode 100644 AdvancedSystems.Connector/Extensions/OptionsExtensions.cs create mode 100644 AdvancedSystems.Connector/Extensions/TypeExtensions.cs diff --git a/AdvancedSystems.Connector/Extensions/DbCommandExtensions.cs b/AdvancedSystems.Connector/Extensions/DbCommandExtensions.cs new file mode 100644 index 0000000..b8116a6 --- /dev/null +++ b/AdvancedSystems.Connector/Extensions/DbCommandExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Data.Common; + +namespace AdvancedSystems.Connector.Extensions; + +internal static class DbCommandExtensions +{ + internal static DbDataAdapter CreateDbDataAdapter(this DbCommand dbCommand) + { + DbConnection? dbConnection = dbCommand.Connection; + ArgumentNullException.ThrowIfNull(dbConnection, nameof(dbCommand)); + DbProviderFactory? factory = DbProviderFactories.GetFactory(dbConnection); + + DbDataAdapter? adapter = factory?.CreateDataAdapter(); + ArgumentNullException.ThrowIfNull(adapter, nameof(dbCommand)); + adapter.SelectCommand = dbCommand; + + return adapter; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/InternalExtensions.cs b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs similarity index 63% rename from AdvancedSystems.Connector/Extensions/InternalExtensions.cs rename to AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs index 0ee4e77..5aecd2d 100644 --- a/AdvancedSystems.Connector/Extensions/InternalExtensions.cs +++ b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs @@ -9,10 +9,8 @@ namespace AdvancedSystems.Connector.Extensions; -internal static class InternalExtensions +internal static class DbParameterExtensions { - #region IDbParameter - internal static string? FormatValue(this IDbParameter dbParameter) { CultureInfo cultureInfo = CultureInfo.InvariantCulture; @@ -37,7 +35,7 @@ internal static DbParameter DeriveFrom(this IDbParameter dbParameter, DbCommand { return dbCommand switch { - SqlCommand _ => new SqlParameter + SqlCommand => new SqlParameter { ParameterName = dbParameter.ParameterName, DbType = dbParameter.DbType, @@ -46,30 +44,4 @@ internal static DbParameter DeriveFrom(this IDbParameter dbParameter, DbCommand _ => throw new NotImplementedException(), }; } - - #endregion - - #region Type - - internal static DbType DeriveFrom(this Type type) - { - TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); - - return typeCode switch - { - TypeCode.String or TypeCode.Char => DbType.String, - TypeCode.Boolean => DbType.Boolean, - TypeCode.Byte => DbType.Byte, - TypeCode.SByte or TypeCode.Int16 => DbType.Int16, - TypeCode.Int32 => DbType.Int32, - TypeCode.Int64 => DbType.Int64, - TypeCode.Single => DbType.Single, - TypeCode.Double => DbType.Double, - TypeCode.Decimal => DbType.Decimal, - TypeCode.DateTime => DbType.DateTime, - _ => throw new NotImplementedException($"Failed to infer type from {typeCode}."), - }; - } - - #endregion -} \ No newline at end of file +} diff --git a/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs b/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs new file mode 100644 index 0000000..caffcc4 --- /dev/null +++ b/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Data.Common; + +using AdvancedSystems.Connector.Options; + +using Microsoft.Data.SqlClient; + +namespace AdvancedSystems.Connector.Extensions; + +internal static class OptionsExtensions +{ + private const string MASK = "******"; + + internal static string CreateGenericConnectionString(this DbOptions options, bool maskPassword) + { + var builder = new DbConnectionStringBuilder(); + + if (options.ApplicationName is not null) + { + builder.Add("Application Name", options.ApplicationName); + } + + builder.Add("Data Source", options.DataSource ?? throw new ArgumentNullException(nameof(options))); + builder.Add("Initial Catalog", options.InitialCatalog ?? throw new ArgumentNullException(nameof(options))); + builder.Add("Password", options.Password ?? throw new ArgumentNullException(nameof(options))); + builder.Add("User ID", maskPassword ? MASK : options.UserID ?? throw new ArgumentNullException(nameof(options))); + + return builder.ConnectionString; + } + + internal static string CreateMsSqlConnectionString(this MsSqlOptions options, bool maskPassword) + { + var builder = new SqlConnectionStringBuilder + { + ApplicationName = options.ApplicationName, + DataSource = options.DataSource, + InitialCatalog = options.InitialCatalog, + Password = maskPassword ? MASK : options.Password, + UserID = options.UserID, + CommandTimeout = options.CommandTimeout, + ConnectTimeout = options.ConnectTimeout, + Encrypt = options.Encrypt, + IntegratedSecurity = options.IntegratedSecurity, + MaxPoolSize = options.MaxPoolSize, + MinPoolSize = options.MinPoolSize, + Pooling = options.Pooling, + TrustServerCertificate = options.TrustServerCertificate, + }; + + return builder.ToString(); + } +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/TypeExtensions.cs b/AdvancedSystems.Connector/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..4755911 --- /dev/null +++ b/AdvancedSystems.Connector/Extensions/TypeExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Data; + +namespace AdvancedSystems.Connector.Extensions; + +internal static class TypeExtensions +{ + internal static DbType DeriveFrom(this Type type) + { + TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); + + return typeCode switch + { + TypeCode.String or TypeCode.Char => DbType.String, + TypeCode.Boolean => DbType.Boolean, + TypeCode.Byte => DbType.Byte, + TypeCode.SByte or TypeCode.Int16 => DbType.Int16, + TypeCode.Int32 => DbType.Int32, + TypeCode.Int64 => DbType.Int64, + TypeCode.Single => DbType.Single, + TypeCode.Double => DbType.Double, + TypeCode.Decimal => DbType.Decimal, + TypeCode.DateTime => DbType.DateTime, + _ => throw new NotImplementedException($"Failed to infer type from {typeCode}."), + }; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 85fe405..07dea8f 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -1,10 +1,9 @@ using System; -using System.Data.Common; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Extensions; using AdvancedSystems.Connector.Options; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -25,56 +24,11 @@ public string Create(DbProvider provider, bool maskPassword = false) { return provider switch { - DbProvider.Generic => CreateGenericConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), - DbProvider.MsSql => CreateMsSqlConnectionString(this._serviceProvider.GetRequiredService>().Value, maskPassword), + DbProvider.Generic => this._serviceProvider.GetRequiredService>().Value.CreateGenericConnectionString(maskPassword), + DbProvider.MsSql => this._serviceProvider.GetRequiredService>().Value.CreateMsSqlConnectionString(maskPassword), _ => throw new NotImplementedException(), }; } #endregion - - #region Helpers - - private static string CreateGenericConnectionString(DbOptions options, bool maskPassword) - { - var builder = new DbConnectionStringBuilder(); - - if (options.ApplicationName is not null) - { - builder.Add("Application Name", options.ApplicationName); - } - - builder.Add("Data Source", options.DataSource ?? throw new ArgumentNullException(nameof(options))); - builder.Add("Initial Catalog", options.InitialCatalog ?? throw new ArgumentNullException(nameof(options))); - builder.Add("Password", options.Password ?? throw new ArgumentNullException(nameof(options))); - builder.Add("User ID", options.UserID ?? throw new ArgumentNullException(nameof(options))); - - return builder.ConnectionString; - } - - private static string CreateMsSqlConnectionString(MsSqlOptions options, bool maskPassword) - { - var builder = new SqlConnectionStringBuilder - { - // Database Options - ApplicationName = options.ApplicationName, - DataSource = options.DataSource, - InitialCatalog = options.InitialCatalog, - Password = maskPassword ? "******" : options.Password, - UserID = options.UserID, - // Microsoft SQL Server Options - CommandTimeout = options.CommandTimeout, - ConnectTimeout = options.ConnectTimeout, - Encrypt = options.Encrypt, - IntegratedSecurity = options.IntegratedSecurity, - MaxPoolSize = options.MaxPoolSize, - MinPoolSize = options.MinPoolSize, - Pooling = options.Pooling, - TrustServerCertificate = options.TrustServerCertificate, - }; - - return builder.ToString(); - } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index 0e48921..da07874 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -2,9 +2,12 @@ using System.Data.Common; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Extensions; using Microsoft.Data.SqlClient; +using DbCommand = System.Data.Common.DbCommand; + namespace AdvancedSystems.Connector.Internals; public sealed class DbDataAdapterFactory : IDbDataAdapterFactory @@ -16,6 +19,7 @@ public DbDataAdapter Create(DbCommand dbCommand) return dbCommand switch { SqlCommand sqlCommand => new SqlDataAdapter(sqlCommand), + DbCommand => dbCommand.CreateDbDataAdapter(), _ => throw new NotSupportedException() }; } From 9d1343f238e44014854db24ff14facbecbfe1ade Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 26 Apr 2025 21:45:09 +0200 Subject: [PATCH 22/24] Improve exception messages and add DbCommandException to project --- .../IDbConnectionFactory.cs | 8 ++-- .../IDbConnectionStringFactory.cs | 6 +-- .../IDbDataAdapterFactory.cs | 2 +- ...cedSystems - Backup.Connector.Tests.csproj | 29 ------------- .../Exceptions/DbCommandException.cs | 42 +++++++++++++++++++ .../Extensions/DbParameterExtensions.cs | 4 +- .../Extensions/OptionsExtensions.cs | 6 +-- .../Internals/DbCommandFactory.cs | 7 ++-- .../Internals/DbConnectionFactory.cs | 6 +-- .../Internals/DbConnectionStringFactory.cs | 6 +-- .../Internals/DbDataAdapterFactory.cs | 2 +- 11 files changed, 66 insertions(+), 52 deletions(-) delete mode 100644 AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj create mode 100644 AdvancedSystems.Connector/Exceptions/DbCommandException.cs diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs index 972ccfd..cd56eb6 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs @@ -14,7 +14,7 @@ public interface IDbConnectionFactory /// Creates a new instance of based on the /// specified provider. /// - /// + /// /// The database provider for which the connection service should be created. /// /// @@ -24,10 +24,10 @@ public interface IDbConnectionFactory /// An instance of configured for the /// specified provider. /// - /// - /// Thrown when the specified is not supported by the factory. + /// + /// Thrown when the specified is not implemented by the factory yet. /// - DbConnection Create(DbProvider provider, string connectionString); + DbConnection Create(DbProvider dbProvider, string connectionString); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs index cdf71d6..ead061d 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs @@ -12,7 +12,7 @@ public interface IDbConnectionStringFactory /// /// Creates a database connection string based on the specified provider. /// - /// + /// /// A that specifies the type of database to connect to. /// /// @@ -22,9 +22,9 @@ public interface IDbConnectionStringFactory /// A database connection string. /// /// - /// Thrown when the specified is not implemented yet. + /// Thrown when the specified is not implemented yet. /// - string Create(DbProvider provider, bool maskPassword = false); + string Create(DbProvider dbProvider, bool maskPassword = false); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs index 75553b1..2c0b397 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbDataAdapterFactory.cs @@ -21,7 +21,7 @@ public interface IDbDataAdapterFactory /// An instance of that is configured to work with the type of /// provided. /// - /// + /// /// Thrown when the specified provider is not supported by the factory. /// DbDataAdapter Create(DbCommand dbCommand); diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj deleted file mode 100644 index c9b6f41..0000000 --- a/AdvancedSystems.Connector.Tests/AdvancedSystems - Backup.Connector.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - Unit test project for AdvancedSystems.Connector. - AdvancedSystems.Security.Tests - AdvancedSystems.Security.Tests - true - - - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/AdvancedSystems.Connector/Exceptions/DbCommandException.cs b/AdvancedSystems.Connector/Exceptions/DbCommandException.cs new file mode 100644 index 0000000..d8cbc97 --- /dev/null +++ b/AdvancedSystems.Connector/Exceptions/DbCommandException.cs @@ -0,0 +1,42 @@ +using System; + +namespace AdvancedSystems.Connector.Exceptions; + +/// +/// Represents errors that occur during the instantiation or execution of a database command. +/// +public class DbCommandException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public DbCommandException() + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error . + /// + /// + /// The message that describes the error. + /// + public DbCommandException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error and a reference to the + /// exception that is the cause of this exception. + /// + /// + /// The message that describes the error. + /// + /// + /// The exception that is the cause of the current exception. + /// + public DbCommandException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs index 5aecd2d..33b367f 100644 --- a/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs +++ b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs @@ -27,7 +27,7 @@ internal static class DbParameterExtensions DbType.Double => ((double?)dbParameter.Value)?.ToString(cultureInfo), DbType.Decimal => ((decimal?)dbParameter.Value)?.ToString(cultureInfo), DbType.DateTime => ((DateTime?)dbParameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", cultureInfo), - _ => throw new NotImplementedException(), + _ => throw new NotImplementedException($"Failed to infer type from {dbParameter.DbType}."), }; } @@ -44,4 +44,4 @@ internal static DbParameter DeriveFrom(this IDbParameter dbParameter, DbCommand _ => throw new NotImplementedException(), }; } -} +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs b/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs index caffcc4..23f5f76 100644 --- a/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs +++ b/AdvancedSystems.Connector/Extensions/OptionsExtensions.cs @@ -7,11 +7,11 @@ namespace AdvancedSystems.Connector.Extensions; -internal static class OptionsExtensions +public static class OptionsExtensions { private const string MASK = "******"; - internal static string CreateGenericConnectionString(this DbOptions options, bool maskPassword) + public static string CreateGenericConnectionString(this DbOptions options, bool maskPassword) { var builder = new DbConnectionStringBuilder(); @@ -28,7 +28,7 @@ internal static string CreateGenericConnectionString(this DbOptions options, boo return builder.ConnectionString; } - internal static string CreateMsSqlConnectionString(this MsSqlOptions options, bool maskPassword) + public static string CreateMsSqlConnectionString(this MsSqlOptions options, bool maskPassword) { var builder = new SqlConnectionStringBuilder { diff --git a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs index 3438860..e717f73 100644 --- a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs @@ -3,6 +3,7 @@ using System.Data.Common; using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Exceptions; using Microsoft.Data.SqlClient; @@ -18,7 +19,7 @@ public DbCommand Create(DbProvider dbProvider, DbConnection dbConnection, string { DbProvider.Generic => CreateGenericCommand(dbConnection, commandText, commandType), DbProvider.MsSql => CreateSqlCommand(dbConnection, commandText, commandType), - _ => throw new NotImplementedException(), + _ => throw new NotImplementedException($"Operation not supported for the '{dbProvider}' database provider."), }; } @@ -34,7 +35,7 @@ private static DbCommand CreateGenericCommand(DbConnection dbConnection, string return dbCommand; } - private static DbCommand CreateSqlCommand(DbConnection dbConnection, string commandText, CommandType commandType) + private static SqlCommand CreateSqlCommand(DbConnection dbConnection, string commandText, CommandType commandType) { if (dbConnection is SqlConnection sqlConnection) { @@ -44,7 +45,7 @@ private static DbCommand CreateSqlCommand(DbConnection dbConnection, string comm return sqlCommand; } - throw new Exception(); // TODO: Create DbCommandException + throw new DbCommandException(); } #endregion diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index 6373112..f416944 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -11,12 +11,12 @@ public sealed class DbConnectionFactory : IDbConnectionFactory { #region Methods - public DbConnection Create(DbProvider provider, string connectionString) + public DbConnection Create(DbProvider dbProvider, string connectionString) { - return provider switch + return dbProvider switch { DbProvider.MsSql => new SqlConnection(connectionString), - _ => throw new NotSupportedException(Enum.GetName(provider)), + _ => throw new NotImplementedException($"Operation not supported for the '{dbProvider}' database provider."), }; } diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs index 07dea8f..9e1998d 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs @@ -20,13 +20,13 @@ public DbConnectionStringFactory(IServiceProvider serviceProvider) #region Methods - public string Create(DbProvider provider, bool maskPassword = false) + public string Create(DbProvider dbProvider, bool maskPassword = false) { - return provider switch + return dbProvider switch { DbProvider.Generic => this._serviceProvider.GetRequiredService>().Value.CreateGenericConnectionString(maskPassword), DbProvider.MsSql => this._serviceProvider.GetRequiredService>().Value.CreateMsSqlConnectionString(maskPassword), - _ => throw new NotImplementedException(), + _ => throw new NotImplementedException($"Operation not supported for the '{dbProvider}' database dbProvider."), }; } diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs index da07874..a5e2baf 100644 --- a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs @@ -20,7 +20,7 @@ public DbDataAdapter Create(DbCommand dbCommand) { SqlCommand sqlCommand => new SqlDataAdapter(sqlCommand), DbCommand => dbCommand.CreateDbDataAdapter(), - _ => throw new NotSupportedException() + _ => throw new NotImplementedException() }; } From a09ec018b860ad7fcf5cb1264492701c9fa21fcc Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 26 Apr 2025 22:09:47 +0200 Subject: [PATCH 23/24] Move DbCommandException to Abstraction project, improve error handling and add TODO note --- .../Exceptions/DbCommandException.cs | 2 +- .../IDbConnectionService.cs | 8 +-- .../Services/DbConnectionServiceTests.cs | 23 +++++-- .../Internals/DbCommandFactory.cs | 2 +- .../Services/DbConnectionService.cs | 61 ++++++++++++++----- 5 files changed, 68 insertions(+), 28 deletions(-) rename {AdvancedSystems.Connector => AdvancedSystems.Connector.Abstractions}/Exceptions/DbCommandException.cs (95%) diff --git a/AdvancedSystems.Connector/Exceptions/DbCommandException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandException.cs similarity index 95% rename from AdvancedSystems.Connector/Exceptions/DbCommandException.cs rename to AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandException.cs index d8cbc97..095c756 100644 --- a/AdvancedSystems.Connector/Exceptions/DbCommandException.cs +++ b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandException.cs @@ -1,6 +1,6 @@ using System; -namespace AdvancedSystems.Connector.Exceptions; +namespace AdvancedSystems.Connector.Abstractions.Exceptions; /// /// Represents errors that occur during the instantiation or execution of a database command. diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs index 8dcafa9..e946ec6 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs @@ -44,7 +44,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// A containing the results of the executed query. The /// may include one or more objects representing the result sets. /// - /// + /// /// Thrown when an error occurs during the execution of the SQL command, such as a syntax error /// or execution failure. /// @@ -68,7 +68,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// A representing the asynchronous operation. The /// contains the results of the executed query. It may be null if no results are returned or if an error occurs. /// - /// + /// /// Thrown when an error occurs during the execution of the SQL command, such as a syntax error /// or execution failure. /// @@ -89,7 +89,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// The number of rows affected by the command. This number indicates how many rows were impacted by /// the non-query operation. /// - /// + /// /// Thrown when an error occurs during the execution of the SQL command, such as a syntax error /// or execution failure. /// @@ -114,7 +114,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// of rows affected by the command. This indicates how many rows were impacted by the non-query /// operation. /// - /// + /// /// Thrown when an error occurs during the execution of the SQL command, such as a syntax error /// or execution failure. /// diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs index f68a9bc..e01576a 100644 --- a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -1,9 +1,9 @@ using System.ComponentModel; using System.Data; using System.Data.Common; +using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Common; using AdvancedSystems.Connector.Options; using AdvancedSystems.Connector.Tests.Fixtures; @@ -23,9 +23,6 @@ public sealed class DbConnectionServiceTests : IClassFixture sut) { this._sut = sut; - - //this._sut.Options.Setup(x => x.Value) - // .Returns(this._sut.Options.Object.Value with { DbProvider = DbProvider.MsSql }); } #region Tests @@ -52,12 +49,26 @@ public void TestExecuteQuery() Assert.NotNull(response); } - [Fact(Skip = "TODO")] - public void ExecuteQueryAsync() + [Fact(Skip = "WIP")] + public async Task ExecuteQueryAsync() { // Arrange + var dbCommand = new DbCommand + { + CommandText = "SELECT @value", + CommandType = CommandType.Text, + }; + + dbCommand.AddParameter("@value", 1); + + this._sut.DbCommandFactory.Setup(x => x.Create(It.IsAny(), It.IsAny(), dbCommand.CommandText, dbCommand.CommandType)) + .Returns(new SqlCommand { CommandText = dbCommand.CommandText, CommandType = dbCommand.CommandType }); + // Act + DataSet? response = await this._sut.DbConnectionService.ExecuteQueryAsync(dbCommand); + // Assert + Assert.NotNull(response); } [Fact(Skip = "TODO")] diff --git a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs index e717f73..6fc1f51 100644 --- a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs @@ -3,7 +3,7 @@ using System.Data.Common; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Exceptions; +using AdvancedSystems.Connector.Abstractions.Exceptions; using Microsoft.Data.SqlClient; diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 84915e4..7dd9f91 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -68,10 +68,18 @@ public DataSet ExecuteQuery(IDbCommand dbCommand) } DataSet result = new(); - using DbDataAdapter adapter = this._dataAdapterFactory.Create(command); - adapter.Fill(result); - return result; + try + { + using DbDataAdapter adapter = this._dataAdapterFactory.Create(command); + adapter.Fill(result); + + return result; + } + catch (DbException exception) + { + throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); + } } public async ValueTask ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default) @@ -92,10 +100,19 @@ public DataSet ExecuteQuery(IDbCommand dbCommand) } DataTable result = new(); - using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); - result.Load(reader); - return result.DataSet; + try + { + // TODO: mock this dbDataReader? + using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); + result.Load(reader); + + return result.DataSet; + } + catch (DbException exception) + { + throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); + } } public int ExecuteNonQuery(IDbCommand dbCommand) @@ -115,28 +132,40 @@ public int ExecuteNonQuery(IDbCommand dbCommand) command.Parameters.Add(parameter); } - int rowsAffected = command.ExecuteNonQuery(); - - return rowsAffected; + try + { + int rowsAffected = command.ExecuteNonQuery(); + return rowsAffected; + } + catch (DbException exception) + { + throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); + } } - public async ValueTask ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default) + public async ValueTask ExecuteNonQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default) { using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); using DbCommand command = connection.CreateCommand(); - command.CommandText = databaseCommand.CommandText; - command.CommandType = databaseCommand.CommandType; + command.CommandText = dbCommand.CommandText; + command.CommandType = dbCommand.CommandType; - foreach (IDbParameter dbParameter in databaseCommand.Parameters) + foreach (IDbParameter dbParameter in dbCommand.Parameters) { DbParameter parameter = dbParameter.DeriveFrom(command); command.Parameters.Add(parameter); } - int rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); - - return rowsAffected; + try + { + int rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); + return rowsAffected; + } + catch (DbException exception) + { + throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); + } } #endregion From 8d3b66e5fbb9fccb6e70ba9c9577bfe937f70def Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 21 May 2025 22:25:34 +0200 Subject: [PATCH 24/24] Extract DbConnection from DbConnectionService (idea) --- .../IDbCommand.cs | 41 ---- .../IDbCommandFactory.cs | 9 - .../IDbConnection.cs | 40 ++++ .../IDbConnectionService.cs | 17 +- .../IDbConnectionServiceFactory.cs | 2 +- .../IDbParameter.cs | 16 -- .../ServiceCollectionExtensionsTests.cs | 8 +- .../Fixtures/DbConnectionServiceFixture.cs | 17 +- .../Services/DbConnectionServiceTests.cs | 45 +--- AdvancedSystems.Connector/Common/DbCommand.cs | 74 ------ .../Common/DbParameter.cs | 18 -- .../ServiceCollectionExtensions.cs | 3 +- .../Extensions/DbParameterExtensions.cs | 47 ---- .../Extensions/TypeExtensions.cs | 27 --- .../Internals/DbCommandFactory.cs | 52 ----- .../Internals/DbConnection.cs | 218 ++++++++++++++++++ .../Internals/DbConnectionFactory.cs | 2 +- .../Services/DbConnectionService.cs | 173 +------------- .../Services/DbConnectionServiceFactory.cs | 44 +--- 19 files changed, 293 insertions(+), 560 deletions(-) delete mode 100644 AdvancedSystems.Connector.Abstractions/IDbCommand.cs delete mode 100644 AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs create mode 100644 AdvancedSystems.Connector.Abstractions/IDbConnection.cs delete mode 100644 AdvancedSystems.Connector.Abstractions/IDbParameter.cs delete mode 100644 AdvancedSystems.Connector/Common/DbCommand.cs delete mode 100644 AdvancedSystems.Connector/Common/DbParameter.cs delete mode 100644 AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs delete mode 100644 AdvancedSystems.Connector/Extensions/TypeExtensions.cs delete mode 100644 AdvancedSystems.Connector/Internals/DbCommandFactory.cs create mode 100644 AdvancedSystems.Connector/Internals/DbConnection.cs diff --git a/AdvancedSystems.Connector.Abstractions/IDbCommand.cs b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs deleted file mode 100644 index 641443d..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDbCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using System.Data; - -namespace AdvancedSystems.Connector.Abstractions; - -/// -/// Defines an SQL statement or stored procedure to execute against a data source. -/// -public interface IDbCommand -{ - #region Properties - - /// - /// Gets or sets the Transact-SQL statement or stored - /// procedure to execute at the data source. - /// - string CommandText { get; set; } - - /// - /// Gets or sets a value indicating how the - /// property is to be interpreted. - /// - CommandType CommandType { get; set; } - - /// - /// Gets a collection of parameters. - /// - IReadOnlyList Parameters { get; } - - #endregion - - #region Methods - - void AddParameter(string parameterName, object value, DbType type); - - void AddParameter(string parameterName, T value); - - string ToString(); - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs deleted file mode 100644 index a834c1b..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDbCommandFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Data; -using System.Data.Common; - -namespace AdvancedSystems.Connector.Abstractions; - -public interface IDbCommandFactory -{ - DbCommand Create(DbProvider dbProvider, DbConnection dbConnection, string commandText, CommandType commandType); -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnection.cs b/AdvancedSystems.Connector.Abstractions/IDbConnection.cs new file mode 100644 index 0000000..04751b7 --- /dev/null +++ b/AdvancedSystems.Connector.Abstractions/IDbConnection.cs @@ -0,0 +1,40 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace AdvancedSystems.Connector.Abstractions; + +public interface IDbConnection : IDisposable where T : class, IDbSettings, new() +{ + #region Properties + + T Options { get; } + + string ConnectionString { get; } + + /// + /// + /// + /// + /// The values in this enumeration are not designed to be used as a set of flags. + /// + ConnectionState ConnectionState { get; } + + #endregion + + #region Methods + + string ToString(); + + DataSet ExecuteQuery(DbCommand dbCommand); + + ValueTask ExecuteQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default); + + int ExecuteNonQuery(DbCommand dbCommand); + + ValueTask ExecuteNonQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs index e946ec6..fc241a7 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -34,7 +35,7 @@ namespace AdvancedSystems.Connector.Abstractions; #region Methods /// - /// Executes a query defined by the specified synchronously. + /// Executes a query defined by the specified synchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -52,10 +53,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - DataSet ExecuteQuery(IDbCommand dbCommand); + DataSet ExecuteQuery(DbCommand dbCommand); /// - /// Executes a query defined by the specified asynchronously. + /// Executes a query defined by the specified asynchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -76,10 +77,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default); /// - /// Executes a non-query command defined by the specified synchronously. + /// Executes a non-query command defined by the specified synchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -97,10 +98,10 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - int ExecuteNonQuery(IDbCommand dbCommand); + int ExecuteNonQuery(DbCommand dbCommand); /// - /// Executes a non-query command defined by the specified asynchronously. + /// Executes a non-query command defined by the specified asynchronously. /// /// /// The command that defines the query to be executed. It must contain the SQL command @@ -122,7 +123,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// Thrown when there is a failure in communicating with the database, such as a connection /// issue or timeout. /// - ValueTask ExecuteNonQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default); + ValueTask ExecuteNonQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs index 97e8286..1ea3898 100644 --- a/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs @@ -13,7 +13,7 @@ namespace AdvancedSystems.Connector.Abstractions; /// /// The connection options. /// -public interface IDbConnectionServiceFactory : IDisposable where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() +public interface IDbConnectionServiceFactory where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() { #region Properties diff --git a/AdvancedSystems.Connector.Abstractions/IDbParameter.cs b/AdvancedSystems.Connector.Abstractions/IDbParameter.cs deleted file mode 100644 index 5d8704f..0000000 --- a/AdvancedSystems.Connector.Abstractions/IDbParameter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Data; - -namespace AdvancedSystems.Connector.Abstractions; - -public interface IDbParameter -{ - #region Properties - - string ParameterName { get; set; } - - DbType DbType { get; set; } - - object? Value { get; set; } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 4ad591c..86b61e6 100644 --- a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -59,7 +59,7 @@ public ServiceCollectionExtensionsTests() /// dependency injection from . /// [Fact] - public async Task TestAddDbConnectionService_FromOptions() + public async Task AddDbConnectionService_Test_FromOptions() { // Arrange using IHost? host = await new HostBuilder() @@ -112,7 +112,7 @@ public async Task TestAddDbConnectionService_FromOptions() /// dependency injection from appsettings.json. /// [Fact] - public async Task TestAddDbConnectionService_FromAppSettings() + public async Task AddDbConnectionService_Test_FromAppSettings() { // Arrange var appSettings = new Dictionary() @@ -175,7 +175,7 @@ public async Task TestAddDbConnectionService_FromAppSettings() /// dependency injection from and a collection of . /// [Fact(Skip = "TODO")] - public void TestAddDbConnectionServiceFactory_FromOptions() + public void AddDbConnectionServiceFactory_Test_FromOptions() { // Arrange // Act @@ -187,7 +187,7 @@ public void TestAddDbConnectionServiceFactory_FromOptions() /// dependency injection from appsettings.json. /// [Fact(Skip = "TODO")] - public void TestAddDbConnectionServiceFactory_FromAppSettings() + public void AddDbConnectionServiceFactory_Test_FromAppSettings() { // Arrange // Act diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs index fe42a9e..05fd78d 100644 --- a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs +++ b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs @@ -23,7 +23,6 @@ namespace AdvancedSystems.Connector.Tests.Fixtures; private readonly Mock> _options = new(); private readonly Mock _dbConnectionFactory = new(); private readonly Mock _dbConnectionStringFactory = new(); - private readonly Mock _dbCommandFactory = new(); private readonly Mock _dbDataAdapterFactory = new(); public DbConnectionServiceFixture() @@ -50,21 +49,10 @@ public DbConnectionServiceFixture() handler(this._dbConnection.Object, new StateChangeEventArgs(ConnectionState.Closed, ConnectionState.Open)); }); - //this._dbConnection.Setup(x => x.Open()); - - // mock DbCommand.Create - this.DbDataAdapterFactory.Setup(x => x.Create(It.IsAny())) .Returns(this._dbDataAdapter.Object); - this.DbConnectionService = new DbConnectionService( - this.Logger.Object, - this.Options.Object, - this.DbConnectionStringFactory.Object, - this.DbConnectionFactory.Object, - this.DbCommandFactory.Object, - this.DbDataAdapterFactory.Object - ); + this.DbConnectionService = new DbConnectionService(this.Logger.Object, this.Options.Object); } #region Properties @@ -77,8 +65,6 @@ public DbConnectionServiceFixture() public Mock DbConnectionStringFactory => this._dbConnectionStringFactory; - public Mock DbCommandFactory => this._dbCommandFactory; - public Mock DbDataAdapterFactory => this._dbDataAdapterFactory; public IDbConnectionService DbConnectionService { get; set; } @@ -93,7 +79,6 @@ public void ClearInvocations() this.Options.Invocations.Clear(); this.DbConnectionFactory.Invocations.Clear(); this.DbConnectionStringFactory.Invocations.Clear(); - this.DbDataAdapterFactory.Invocations.Clear(); } #endregion diff --git a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs index e01576a..c861658 100644 --- a/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs +++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs @@ -1,9 +1,8 @@ using System.ComponentModel; using System.Data; -using System.Data.Common; +using System.Threading; using System.Threading.Tasks; -using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Options; using AdvancedSystems.Connector.Tests.Fixtures; @@ -11,8 +10,6 @@ using Moq; -using DbCommand = AdvancedSystems.Connector.Common.DbCommand; - namespace AdvancedSystems.Security.Tests.Services; [Category(TestCategory.UNIT)] @@ -27,52 +24,24 @@ public DbConnectionServiceTests(DbConnectionServiceFixture sut) #region Tests - [Fact] - public void TestExecuteQuery() + [Fact(Skip = "TODO")] + public void ExecuteQuery_Test() { // Arrange - var dbCommand = new DbCommand - { - CommandText = "SELECT @value", - CommandType = CommandType.Text, - }; - - dbCommand.AddParameter("@value", 1); - - this._sut.DbCommandFactory.Setup(x => x.Create(It.IsAny(), It.IsAny(), dbCommand.CommandText, dbCommand.CommandType)) - .Returns(new SqlCommand { CommandText = dbCommand.CommandText, CommandType = dbCommand.CommandType }); - // Act - DataSet response = this._sut.DbConnectionService.ExecuteQuery(dbCommand); - // Assert - Assert.NotNull(response); } - [Fact(Skip = "WIP")] - public async Task ExecuteQueryAsync() + [Fact(Skip = "TODO")] + public void ExecuteQueryAsync_Test() { // Arrange - var dbCommand = new DbCommand - { - CommandText = "SELECT @value", - CommandType = CommandType.Text, - }; - - dbCommand.AddParameter("@value", 1); - - this._sut.DbCommandFactory.Setup(x => x.Create(It.IsAny(), It.IsAny(), dbCommand.CommandText, dbCommand.CommandType)) - .Returns(new SqlCommand { CommandText = dbCommand.CommandText, CommandType = dbCommand.CommandType }); - // Act - DataSet? response = await this._sut.DbConnectionService.ExecuteQueryAsync(dbCommand); - // Assert - Assert.NotNull(response); } [Fact(Skip = "TODO")] - public void TestExecuteNonQuery() + public void ExecuteNonQuery_Test() { // Arrange // Act @@ -80,7 +49,7 @@ public void TestExecuteNonQuery() } [Fact(Skip = "TODO")] - public void ExecuteNonQueryAsync() + public void ExecuteNonQueryAsync_Test() { // Arrange // Act diff --git a/AdvancedSystems.Connector/Common/DbCommand.cs b/AdvancedSystems.Connector/Common/DbCommand.cs deleted file mode 100644 index e08fa6b..0000000 --- a/AdvancedSystems.Connector/Common/DbCommand.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Diagnostics; -using System.Text; - -using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Extensions; - -using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; - -namespace AdvancedSystems.Connector.Common; - -[DebuggerDisplay("{CommandText}")] -public sealed class DbCommand : IDbCommand -{ - private readonly List _parameters = []; - - #region Properties - - [DisplayName("Command Text")] - public required string CommandText { get; set; } - - [DisplayName("Command Type")] - public required CommandType CommandType { get; set; } - - [DisplayName("Parameters")] - public IReadOnlyList Parameters => this._parameters; - - #endregion - - #region Methods - - public void AddParameter(string parameterName, object value, DbType type) - { - var databaseParameter = new DbParameter - { - ParameterName = parameterName, - DbType = type, - Value = value - }; - - this._parameters.Add(databaseParameter); - } - - public void AddParameter(string parameterName, T value) - { - var databaseParameter = new DbParameter - { - ParameterName = parameterName, - DbType = typeof(T).DeriveFrom(), - Value = (object?)value ?? DBNull.Value, - }; - - this._parameters.Add(databaseParameter); - } - - public override string ToString() - { - if (this.Parameters.Count == 0) return this.CommandText; - - var commandBuilder = new StringBuilder(this.CommandText); - - foreach (IDbParameter parameter in this.Parameters) - { - commandBuilder.Replace(parameter.ParameterName, parameter.FormatValue()); - } - - return commandBuilder.ToString(); - } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Common/DbParameter.cs b/AdvancedSystems.Connector/Common/DbParameter.cs deleted file mode 100644 index a23315a..0000000 --- a/AdvancedSystems.Connector/Common/DbParameter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Data; - -using AdvancedSystems.Connector.Abstractions; - -namespace AdvancedSystems.Connector.Common; - -public sealed class DbParameter : IDbParameter -{ - #region Properties - - public required string ParameterName { get; set; } - - public required DbType DbType { get; set; } - - public required object? Value { get; set; } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs index 5b8aec5..d7de86b 100644 --- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs @@ -29,9 +29,8 @@ public static class ServiceCollectionExtensions { services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService), typeof(DbConnectionService))); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs deleted file mode 100644 index 33b367f..0000000 --- a/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Globalization; - -using AdvancedSystems.Connector.Abstractions; - -using Microsoft.Data.SqlClient; - -namespace AdvancedSystems.Connector.Extensions; - -internal static class DbParameterExtensions -{ - internal static string? FormatValue(this IDbParameter dbParameter) - { - CultureInfo cultureInfo = CultureInfo.InvariantCulture; - - return dbParameter.DbType switch - { - DbType.String => dbParameter.Value?.ToString(), - DbType.Boolean => ((bool?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Byte => ((byte?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Int16 => ((short?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Int32 => ((int?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Int64 => ((long?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Single => ((float?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Double => ((double?)dbParameter.Value)?.ToString(cultureInfo), - DbType.Decimal => ((decimal?)dbParameter.Value)?.ToString(cultureInfo), - DbType.DateTime => ((DateTime?)dbParameter.Value)?.ToString("yyyy-MM-ddTHH:mm:ss.fff", cultureInfo), - _ => throw new NotImplementedException($"Failed to infer type from {dbParameter.DbType}."), - }; - } - - internal static DbParameter DeriveFrom(this IDbParameter dbParameter, DbCommand dbCommand) - { - return dbCommand switch - { - SqlCommand => new SqlParameter - { - ParameterName = dbParameter.ParameterName, - DbType = dbParameter.DbType, - Value = dbParameter.Value, - }, - _ => throw new NotImplementedException(), - }; - } -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Extensions/TypeExtensions.cs b/AdvancedSystems.Connector/Extensions/TypeExtensions.cs deleted file mode 100644 index 4755911..0000000 --- a/AdvancedSystems.Connector/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Data; - -namespace AdvancedSystems.Connector.Extensions; - -internal static class TypeExtensions -{ - internal static DbType DeriveFrom(this Type type) - { - TypeCode typeCode = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type); - - return typeCode switch - { - TypeCode.String or TypeCode.Char => DbType.String, - TypeCode.Boolean => DbType.Boolean, - TypeCode.Byte => DbType.Byte, - TypeCode.SByte or TypeCode.Int16 => DbType.Int16, - TypeCode.Int32 => DbType.Int32, - TypeCode.Int64 => DbType.Int64, - TypeCode.Single => DbType.Single, - TypeCode.Double => DbType.Double, - TypeCode.Decimal => DbType.Decimal, - TypeCode.DateTime => DbType.DateTime, - _ => throw new NotImplementedException($"Failed to infer type from {typeCode}."), - }; - } -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs b/AdvancedSystems.Connector/Internals/DbCommandFactory.cs deleted file mode 100644 index 6fc1f51..0000000 --- a/AdvancedSystems.Connector/Internals/DbCommandFactory.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; - -using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Abstractions.Exceptions; - -using Microsoft.Data.SqlClient; - -namespace AdvancedSystems.Connector.Internals; - -public sealed class DbCommandFactory : IDbCommandFactory -{ - #region Methods - - public DbCommand Create(DbProvider dbProvider, DbConnection dbConnection, string commandText, CommandType commandType) - { - return dbProvider switch - { - DbProvider.Generic => CreateGenericCommand(dbConnection, commandText, commandType), - DbProvider.MsSql => CreateSqlCommand(dbConnection, commandText, commandType), - _ => throw new NotImplementedException($"Operation not supported for the '{dbProvider}' database provider."), - }; - } - - #endregion - - #region Helpers - - private static DbCommand CreateGenericCommand(DbConnection dbConnection, string commandText, CommandType commandType) - { - DbCommand dbCommand = dbConnection.CreateCommand(); - dbCommand.CommandText = commandText; - dbCommand.CommandType = commandType; - return dbCommand; - } - - private static SqlCommand CreateSqlCommand(DbConnection dbConnection, string commandText, CommandType commandType) - { - if (dbConnection is SqlConnection sqlConnection) - { - SqlCommand sqlCommand = sqlConnection.CreateCommand(); - sqlCommand.CommandText = commandText; - sqlCommand.CommandType = commandType; - return sqlCommand; - } - - throw new DbCommandException(); - } - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnection.cs b/AdvancedSystems.Connector/Internals/DbConnection.cs new file mode 100644 index 0000000..62688de --- /dev/null +++ b/AdvancedSystems.Connector/Internals/DbConnection.cs @@ -0,0 +1,218 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AdvancedSystems.Connector.Abstractions; +using AdvancedSystems.Connector.Abstractions.Exceptions; + +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using DbConnectionInternal = System.Data.Common.DbConnection; + +namespace AdvancedSystems.Connector.Internals; + +public sealed class DbConnection : IDbConnection where T : class, IDbSettings, new() +{ + private readonly ILogger _logger; + private readonly IDbConnectionStringFactory _dbConnectionStringFactory; + private readonly IDbConnectionFactory _dbConnectionFactory; + private readonly IDbDataAdapterFactory _dbDataAdapterFactory; + + private bool _isDisposed = false; + private string? _connectionString; + private readonly DbConnectionInternal _dbConnection; + + public DbConnection(ILogger logger, IOptions options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbDataAdapterFactory dbDataAdapterFactory) + { + this._logger = logger; + this.Options = options.Value; + this._dbConnectionStringFactory = dbConnectionStringFactory; + this._dbConnectionFactory = dbConnectionFactory; + this._dbDataAdapterFactory = dbDataAdapterFactory; + + try + { + string connectionString = this._dbConnectionStringFactory.Create(this.Options.DbProvider, maskPassword: false); + this._dbConnection = this._dbConnectionFactory.Create(this.Options.DbProvider, connectionString); + } + catch (DbException exception) + { + throw new DbConnectionException(exception.Message, exception); + } + } + + public static DbConnection Create(T options, ILogger? logger = null) + { + throw new NotImplementedException(); + } + + public static ValueTask CreateAsync(T options, ILogger? logger = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + #region Properties + + public T Options { get; private set; } + + public string ConnectionString + { + get + { + if (this._connectionString is null) + { + this._connectionString = this._dbConnectionStringFactory.Create(this.Options.DbProvider, maskPassword: true); + } + + return this._connectionString; + } + } + + public ConnectionState ConnectionState { get; private set; } + + #endregion + + #region Methods + + public void Dispose(bool disposing) + { + this._isDisposed = this._dbConnection is null; + + if (this._isDisposed || !disposing) return; + + try + { + // https://learn.microsoft.com/en-us/dotnet/api/microsoft.data.sqlclient.sqlconnection.close#remarks + this._dbConnection?.Close(); + } + catch (SqlException exception) + { + this._logger?.LogError(exception.Message); + this.ConnectionState = ConnectionState.Broken; + } + finally + { + this._isDisposed = true; + this.ConnectionState = ConnectionState.Closed; + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + string IDbConnection.ToString() + { + return this.ToString(); + } + + /// + public override string ToString() + { + return this.ConnectionString; + } + + public DataSet ExecuteQuery(DbCommand dbCommand) + { + DataSet result = new(); + + this.ExecutionHandler((cmd) => + { + using DbDataAdapter adapter = this._dbDataAdapterFactory.Create(cmd); + adapter.Fill(result); + }, dbCommand); + + return result; + } + + public async ValueTask ExecuteQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default) + { + DataTable result = new(); + + await this.ExecutionHandlerAsync(async (cmd) => + { + using DbDataReader reader = await cmd.ExecuteReaderAsync(cancellationToken); + result.Load(reader); + }, dbCommand, cancellationToken); + + return result.DataSet; + } + + public int ExecuteNonQuery(DbCommand dbCommand) + { + int rowsAffected = -1; + + this.ExecutionHandler((cmd) => rowsAffected = cmd.ExecuteNonQuery(), dbCommand); + + return rowsAffected; + } + + public async ValueTask ExecuteNonQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default) + { + int rowsAffected = -1; + + await this.ExecutionHandlerAsync(async (cmd) => rowsAffected = await cmd.ExecuteNonQueryAsync(cancellationToken), dbCommand, cancellationToken); + + return rowsAffected; + } + + #endregion + + #region Helpers + + private void OpenConnection() + { + this._logger.LogTrace("Opening connection to database"); + this._dbConnection.Open(); + this.ConnectionState = ConnectionState.Open; + } + + private async ValueTask OpenConnectionAsync(CancellationToken cancellationToken) + { + this._logger.LogTrace("Closing connection to database"); + await this._dbConnection.OpenAsync(cancellationToken); + this.ConnectionState = ConnectionState.Open; + } + + private void ExecutionHandler(Action action, DbCommand dbCommand) + { + try + { + this.OpenConnection(); + dbCommand.Connection = this._dbConnection; + this.ConnectionState = ConnectionState.Executing; + action(dbCommand); + this.ConnectionState = ConnectionState.Open; + } + catch (DbException exception) + { + this.ConnectionState = ConnectionState.Broken; + throw new DbConnectionException(exception.Message, exception); + } + } + + private async ValueTask ExecutionHandlerAsync(Func action, DbCommand dbCommand, CancellationToken cancellationToken) + { + try + { + await this.OpenConnectionAsync(cancellationToken); + this.ConnectionState = ConnectionState.Executing; + await action(dbCommand); + this.ConnectionState = ConnectionState.Open; + } + catch (DbException exception) + { + this.ConnectionState = ConnectionState.Broken; + throw new DbConnectionException(exception.Message, exception); + } + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs index f416944..5ffc781 100644 --- a/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs +++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs @@ -11,7 +11,7 @@ public sealed class DbConnectionFactory : IDbConnectionFactory { #region Methods - public DbConnection Create(DbProvider dbProvider, string connectionString) + public System.Data.Common.DbConnection Create(DbProvider dbProvider, string connectionString) { return dbProvider switch { diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs index 7dd9f91..1715328 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionService.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs @@ -5,39 +5,21 @@ using System.Threading.Tasks; using AdvancedSystems.Connector.Abstractions; -using AdvancedSystems.Connector.Abstractions.Exceptions; -using AdvancedSystems.Connector.Extensions; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using DbCommand = System.Data.Common.DbCommand; -using DbParameter = System.Data.Common.DbParameter; -using IDbCommand = AdvancedSystems.Connector.Abstractions.IDbCommand; - namespace AdvancedSystems.Connector.Services; /// public sealed class DbConnectionService : IDbConnectionService where T : class, IDbSettings, new() { private readonly ILogger> _logger; - private readonly IDbConnectionStringFactory _dbConnectionStringFactory; - private readonly IDbConnectionFactory _dbConnectionFactory; - private readonly IDbCommandFactory _dbCommandFactory; - private readonly IDbDataAdapterFactory _dataAdapterFactory; - private readonly string _connectionString; - public DbConnectionService(ILogger> logger, IOptions options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbCommandFactory dbCommandFactory, IDbDataAdapterFactory dbDataAdapterFactory) + public DbConnectionService(ILogger> logger, IOptions options) { this._logger = logger; this.Options = options.Value; - this._dbConnectionStringFactory = dbConnectionStringFactory; - this._dbConnectionFactory = dbConnectionFactory; - this._dbCommandFactory = dbCommandFactory; - this._dataAdapterFactory = dbDataAdapterFactory; - - this._connectionString = this._dbConnectionStringFactory.Create(this.Options.DbProvider, maskPassword: false); } #region Properties @@ -50,161 +32,24 @@ public DbConnectionService(ILogger> logger, IOptions o #region Methods - public DataSet ExecuteQuery(IDbCommand dbCommand) - { - using DbConnection dbConnection = this.InitializeConnection(); - - using DbCommand command = this._dbCommandFactory.Create( - this.Options.DbProvider, - dbConnection, - dbCommand.CommandText, - dbCommand.CommandType - ); - - foreach (IDbParameter dbParameter in dbCommand.Parameters) - { - DbParameter parameter = dbParameter.DeriveFrom(command); - command.Parameters.Add(parameter); - } - - DataSet result = new(); - - try - { - using DbDataAdapter adapter = this._dataAdapterFactory.Create(command); - adapter.Fill(result); - - return result; - } - catch (DbException exception) - { - throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); - } - } - - public async ValueTask ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default) - { - using DbConnection dbConnection = await this.InitializeConnectionAsync(cancellationToken); - - using DbCommand command = this._dbCommandFactory.Create( - this.Options.DbProvider, - dbConnection, - dbCommand.CommandText, - dbCommand.CommandType - ); - - foreach (IDbParameter dbParameter in dbCommand.Parameters) - { - DbParameter parameter = dbParameter.DeriveFrom(command); - command.Parameters.Add(parameter); - } - - DataTable result = new(); - - try - { - // TODO: mock this dbDataReader? - using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); - result.Load(reader); - - return result.DataSet; - } - catch (DbException exception) - { - throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); - } - } - - public int ExecuteNonQuery(IDbCommand dbCommand) + public DataSet ExecuteQuery(DbCommand dbCommand) { - using DbConnection dbConnection = this.InitializeConnection(); - - using DbCommand command = this._dbCommandFactory.Create( - this.Options.DbProvider, - dbConnection, - dbCommand.CommandText, - dbCommand.CommandType - ); - - foreach (IDbParameter dbParameter in dbCommand.Parameters) - { - DbParameter parameter = dbParameter.DeriveFrom(command); - command.Parameters.Add(parameter); - } - - try - { - int rowsAffected = command.ExecuteNonQuery(); - return rowsAffected; - } - catch (DbException exception) - { - throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); - } + throw new NotImplementedException(); } - public async ValueTask ExecuteNonQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default) - { - using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken); - - using DbCommand command = connection.CreateCommand(); - command.CommandText = dbCommand.CommandText; - command.CommandType = dbCommand.CommandType; - - foreach (IDbParameter dbParameter in dbCommand.Parameters) - { - DbParameter parameter = dbParameter.DeriveFrom(command); - command.Parameters.Add(parameter); - } - - try - { - int rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); - return rowsAffected; - } - catch (DbException exception) - { - throw new DbCommandException($"An error occured during the execution of the SQL command: {dbCommand}", exception); - } - } - - #endregion - - #region Helpers - - private void ConnectionStateHandler(object sender, StateChangeEventArgs e) + public ValueTask ExecuteQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default) { - this.ConnectionState = e.CurrentState; + throw new NotImplementedException(); } - private DbConnection InitializeConnection() + public int ExecuteNonQuery(DbCommand dbCommand) { - try - { - DbConnection connection = this._dbConnectionFactory.Create(this.Options.DbProvider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - connection.Open(); - return connection; - } - catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) - { - throw new DbConnectionException($"Failed to initialize dbConnection to database: {exception.Message}", exception); - } + throw new NotImplementedException(); } - private async ValueTask InitializeConnectionAsync(CancellationToken cancellationToken) + public ValueTask ExecuteNonQueryAsync(DbCommand dbCommand, CancellationToken cancellationToken = default) { - try - { - DbConnection connection = this._dbConnectionFactory.Create(this.Options.DbProvider, this._connectionString); - connection.StateChange += this.ConnectionStateHandler; - await connection.OpenAsync(cancellationToken); - return connection; - } - catch (Exception exception) when (exception is SqlException or DbException or InvalidOperationException) - { - throw new DbConnectionException($"Failed to initialize dbConnection to database: {exception.Message}", exception); - } + throw new NotImplementedException(); } #endregion diff --git a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs index 7a9315f..54fc4b1 100644 --- a/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs +++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs @@ -5,7 +5,6 @@ using AdvancedSystems.Connector.Abstractions; using AdvancedSystems.Connector.Abstractions.Exceptions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -14,19 +13,15 @@ namespace AdvancedSystems.Connector.Services; /// public sealed class DbConnectionServiceFactory : IDbConnectionServiceFactory where T : class, IDbFactorySettings, new() where U : class, IDbSettings, new() { - private bool _isDisposed = false; - private readonly ILogger> _logger; private readonly List _dbConnectionOptions; private readonly ILoggerFactory _loggerFactory; - private readonly IServiceProvider _serviceProvider; private readonly List> _dbConnectionServices; - public DbConnectionServiceFactory(ILogger> logger, ILoggerFactory loggerFactory, IOptions factoryOptions, IServiceProvider serviceProvider) + public DbConnectionServiceFactory(ILogger> logger, ILoggerFactory loggerFactory, IOptions factoryOptions) { this._logger = logger; this._loggerFactory = loggerFactory; - this._serviceProvider = serviceProvider; this.Options = factoryOptions.Value; this._dbConnectionOptions = this.Options.Options; this._dbConnectionServices = [.. this.Create()]; @@ -40,12 +35,6 @@ public DbConnectionServiceFactory(ILogger> logg #region Methods - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - public IDbConnectionService GetConnection(string dbUser) { IDbConnectionService? dbConnectionService = this._dbConnectionServices.FirstOrDefault(x => string.Equals(dbUser, x.Options.UserID, StringComparison.Ordinal)); @@ -71,37 +60,8 @@ private IEnumerable> Create() U dbSettings = dbOption; IOptions connectionOptions = Microsoft.Extensions.Options.Options.Create(dbSettings); - IDbConnectionStringFactory dbConnectionStringFactory = this._serviceProvider.GetRequiredService(); - IDbConnectionFactory dbConnectionFactory = this._serviceProvider.GetRequiredService(); - IDbCommandFactory dbCommandFactory = this._serviceProvider.GetRequiredService(); - IDbDataAdapterFactory dbDataAdapterFactory = this._serviceProvider.GetRequiredService(); - - this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.DbProvider, maskPassword: true)); - - yield return new DbConnectionService( - logger, - connectionOptions, - dbConnectionStringFactory, - dbConnectionFactory, - dbCommandFactory, - dbDataAdapterFactory - ); - } - } - - private void Dispose(bool disposing) - { - if (this._isDisposed) return; - - if (disposing) - { - this._logger.LogTrace("Disposing {Service}.", nameof(DbConnectionServiceFactory)); - - this._dbConnectionOptions.Clear(); - this._dbConnectionServices.Clear(); + yield return new DbConnectionService(logger, connectionOptions); } - - this._isDisposed = true; } #endregion