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="http://www.w3.org/2000/svg",e="http://www.w3.org/1999/xlink",s="http://www.w3.org/1999/xhtml",r=2;if(document.createElementNS(t,"meshgradient").x)return;const n=(t,e,s,r)=>{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=>{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>s?e:s,i=r>n?r:n;return 18*(o>i?o:i)},i=(t,e)=>Math.sqrt(t.distSquared(e)),a=(t,e)=>t.scale(2/3).add(e.scale(1/3)),h=t=>{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=>{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case"translate":2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error("mesh.js: translate does not have 2 arguments!"),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case"scale":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("mesh.js: scale does not have 1 or 2 arguments!"),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case"rotate":if(3===l.length&&(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)<1e-16&&(t=0),Math.abs(e)<1e-16&&(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error("math.js: No argument to rotate transform!");3===l.length&&(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case"skewX":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("math.js: No argument to skewX transform!");break;case"skewY":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("math.js: No argument to skewY transform!");break;case"matrix":6===l.length?h=h.append(new g(...l)):console.error("math.js: Incorrect number of arguments for matrix!");break;default:console.error("mesh.js: Unhandled transform type: "+d)}}),h},l=t=>{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t<r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=>{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=>{let o,i,a=[0,0,0,0];for(let h=0;h<3;++h)e[h]<t[h]&&e[h]<s[h]||t[h]<e[h]&&s[h]<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]>o?a[h]=o:a[h]>i&&(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=>{let e=[];for(let s=0;s<16;++s){e[s]=0;for(let r=0;r<16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=>{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=>{let e=[],s=[],r=[];for(let s=0;s<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<8;++t){s[t]=[];for(let r=0;r<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<8;++t){r[t]=[];for(let e=0;e<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("mesh.js: argument to Affine.append is not affine!");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)>r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t<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>=0&&s<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<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<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<4;++t)if((s=o([this.nodes[0][t],this.nodes[1][t],this.nodes[2][t],this.nodes[3][t]]))>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("type")||"bilinear"}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute("x")),n=Number(t.getAttribute("y"));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t<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<o;++n){let o=r[n].children;for(let r=0,i=o.length;r<i;++r){let i=r;0!==t&&++i;let h,d=o[r].getAttribute("path"),c="l";null!=d&&(c=(h=d.match(/\s*([lLcC])\s*(.*)/))[1]);let u=l(h[2]);switch(c){case"l":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&&(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"L":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&&(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"c":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&&(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"C":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&&(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("mesh.js: "+c+" invalid path type.")}if(0===t&&0===n||r>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&&(h=Math.floor(255*a)),e&&(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&&(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("bilinear"===this.type||s<2||r<2){let n;for(let o=0;o<s;++o)for(let s=0;s<r;++s){let r=[];for(let t=3*o,e=3*o+4;t<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<s;++t){w[t]=new Array(r);for(let e=0;e<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<s;++t)for(let e=0;e<r;++e)0!==t&&t!==x&&(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&&e!==g&&(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<r;++t){w[0][t][2]=[],w[x][t][2]=[];for(let e=0;e<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>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>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<s;++t){w[t][0][3]=[],w[t][g][3]=[];for(let e=0;e<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>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>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<x;++s)for(let r=0;r<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<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<9;++e){g[t][e]=[];for(let s=0;s<9;++s)g[t][e][s]=p(u,e/8,s/8),g[t][e][s]>255?g[t][e][s]=255:g[t][e][s]<0&&(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t<e;++t)h.push(this.nodes[t].slice(3*r,3*r+4));l=y(h);for(let s=0;s<8;++s)for(let r=0;r<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<s;++e)for(let s=0,r=this.nodes[0].length;s<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<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].transform(t)}scale(t){for(let e=0,s=this.nodes.length;e<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].scale(t)}}document.querySelectorAll("rect,circle,ellipse,path,text").forEach((r,n)=>{let o=r.getAttribute("id");o||(o="patchjs_shape"+n,r.setAttribute("id",o));const i=r.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/),a=r.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(i&&i[1]){const a=document.getElementById(i[1]);if(a&&"meshgradient"===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,"canvas");d(l,{width:i.width,height:i.height});const c=l.getContext("2d");let u=c.createImageData(i.width,i.height);const f=new b(a);"objectBoundingBox"===a.getAttribute("gradientUnits")&&f.scale(new x(i.width,i.height));const p=a.getAttribute("gradientTransform");null!=p&&f.transform(h(p)),"userSpaceOnUse"===a.getAttribute("gradientUnits")&&f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,"image");d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,"xlink:href",g),r.parentNode.insertBefore(y,r),r.style.fill="none";const w=document.createElementNS(t,"use");w.setAttributeNS(e,"xlink:href","#"+o);const m="patchjs_clip"+n,M=document.createElementNS(t,"clipPath");M.setAttribute("id",m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute("clip-path","url(#"+m+")"),u=null,l=null,g=null}}if(a&&a[1]){const o=document.getElementById(a[1]);if(o&&"meshgradient"===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute("stroke-miterlimit"))||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,"canvas");d(p,{width:l,height:c});const y=p.getContext("2d");let g=y.createImageData(l,c);const w=new b(o);"objectBoundingBox"===o.getAttribute("gradientUnits")&&w.scale(new x(l,c));const m=o.getAttribute("gradientTransform");null!=m&&w.transform(h(m)),"userSpaceOnUse"===o.getAttribute("gradientUnits")&&w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,"image");d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,"xlink:href",S);const k="pattern_clip"+n,A=document.createElementNS(t,"pattern");d(A,{id:k,patternUnits:"userSpaceOnUse",width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke="url(#"+k+")",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="http://www.w3.org/2000/svg",e="http://www.w3.org/1999/xlink",s="http://www.w3.org/1999/xhtml",r=2;if(document.createElementNS(t,"meshgradient").x)return;const n=(t,e,s,r)=>{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=>{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>s?e:s,i=r>n?r:n;return 18*(o>i?o:i)},i=(t,e)=>Math.sqrt(t.distSquared(e)),a=(t,e)=>t.scale(2/3).add(e.scale(1/3)),h=t=>{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=>{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case"translate":2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error("mesh.js: translate does not have 2 arguments!"),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case"scale":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("mesh.js: scale does not have 1 or 2 arguments!"),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case"rotate":if(3===l.length&&(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)<1e-16&&(t=0),Math.abs(e)<1e-16&&(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error("math.js: No argument to rotate transform!");3===l.length&&(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case"skewX":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("math.js: No argument to skewX transform!");break;case"skewY":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("math.js: No argument to skewY transform!");break;case"matrix":6===l.length?h=h.append(new g(...l)):console.error("math.js: Incorrect number of arguments for matrix!");break;default:console.error("mesh.js: Unhandled transform type: "+d)}}),h},l=t=>{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t<r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=>{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=>{let o,i,a=[0,0,0,0];for(let h=0;h<3;++h)e[h]<t[h]&&e[h]<s[h]||t[h]<e[h]&&s[h]<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]>o?a[h]=o:a[h]>i&&(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=>{let e=[];for(let s=0;s<16;++s){e[s]=0;for(let r=0;r<16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=>{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=>{let e=[],s=[],r=[];for(let s=0;s<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<8;++t){s[t]=[];for(let r=0;r<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<8;++t){r[t]=[];for(let e=0;e<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("mesh.js: argument to Affine.append is not affine!");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)>r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t<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>=0&&s<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<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<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<4;++t)if((s=o([this.nodes[0][t],this.nodes[1][t],this.nodes[2][t],this.nodes[3][t]]))>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("type")||"bilinear"}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute("x")),n=Number(t.getAttribute("y"));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t<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<o;++n){let o=r[n].children;for(let r=0,i=o.length;r<i;++r){let i=r;0!==t&&++i;let h,d=o[r].getAttribute("path"),c="l";null!=d&&(c=(h=d.match(/\s*([lLcC])\s*(.*)/))[1]);let u=l(h[2]);switch(c){case"l":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&&(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"L":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&&(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"c":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&&(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"C":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&&(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("mesh.js: "+c+" invalid path type.")}if(0===t&&0===n||r>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&&(h=Math.floor(255*a)),e&&(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&&(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("bilinear"===this.type||s<2||r<2){let n;for(let o=0;o<s;++o)for(let s=0;s<r;++s){let r=[];for(let t=3*o,e=3*o+4;t<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<s;++t){w[t]=new Array(r);for(let e=0;e<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<s;++t)for(let e=0;e<r;++e)0!==t&&t!==x&&(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&&e!==g&&(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<r;++t){w[0][t][2]=[],w[x][t][2]=[];for(let e=0;e<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>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>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<s;++t){w[t][0][3]=[],w[t][g][3]=[];for(let e=0;e<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>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>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<x;++s)for(let r=0;r<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<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<9;++e){g[t][e]=[];for(let s=0;s<9;++s)g[t][e][s]=p(u,e/8,s/8),g[t][e][s]>255?g[t][e][s]=255:g[t][e][s]<0&&(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t<e;++t)h.push(this.nodes[t].slice(3*r,3*r+4));l=y(h);for(let s=0;s<8;++s)for(let r=0;r<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<s;++e)for(let s=0,r=this.nodes[0].length;s<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<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].transform(t)}scale(t){for(let e=0,s=this.nodes.length;e<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].scale(t)}}document.querySelectorAll("rect,circle,ellipse,path,text").forEach((r,n)=>{let o=r.getAttribute("id");o||(o="patchjs_shape"+n,r.setAttribute("id",o));const i=r.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/),a=r.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(i&&i[1]){const a=document.getElementById(i[1]);if(a&&"meshgradient"===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,"canvas");d(l,{width:i.width,height:i.height});const c=l.getContext("2d");let u=c.createImageData(i.width,i.height);const f=new b(a);"objectBoundingBox"===a.getAttribute("gradientUnits")&&f.scale(new x(i.width,i.height));const p=a.getAttribute("gradientTransform");null!=p&&f.transform(h(p)),"userSpaceOnUse"===a.getAttribute("gradientUnits")&&f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,"image");d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,"xlink:href",g),r.parentNode.insertBefore(y,r),r.style.fill="none";const w=document.createElementNS(t,"use");w.setAttributeNS(e,"xlink:href","#"+o);const m="patchjs_clip"+n,M=document.createElementNS(t,"clipPath");M.setAttribute("id",m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute("clip-path","url(#"+m+")"),u=null,l=null,g=null}}if(a&&a[1]){const o=document.getElementById(a[1]);if(o&&"meshgradient"===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute("stroke-miterlimit"))||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,"canvas");d(p,{width:l,height:c});const y=p.getContext("2d");let g=y.createImageData(l,c);const w=new b(o);"objectBoundingBox"===o.getAttribute("gradientUnits")&&w.scale(new x(l,c));const m=o.getAttribute("gradientTransform");null!=m&&w.transform(h(m)),"userSpaceOnUse"===o.getAttribute("gradientUnits")&&w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,"image");d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,"xlink:href",S);const k="pattern_clip"+n,A=document.createElementNS(t,"pattern");d(A,{id:k,patternUnits:"userSpaceOnUse",width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke="url(#"+k+")",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> +[](https://github.com/Advanced-Systems/connector/actions/workflows/dotnet-tests.yml) +[](https://github.com/Advanced-Systems/connector/actions/workflows/codeql.yml) +[](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 +```