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/.editorconfig b/.editorconfig
index 57c8b2e..23a9fe2 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
@@ -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/.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.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 @@
   <PropertyGroup>
       <TargetFramework>net8.0</TargetFramework>
       <Version>0.0.0-alpha</Version>
-      <Description>TODO</Description>
+      <Description>Abstractions for AdvancedSystems.Connector.</Description>
       <PackageId>AdvancedSystems.Connector.Abstractions</PackageId>
       <RootNamespace>AdvancedSystems.Connector.Abstractions</RootNamespace>
       <Title>Advanced Systems Connector Abstractions Library</Title>
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;
-
-/// <summary>
-///     Specifies how a command string is interpreted.
-/// </summary>
-public enum DatabaseCommandType
-{
-    /// <summary>
-    ///     An SQL command.
-    /// </summary>
-    Text,
-    /// <summary>
-    ///     The name of a stored procedure.
-    /// </summary>
-    StoredProcedure
-}
diff --git a/AdvancedSystems.Connector.Abstractions/Provider.cs b/AdvancedSystems.Connector.Abstractions/DbProvider.cs
similarity index 50%
rename from AdvancedSystems.Connector.Abstractions/Provider.cs
rename to AdvancedSystems.Connector.Abstractions/DbProvider.cs
index ae0b0dc..7ff8f9f 100644
--- a/AdvancedSystems.Connector.Abstractions/Provider.cs
+++ b/AdvancedSystems.Connector.Abstractions/DbProvider.cs
@@ -1,12 +1,17 @@
-namespace AdvancedSystems.Connector;
+namespace AdvancedSystems.Connector.Abstractions;
 
 /// <summary>
 ///     Represents the different database providers supported by the system.
 /// </summary>
-public enum Provider
+public enum DbProvider
 {
+    /// <summary>
+    ///     Generic SQL Server.
+    /// </summary>
+    Generic = 0,
+
     /// <summary>
     ///     Microsoft SQL Server.
     /// </summary>
-    MsSql,
-}
+    MsSql = 1,
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs
deleted file mode 100644
index 5d1cdca..0000000
--- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbCommandExecutionException.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-
-namespace AdvancedSystems.Connector.Abstractions.Exceptions;
-
-/// <summary>
-///     Represents an error that occurs during the execution of a database command.
-/// </summary>
-public class DbCommandExecutionException : Exception
-{
-    /// <summary>
-    ///     Initializes a new instance of the <seealso cref="DbCommandExecutionException"/> class.
-    /// </summary>
-    public DbCommandExecutionException()
-    {
-
-    }
-
-    /// <summary>
-    ///     Initializes a new instance of the <seealso cref="DbCommandExecutionException"/> class
-    ///     with a specified error <paramref name="message"/>.
-    /// </summary>
-    /// <param name="message">
-    ///     The error message that explains the reason for the exception.
-    /// </param>
-    public DbCommandExecutionException(string message) : base(message)
-    {
-
-    }
-
-    /// <summary>
-    ///      Initializes a new instance of the <seealso cref="DbCommandExecutionException"/> class
-    ///      with a specified error <paramref name="message"/> a reference to the <paramref name="inner"/>
-    ///      exception that is the cause of this exception.
-    /// </summary>
-    /// <param name="message">
-    ///     The error message that explains the reason for the exception.
-    /// </param>
-    /// <param name="inner">
-    ///     The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
-    /// </param>
-    public DbCommandExecutionException(string message, Exception inner) : base(message, inner)
-    {
-
-    }
-}
diff --git a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs
index e5111cc..fd01fe5 100644
--- a/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs
+++ b/AdvancedSystems.Connector.Abstractions/Exceptions/DbConnectionException.cs
@@ -1,11 +1,12 @@
 using System;
+using System.Data.Common;
 
 namespace AdvancedSystems.Connector.Abstractions.Exceptions;
 
 /// <summary>
-///     Represents an error that occurs during the communication with a database.
+///     Represents an exception that occurs when a database connection fails.
 /// </summary>
-public class DbConnectionException : Exception
+public class DbConnectionException : DbException
 {
     /// <summary>
     ///     Initializes a new instance of the <seealso cref="DbConnectionException"/> class.
@@ -17,10 +18,10 @@ public DbConnectionException()
 
     /// <summary>
     ///     Initializes a new instance of the <seealso cref="DbConnectionException"/> class
-    ///     with a specified error <paramref name="message"/>.
+    ///     with the specified error <paramref name="message"/>.
     /// </summary>
     /// <param name="message">
-    ///     The error message that explains the reason for the exception.
+    ///     The message to display for this exception.
     /// </param>
     public DbConnectionException(string message) : base(message)
     {
@@ -29,17 +30,17 @@ public DbConnectionException(string message) : base(message)
 
     /// <summary>
     ///     Initializes a new instance of the <seealso cref="DbConnectionException"/> class
-    ///     with a specified error <paramref name="message"/> a reference to the <paramref name="inner"/>
-    ///     exception that is the cause of this exception.
+    ///     with the specified error <paramref name="message"/> and a reference to the
+    ///     <paramref name="innerException"/> that is cause of this exception.
     /// </summary>
     /// <param name="message">
-    ///     The error message that explains the reason for the exception.
+    ///     The message to display for this exception.
     /// </param>
-    /// <param name="inner">
-    ///     The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+    /// <param name="innerException">
+    ///     The inner exception reference.
     /// </param>
-    public DbConnectionException(string message, Exception inner) : base(message, inner)
+    public DbConnectionException(string message, Exception innerException) : base(message, innerException)
     {
 
     }
-}
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs b/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs
deleted file mode 100644
index 141bed7..0000000
--- a/AdvancedSystems.Connector.Abstractions/IDatabaseCommand.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Collections.Generic;
-
-namespace AdvancedSystems.Connector.Abstractions;
-
-/// <summary>
-///     Defines an SQL statement or stored procedure to execute against a data source.
-/// </summary>
-public interface IDatabaseCommand
-{
-    #region Properties
-
-    /// <summary>
-    ///     Gets or sets the Transact-SQL statement or stored
-    ///     procedure to execute at the data source.
-    /// </summary>
-    string CommandText { get; set; }
-
-    /// <summary>
-    ///     Gets or sets a value indicating how the <seealso cref="CommandText"/>
-    ///     property is to be interpreted.
-    /// </summary>
-    DatabaseCommandType CommandType { get; set; }
-
-    /// <summary>
-    ///     Gets a collection of <seealso cref="IDatabaseParameter"/> parameters.
-    /// </summary>
-    List<IDatabaseParameter> Parameters { get; }
-
-    #endregion
-
-    #region Methods
-
-    /// <summary>
-    ///     Adds a <seealso cref="IDatabaseParameter"/> to <seealso cref="Parameters"/>.
-    /// </summary>
-    /// <param name="parameter">The parameter to add.</param>
-    void AddParameter(IDatabaseParameter parameter);
-
-    /// <summary>
-    ///     Adds a <seealso cref="IDatabaseParameter"/> to <seealso cref="Parameters"/>.
-    /// </summary>
-    /// <typeparam name="T">The type of the parameter to add.</typeparam>
-    /// <param name="name">The name of the parameter to add.</param>
-    /// <param name="value">The value of the parameter to add.</param>
-    void AddParameter<T>(string name, T value);
-
-    #endregion
-}
diff --git a/AdvancedSystems.Connector.Abstractions/IDbCommand.cs b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs
new file mode 100644
index 0000000..641443d
--- /dev/null
+++ b/AdvancedSystems.Connector.Abstractions/IDbCommand.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Data;
+
+namespace AdvancedSystems.Connector.Abstractions;
+
+/// <summary>
+///     Defines an SQL statement or stored procedure to execute against a data source.
+/// </summary>
+public interface IDbCommand
+{
+    #region Properties
+
+    /// <summary>
+    ///     Gets or sets the Transact-SQL statement or stored
+    ///     procedure to execute at the data source.
+    /// </summary>
+    string CommandText { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a value indicating how the <seealso cref="CommandText"/>
+    ///     property is to be interpreted.
+    /// </summary>
+    CommandType CommandType { get; set; }
+
+    /// <summary>
+    ///     Gets a collection of <seealso cref="IDbParameter"/> parameters.
+    /// </summary>
+    IReadOnlyList<IDbParameter> Parameters { get; }
+
+    #endregion
+
+    #region Methods
+
+    void AddParameter(string parameterName, object value, DbType type);
+
+    void AddParameter<T>(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
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/IDatabaseConnectionFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs
similarity index 55%
rename from AdvancedSystems.Connector.Abstractions/IDatabaseConnectionFactory.cs
rename to AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs
index 8a95485..972ccfd 100644
--- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionFactory.cs
+++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionFactory.cs
@@ -1,25 +1,33 @@
 using System;
+using System.Data.Common;
 
 namespace AdvancedSystems.Connector.Abstractions;
 
 /// <summary>
-///     Defines a factory for creating instances of <seealso cref="IDatabaseConnectionService"/>.
+///     Defines a factory for creating instances of <seealso cref="DbConnection"/>.
 /// </summary>
-public interface IDatabaseConnectionFactory
+public interface IDbConnectionFactory
 {
+    #region Methods
+
     /// <summary>
-    ///     Creates a new instance of <see cref="IDatabaseConnectionService"/> based on the
+    ///     Creates a new instance of <see cref="DbConnection"/> based on the
     ///     specified provider.
     /// </summary>
     /// <param name="provider">
     ///     The database provider for which the connection service should be created.
     /// </param>
+    /// <param name="connectionString">
+    ///     The connection string used to establish the connection with the database.
+    /// </param>
     /// <returns>
-    ///     An instance of <see cref="IDatabaseConnectionService"/> configured for the
+    ///     An instance of <see cref="DbConnection"/> configured for the
     ///     specified provider.
     /// </returns>
     /// <exception cref="NotSupportedException">
     ///     Thrown when the specified <paramref name="provider"/> is not supported by the factory.
     /// </exception>
-    IDatabaseConnectionService Create(Provider provider);
-}
+    DbConnection Create(DbProvider provider, string connectionString);
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs
similarity index 80%
rename from AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs
rename to AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs
index 249785b..8dcafa9 100644
--- a/AdvancedSystems.Connector.Abstractions/IDatabaseConnectionService.cs
+++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionService.cs
@@ -1,40 +1,42 @@
 using System.Data;
-using System.Threading.Tasks;
 using System.Threading;
+using System.Threading.Tasks;
 
 using AdvancedSystems.Connector.Abstractions.Exceptions;
 
 namespace AdvancedSystems.Connector.Abstractions;
 
 /// <summary>
-///     Defines the contract for a service responsible for managing database connections.
+///     Defines a contract for a service responsible for managing database connections.
 /// </summary>
 /// <remarks>
 ///     The connection is managed internally and will be closed after the query execution is complete.
 /// </remarks>
-public interface IDatabaseConnectionService
+/// <typeparam name="T">
+///     The <typeparamref name="T"/> connection options.
+/// </typeparam>
+public interface IDbConnectionService<T> where T : class, IDbSettings, new()
 {
     #region Properties
 
     /// <summary>
-    ///     Gets or sets the string used to open a SQL Server database.
+    ///     <inheritdoc cref="System.Data.ConnectionState" path="/summary"/>
     /// </summary>
-    string ConnectionString { get; }
+    ConnectionState ConnectionState { get; }
 
     /// <summary>
-    ///     Indicates the state of the connection during the most recent network
-    ///     operation performed on the connection.
+    ///     The <typeparamref name="T"/> connection options.
     /// </summary>
-    ConnectionState ConnectionState { get; }
+    T Options { get; }
 
     #endregion
 
     #region Methods
 
     /// <summary>
-    ///     Executes a query defined by the specified <see cref="IDatabaseCommand"/> synchronously.
+    ///     Executes a query defined by the specified <see cref="IDbCommand"/> synchronously.
     /// </summary>
-    /// <param name="databaseCommand">
+    /// <param name="dbCommand">
     ///     The command that defines the query to be executed. It must contain the SQL command
     ///     text and any required parameters.
     /// </param>
@@ -50,12 +52,12 @@ public interface IDatabaseConnectionService
     ///     Thrown when there is a failure in communicating with the database, such as a connection
     ///     issue or timeout.
     /// </exception>
-    DataSet ExecuteQuery(IDatabaseCommand databaseCommand);
+    DataSet ExecuteQuery(IDbCommand dbCommand);
 
     /// <summary>
-    ///     Executes a query defined by the specified <see cref="IDatabaseCommand"/> asynchronously.
+    ///     Executes a query defined by the specified <see cref="IDbCommand"/> asynchronously.
     /// </summary>
-    /// <param name="databaseCommand">
+    /// <param name="dbCommand">
     ///     The command that defines the query to be executed. It must contain the SQL command
     ///     text and any required parameters.
     /// </param>
@@ -74,12 +76,12 @@ public interface IDatabaseConnectionService
     ///     Thrown when there is a failure in communicating with the database, such as a connection
     ///     issue or timeout.
     /// </exception>
-    ValueTask<DataSet?> ExecuteQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default);
+    ValueTask<DataSet?> ExecuteQueryAsync(IDbCommand dbCommand, CancellationToken cancellationToken = default);
 
     /// <summary>
-    ///     Executes a non-query command defined by the specified <see cref="IDatabaseCommand"/> synchronously.
+    ///     Executes a non-query command defined by the specified <see cref="IDbCommand"/> synchronously.
     /// </summary>
-    /// <param name="databaseCommand">
+    /// <param name="dbCommand">
     ///     The command that defines the query to be executed. It must contain the SQL command
     ///     text and any required parameters.
     /// </param>
@@ -95,12 +97,12 @@ public interface IDatabaseConnectionService
     ///     Thrown when there is a failure in communicating with the database, such as a connection
     ///     issue or timeout.
     /// </exception>
-    int ExecuteNonQuery(IDatabaseCommand databaseCommand);
+    int ExecuteNonQuery(IDbCommand dbCommand);
 
     /// <summary>
-    ///     Executes a non-query command defined by the specified <see cref="IDatabaseCommand"/> asynchronously.
+    ///     Executes a non-query command defined by the specified <see cref="IDbCommand"/> asynchronously.
     /// </summary>
-    /// <param name="databaseCommand">
+    /// <param name="dbCommand">
     ///      The command that defines the query to be executed. It must contain the SQL command
     ///      text and any required parameters.
     /// </param>
@@ -120,7 +122,7 @@ public interface IDatabaseConnectionService
     ///     Thrown when there is a failure in communicating with the database, such as a connection
     ///     issue or timeout.
     /// </exception>
-    ValueTask<int> ExecuteNonQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default);
+    ValueTask<int> ExecuteNonQueryAsync(IDbCommand 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
new file mode 100644
index 0000000..97e8286
--- /dev/null
+++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionServiceFactory.cs
@@ -0,0 +1,58 @@
+using System;
+
+using AdvancedSystems.Connector.Abstractions.Exceptions;
+
+namespace AdvancedSystems.Connector.Abstractions;
+
+/// <summary>
+///     Defines a contract for creating and managing multiple connection instances.
+/// </summary>
+/// <typeparam name="T">
+///     The <typeparamref name="T"/> factory options.
+/// </typeparam>
+/// <typeparam name="U">
+///     The <typeparamref name="U"/> connection options.
+/// </typeparam>
+public interface IDbConnectionServiceFactory<T, U> : IDisposable where T : class, IDbFactorySettings<U>, new() where U : class, IDbSettings, new()
+{
+    #region Properties
+
+    /// <summary>
+    ///     The <typeparamref name="T"/> factory options.
+    /// </summary>
+    T Options { get; }
+
+    #endregion
+
+    #region Methods
+
+    /// <summary>
+    ///     Retrieves a connection service configured for the specified <paramref name="dbUser"/>.
+    /// </summary>
+    /// <param name="dbUser">
+    ///     The username of the database user for whom the connection service is to be retrieved.
+    /// </param>
+    /// <returns>
+    ///     Returns a connection service.
+    /// </returns>
+    /// <exception cref="DbConnectionException">
+    ///     Raised if the no connection matching the search criteria could be returned.
+    /// </exception>
+    IDbConnectionService<U> GetConnection(string dbUser);
+
+    /// <summary>
+    ///     Retrieves a connection service from the factory pool determined by the <paramref name="predicate"/>.
+    /// </summary>
+    /// <param name="predicate">
+    ///     The filter to apply for retrieving a connection.
+    /// </param>
+    /// <returns>
+    ///     Returns a connection service.
+    /// </returns>
+    /// <exception cref="DbConnectionException">
+    ///     Raised if the no connection matching the search criteria could be returned.
+    /// </exception>
+    IDbConnectionService<U> GetConnection(Predicate<U> predicate);
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs
new file mode 100644
index 0000000..cdf71d6
--- /dev/null
+++ b/AdvancedSystems.Connector.Abstractions/IDbConnectionStringFactory.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace AdvancedSystems.Connector.Abstractions;
+
+/// <summary>
+///     Defines a factory for creating connection strings for various <seealso cref="DbProvider"/>.
+/// </summary>
+public interface IDbConnectionStringFactory
+{
+    #region Methods
+
+    /// <summary>
+    ///     Creates a database connection string based on the specified provider.
+    /// </summary>
+    /// <param name="provider">
+    ///     A <seealso cref="DbProvider"/> that specifies the type of database to connect to.
+    /// </param>
+    /// <param name="maskPassword">
+    ///     A boolean value indicating whether the password in the connection string should be masked.
+    /// </param>
+    /// <returns>
+    ///     A database connection string.
+    /// </returns>
+    /// <exception cref="NotImplementedException">
+    ///     Thrown when the specified <paramref name="provider"/> is not implemented yet.
+    /// </exception>
+    string Create(DbProvider provider, bool maskPassword = false);
+
+    #endregion
+}
\ No newline at end of file
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;
+
+/// <summary>
+///     Defines a factory for creating instances of <see cref="DbDataAdapter"/> based on the
+///     specified <see cref="DbCommand"/>.
+/// </summary>
+public interface IDbDataAdapterFactory
+{
+    /// <summary>
+    ///     Creates an instance of <see cref="DbDataAdapter"/> based on the specific implementation
+    ///     derived from <see cref="DbCommand"/>.
+    /// </summary>
+    /// <param name="dbCommand">
+    ///     The <see cref="DbCommand"/> that will be used to determine the type of <see cref="DbDataAdapter"/>
+    ///     to create.
+    /// </param>
+    /// <returns>
+    ///      An instance of <see cref="DbDataAdapter"/> that is configured to work with the type of
+    ///      <see cref="DbCommand"/> provided.
+    /// </returns>
+    /// <exception cref="NotSupportedException">
+    ///     Thrown when the specified provider is not supported by the factory.
+    /// </exception>
+    DbDataAdapter Create(DbCommand dbCommand);
+}
\ 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..b5a565c
--- /dev/null
+++ b/AdvancedSystems.Connector.Abstractions/IDbFactorySettings.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace AdvancedSystems.Connector.Abstractions;
+
+/// <summary>
+///     Defines a contract for multiple configuring connection strings through
+///     <seealso cref="IDbConnectionServiceFactory{T, U}"/>.
+/// </summary>
+/// <typeparam name="T">
+///     The <typeparamref name="T"/> factory options.
+/// </typeparam>
+public interface IDbFactorySettings<T> : IDbSettings where T : class, IDbSettings, new()
+{
+    #region Properties
+
+    /// <summary>
+    ///     Contains a list of connection options consumed by this factory.
+    /// </summary>
+    List<T> 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 71%
rename from AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs
rename to AdvancedSystems.Connector.Abstractions/IDbParameter.cs
index ef6f365..5d8704f 100644
--- a/AdvancedSystems.Connector.Abstractions/IDatabaseParameter.cs
+++ b/AdvancedSystems.Connector.Abstractions/IDbParameter.cs
@@ -2,15 +2,15 @@
 
 namespace AdvancedSystems.Connector.Abstractions;
 
-public interface IDatabaseParameter
+public interface IDbParameter
 {
     #region Properties
 
     string ParameterName { get; set; }
 
-    SqlDbType SqlDbType { get; set; }
+    DbType DbType { get; set; }
 
     object? Value { get; set; }
 
     #endregion
-}
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Abstractions/IDbSettings.cs b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs
new file mode 100644
index 0000000..9bc4e18
--- /dev/null
+++ b/AdvancedSystems.Connector.Abstractions/IDbSettings.cs
@@ -0,0 +1,41 @@
+namespace AdvancedSystems.Connector.Abstractions;
+
+/// <summary>
+///     Defines common properties for creating connection strings.
+/// </summary>
+public interface IDbSettings
+{
+    #region Properties
+
+    /// <summary>
+    ///     Gets or sets the name of the application associated with the connection string.
+    /// </summary>
+    string? ApplicationName { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the name or network address of the instance of SQL Server to connect to.
+    /// </summary>
+    string? DataSource { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the name of the database associated with the connection.
+    /// </summary>
+    string? InitialCatalog { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the password for the SQL Server account.
+    /// </summary>
+    string? Password { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the database provider.
+    /// </summary>
+    DbProvider DbProvider { get; }
+
+    /// <summary>
+    ///     Gets or sets the user ID to be used when connecting to SQL Server.
+    /// </summary>
+    string? UserID { get; set; }
+
+    #endregion
+}
\ No newline at end of file
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 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <Description>Unit test project for AdvancedSystems.Connector.</Description>
+    <PackageId>AdvancedSystems.Security.Tests</PackageId>
+    <RootNamespace>AdvancedSystems.Security.Tests</RootNamespace>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="coverlet.collector" Version="6.0.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
+    <PackageReference Include="xunit" Version="2.5.3" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\AdvancedSystems.Connector.Abstractions\AdvancedSystems.Connector.Abstractions.csproj" />
+    <ProjectReference Include="..\AdvancedSystems.Connector\AdvancedSystems.Connector.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="appsettings.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>
diff --git a/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj
new file mode 100644
index 0000000..75ebabf
--- /dev/null
+++ b/AdvancedSystems.Connector.Tests/AdvancedSystems.Connector.Tests.csproj
@@ -0,0 +1,43 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <Description>Unit test project for AdvancedSystems.Connector.</Description>
+    <PackageId>AdvancedSystems.Security.Tests</PackageId>
+    <RootNamespace>AdvancedSystems.Security.Tests</RootNamespace>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="coverlet.collector" Version="6.0.2">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.7" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
+    <PackageReference Include="Moq" Version="4.20.70" />
+    <PackageReference Include="xunit" Version="2.9.0" />
+    <PackageReference Include="xunit.analyzers" Version="1.15.0">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="xunit.runner.console" Version="2.9.0">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\AdvancedSystems.Connector.Abstractions\AdvancedSystems.Connector.Abstractions.csproj" />
+    <ProjectReference Include="..\AdvancedSystems.Connector\AdvancedSystems.Connector.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="Xunit" />
+  </ItemGroup>
+
+</Project>
diff --git a/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..4ad591c
--- /dev/null
+++ b/AdvancedSystems.Connector.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,198 @@
+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;
+
+/// <summary>
+///     Tests the public methods in <seealso cref="Connector.DependencyInjection.ServiceCollectionExtensions"/>.
+/// </summary>
+public sealed class ServiceCollectionExtensionsTests
+{
+    /// <summary>
+    ///     Mock options for <seealso cref="IDbConnectionService{T}"/>.
+    /// </summary>
+    private readonly MsSqlOptions _dbConnectionOptions;
+
+    /// <summary>
+    ///     Dummy decryption function to test the mechanics of calling
+    ///     a delegate at option registration.
+    /// </summary>
+    /// <param name="password">
+    ///     The password to decrypt.
+    /// </param>
+    /// <returns>
+    ///     The password in plaintext.
+    /// </returns>
+    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
+
+    /// <summary>
+    ///     Tests that <seealso cref="IDbConnectionService{T}"/> can be initialized through
+    ///     dependency injection from <seealso cref="IOptions{T}"/>.
+    /// </summary>
+    [Fact]
+    public async Task TestAddDbConnectionService_FromOptions()
+    {
+        // Arrange
+        using IHost? host = await new HostBuilder()
+            .ConfigureWebHostDefaults(builder =>
+            {
+                builder.UseTestServer();
+
+                builder.ConfigureTestServices(services =>
+                {
+                    services.AddDbConnectionService<MsSqlOptions>(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<MsSqlOptions>? dbConnectionService = host.Services.GetService<IDbConnectionService<MsSqlOptions>>();
+
+        // 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();
+    }
+
+    /// <summary>
+    ///     Tests that <seealso cref="IDbConnectionService{T}"/> can be initialized through
+    ///     dependency injection from <c>appsettings.json</c>.
+    /// </summary>
+    [Fact]
+    public async Task TestAddDbConnectionService_FromAppSettings()
+    {
+        // Arrange
+        var appSettings = new Dictionary<string, string?>()
+        {
+            { $"{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<MsSqlOptions>(configurationRoot.GetRequiredSection(Sections.DATABASE), DecryptPassword);
+                });
+
+                builder.Configure(app =>
+                {
+
+                });
+            })
+            .StartAsync();
+
+        // Act
+        IDbConnectionService<MsSqlOptions>? dbConnectionService = host.Services.GetService<IDbConnectionService<MsSqlOptions>>();
+
+        // 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
+
+    #region AddDbConnectionServiceFactory
+
+    /// <summary>
+    ///     Tests that <seealso cref="IDbConnectionServiceFactory{T, U}"/> can be initialized through
+    ///     dependency injection from <seealso cref="IOptions{T}"/> and a collection of <seealso cref="IOptions{U}"/>.
+    /// </summary>
+    [Fact(Skip = "TODO")]
+    public void TestAddDbConnectionServiceFactory_FromOptions()
+    {
+        // Arrange
+        // Act
+        // Assert
+    }
+
+    /// <summary>
+    ///     Tests that <seealso cref="IDbConnectionServiceFactory{T, U}"/> can be initialized through
+    ///     dependency injection from <c>appsettings.json</c>.
+    /// </summary>
+    [Fact(Skip = "TODO")]
+    public void TestAddDbConnectionServiceFactory_FromAppSettings()
+    {
+        // Arrange
+        // Act
+        // Assert
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs
new file mode 100644
index 0000000..fe42a9e
--- /dev/null
+++ b/AdvancedSystems.Connector.Tests/Fixtures/DbConnectionServiceFixture.cs
@@ -0,0 +1,100 @@
+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<T> where T : class, IDbSettings, new()
+{
+    private readonly Mock<DbConnection> _dbConnection = new();
+    private readonly Mock<DbDataAdapter> _dbDataAdapter = new();
+
+    private readonly Mock<ILogger<DbConnectionService<T>>> _logger = new();
+    private readonly Mock<IOptions<T>> _options = new();
+    private readonly Mock<IDbConnectionFactory> _dbConnectionFactory = new();
+    private readonly Mock<IDbConnectionStringFactory> _dbConnectionStringFactory = new();
+    private readonly Mock<IDbCommandFactory> _dbCommandFactory = new();
+    private readonly Mock<IDbDataAdapterFactory> _dbDataAdapterFactory = new();
+
+    public DbConnectionServiceFixture()
+    {
+        this.Options.Setup(x => x.Value)
+            .Returns(new T
+            {
+                ApplicationName = nameof(DbConnectionServiceFixture<T>),
+                UserID = "admin",
+                Password = "password",
+                DataSource = new DataSource("localhost", 1433),
+                InitialCatalog = "TEST",
+            });
+
+        this.DbConnectionStringFactory.Setup(x => x.Create(It.IsAny<DbProvider>(), true))
+            .Returns(It.IsAny<string>());
+
+        this.DbConnectionFactory.Setup(x => x.Create(It.IsAny<DbProvider>(), It.IsAny<string>()))
+            .Returns(this._dbConnection.Object);
+
+        this._dbConnection.SetupAdd(x => x.StateChange += It.IsAny<StateChangeEventHandler>())
+            .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<DbCommand>()))
+            .Returns(this._dbDataAdapter.Object);
+
+        this.DbConnectionService = new DbConnectionService<T>(
+            this.Logger.Object,
+            this.Options.Object,
+            this.DbConnectionStringFactory.Object,
+            this.DbConnectionFactory.Object,
+            this.DbCommandFactory.Object,
+            this.DbDataAdapterFactory.Object
+        );
+    }
+
+    #region Properties
+
+    public Mock<ILogger<DbConnectionService<T>>> Logger => this._logger;
+
+    public Mock<IOptions<T>> Options => this._options;
+
+    public Mock<IDbConnectionFactory> DbConnectionFactory => this._dbConnectionFactory;
+
+    public Mock<IDbConnectionStringFactory> DbConnectionStringFactory => this._dbConnectionStringFactory;
+
+    public Mock<IDbCommandFactory> DbCommandFactory => this._dbCommandFactory;
+
+    public Mock<IDbDataAdapterFactory> DbDataAdapterFactory => this._dbDataAdapterFactory;
+
+    public IDbConnectionService<T> 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
new file mode 100644
index 0000000..f68a9bc
--- /dev/null
+++ b/AdvancedSystems.Connector.Tests/Services/DbConnectionServiceTests.cs
@@ -0,0 +1,80 @@
+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;
+
+[Category(TestCategory.UNIT)]
+public sealed class DbConnectionServiceTests : IClassFixture<DbConnectionServiceFixture<MsSqlOptions>>
+{
+    private readonly DbConnectionServiceFixture<MsSqlOptions> _sut;
+
+    public DbConnectionServiceTests(DbConnectionServiceFixture<MsSqlOptions> sut)
+    {
+        this._sut = sut;
+
+        //this._sut.Options.Setup(x => x.Value)
+        //    .Returns(this._sut.Options.Object.Value with { DbProvider = DbProvider.MsSql });
+    }
+
+    #region Tests
+
+    [Fact]
+    public void TestExecuteQuery()
+    {
+        // Arrange
+        var dbCommand = new DbCommand
+        {
+            CommandText = "SELECT @value",
+            CommandType = CommandType.Text,
+        };
+
+        dbCommand.AddParameter<int>("@value", 1);
+
+        this._sut.DbCommandFactory.Setup(x => x.Create(It.IsAny<DbProvider>(), It.IsAny<DbConnection>(), 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")]
+    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
+}
\ No newline at end of file
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
+{
+    /// <summary>
+    ///     Denotes a unit test category.
+    /// </summary>
+    internal const string UNIT = "UNIT";
+}
\ 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
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 @@
   <PropertyGroup>
       <TargetFramework>net8.0</TargetFramework>
       <Version>0.0.0-alpha</Version>
-      <Description>TODO</Description>
+      <Description>Provides a database access layer to streamline database connections.</Description>
       <PackageId>AdvancedSystems.Connector</PackageId>
       <RootNamespace>AdvancedSystems.Connector</RootNamespace>
       <Title>Advanced Systems Connector Library</Title>
diff --git a/AdvancedSystems.Connector/Common/DataSource.cs b/AdvancedSystems.Connector/Common/DataSource.cs
index 49da794..f058636 100644
--- a/AdvancedSystems.Connector/Common/DataSource.cs
+++ b/AdvancedSystems.Connector/Common/DataSource.cs
@@ -1,18 +1,30 @@
-namespace AdvancedSystems.Connector.Common;
-
-public sealed class DataSource
+using AdvancedSystems.Connector.Options;
+
+namespace AdvancedSystems.Connector.Common;
+
+/// <summary>
+///     Represents a data source with a host and port.
+/// </summary>
+/// <param name="Host">
+///     The host of the data source.
+/// </param>
+/// <param name="Port">
+///     The port of the data source.
+/// </param>
+/// <seealso cref="DbOptions"/>
+public sealed record DataSource(string Host, int Port)
 {
-    public DataSource(string host, int port)
-    {
-        this.Host = host;
-        this.Port = port;
-    }
-
     #region Properties
 
-    public string Host { get; set; }
+    /// <summary>
+    ///     Gets or sets the host of the data source.
+    /// </summary>
+    public string Host { get; set; } = Host;
 
-    public int Port { get; set; }
+    /// <summary>
+    ///     Gets or sets the port of the data source.
+    /// </summary>
+    public int Port { get; set; } = Port;
 
     #endregion
 
@@ -33,4 +45,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
deleted file mode 100644
index 4477e88..0000000
--- a/AdvancedSystems.Connector/Common/DatabaseCommand.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Diagnostics;
-using System.Globalization;
-using System.Text;
-
-using AdvancedSystems.Connector.Abstractions;
-using AdvancedSystems.Connector.Converters;
-
-namespace AdvancedSystems.Connector.Common;
-
-[DebuggerDisplay("{CommandText}")]
-public sealed class DatabaseCommand : IDatabaseCommand
-{
-    #region Properties
-
-    [DisplayName("Command Text")]
-    public required string CommandText {  get; set; }
-
-    [DisplayName("Command Type")]
-    public required DatabaseCommandType CommandType { get; set; }
-
-    [DisplayName("Parameters")]
-    public List<IDatabaseParameter> Parameters { get; } = [];
-
-    #endregion
-
-    #region Methods
-
-    public void AddParameter(IDatabaseParameter parameter)
-    {
-        this.Parameters.Add(parameter);
-    }
-
-    public void AddParameter<T>(string name, T value)
-    {
-        var parameter = new DatabaseParameter
-        {
-            ParameterName = name,
-            SqlDbType = typeof(T).Cast(),
-            Value = (object?)value ?? DBNull.Value
-        };
-
-        this.Parameters.Add(parameter);
-    }
-
-    private static string? FormatValue(IDatabaseParameter parameter)
-    {
-        return parameter.SqlDbType 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)}'",
-            _ => 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, DatabaseCommand.FormatValue(parameter));
-        }
-
-        return commandBuilder.ToString();
-    }
-
-    #endregion
-}
diff --git a/AdvancedSystems.Connector/Common/DbCommand.cs b/AdvancedSystems.Connector/Common/DbCommand.cs
new file mode 100644
index 0000000..e08fa6b
--- /dev/null
+++ b/AdvancedSystems.Connector/Common/DbCommand.cs
@@ -0,0 +1,74 @@
+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<DbParameter> _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<IDbParameter> 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<T>(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/DatabaseParameter.cs b/AdvancedSystems.Connector/Common/DbParameter.cs
similarity index 69%
rename from AdvancedSystems.Connector/Common/DatabaseParameter.cs
rename to AdvancedSystems.Connector/Common/DbParameter.cs
index d9e0301..a23315a 100644
--- a/AdvancedSystems.Connector/Common/DatabaseParameter.cs
+++ b/AdvancedSystems.Connector/Common/DbParameter.cs
@@ -4,15 +4,15 @@
 
 namespace AdvancedSystems.Connector.Common;
 
-public sealed class DatabaseParameter : IDatabaseParameter
+public sealed class DbParameter : IDbParameter
 {
     #region Properties
 
     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/Common/Sections.cs b/AdvancedSystems.Connector/Common/Sections.cs
new file mode 100644
index 0000000..0e50aca
--- /dev/null
+++ b/AdvancedSystems.Connector/Common/Sections.cs
@@ -0,0 +1,8 @@
+namespace AdvancedSystems.Connector.Common;
+
+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/Converters/MsSqlServerExtensions.cs b/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs
deleted file mode 100644
index fc286ca..0000000
--- a/AdvancedSystems.Connector/Converters/MsSqlServerExtensions.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System;
-using System.Data;
-
-using AdvancedSystems.Connector.Abstractions;
-using AdvancedSystems.Connector.Options;
-
-using Microsoft.Data.SqlClient;
-
-namespace AdvancedSystems.Connector.Converters;
-
-internal static class MsSqlServerExtensions
-{
-    #region Helpers
-
-    internal static string CreateConnectionString(this MsSqlServerSettings 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 CommandType Cast(this DatabaseCommandType commandType)
-    {
-        return commandType switch
-        {
-            DatabaseCommandType.Text => CommandType.Text,
-            DatabaseCommandType.StoredProcedure => CommandType.StoredProcedure,
-            _ => throw new NotImplementedException(Enum.GetName(commandType)),
-        };
-    }
-
-    internal static SqlDbType Cast(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,
-            _ => throw new ArgumentException($"Failed to infer type from {typeCode}."),
-        };
-    }
-
-    #endregion
-}
diff --git a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs
index f328a45..5b8aec5 100644
--- a/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/AdvancedSystems.Connector/DependencyInjection/ServiceCollectionExtensions.cs
@@ -1,7 +1,7 @@
 using System;
 
 using AdvancedSystems.Connector.Abstractions;
-using AdvancedSystems.Connector.Options;
+using AdvancedSystems.Connector.Internals;
 using AdvancedSystems.Connector.Services;
 
 using Microsoft.Extensions.Configuration;
@@ -12,24 +12,56 @@ namespace AdvancedSystems.Connector.DependencyInjection;
 
 public static class ServiceCollectionExtensions
 {
+    /// <summary>
+    ///     Decrypts an encrypted password.
+    /// </summary>
+    /// <param name="cipher">
+    ///     The encrypted password to be decrypted.
+    /// </param>
+    /// <returns>
+    ///     The decrypted plain-text password.
+    /// </returns>
     public delegate string DecryptPassword(string cipher);
 
-    #region DbConnectionService
+    #region AddDbConnectionService
 
-    private static IServiceCollection AddDbConnectionService<T>(this IServiceCollection services) where T : class, IDatabaseConnectionService
+    private static IServiceCollection AddDbConnectionService<T>(this IServiceCollection services) where T : class, IDbSettings, new()
     {
-        services.TryAdd(ServiceDescriptor.Singleton<IDatabaseConnectionService, T>());
+        services.TryAdd(ServiceDescriptor.Singleton<IDbConnectionStringFactory, DbConnectionStringFactory>());
+        services.TryAdd(ServiceDescriptor.Singleton<IDbConnectionFactory, DbConnectionFactory>());
+        services.TryAdd(ServiceDescriptor.Singleton<IDbCommandFactory, DbCommandFactory>());
+        services.TryAdd(ServiceDescriptor.Singleton<IDbDataAdapterFactory, DbDataAdapterFactory>());
+        services.TryAdd(ServiceDescriptor.Singleton(typeof(IDbConnectionService<T>), typeof(DbConnectionService<T>)));
         return services;
     }
 
-    private static IServiceCollection AddDbConnectionService<T, U>(this IServiceCollection services, Action<U> setupAction, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions
+    /// <summary>
+    ///     Adds the default implementation of <seealso cref="IDbConnectionService{T}"/> to <paramref name="services"/>.
+    /// </summary>
+    /// <typeparam name="T">
+    ///     The type of option to use for the connection service.
+    /// </typeparam>
+    /// <param name="services">
+    ///     The service collection containing the service.
+    /// </param>
+    /// <param name="configuration">
+    ///     A configuration section targeting <typeparamref name="T"/>.
+    /// </param>
+    /// <param name="decryptPassword">
+    ///     Defines a function handler for optional password decryption.
+    /// </param>
+    /// <returns>
+    ///     The value of <paramref name="services"/>.
+    /// </returns>
+    public static IServiceCollection AddDbConnectionService<T>(this IServiceCollection services, IConfigurationSection configuration, DecryptPassword? decryptPassword = null) where T : class, IDbSettings, new()
     {
-        services.AddOptions()
-            .Configure(setupAction)
-            .PostConfigure<U>(options =>
+        services.AddOptions<T>()
+            .Bind(configuration)
+            .PostConfigure(options =>
             {
                 if (decryptPassword != null)
                 {
+                    ArgumentNullException.ThrowIfNull(options.Password);
                     options.Password = decryptPassword(options.Password);
                 }
             });
@@ -38,39 +70,40 @@ private static IServiceCollection AddDbConnectionService<T, U>(this IServiceColl
         return services;
     }
 
-    private static IServiceCollection AddDbConnectionService<T, U>(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null) where T : class, IDatabaseConnectionService where U : DatabaseOptions
+    /// <summary>
+    ///     Adds the default implementation of <seealso cref="IDbConnectionService{T}"/> to <paramref name="services"/>.
+    /// </summary>
+    /// <typeparam name="T">
+    ///     The type of option to use for the connection service.
+    /// </typeparam>
+    /// <param name="services">
+    ///     The service collection containing the service.
+    /// </param>
+    /// <param name="setupAction">
+    ///     An action used to configure <typeparamref name="T"/>.
+    /// </param>
+    /// <param name="decryptPassword">
+    ///     Defines a function handler for optional password decryption.
+    /// </param>
+    /// <returns>
+    ///     The value of <paramref name="services"/>.
+    /// </returns>
+    public static IServiceCollection AddDbConnectionService<T>(this IServiceCollection services, Action<T> setupAction, DecryptPassword? decryptPassword = null) where T : class, IDbSettings, new()
     {
-        services.AddOptions<U>()
-            .Bind(configuration.GetRequiredSection(Sections.DATABASE))
+        services.AddOptions<T>()
+            .Configure(setupAction)
             .PostConfigure(options =>
             {
                 if (decryptPassword != null)
                 {
+                    ArgumentNullException.ThrowIfNull(options.Password);
                     options.Password = decryptPassword(options.Password);
                 }
-            })
-            .ValidateDataAnnotations()
-            .ValidateOnStart();
+            });
 
         services.AddDbConnectionService<T>();
         return services;
     }
 
     #endregion
-
-    #region Microsoft SQL Server
-
-    public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, Action<MsSqlServerSettings> setupAction, DecryptPassword? decryptPassword = null)
-    {
-        services.AddDbConnectionService<MsSqlServerConnectionService, MsSqlServerSettings>(setupAction, decryptPassword);
-        return services;
-    }
-
-    public static IServiceCollection AddMsSqlServerConnectionService(this IServiceCollection services, IConfiguration configuration, DecryptPassword? decryptPassword = null)
-    {
-        services.AddDbConnectionService<MsSqlServerConnectionService, MsSqlServerSettings>(configuration, decryptPassword);
-        return services;
-    }
-
-    #endregion
-}
+}
\ No newline at end of file
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/DbParameterExtensions.cs b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs
new file mode 100644
index 0000000..5aecd2d
--- /dev/null
+++ b/AdvancedSystems.Connector/Extensions/DbParameterExtensions.cs
@@ -0,0 +1,47 @@
+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(),
+        };
+    }
+
+    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(),
+        };
+    }
+}
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/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/DbConnectionFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs
new file mode 100644
index 0000000..6373112
--- /dev/null
+++ b/AdvancedSystems.Connector/Internals/DbConnectionFactory.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Data.Common;
+
+using AdvancedSystems.Connector.Abstractions;
+
+using Microsoft.Data.SqlClient;
+
+namespace AdvancedSystems.Connector.Internals;
+
+public sealed class DbConnectionFactory : IDbConnectionFactory
+{
+    #region Methods
+
+    public DbConnection Create(DbProvider provider, string connectionString)
+    {
+        return provider switch
+        {
+            DbProvider.MsSql => new SqlConnection(connectionString),
+            _ => throw new NotSupportedException(Enum.GetName(provider)),
+        };
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs
new file mode 100644
index 0000000..07dea8f
--- /dev/null
+++ b/AdvancedSystems.Connector/Internals/DbConnectionStringFactory.cs
@@ -0,0 +1,34 @@
+using System;
+
+using AdvancedSystems.Connector.Abstractions;
+using AdvancedSystems.Connector.Extensions;
+using AdvancedSystems.Connector.Options;
+
+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 Methods
+
+    public string Create(DbProvider provider, bool maskPassword = false)
+    {
+        return provider switch
+        {
+            DbProvider.Generic => this._serviceProvider.GetRequiredService<IOptions<DbOptions>>().Value.CreateGenericConnectionString(maskPassword),
+            DbProvider.MsSql => this._serviceProvider.GetRequiredService<IOptions<MsSqlOptions>>().Value.CreateMsSqlConnectionString(maskPassword),
+            _ => throw new NotImplementedException(),
+        };
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs
new file mode 100644
index 0000000..da07874
--- /dev/null
+++ b/AdvancedSystems.Connector/Internals/DbDataAdapterFactory.cs
@@ -0,0 +1,28 @@
+using System;
+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
+{
+    #region Methods
+
+    public DbDataAdapter Create(DbCommand dbCommand)
+    {
+        return dbCommand switch
+        {
+            SqlCommand sqlCommand => new SqlDataAdapter(sqlCommand),
+            DbCommand => dbCommand.CreateDbDataAdapter(),
+            _ => throw new NotSupportedException()
+        };
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Options/DatabaseOptions.cs b/AdvancedSystems.Connector/Options/DatabaseOptions.cs
deleted file mode 100644
index b5321e6..0000000
--- a/AdvancedSystems.Connector/Options/DatabaseOptions.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
-
-using Microsoft.Data.SqlClient;
-
-namespace AdvancedSystems.Connector.Options;
-
-public class DatabaseOptions
-{
-    /// <summary>
-    ///     Gets or sets the name of the application associated with the connection string.
-    /// </summary>
-    [DisplayName("Application Name")]
-    public required string ApplicationName { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the name or network address of the instance of SQL Server to connect to.
-    /// </summary>
-    [Required]
-    [DisplayName("Data Source")]
-    public required string DataSource { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the name of the database associated with the connection.
-    /// </summary>
-    [Required]
-    [DisplayName("Initial Catalog")]
-    public required string InitialCatalog { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the password for the SQL Server account.
-    /// </summary>
-    [Required]
-    [DisplayName("Password")]
-    public required string Password { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the user ID to be used when connecting to SQL Server.
-    /// </summary>
-    [Required]
-    [DisplayName("User ID")]
-    public required string UserID { get; set; }
-}
diff --git a/AdvancedSystems.Connector/Options/DbOptions.cs b/AdvancedSystems.Connector/Options/DbOptions.cs
new file mode 100644
index 0000000..24b53bd
--- /dev/null
+++ b/AdvancedSystems.Connector/Options/DbOptions.cs
@@ -0,0 +1,46 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+using AdvancedSystems.Connector.Abstractions;
+
+namespace AdvancedSystems.Connector.Options;
+
+/// <summary>
+///     <inheritdoc cref="IDbSettings" path="/summary"/>
+/// </summary>
+public record DbOptions : IDbSettings
+{
+    public DbOptions(DbProvider dbProvider = DbProvider.Generic)
+    {
+        this.DbProvider = dbProvider;
+    }
+
+    #region Properties
+
+    [DisplayName("Application Name")]
+    public string? ApplicationName { get; set; }
+
+    [Required]
+    [DisplayName("Data Source")]
+    public string? DataSource { get; set; }
+
+    [Required]
+    [DisplayName("Initial Catalog")]
+    public string? InitialCatalog { get; set; }
+
+    [Required]
+    [DisplayName("Password")]
+    public string? Password { get; set; }
+
+    [Required]
+    [DisplayName("DB DbProvider")]
+    [JsonConverter(typeof(DbProvider))]
+    public DbProvider DbProvider { get; private set; }
+
+    [Required]
+    [DisplayName("User ID")]
+    public string? UserID { get; set; }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs b/AdvancedSystems.Connector/Options/MsSqlOptions.cs
similarity index 51%
rename from AdvancedSystems.Connector/Options/MsSqlServerSettings.cs
rename to AdvancedSystems.Connector/Options/MsSqlOptions.cs
index c5c8596..e80d1bd 100644
--- a/AdvancedSystems.Connector/Options/MsSqlServerSettings.cs
+++ b/AdvancedSystems.Connector/Options/MsSqlOptions.cs
@@ -1,65 +1,117 @@
 using System.ComponentModel;
 
+using AdvancedSystems.Connector.Abstractions;
+
 using Microsoft.Data.SqlClient;
 
 namespace AdvancedSystems.Connector.Options;
 
-public sealed class MsSqlServerSettings : DatabaseOptions
+public sealed record MsSqlOptions() : DbOptions(DbProvider.MsSql)
 {
     /// <summary>
     ///     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.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <c>30</c>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Command Timeout")]
-    public int CommandTimeout { get; set; }
+    public int CommandTimeout { get; set; } = 30;
 
     /// <summary>
     ///     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.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <c>15</c>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Connect Timeout")]
-    public int ConnectTimeout { get; set; }
+    public int ConnectTimeout { get; set; } = 15;
 
     /// <summary>
     ///     Gets or sets a <seealso cref="SqlConnectionEncryptOption"/> value that indicates whether
     ///     TLS encryption is required for all data sent between the client and server.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <seealso cref="SqlConnectionEncryptOption.Optional"/>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Encrypt")]
-    public SqlConnectionEncryptOption? Encrypt { get; set; }
+    public SqlConnectionEncryptOption? Encrypt { get; set; } = SqlConnectionEncryptOption.Optional;
 
     /// <summary>
     ///     Gets or sets a Boolean value that indicates whether User ID and Password are
     ///     specified in the connection (when false) or whether the current Windows account
     ///     credentials are used for authentication (when true).
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <see langword="false"/>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Integrated Security")]
-    public bool IntegratedSecurity { get; set; }
+    public bool IntegratedSecurity { get; set; } = false;
 
     /// <summary>
     ///     Gets or sets the maximum number of connections allowed in the connection pool for this specific
     ///     connection string.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <c>100</c>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Max Pool Size")]
-    public int MaxPoolSize { get; set; }
+    public int MaxPoolSize { get; set; } = 100;
 
     /// <summary>
     ///     Gets or sets the minimum number of connections allowed in the connection pool for this specific
     ///     connection string.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <c>0</c>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Min Pool Size")]
-    public int MinPoolSize { get; set; }
+    public int MinPoolSize { get; set; } = 0;
 
     /// <summary>
     ///     Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly
     ///     opened every time that the connection is requested.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <see langword="false"/>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Pooling")]
-    public bool Pooling { get; set; }
+    public bool Pooling { get; set; } = true;
 
     /// <summary>
     ///     Gets or sets a value that indicates whether the channel will be encrypted while bypassing
     ///     walking the certificate chain to validate trust.
     /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Defaults to <see langword="false"/>.
+    ///     </i>
+    /// </remarks>
     [DisplayName("Trust Server Certificate")]
-    public bool TrustServerCertificate { get; set; }
-}
+    public bool TrustServerCertificate { get; set; } = false;
+
+    /// <summary>
+    ///     <inheritdoc cref="DbOptions.DbProvider"/>
+    /// </summary>
+    /// <remarks>
+    ///     <i>
+    ///         Configured as <seealso cref="DbProvider.MsSql"/>.
+    ///     </i>
+    /// </remarks>
+    public new DbProvider DbProvider { get; private set; }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Options/Sections.cs b/AdvancedSystems.Connector/Options/Sections.cs
deleted file mode 100644
index 22cc1f8..0000000
--- a/AdvancedSystems.Connector/Options/Sections.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace AdvancedSystems.Connector.Options;
-
-public readonly record struct Sections
-{
-    public const string DATABASE = "Database";
-}
diff --git a/AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs b/AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs
deleted file mode 100644
index cc0d8b8..0000000
--- a/AdvancedSystems.Connector/Services/DatabaseConnectionFactory.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-
-using AdvancedSystems.Connector.Abstractions;
-
-using Microsoft.Extensions.DependencyInjection;
-
-namespace AdvancedSystems.Connector.Services;
-
-public sealed class DatabaseConnectionFactory : IDatabaseConnectionFactory
-{
-    private readonly IServiceProvider _serviceProvider;
-
-    public DatabaseConnectionFactory(IServiceProvider serviceProvider)
-    {
-        this._serviceProvider = serviceProvider;
-    }
-
-    public IDatabaseConnectionService Create(Provider provider)
-    {
-        return provider switch
-        {
-            Provider.MsSql => this._serviceProvider.GetRequiredService<MsSqlServerConnectionService>(),
-            _ => throw new NotSupportedException(Enum.GetName(provider)),
-        };
-    }
-}
diff --git a/AdvancedSystems.Connector/Services/DbConnectionService.cs b/AdvancedSystems.Connector/Services/DbConnectionService.cs
new file mode 100644
index 0000000..84915e4
--- /dev/null
+++ b/AdvancedSystems.Connector/Services/DbConnectionService.cs
@@ -0,0 +1,182 @@
+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.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;
+
+/// <inheritdoc cref="IDbConnectionService{T}"/>
+public sealed class DbConnectionService<T> : IDbConnectionService<T> where T : class, IDbSettings, new()
+{
+    private readonly ILogger<DbConnectionService<T>> _logger;
+    private readonly IDbConnectionStringFactory _dbConnectionStringFactory;
+    private readonly IDbConnectionFactory _dbConnectionFactory;
+    private readonly IDbCommandFactory _dbCommandFactory;
+    private readonly IDbDataAdapterFactory _dataAdapterFactory;
+    private readonly string _connectionString;
+
+    public DbConnectionService(ILogger<DbConnectionService<T>> logger, IOptions<T> options, IDbConnectionStringFactory dbConnectionStringFactory, IDbConnectionFactory dbConnectionFactory, IDbCommandFactory dbCommandFactory, IDbDataAdapterFactory dbDataAdapterFactory)
+    {
+        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
+
+    public ConnectionState ConnectionState { get; private set; }
+
+    public T Options { get; private set; }
+
+    #endregion
+
+    #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();
+        using DbDataAdapter adapter = this._dataAdapterFactory.Create(command);
+        adapter.Fill(result);
+
+        return result;
+    }
+
+    public async ValueTask<DataSet?> 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();
+        using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
+        result.Load(reader);
+
+        return result.DataSet;
+    }
+
+    public int ExecuteNonQuery(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);
+        }
+
+        int rowsAffected = command.ExecuteNonQuery();
+
+        return rowsAffected;
+    }
+
+    public async ValueTask<int> ExecuteNonQueryAsync(IDbCommand databaseCommand, CancellationToken cancellationToken = default)
+    {
+        using DbConnection connection = await this.InitializeConnectionAsync(cancellationToken);
+
+        using DbCommand command = connection.CreateCommand();
+        command.CommandText = databaseCommand.CommandText;
+        command.CommandType = databaseCommand.CommandType;
+
+        foreach (IDbParameter dbParameter in databaseCommand.Parameters)
+        {
+            DbParameter parameter = dbParameter.DeriveFrom(command);
+            command.Parameters.Add(parameter);
+        }
+
+        int rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken);
+
+        return rowsAffected;
+    }
+
+    #endregion
+
+    #region Helpers
+
+    private void ConnectionStateHandler(object sender, StateChangeEventArgs e)
+    {
+        this.ConnectionState = e.CurrentState;
+    }
+
+    private DbConnection InitializeConnection()
+    {
+        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);
+        }
+    }
+
+    private async ValueTask<DbConnection> InitializeConnectionAsync(CancellationToken cancellationToken)
+    {
+        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);
+        }
+    }
+
+    #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..7a9315f
--- /dev/null
+++ b/AdvancedSystems.Connector/Services/DbConnectionServiceFactory.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using AdvancedSystems.Connector.Abstractions;
+using AdvancedSystems.Connector.Abstractions.Exceptions;
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace AdvancedSystems.Connector.Services;
+
+/// <inheritdoc cref="IDbConnectionServiceFactory{T, U}"/>
+public sealed class DbConnectionServiceFactory<T, U> : IDbConnectionServiceFactory<T, U> where T : class, IDbFactorySettings<U>, new() where U : class, IDbSettings, new()
+{
+    private bool _isDisposed = false;
+
+    private readonly ILogger<DbConnectionServiceFactory<T, U>> _logger;
+    private readonly List<U> _dbConnectionOptions;
+    private readonly ILoggerFactory _loggerFactory;
+    private readonly IServiceProvider _serviceProvider;
+    private readonly List<IDbConnectionService<U>> _dbConnectionServices;
+
+    public DbConnectionServiceFactory(ILogger<DbConnectionServiceFactory<T, U>> logger, ILoggerFactory loggerFactory, IOptions<T> 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()];
+    }
+
+    #region Properties
+
+    public T Options { get; private set; }
+
+    #endregion
+
+    #region Methods
+
+    public void Dispose()
+    {
+        this.Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    public IDbConnectionService<U> GetConnection(string dbUser)
+    {
+        IDbConnectionService<U>? 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<U> GetConnection(Predicate<U> predicate)
+    {
+        IDbConnectionService<U>? dbConnectionService = this._dbConnectionServices.FirstOrDefault(x => predicate(x.Options));
+        return dbConnectionService ?? throw new DbConnectionException("Failed to retrieve connection from factory (predicate).");
+    }
+
+    #endregion
+
+    #region Helpers
+
+    private IEnumerable<IDbConnectionService<U>> Create()
+    {
+        ILogger<DbConnectionService<U>> logger = this._loggerFactory.CreateLogger<DbConnectionService<U>>();
+
+        foreach (U dbOption in this._dbConnectionOptions)
+        {
+            U dbSettings = dbOption;
+            IOptions<U> connectionOptions = Microsoft.Extensions.Options.Options.Create(dbSettings);
+
+            IDbConnectionStringFactory dbConnectionStringFactory = this._serviceProvider.GetRequiredService<IDbConnectionStringFactory>();
+            IDbConnectionFactory dbConnectionFactory = this._serviceProvider.GetRequiredService<IDbConnectionFactory>();
+            IDbCommandFactory dbCommandFactory = this._serviceProvider.GetRequiredService<IDbCommandFactory>();
+            IDbDataAdapterFactory dbDataAdapterFactory = this._serviceProvider.GetRequiredService<IDbDataAdapterFactory>();
+
+            this._logger.LogDebug("Building {ConnectionString}.", dbConnectionStringFactory.Create(dbSettings.DbProvider, maskPassword: true));
+
+            yield return new DbConnectionService<U>(
+                logger,
+                connectionOptions,
+                dbConnectionStringFactory,
+                dbConnectionFactory,
+                dbCommandFactory,
+                dbDataAdapterFactory
+            );
+        }
+    }
+
+    private void Dispose(bool disposing)
+    {
+        if (this._isDisposed) return;
+
+        if (disposing)
+        {
+            this._logger.LogTrace("Disposing {Service}.", nameof(DbConnectionServiceFactory<T, U>));
+
+            this._dbConnectionOptions.Clear();
+            this._dbConnectionServices.Clear();
+        }
+
+        this._isDisposed = true;
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs b/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs
deleted file mode 100644
index 11b8d93..0000000
--- a/AdvancedSystems.Connector/Services/MsSqlServerConnectionService.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-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.Options;
-
-using Microsoft.Data.SqlClient;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-
-namespace AdvancedSystems.Connector.Services;
-
-public sealed class MsSqlServerConnectionService : IDatabaseConnectionService
-{
-    private readonly ILogger<MsSqlServerConnectionService> _logger;
-    private readonly MsSqlServerSettings _settings;
-
-    public MsSqlServerConnectionService(ILogger<MsSqlServerConnectionService> logger, IOptions<MsSqlServerSettings> options)
-    {
-        this._logger = logger;
-        this._settings = options.Value;
-
-        this.ConnectionString = this._settings.CreateConnectionString();
-    }
-
-    #region Properties
-
-    public string ConnectionString { get; private set; }
-
-    public ConnectionState ConnectionState { get; private set; }
-
-    #endregion
-
-    #region Helpers
-
-    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<IDatabaseCommand> 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<IDatabaseCommand, CancellationToken, ValueTask> 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 Methods
-
-    public DataSet ExecuteQuery(IDatabaseCommand databaseCommand)
-    {
-        DataSet result = new();
-
-        this.InvokeExceptionHandler((databaseCommand) =>
-        {
-            using var connection = new SqlConnection(this.ConnectionString);
-            connection.StateChange += ConnectionStateHandler;
-            connection.InfoMessage += InfoMessageHandler;
-            connection.Open();
-
-            using var sqlCommand = connection.CreateCommand();
-            sqlCommand.CommandText = databaseCommand.CommandText;
-            sqlCommand.CommandType = databaseCommand.CommandType.Cast();
-
-            foreach (var parameter in databaseCommand.Parameters)
-            {
-                sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value);
-            }
-
-            using var adapter = new SqlDataAdapter(sqlCommand);
-            adapter.Fill(result);
-        }, databaseCommand);
-
-        return result;
-    }
-
-    public async ValueTask<DataSet?> ExecuteQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default)
-    {
-        DataTable result = new();
-
-        await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) =>
-        {
-            using var connection = new SqlConnection(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();
-
-            foreach (var parameter in databaseCommand.Parameters)
-            {
-                sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value);
-            }
-
-            using var reader = await sqlCommand.ExecuteReaderAsync(cancellationToken);
-            result.Load(reader);
-        }, databaseCommand, cancellationToken);
-
-        return result.DataSet;
-    }
-
-    public int ExecuteNonQuery(IDatabaseCommand databaseCommand)
-    {
-        int rowsAffected = default;
-
-        this.InvokeExceptionHandler((databaseCommand) =>
-        {
-            using var connection = new SqlConnection(this.ConnectionString);
-            connection.StateChange += ConnectionStateHandler;
-            connection.InfoMessage += InfoMessageHandler;
-            connection.Open();
-
-            using var sqlCommand = connection.CreateCommand();
-            sqlCommand.CommandText = databaseCommand.CommandText;
-            sqlCommand.CommandType = databaseCommand.CommandType.Cast();
-
-            foreach (var parameter in databaseCommand.Parameters)
-            {
-                sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value);
-            }
-
-            rowsAffected = sqlCommand.ExecuteNonQuery();
-        }, databaseCommand);
-
-        return rowsAffected;
-    }
-
-    public async ValueTask<int> ExecuteNonQueryAsync(IDatabaseCommand databaseCommand, CancellationToken cancellationToken = default)
-    {
-        int rowsAffected = default;
-
-        await this.InvokeExceptionHandlerAsync(async (databaseCommand, cancellationToken) =>
-        {
-            using var connection = new SqlConnection(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();
-
-            foreach (var parameter in databaseCommand.Parameters)
-            {
-                sqlCommand.Parameters.AddWithValue(parameter.ParameterName, parameter.Value);
-            }
-
-            rowsAffected = await sqlCommand.ExecuteNonQueryAsync(cancellationToken);
-        }, databaseCommand, cancellationToken);
-
-        return rowsAffected;
-    }
-
-    #endregion
-}
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 @@
+<svg width="90" height="45" viewBox="0 0 23.812 11.906" version="1.1" id="svg1231" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><defs id="defs1225"><path id="rect972" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect878" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect918" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect950" d="M92.604-126.333h317.5V19.188h-317.5z"/><linearGradient id="linearGradient876"><stop style="stop-color:#0f0f0f;stop-opacity:.9921568" offset="0" id="stop872"/><stop style="stop-color:#191919;stop-opacity:.99215686" offset="1" id="stop874"/></linearGradient><linearGradient xlink:href="#linearGradient876" id="linearGradient954" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.656 389.783)" x1="238.125" y1="56.229" x2="171.979" y2="-7.271"/></defs><g id="layer5" style="display:inline" transform="matrix(.85368 0 0 .85368 -149.822 -289.622)"><g id="g950" transform="translate(58.37 108.295) scale(.68333)"><g id="g942"><path id="path934" style="fill:#641220;fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M206.11 348.21s0 3.175-3.175 3.175h-7.937v-3.175h7.937z"/><path style="fill:#1f1f1f;fill-opacity:.992157;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M206.11 356.147h-3.175c0-12.7-7.937-12.7-7.937-12.7v-3.175s11.112 0 11.112 15.875z" id="path936"/><path style="fill:#641220;fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M207.698 356.147h-2.54c0-12.7-10.16-12.7-10.16-12.7v-3.175s12.7 0 12.7 15.875z" id="path938"/></g><path id="path932" style="display:inline;fill:#202020;fill-opacity:.992157;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M188.648 340.272v10.478l-5.239-5.24-1.587-1.587-2.245 2.245 1.587 1.588 5.216 5.216h-10.432v3.175h15.875v-15.875z"/><path style="fill:url(#linearGradient954);fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m181.885 343.987 6.826 6.826.03 2.216-2.297.007-6.804-6.804z" id="path940"/></g></g><script id="mesh_polyfill" type="text/javascript">!function(){const t=&quot;http://www.w3.org/2000/svg&quot;,e=&quot;http://www.w3.org/1999/xlink&quot;,s=&quot;http://www.w3.org/1999/xhtml&quot;,r=2;if(document.createElementNS(t,&quot;meshgradient&quot;).x)return;const n=(t,e,s,r)=&gt;{let n=new x(.5*(e.x+s.x),.5*(e.y+s.y)),o=new x(.5*(t.x+e.x),.5*(t.y+e.y)),i=new x(.5*(s.x+r.x),.5*(s.y+r.y)),a=new x(.5*(n.x+o.x),.5*(n.y+o.y)),h=new x(.5*(n.x+i.x),.5*(n.y+i.y)),l=new x(.5*(a.x+h.x),.5*(a.y+h.y));return[[t,o,a,l],[l,h,i,r]]},o=t=&gt;{let e=t[0].distSquared(t[1]),s=t[2].distSquared(t[3]),r=.25*t[0].distSquared(t[2]),n=.25*t[1].distSquared(t[3]),o=e&gt;s?e:s,i=r&gt;n?r:n;return 18*(o&gt;i?o:i)},i=(t,e)=&gt;Math.sqrt(t.distSquared(e)),a=(t,e)=&gt;t.scale(2/3).add(e.scale(1/3)),h=t=&gt;{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=&gt;{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case&quot;translate&quot;:2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error(&quot;mesh.js: translate does not have 2 arguments!&quot;),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case&quot;scale&quot;:1===l.length?s=new g(l[0],0,0,l[0],0,0):2===l.length?s=new g(l[0],0,0,l[1],0,0):(console.error(&quot;mesh.js: scale does not have 1 or 2 arguments!&quot;),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case&quot;rotate&quot;:if(3===l.length&amp;&amp;(e=new g(1,0,0,1,l[1],l[2]),h=h.append(e)),l[0]){r=l[0]*Math.PI/180;let t=Math.cos(r),e=Math.sin(r);Math.abs(t)&lt;1e-16&amp;&amp;(t=0),Math.abs(e)&lt;1e-16&amp;&amp;(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error(&quot;math.js: No argument to rotate transform!&quot;);3===l.length&amp;&amp;(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case&quot;skewX&quot;:l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),o=new g(1,0,n,1,0,0),h=h.append(o)):console.error(&quot;math.js: No argument to skewX transform!&quot;);break;case&quot;skewY&quot;:l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),i=new g(1,n,0,1,0,0),h=h.append(i)):console.error(&quot;math.js: No argument to skewY transform!&quot;);break;case&quot;matrix&quot;:6===l.length?h=h.append(new g(...l)):console.error(&quot;math.js: Incorrect number of arguments for matrix!&quot;);break;default:console.error(&quot;mesh.js: Unhandled transform type: &quot;+d)}}),h},l=t=&gt;{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t&lt;r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=&gt;{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=&gt;{let o,i,a=[0,0,0,0];for(let h=0;h&lt;3;++h)e[h]&lt;t[h]&amp;&amp;e[h]&lt;s[h]||t[h]&lt;e[h]&amp;&amp;s[h]&lt;e[h]?a[h]=0:(a[h]=.5*((e[h]-t[h])/r+(s[h]-e[h])/n),o=Math.abs(3*(e[h]-t[h])/r),i=Math.abs(3*(s[h]-e[h])/n),a[h]&gt;o?a[h]=o:a[h]&gt;i&amp;&amp;(a[h]=i));return a},u=[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],[-3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0],[2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0],[0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0],[-3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0],[0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0],[9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1],[-6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1],[2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0],[0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0],[-6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1],[4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1]],f=t=&gt;{let e=[];for(let s=0;s&lt;16;++s){e[s]=0;for(let r=0;r&lt;16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=&gt;{const r=e*e,n=s*s,o=e*e*e,i=s*s*s;return t[0]+t[1]*e+t[2]*r+t[3]*o+t[4]*s+t[5]*s*e+t[6]*s*r+t[7]*s*o+t[8]*n+t[9]*n*e+t[10]*n*r+t[11]*n*o+t[12]*i+t[13]*i*e+t[14]*i*r+t[15]*i*o},y=t=&gt;{let e=[],s=[],r=[];for(let s=0;s&lt;4;++s)e[s]=[],e[s][0]=n(t[0][s],t[1][s],t[2][s],t[3][s]),e[s][1]=[],e[s][1].push(...n(...e[s][0][0])),e[s][1].push(...n(...e[s][0][1])),e[s][2]=[],e[s][2].push(...n(...e[s][1][0])),e[s][2].push(...n(...e[s][1][1])),e[s][2].push(...n(...e[s][1][2])),e[s][2].push(...n(...e[s][1][3]));for(let t=0;t&lt;8;++t){s[t]=[];for(let r=0;r&lt;4;++r)s[t][r]=[],s[t][r][0]=n(e[0][2][t][r],e[1][2][t][r],e[2][2][t][r],e[3][2][t][r]),s[t][r][1]=[],s[t][r][1].push(...n(...s[t][r][0][0])),s[t][r][1].push(...n(...s[t][r][0][1])),s[t][r][2]=[],s[t][r][2].push(...n(...s[t][r][1][0])),s[t][r][2].push(...n(...s[t][r][1][1])),s[t][r][2].push(...n(...s[t][r][1][2])),s[t][r][2].push(...n(...s[t][r][1][3]))}for(let t=0;t&lt;8;++t){r[t]=[];for(let e=0;e&lt;8;++e)r[t][e]=[],r[t][e][0]=s[t][0][2][e],r[t][e][1]=s[t][1][2][e],r[t][e][2]=s[t][2][2][e],r[t][e][3]=s[t][3][2][e]}return r};class x{constructor(t,e){this.x=t||0,this.y=e||0}toString(){return`(x=${this.x}, y=${this.y})`}clone(){return new x(this.x,this.y)}add(t){return new x(this.x+t.x,this.y+t.y)}scale(t){return void 0===t.x?new x(this.x*t,this.y*t):new x(this.x*t.x,this.y*t.y)}distSquared(t){let e=this.x-t.x,s=this.y-t.y;return e*e+s*s}transform(t){let e=this.x*t.a+this.y*t.c+t.e,s=this.x*t.b+this.y*t.d+t.f;return new x(e,s)}}class g{constructor(t,e,s,r,n,o){void 0===t?(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0):(this.a=t,this.b=e,this.c=s,this.d=r,this.e=n,this.f=o)}toString(){return`affine: ${this.a} ${this.c} ${this.e} \n       ${this.b} ${this.d} ${this.f}`}append(t){t instanceof g||console.error(&quot;mesh.js: argument to Affine.append is not affine!&quot;);let e=this.a*t.a+this.c*t.b,s=this.b*t.a+this.d*t.b,r=this.a*t.c+this.c*t.d,n=this.b*t.c+this.d*t.d,o=this.a*t.e+this.c*t.f+this.e,i=this.b*t.e+this.d*t.f+this.f;return new g(e,s,r,n,o,i)}}class w{constructor(t,e){this.nodes=t,this.colors=e}paintCurve(t,e){if(o(this.nodes)&gt;r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t&lt;4;++t)r[0][t]=this.colors[0][t],r[1][t]=(this.colors[0][t]+this.colors[1][t])/2,o[0][t]=r[1][t],o[1][t]=this.colors[1][t];let i=new w(s[0],r),a=new w(s[1],o);i.paintCurve(t,e),a.paintCurve(t,e)}else{let s=Math.round(this.nodes[0].x);if(s&gt;=0&amp;&amp;s&lt;e){let r=4*(~~this.nodes[0].y*e+s);t[r]=Math.round(this.colors[0][0]),t[r+1]=Math.round(this.colors[0][1]),t[r+2]=Math.round(this.colors[0][2]),t[r+3]=Math.round(this.colors[0][3])}}}}class m{constructor(t,e){this.nodes=t,this.colors=e}split(){let t=[[],[],[],[]],e=[[],[],[],[]],s=[[[],[]],[[],[]]],r=[[[],[]],[[],[]]];for(let s=0;s&lt;4;++s){const r=n(this.nodes[0][s],this.nodes[1][s],this.nodes[2][s],this.nodes[3][s]);t[0][s]=r[0][0],t[1][s]=r[0][1],t[2][s]=r[0][2],t[3][s]=r[0][3],e[0][s]=r[1][0],e[1][s]=r[1][1],e[2][s]=r[1][2],e[3][s]=r[1][3]}for(let t=0;t&lt;4;++t)s[0][0][t]=this.colors[0][0][t],s[0][1][t]=this.colors[0][1][t],s[1][0][t]=(this.colors[0][0][t]+this.colors[1][0][t])/2,s[1][1][t]=(this.colors[0][1][t]+this.colors[1][1][t])/2,r[0][0][t]=s[1][0][t],r[0][1][t]=s[1][1][t],r[1][0][t]=this.colors[1][0][t],r[1][1][t]=this.colors[1][1][t];return[new m(t,s),new m(e,r)]}paint(t,e){let s,n=!1;for(let t=0;t&lt;4;++t)if((s=o([this.nodes[0][t],this.nodes[1][t],this.nodes[2][t],this.nodes[3][t]]))&gt;r){n=!0;break}if(n){let s=this.split();s[0].paint(t,e),s[1].paint(t,e)}else{new w([...this.nodes[0]],[...this.colors[0]]).paintCurve(t,e)}}}class b{constructor(t){this.readMesh(t),this.type=t.getAttribute(&quot;type&quot;)||&quot;bilinear&quot;}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute(&quot;x&quot;)),n=Number(t.getAttribute(&quot;y&quot;));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t&lt;r;++t){e[3*t+1]=[],e[3*t+2]=[],e[3*t+3]=[],s[t+1]=[];let r=o[t].children;for(let n=0,o=r.length;n&lt;o;++n){let o=r[n].children;for(let r=0,i=o.length;r&lt;i;++r){let i=r;0!==t&amp;&amp;++i;let h,d=o[r].getAttribute(&quot;path&quot;),c=&quot;l&quot;;null!=d&amp;&amp;(c=(h=d.match(/\s*([lLcC])\s*(.*)/))[1]);let u=l(h[2]);switch(c){case&quot;l&quot;:0===i?(e[3*t][3*n+3]=u[0].add(e[3*t][3*n]),e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&amp;&amp;(e[3*t+3][3*n+0]=u[0].add(e[3*t+3][3*n+3])),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case&quot;L&quot;:0===i?(e[3*t][3*n+3]=u[0],e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0],e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&amp;&amp;(e[3*t+3][3*n+0]=u[0]),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case&quot;c&quot;:0===i?(e[3*t][3*n+1]=u[0].add(e[3*t][3*n]),e[3*t][3*n+2]=u[1].add(e[3*t][3*n]),e[3*t][3*n+3]=u[2].add(e[3*t][3*n])):1===i?(e[3*t+1][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+2][3*n+3]=u[1].add(e[3*t][3*n+3]),e[3*t+3][3*n+3]=u[2].add(e[3*t][3*n+3])):2===i?(e[3*t+3][3*n+2]=u[0].add(e[3*t+3][3*n+3]),e[3*t+3][3*n+1]=u[1].add(e[3*t+3][3*n+3]),0===n&amp;&amp;(e[3*t+3][3*n+0]=u[2].add(e[3*t+3][3*n+3]))):(e[3*t+2][3*n]=u[0].add(e[3*t+3][3*n]),e[3*t+1][3*n]=u[1].add(e[3*t+3][3*n]));break;case&quot;C&quot;:0===i?(e[3*t][3*n+1]=u[0],e[3*t][3*n+2]=u[1],e[3*t][3*n+3]=u[2]):1===i?(e[3*t+1][3*n+3]=u[0],e[3*t+2][3*n+3]=u[1],e[3*t+3][3*n+3]=u[2]):2===i?(e[3*t+3][3*n+2]=u[0],e[3*t+3][3*n+1]=u[1],0===n&amp;&amp;(e[3*t+3][3*n+0]=u[2])):(e[3*t+2][3*n]=u[0],e[3*t+1][3*n]=u[1]);break;default:console.error(&quot;mesh.js: &quot;+c+&quot; invalid path type.&quot;)}if(0===t&amp;&amp;0===n||r&gt;0){let e=window.getComputedStyle(o[r]).stopColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i),a=window.getComputedStyle(o[r]).stopOpacity,h=255;a&amp;&amp;(h=Math.floor(255*a)),e&amp;&amp;(0===i?(s[t][n]=[],s[t][n][0]=Math.floor(e[1]),s[t][n][1]=Math.floor(e[2]),s[t][n][2]=Math.floor(e[3]),s[t][n][3]=h):1===i?(s[t][n+1]=[],s[t][n+1][0]=Math.floor(e[1]),s[t][n+1][1]=Math.floor(e[2]),s[t][n+1][2]=Math.floor(e[3]),s[t][n+1][3]=h):2===i?(s[t+1][n+1]=[],s[t+1][n+1][0]=Math.floor(e[1]),s[t+1][n+1][1]=Math.floor(e[2]),s[t+1][n+1][2]=Math.floor(e[3]),s[t+1][n+1][3]=h):3===i&amp;&amp;(s[t+1][n]=[],s[t+1][n][0]=Math.floor(e[1]),s[t+1][n][1]=Math.floor(e[2]),s[t+1][n][2]=Math.floor(e[3]),s[t+1][n][3]=h))}}e[3*t+1][3*n+1]=new x,e[3*t+1][3*n+2]=new x,e[3*t+2][3*n+1]=new x,e[3*t+2][3*n+2]=new x,e[3*t+1][3*n+1].x=(-4*e[3*t][3*n].x+6*(e[3*t][3*n+1].x+e[3*t+1][3*n].x)+-2*(e[3*t][3*n+3].x+e[3*t+3][3*n].x)+3*(e[3*t+3][3*n+1].x+e[3*t+1][3*n+3].x)+-1*e[3*t+3][3*n+3].x)/9,e[3*t+1][3*n+2].x=(-4*e[3*t][3*n+3].x+6*(e[3*t][3*n+2].x+e[3*t+1][3*n+3].x)+-2*(e[3*t][3*n].x+e[3*t+3][3*n+3].x)+3*(e[3*t+3][3*n+2].x+e[3*t+1][3*n].x)+-1*e[3*t+3][3*n].x)/9,e[3*t+2][3*n+1].x=(-4*e[3*t+3][3*n].x+6*(e[3*t+3][3*n+1].x+e[3*t+2][3*n].x)+-2*(e[3*t+3][3*n+3].x+e[3*t][3*n].x)+3*(e[3*t][3*n+1].x+e[3*t+2][3*n+3].x)+-1*e[3*t][3*n+3].x)/9,e[3*t+2][3*n+2].x=(-4*e[3*t+3][3*n+3].x+6*(e[3*t+3][3*n+2].x+e[3*t+2][3*n+3].x)+-2*(e[3*t+3][3*n].x+e[3*t][3*n+3].x)+3*(e[3*t][3*n+2].x+e[3*t+2][3*n].x)+-1*e[3*t][3*n].x)/9,e[3*t+1][3*n+1].y=(-4*e[3*t][3*n].y+6*(e[3*t][3*n+1].y+e[3*t+1][3*n].y)+-2*(e[3*t][3*n+3].y+e[3*t+3][3*n].y)+3*(e[3*t+3][3*n+1].y+e[3*t+1][3*n+3].y)+-1*e[3*t+3][3*n+3].y)/9,e[3*t+1][3*n+2].y=(-4*e[3*t][3*n+3].y+6*(e[3*t][3*n+2].y+e[3*t+1][3*n+3].y)+-2*(e[3*t][3*n].y+e[3*t+3][3*n+3].y)+3*(e[3*t+3][3*n+2].y+e[3*t+1][3*n].y)+-1*e[3*t+3][3*n].y)/9,e[3*t+2][3*n+1].y=(-4*e[3*t+3][3*n].y+6*(e[3*t+3][3*n+1].y+e[3*t+2][3*n].y)+-2*(e[3*t+3][3*n+3].y+e[3*t][3*n].y)+3*(e[3*t][3*n+1].y+e[3*t+2][3*n+3].y)+-1*e[3*t][3*n+3].y)/9,e[3*t+2][3*n+2].y=(-4*e[3*t+3][3*n+3].y+6*(e[3*t+3][3*n+2].y+e[3*t+2][3*n+3].y)+-2*(e[3*t+3][3*n].y+e[3*t][3*n+3].y)+3*(e[3*t][3*n+2].y+e[3*t+2][3*n].y)+-1*e[3*t][3*n].y)/9}}this.nodes=e,this.colors=s}paintMesh(t,e){let s=(this.nodes.length-1)/3,r=(this.nodes[0].length-1)/3;if(&quot;bilinear&quot;===this.type||s&lt;2||r&lt;2){let n;for(let o=0;o&lt;s;++o)for(let s=0;s&lt;r;++s){let r=[];for(let t=3*o,e=3*o+4;t&lt;e;++t)r.push(this.nodes[t].slice(3*s,3*s+4));let i=[];i.push(this.colors[o].slice(s,s+2)),i.push(this.colors[o+1].slice(s,s+2)),(n=new m(r,i)).paint(t,e)}}else{let n,o,a,h,l,d,u;const x=s,g=r;s++,r++;let w=new Array(s);for(let t=0;t&lt;s;++t){w[t]=new Array(r);for(let e=0;e&lt;r;++e)w[t][e]=[],w[t][e][0]=this.nodes[3*t][3*e],w[t][e][1]=this.colors[t][e]}for(let t=0;t&lt;s;++t)for(let e=0;e&lt;r;++e)0!==t&amp;&amp;t!==x&amp;&amp;(n=i(w[t-1][e][0],w[t][e][0]),o=i(w[t+1][e][0],w[t][e][0]),w[t][e][2]=c(w[t-1][e][1],w[t][e][1],w[t+1][e][1],n,o)),0!==e&amp;&amp;e!==g&amp;&amp;(n=i(w[t][e-1][0],w[t][e][0]),o=i(w[t][e+1][0],w[t][e][0]),w[t][e][3]=c(w[t][e-1][1],w[t][e][1],w[t][e+1][1],n,o));for(let t=0;t&lt;r;++t){w[0][t][2]=[],w[x][t][2]=[];for(let e=0;e&lt;4;++e)n=i(w[1][t][0],w[0][t][0]),o=i(w[x][t][0],w[x-1][t][0]),w[0][t][2][e]=n&gt;0?2*(w[1][t][1][e]-w[0][t][1][e])/n-w[1][t][2][e]:0,w[x][t][2][e]=o&gt;0?2*(w[x][t][1][e]-w[x-1][t][1][e])/o-w[x-1][t][2][e]:0}for(let t=0;t&lt;s;++t){w[t][0][3]=[],w[t][g][3]=[];for(let e=0;e&lt;4;++e)n=i(w[t][1][0],w[t][0][0]),o=i(w[t][g][0],w[t][g-1][0]),w[t][0][3][e]=n&gt;0?2*(w[t][1][1][e]-w[t][0][1][e])/n-w[t][1][3][e]:0,w[t][g][3][e]=o&gt;0?2*(w[t][g][1][e]-w[t][g-1][1][e])/o-w[t][g-1][3][e]:0}for(let s=0;s&lt;x;++s)for(let r=0;r&lt;g;++r){let n=i(w[s][r][0],w[s+1][r][0]),o=i(w[s][r+1][0],w[s+1][r+1][0]),c=i(w[s][r][0],w[s][r+1][0]),x=i(w[s+1][r][0],w[s+1][r+1][0]),g=[[],[],[],[]];for(let t=0;t&lt;4;++t){(d=[])[0]=w[s][r][1][t],d[1]=w[s+1][r][1][t],d[2]=w[s][r+1][1][t],d[3]=w[s+1][r+1][1][t],d[4]=w[s][r][2][t]*n,d[5]=w[s+1][r][2][t]*n,d[6]=w[s][r+1][2][t]*o,d[7]=w[s+1][r+1][2][t]*o,d[8]=w[s][r][3][t]*c,d[9]=w[s+1][r][3][t]*x,d[10]=w[s][r+1][3][t]*c,d[11]=w[s+1][r+1][3][t]*x,d[12]=0,d[13]=0,d[14]=0,d[15]=0,u=f(d);for(let e=0;e&lt;9;++e){g[t][e]=[];for(let s=0;s&lt;9;++s)g[t][e][s]=p(u,e/8,s/8),g[t][e][s]&gt;255?g[t][e][s]=255:g[t][e][s]&lt;0&amp;&amp;(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t&lt;e;++t)h.push(this.nodes[t].slice(3*r,3*r+4));l=y(h);for(let s=0;s&lt;8;++s)for(let r=0;r&lt;8;++r)(a=new m(l[s][r],[[[g[0][s][r],g[1][s][r],g[2][s][r],g[3][s][r]],[g[0][s][r+1],g[1][s][r+1],g[2][s][r+1],g[3][s][r+1]]],[[g[0][s+1][r],g[1][s+1][r],g[2][s+1][r],g[3][s+1][r]],[g[0][s+1][r+1],g[1][s+1][r+1],g[2][s+1][r+1],g[3][s+1][r+1]]]])).paint(t,e)}}}transform(t){if(t instanceof x)for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].add(t);else if(t instanceof g)for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].transform(t)}scale(t){for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].scale(t)}}document.querySelectorAll(&quot;rect,circle,ellipse,path,text&quot;).forEach((r,n)=&gt;{let o=r.getAttribute(&quot;id&quot;);o||(o=&quot;patchjs_shape&quot;+n,r.setAttribute(&quot;id&quot;,o));const i=r.style.fill.match(/^url\(\s*&quot;?\s*#([^\s&quot;]+)&quot;?\s*\)/),a=r.style.stroke.match(/^url\(\s*&quot;?\s*#([^\s&quot;]+)&quot;?\s*\)/);if(i&amp;&amp;i[1]){const a=document.getElementById(i[1]);if(a&amp;&amp;&quot;meshgradient&quot;===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,&quot;canvas&quot;);d(l,{width:i.width,height:i.height});const c=l.getContext(&quot;2d&quot;);let u=c.createImageData(i.width,i.height);const f=new b(a);&quot;objectBoundingBox&quot;===a.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;f.scale(new x(i.width,i.height));const p=a.getAttribute(&quot;gradientTransform&quot;);null!=p&amp;&amp;f.transform(h(p)),&quot;userSpaceOnUse&quot;===a.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,&quot;image&quot;);d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,&quot;xlink:href&quot;,g),r.parentNode.insertBefore(y,r),r.style.fill=&quot;none&quot;;const w=document.createElementNS(t,&quot;use&quot;);w.setAttributeNS(e,&quot;xlink:href&quot;,&quot;#&quot;+o);const m=&quot;patchjs_clip&quot;+n,M=document.createElementNS(t,&quot;clipPath&quot;);M.setAttribute(&quot;id&quot;,m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute(&quot;clip-path&quot;,&quot;url(#&quot;+m+&quot;)&quot;),u=null,l=null,g=null}}if(a&amp;&amp;a[1]){const o=document.getElementById(a[1]);if(o&amp;&amp;&quot;meshgradient&quot;===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute(&quot;stroke-miterlimit&quot;))||1),a=r.getBBox(),l=Math.trunc(a.width+i),c=Math.trunc(a.height+i),u=Math.trunc(a.x-i/2),f=Math.trunc(a.y-i/2);let p=document.createElementNS(s,&quot;canvas&quot;);d(p,{width:l,height:c});const y=p.getContext(&quot;2d&quot;);let g=y.createImageData(l,c);const w=new b(o);&quot;objectBoundingBox&quot;===o.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;w.scale(new x(l,c));const m=o.getAttribute(&quot;gradientTransform&quot;);null!=m&amp;&amp;w.transform(h(m)),&quot;userSpaceOnUse&quot;===o.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,&quot;image&quot;);d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,&quot;xlink:href&quot;,S);const k=&quot;pattern_clip&quot;+n,A=document.createElementNS(t,&quot;pattern&quot;);d(A,{id:k,patternUnits:&quot;userSpaceOnUse&quot;,width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke=&quot;url(#&quot;+k+&quot;)&quot;,g=null,p=null,S=null}}})}();</script></svg>
\ 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 @@
+<svg width="128" height="128" viewBox="0 0 33.867 33.867" version="1.1" id="svg1231" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><defs id="defs1225"><path id="rect972" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect878" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect918" d="M92.604-126.333h317.5V19.188h-317.5z"/><path id="rect950" d="M92.604-126.333h317.5V19.188h-317.5z"/><linearGradient id="linearGradient876"><stop style="stop-color:#0f0f0f;stop-opacity:.9921568" offset="0" id="stop872"/><stop style="stop-color:#191919;stop-opacity:.99215686" offset="1" id="stop874"/></linearGradient><linearGradient xlink:href="#linearGradient876" id="linearGradient954" gradientUnits="userSpaceOnUse" gradientTransform="translate(43.656 389.783)" x1="238.125" y1="56.229" x2="171.979" y2="-7.271"/></defs><g id="layer5" style="display:inline" transform="translate(-175.954 -339.492)"><g id="g950" transform="translate(1.065 8.215)"><g id="g942"><path id="path934" style="fill:#641220;fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M206.11 348.21s0 3.175-3.175 3.175H194.998v-3.175H202.935z"/><path style="fill:#1f1f1f;fill-opacity:.992157;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M206.11 356.147h-3.175c0-12.7-7.937-12.7-7.937-12.7v-3.175s11.112 0 11.112 15.875z" id="path936"/><path style="fill:#641220;fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M207.698 356.147h-2.54c0-12.7-10.16-12.7-10.16-12.7v-3.175s12.7 0 12.7 15.875z" id="path938"/></g><path id="path932" style="display:inline;fill:#202020;fill-opacity:.992157;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M188.648 340.272v10.478l-5.239-5.24-1.587-1.587-2.245 2.245 1.587 1.588 5.216 5.216h-10.432v3.175h15.875v-15.875z"/><path style="fill:url(#linearGradient954);fill-opacity:1;stroke:none;stroke-width:.03175px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m181.885 343.987 6.826 6.826.03 2.216-2.297.007-6.804-6.804z" id="path940"/></g></g><script id="mesh_polyfill" type="text/javascript">!function(){const t=&quot;http://www.w3.org/2000/svg&quot;,e=&quot;http://www.w3.org/1999/xlink&quot;,s=&quot;http://www.w3.org/1999/xhtml&quot;,r=2;if(document.createElementNS(t,&quot;meshgradient&quot;).x)return;const n=(t,e,s,r)=&gt;{let n=new x(.5*(e.x+s.x),.5*(e.y+s.y)),o=new x(.5*(t.x+e.x),.5*(t.y+e.y)),i=new x(.5*(s.x+r.x),.5*(s.y+r.y)),a=new x(.5*(n.x+o.x),.5*(n.y+o.y)),h=new x(.5*(n.x+i.x),.5*(n.y+i.y)),l=new x(.5*(a.x+h.x),.5*(a.y+h.y));return[[t,o,a,l],[l,h,i,r]]},o=t=&gt;{let e=t[0].distSquared(t[1]),s=t[2].distSquared(t[3]),r=.25*t[0].distSquared(t[2]),n=.25*t[1].distSquared(t[3]),o=e&gt;s?e:s,i=r&gt;n?r:n;return 18*(o&gt;i?o:i)},i=(t,e)=&gt;Math.sqrt(t.distSquared(e)),a=(t,e)=&gt;t.scale(2/3).add(e.scale(1/3)),h=t=&gt;{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=&gt;{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case&quot;translate&quot;:2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error(&quot;mesh.js: translate does not have 2 arguments!&quot;),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case&quot;scale&quot;:1===l.length?s=new g(l[0],0,0,l[0],0,0):2===l.length?s=new g(l[0],0,0,l[1],0,0):(console.error(&quot;mesh.js: scale does not have 1 or 2 arguments!&quot;),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case&quot;rotate&quot;:if(3===l.length&amp;&amp;(e=new g(1,0,0,1,l[1],l[2]),h=h.append(e)),l[0]){r=l[0]*Math.PI/180;let t=Math.cos(r),e=Math.sin(r);Math.abs(t)&lt;1e-16&amp;&amp;(t=0),Math.abs(e)&lt;1e-16&amp;&amp;(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error(&quot;math.js: No argument to rotate transform!&quot;);3===l.length&amp;&amp;(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case&quot;skewX&quot;:l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),o=new g(1,0,n,1,0,0),h=h.append(o)):console.error(&quot;math.js: No argument to skewX transform!&quot;);break;case&quot;skewY&quot;:l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),i=new g(1,n,0,1,0,0),h=h.append(i)):console.error(&quot;math.js: No argument to skewY transform!&quot;);break;case&quot;matrix&quot;:6===l.length?h=h.append(new g(...l)):console.error(&quot;math.js: Incorrect number of arguments for matrix!&quot;);break;default:console.error(&quot;mesh.js: Unhandled transform type: &quot;+d)}}),h},l=t=&gt;{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t&lt;r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=&gt;{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=&gt;{let o,i,a=[0,0,0,0];for(let h=0;h&lt;3;++h)e[h]&lt;t[h]&amp;&amp;e[h]&lt;s[h]||t[h]&lt;e[h]&amp;&amp;s[h]&lt;e[h]?a[h]=0:(a[h]=.5*((e[h]-t[h])/r+(s[h]-e[h])/n),o=Math.abs(3*(e[h]-t[h])/r),i=Math.abs(3*(s[h]-e[h])/n),a[h]&gt;o?a[h]=o:a[h]&gt;i&amp;&amp;(a[h]=i));return a},u=[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],[-3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0],[2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0],[0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0],[-3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0],[0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0],[9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1],[-6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1],[2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0],[0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0],[-6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1],[4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1]],f=t=&gt;{let e=[];for(let s=0;s&lt;16;++s){e[s]=0;for(let r=0;r&lt;16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=&gt;{const r=e*e,n=s*s,o=e*e*e,i=s*s*s;return t[0]+t[1]*e+t[2]*r+t[3]*o+t[4]*s+t[5]*s*e+t[6]*s*r+t[7]*s*o+t[8]*n+t[9]*n*e+t[10]*n*r+t[11]*n*o+t[12]*i+t[13]*i*e+t[14]*i*r+t[15]*i*o},y=t=&gt;{let e=[],s=[],r=[];for(let s=0;s&lt;4;++s)e[s]=[],e[s][0]=n(t[0][s],t[1][s],t[2][s],t[3][s]),e[s][1]=[],e[s][1].push(...n(...e[s][0][0])),e[s][1].push(...n(...e[s][0][1])),e[s][2]=[],e[s][2].push(...n(...e[s][1][0])),e[s][2].push(...n(...e[s][1][1])),e[s][2].push(...n(...e[s][1][2])),e[s][2].push(...n(...e[s][1][3]));for(let t=0;t&lt;8;++t){s[t]=[];for(let r=0;r&lt;4;++r)s[t][r]=[],s[t][r][0]=n(e[0][2][t][r],e[1][2][t][r],e[2][2][t][r],e[3][2][t][r]),s[t][r][1]=[],s[t][r][1].push(...n(...s[t][r][0][0])),s[t][r][1].push(...n(...s[t][r][0][1])),s[t][r][2]=[],s[t][r][2].push(...n(...s[t][r][1][0])),s[t][r][2].push(...n(...s[t][r][1][1])),s[t][r][2].push(...n(...s[t][r][1][2])),s[t][r][2].push(...n(...s[t][r][1][3]))}for(let t=0;t&lt;8;++t){r[t]=[];for(let e=0;e&lt;8;++e)r[t][e]=[],r[t][e][0]=s[t][0][2][e],r[t][e][1]=s[t][1][2][e],r[t][e][2]=s[t][2][2][e],r[t][e][3]=s[t][3][2][e]}return r};class x{constructor(t,e){this.x=t||0,this.y=e||0}toString(){return`(x=${this.x}, y=${this.y})`}clone(){return new x(this.x,this.y)}add(t){return new x(this.x+t.x,this.y+t.y)}scale(t){return void 0===t.x?new x(this.x*t,this.y*t):new x(this.x*t.x,this.y*t.y)}distSquared(t){let e=this.x-t.x,s=this.y-t.y;return e*e+s*s}transform(t){let e=this.x*t.a+this.y*t.c+t.e,s=this.x*t.b+this.y*t.d+t.f;return new x(e,s)}}class g{constructor(t,e,s,r,n,o){void 0===t?(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0):(this.a=t,this.b=e,this.c=s,this.d=r,this.e=n,this.f=o)}toString(){return`affine: ${this.a} ${this.c} ${this.e} \n       ${this.b} ${this.d} ${this.f}`}append(t){t instanceof g||console.error(&quot;mesh.js: argument to Affine.append is not affine!&quot;);let e=this.a*t.a+this.c*t.b,s=this.b*t.a+this.d*t.b,r=this.a*t.c+this.c*t.d,n=this.b*t.c+this.d*t.d,o=this.a*t.e+this.c*t.f+this.e,i=this.b*t.e+this.d*t.f+this.f;return new g(e,s,r,n,o,i)}}class w{constructor(t,e){this.nodes=t,this.colors=e}paintCurve(t,e){if(o(this.nodes)&gt;r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t&lt;4;++t)r[0][t]=this.colors[0][t],r[1][t]=(this.colors[0][t]+this.colors[1][t])/2,o[0][t]=r[1][t],o[1][t]=this.colors[1][t];let i=new w(s[0],r),a=new w(s[1],o);i.paintCurve(t,e),a.paintCurve(t,e)}else{let s=Math.round(this.nodes[0].x);if(s&gt;=0&amp;&amp;s&lt;e){let r=4*(~~this.nodes[0].y*e+s);t[r]=Math.round(this.colors[0][0]),t[r+1]=Math.round(this.colors[0][1]),t[r+2]=Math.round(this.colors[0][2]),t[r+3]=Math.round(this.colors[0][3])}}}}class m{constructor(t,e){this.nodes=t,this.colors=e}split(){let t=[[],[],[],[]],e=[[],[],[],[]],s=[[[],[]],[[],[]]],r=[[[],[]],[[],[]]];for(let s=0;s&lt;4;++s){const r=n(this.nodes[0][s],this.nodes[1][s],this.nodes[2][s],this.nodes[3][s]);t[0][s]=r[0][0],t[1][s]=r[0][1],t[2][s]=r[0][2],t[3][s]=r[0][3],e[0][s]=r[1][0],e[1][s]=r[1][1],e[2][s]=r[1][2],e[3][s]=r[1][3]}for(let t=0;t&lt;4;++t)s[0][0][t]=this.colors[0][0][t],s[0][1][t]=this.colors[0][1][t],s[1][0][t]=(this.colors[0][0][t]+this.colors[1][0][t])/2,s[1][1][t]=(this.colors[0][1][t]+this.colors[1][1][t])/2,r[0][0][t]=s[1][0][t],r[0][1][t]=s[1][1][t],r[1][0][t]=this.colors[1][0][t],r[1][1][t]=this.colors[1][1][t];return[new m(t,s),new m(e,r)]}paint(t,e){let s,n=!1;for(let t=0;t&lt;4;++t)if((s=o([this.nodes[0][t],this.nodes[1][t],this.nodes[2][t],this.nodes[3][t]]))&gt;r){n=!0;break}if(n){let s=this.split();s[0].paint(t,e),s[1].paint(t,e)}else{new w([...this.nodes[0]],[...this.colors[0]]).paintCurve(t,e)}}}class b{constructor(t){this.readMesh(t),this.type=t.getAttribute(&quot;type&quot;)||&quot;bilinear&quot;}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute(&quot;x&quot;)),n=Number(t.getAttribute(&quot;y&quot;));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t&lt;r;++t){e[3*t+1]=[],e[3*t+2]=[],e[3*t+3]=[],s[t+1]=[];let r=o[t].children;for(let n=0,o=r.length;n&lt;o;++n){let o=r[n].children;for(let r=0,i=o.length;r&lt;i;++r){let i=r;0!==t&amp;&amp;++i;let h,d=o[r].getAttribute(&quot;path&quot;),c=&quot;l&quot;;null!=d&amp;&amp;(c=(h=d.match(/\s*([lLcC])\s*(.*)/))[1]);let u=l(h[2]);switch(c){case&quot;l&quot;:0===i?(e[3*t][3*n+3]=u[0].add(e[3*t][3*n]),e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&amp;&amp;(e[3*t+3][3*n+0]=u[0].add(e[3*t+3][3*n+3])),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case&quot;L&quot;:0===i?(e[3*t][3*n+3]=u[0],e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0],e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&amp;&amp;(e[3*t+3][3*n+0]=u[0]),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case&quot;c&quot;:0===i?(e[3*t][3*n+1]=u[0].add(e[3*t][3*n]),e[3*t][3*n+2]=u[1].add(e[3*t][3*n]),e[3*t][3*n+3]=u[2].add(e[3*t][3*n])):1===i?(e[3*t+1][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+2][3*n+3]=u[1].add(e[3*t][3*n+3]),e[3*t+3][3*n+3]=u[2].add(e[3*t][3*n+3])):2===i?(e[3*t+3][3*n+2]=u[0].add(e[3*t+3][3*n+3]),e[3*t+3][3*n+1]=u[1].add(e[3*t+3][3*n+3]),0===n&amp;&amp;(e[3*t+3][3*n+0]=u[2].add(e[3*t+3][3*n+3]))):(e[3*t+2][3*n]=u[0].add(e[3*t+3][3*n]),e[3*t+1][3*n]=u[1].add(e[3*t+3][3*n]));break;case&quot;C&quot;:0===i?(e[3*t][3*n+1]=u[0],e[3*t][3*n+2]=u[1],e[3*t][3*n+3]=u[2]):1===i?(e[3*t+1][3*n+3]=u[0],e[3*t+2][3*n+3]=u[1],e[3*t+3][3*n+3]=u[2]):2===i?(e[3*t+3][3*n+2]=u[0],e[3*t+3][3*n+1]=u[1],0===n&amp;&amp;(e[3*t+3][3*n+0]=u[2])):(e[3*t+2][3*n]=u[0],e[3*t+1][3*n]=u[1]);break;default:console.error(&quot;mesh.js: &quot;+c+&quot; invalid path type.&quot;)}if(0===t&amp;&amp;0===n||r&gt;0){let e=window.getComputedStyle(o[r]).stopColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i),a=window.getComputedStyle(o[r]).stopOpacity,h=255;a&amp;&amp;(h=Math.floor(255*a)),e&amp;&amp;(0===i?(s[t][n]=[],s[t][n][0]=Math.floor(e[1]),s[t][n][1]=Math.floor(e[2]),s[t][n][2]=Math.floor(e[3]),s[t][n][3]=h):1===i?(s[t][n+1]=[],s[t][n+1][0]=Math.floor(e[1]),s[t][n+1][1]=Math.floor(e[2]),s[t][n+1][2]=Math.floor(e[3]),s[t][n+1][3]=h):2===i?(s[t+1][n+1]=[],s[t+1][n+1][0]=Math.floor(e[1]),s[t+1][n+1][1]=Math.floor(e[2]),s[t+1][n+1][2]=Math.floor(e[3]),s[t+1][n+1][3]=h):3===i&amp;&amp;(s[t+1][n]=[],s[t+1][n][0]=Math.floor(e[1]),s[t+1][n][1]=Math.floor(e[2]),s[t+1][n][2]=Math.floor(e[3]),s[t+1][n][3]=h))}}e[3*t+1][3*n+1]=new x,e[3*t+1][3*n+2]=new x,e[3*t+2][3*n+1]=new x,e[3*t+2][3*n+2]=new x,e[3*t+1][3*n+1].x=(-4*e[3*t][3*n].x+6*(e[3*t][3*n+1].x+e[3*t+1][3*n].x)+-2*(e[3*t][3*n+3].x+e[3*t+3][3*n].x)+3*(e[3*t+3][3*n+1].x+e[3*t+1][3*n+3].x)+-1*e[3*t+3][3*n+3].x)/9,e[3*t+1][3*n+2].x=(-4*e[3*t][3*n+3].x+6*(e[3*t][3*n+2].x+e[3*t+1][3*n+3].x)+-2*(e[3*t][3*n].x+e[3*t+3][3*n+3].x)+3*(e[3*t+3][3*n+2].x+e[3*t+1][3*n].x)+-1*e[3*t+3][3*n].x)/9,e[3*t+2][3*n+1].x=(-4*e[3*t+3][3*n].x+6*(e[3*t+3][3*n+1].x+e[3*t+2][3*n].x)+-2*(e[3*t+3][3*n+3].x+e[3*t][3*n].x)+3*(e[3*t][3*n+1].x+e[3*t+2][3*n+3].x)+-1*e[3*t][3*n+3].x)/9,e[3*t+2][3*n+2].x=(-4*e[3*t+3][3*n+3].x+6*(e[3*t+3][3*n+2].x+e[3*t+2][3*n+3].x)+-2*(e[3*t+3][3*n].x+e[3*t][3*n+3].x)+3*(e[3*t][3*n+2].x+e[3*t+2][3*n].x)+-1*e[3*t][3*n].x)/9,e[3*t+1][3*n+1].y=(-4*e[3*t][3*n].y+6*(e[3*t][3*n+1].y+e[3*t+1][3*n].y)+-2*(e[3*t][3*n+3].y+e[3*t+3][3*n].y)+3*(e[3*t+3][3*n+1].y+e[3*t+1][3*n+3].y)+-1*e[3*t+3][3*n+3].y)/9,e[3*t+1][3*n+2].y=(-4*e[3*t][3*n+3].y+6*(e[3*t][3*n+2].y+e[3*t+1][3*n+3].y)+-2*(e[3*t][3*n].y+e[3*t+3][3*n+3].y)+3*(e[3*t+3][3*n+2].y+e[3*t+1][3*n].y)+-1*e[3*t+3][3*n].y)/9,e[3*t+2][3*n+1].y=(-4*e[3*t+3][3*n].y+6*(e[3*t+3][3*n+1].y+e[3*t+2][3*n].y)+-2*(e[3*t+3][3*n+3].y+e[3*t][3*n].y)+3*(e[3*t][3*n+1].y+e[3*t+2][3*n+3].y)+-1*e[3*t][3*n+3].y)/9,e[3*t+2][3*n+2].y=(-4*e[3*t+3][3*n+3].y+6*(e[3*t+3][3*n+2].y+e[3*t+2][3*n+3].y)+-2*(e[3*t+3][3*n].y+e[3*t][3*n+3].y)+3*(e[3*t][3*n+2].y+e[3*t+2][3*n].y)+-1*e[3*t][3*n].y)/9}}this.nodes=e,this.colors=s}paintMesh(t,e){let s=(this.nodes.length-1)/3,r=(this.nodes[0].length-1)/3;if(&quot;bilinear&quot;===this.type||s&lt;2||r&lt;2){let n;for(let o=0;o&lt;s;++o)for(let s=0;s&lt;r;++s){let r=[];for(let t=3*o,e=3*o+4;t&lt;e;++t)r.push(this.nodes[t].slice(3*s,3*s+4));let i=[];i.push(this.colors[o].slice(s,s+2)),i.push(this.colors[o+1].slice(s,s+2)),(n=new m(r,i)).paint(t,e)}}else{let n,o,a,h,l,d,u;const x=s,g=r;s++,r++;let w=new Array(s);for(let t=0;t&lt;s;++t){w[t]=new Array(r);for(let e=0;e&lt;r;++e)w[t][e]=[],w[t][e][0]=this.nodes[3*t][3*e],w[t][e][1]=this.colors[t][e]}for(let t=0;t&lt;s;++t)for(let e=0;e&lt;r;++e)0!==t&amp;&amp;t!==x&amp;&amp;(n=i(w[t-1][e][0],w[t][e][0]),o=i(w[t+1][e][0],w[t][e][0]),w[t][e][2]=c(w[t-1][e][1],w[t][e][1],w[t+1][e][1],n,o)),0!==e&amp;&amp;e!==g&amp;&amp;(n=i(w[t][e-1][0],w[t][e][0]),o=i(w[t][e+1][0],w[t][e][0]),w[t][e][3]=c(w[t][e-1][1],w[t][e][1],w[t][e+1][1],n,o));for(let t=0;t&lt;r;++t){w[0][t][2]=[],w[x][t][2]=[];for(let e=0;e&lt;4;++e)n=i(w[1][t][0],w[0][t][0]),o=i(w[x][t][0],w[x-1][t][0]),w[0][t][2][e]=n&gt;0?2*(w[1][t][1][e]-w[0][t][1][e])/n-w[1][t][2][e]:0,w[x][t][2][e]=o&gt;0?2*(w[x][t][1][e]-w[x-1][t][1][e])/o-w[x-1][t][2][e]:0}for(let t=0;t&lt;s;++t){w[t][0][3]=[],w[t][g][3]=[];for(let e=0;e&lt;4;++e)n=i(w[t][1][0],w[t][0][0]),o=i(w[t][g][0],w[t][g-1][0]),w[t][0][3][e]=n&gt;0?2*(w[t][1][1][e]-w[t][0][1][e])/n-w[t][1][3][e]:0,w[t][g][3][e]=o&gt;0?2*(w[t][g][1][e]-w[t][g-1][1][e])/o-w[t][g-1][3][e]:0}for(let s=0;s&lt;x;++s)for(let r=0;r&lt;g;++r){let n=i(w[s][r][0],w[s+1][r][0]),o=i(w[s][r+1][0],w[s+1][r+1][0]),c=i(w[s][r][0],w[s][r+1][0]),x=i(w[s+1][r][0],w[s+1][r+1][0]),g=[[],[],[],[]];for(let t=0;t&lt;4;++t){(d=[])[0]=w[s][r][1][t],d[1]=w[s+1][r][1][t],d[2]=w[s][r+1][1][t],d[3]=w[s+1][r+1][1][t],d[4]=w[s][r][2][t]*n,d[5]=w[s+1][r][2][t]*n,d[6]=w[s][r+1][2][t]*o,d[7]=w[s+1][r+1][2][t]*o,d[8]=w[s][r][3][t]*c,d[9]=w[s+1][r][3][t]*x,d[10]=w[s][r+1][3][t]*c,d[11]=w[s+1][r+1][3][t]*x,d[12]=0,d[13]=0,d[14]=0,d[15]=0,u=f(d);for(let e=0;e&lt;9;++e){g[t][e]=[];for(let s=0;s&lt;9;++s)g[t][e][s]=p(u,e/8,s/8),g[t][e][s]&gt;255?g[t][e][s]=255:g[t][e][s]&lt;0&amp;&amp;(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t&lt;e;++t)h.push(this.nodes[t].slice(3*r,3*r+4));l=y(h);for(let s=0;s&lt;8;++s)for(let r=0;r&lt;8;++r)(a=new m(l[s][r],[[[g[0][s][r],g[1][s][r],g[2][s][r],g[3][s][r]],[g[0][s][r+1],g[1][s][r+1],g[2][s][r+1],g[3][s][r+1]]],[[g[0][s+1][r],g[1][s+1][r],g[2][s+1][r],g[3][s+1][r]],[g[0][s+1][r+1],g[1][s+1][r+1],g[2][s+1][r+1],g[3][s+1][r+1]]]])).paint(t,e)}}}transform(t){if(t instanceof x)for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].add(t);else if(t instanceof g)for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].transform(t)}scale(t){for(let e=0,s=this.nodes.length;e&lt;s;++e)for(let s=0,r=this.nodes[0].length;s&lt;r;++s)this.nodes[e][s]=this.nodes[e][s].scale(t)}}document.querySelectorAll(&quot;rect,circle,ellipse,path,text&quot;).forEach((r,n)=&gt;{let o=r.getAttribute(&quot;id&quot;);o||(o=&quot;patchjs_shape&quot;+n,r.setAttribute(&quot;id&quot;,o));const i=r.style.fill.match(/^url\(\s*&quot;?\s*#([^\s&quot;]+)&quot;?\s*\)/),a=r.style.stroke.match(/^url\(\s*&quot;?\s*#([^\s&quot;]+)&quot;?\s*\)/);if(i&amp;&amp;i[1]){const a=document.getElementById(i[1]);if(a&amp;&amp;&quot;meshgradient&quot;===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,&quot;canvas&quot;);d(l,{width:i.width,height:i.height});const c=l.getContext(&quot;2d&quot;);let u=c.createImageData(i.width,i.height);const f=new b(a);&quot;objectBoundingBox&quot;===a.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;f.scale(new x(i.width,i.height));const p=a.getAttribute(&quot;gradientTransform&quot;);null!=p&amp;&amp;f.transform(h(p)),&quot;userSpaceOnUse&quot;===a.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,&quot;image&quot;);d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,&quot;xlink:href&quot;,g),r.parentNode.insertBefore(y,r),r.style.fill=&quot;none&quot;;const w=document.createElementNS(t,&quot;use&quot;);w.setAttributeNS(e,&quot;xlink:href&quot;,&quot;#&quot;+o);const m=&quot;patchjs_clip&quot;+n,M=document.createElementNS(t,&quot;clipPath&quot;);M.setAttribute(&quot;id&quot;,m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute(&quot;clip-path&quot;,&quot;url(#&quot;+m+&quot;)&quot;),u=null,l=null,g=null}}if(a&amp;&amp;a[1]){const o=document.getElementById(a[1]);if(o&amp;&amp;&quot;meshgradient&quot;===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute(&quot;stroke-miterlimit&quot;))||1),a=r.getBBox(),l=Math.trunc(a.width+i),c=Math.trunc(a.height+i),u=Math.trunc(a.x-i/2),f=Math.trunc(a.y-i/2);let p=document.createElementNS(s,&quot;canvas&quot;);d(p,{width:l,height:c});const y=p.getContext(&quot;2d&quot;);let g=y.createImageData(l,c);const w=new b(o);&quot;objectBoundingBox&quot;===o.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;w.scale(new x(l,c));const m=o.getAttribute(&quot;gradientTransform&quot;);null!=m&amp;&amp;w.transform(h(m)),&quot;userSpaceOnUse&quot;===o.getAttribute(&quot;gradientUnits&quot;)&amp;&amp;w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,&quot;image&quot;);d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,&quot;xlink:href&quot;,S);const k=&quot;pattern_clip&quot;+n,A=document.createElementNS(t,&quot;pattern&quot;);d(A,{id:k,patternUnits:&quot;userSpaceOnUse&quot;,width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke=&quot;url(#&quot;+k+&quot;)&quot;,g=null,p=null,S=null}}})}();</script></svg>
\ 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
diff --git a/readme.md b/readme.md
index c372877..dd78181 100644
--- a/readme.md
+++ b/readme.md
@@ -6,10 +6,51 @@
 
 <h1 align="center">Advanced Systems Connector</h1>
 
+[![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 <https://symbols.nuget.org/download/symbols>
+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\ --nologo
+```
+
+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
+```