diff --git a/.editorconfig b/.editorconfig index 1884bec..133850c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,6 +47,9 @@ dotnet_diagnostic.RCS1227.severity = none # RCS1124: Inline local variable dotnet_diagnostic.RCS1124.severity = suggestion +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = none + [*.{cs,vb}] #### Naming styles #### diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d7064..e5d9a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [1.1.0] - TODO: Add date of release ### Added +- Fluent configuration API for general settings and entity mappings (Fixes [issue #3](https://github.com/rent-a-developer/DbConnectionPlus/issues/3)) - Support for column name mapping via System.ComponentModel.DataAnnotations.Schema.ColumnAttribute (Fixes [issue #1](https://github.com/rent-a-developer/DbConnectionPlus/issues/1)) - Throw helper for common exceptions ### Changed - Updated all dependencies to latest stable versions +- Refactored unit and integration tests for better maintainability ## [1.0.0] - 2026-01-24 diff --git a/README.md b/README.md index 4fcf945..4c9bbc1 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ All examples in this document use SQL Server. - [EnumSerializationMode](#enumserializationmode) - [InterceptDbCommand](#interceptdbcommand) - [Entity Mapping](#entity-mapping) + - [Fluent API](#fluent-api) - [Data annotation attributes](#data-annotation-attributes) - [General-purpose methods](#general-purpose-methods) - [ExecuteNonQuery / ExecuteNonQueryAsync](#executenonquery--executenonqueryasync) @@ -326,18 +327,9 @@ Configuration: - [EnumSerializationMode](#enumserializationmode) - Configure how enum values are serialized when sent to the database - [InterceptDbCommand](#interceptdbcommand) - Configure a delegate to intercept `DbCommand`s executed by DbConnectionPlus -Entity mapping: -Data annotation attributes: -- [`System.ComponentModel.DataAnnotations.Schema.TableAttribute`](#systemcomponentmodeldataannotationsschematableattribute) -Specify the name of the table where entities of an entity type are stored -- [`System.ComponentModel.DataAnnotations.Schema.ColumnAttribute`](#systemcomponentmodeldataannotationsschemacolumnattribute) -Specify the name of the column where a property of an entity type is stored -- [`System.ComponentModel.DataAnnotations.KeyAttribute`](#systemcomponentmodeldataannotationskeyattribute) -Specify the key property / properties of an entity type -- [`System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedAttribute`](#systemcomponentmodeldataannotationsschemadatabasegeneratedattribute) -Specify that a property of an entity type is generated by the database -- [`System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute`](#systemcomponentmodeldataannotationsschemanotmappedattribute) -Specify that a property of an entity type is not mapped to a column in the database +Entity mapping: +- [Fluent API](#fluent-api) - Configure entity mapping via fluent API +- [Data annotation attributes](#data-annotation-attributes) - Configure entity mapping via data annotation attributes General-purpose methods: - [ExecuteNonQuery / ExecuteNonQueryAsync](#executenonquery--executenonqueryasync) - Execute a non-query and return @@ -374,11 +366,28 @@ it inside an SQL statement ### Configuration +Use `DbConnectionExtensions.Configure` to configure DbConnectionPlus. + +```csharp +using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; + +DbConnectionExtensions.Configure(config => +{ + // Configuration options go here +}); +``` + +> [!NOTE] +> `DbConnectionExtensions.Configure` can only be called once. +> After it has been called the configuration of DbConnectionPlus is frozen and cannot be changed anymore. + #### EnumSerializationMode -Use `DbConnectionExtensions.EnumSerializationMode` to configure how enum values are serialized when they are sent to a -database. +Use `EnumSerializationMode` to configure how enum values are serialized when they are sent to a database. The default value is `EnumSerializationMode.Strings`, which serializes enum values as their string representation. +When `EnumSerializationMode` is set to `EnumSerializationMode.Strings`, enum values are serialized as strings. +When `EnumSerializationMode` is set to `EnumSerializationMode.Integers`, enum values are serialized as integers. + ```csharp using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; @@ -404,36 +413,42 @@ var user = new User Role = UserRole.User }; -DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; +DbConnectionExtensions.Configure(config => +{ + config.EnumSerializationMode = EnumSerializationMode.Strings; +}); + connection.InsertEntity(user); // Column "Role" will contain the string "User". -DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; +DbConnectionExtensions.Configure(config => +{ + config.EnumSerializationMode = EnumSerializationMode.Integers; +}); + connection.InsertEntity(user); // Column "Role" will contain the integer 2. ``` -When `DbConnectionExtensions.EnumSerializationMode` is set to `EnumSerializationMode.Strings`, enum values are -serialized as strings. -When `DbConnectionExtensions.EnumSerializationMode` is set to `EnumSerializationMode.Integers`, enum values are -serialized as integers. - #### InterceptDbCommand -Use `DbConnectionExtensions.InterceptDbCommand` to configure a delegate that intercepts a `DbCommand` before it is -executed. This can be useful for logging, modifying the command text, or applying additional configuration. +Use `InterceptDbCommand` to configure a delegate that intercepts a `DbCommand` before it is executed. This can be +useful for logging, modifying the command text, or applying additional configuration. ```csharp using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; -DbConnectionExtensions.InterceptDbCommand = (dbCommand, temporaryTables) => +DbConnectionExtensions.Configure(config => { - // Log the command text - Console.WriteLine("Executing SQL Command: " + dbCommand.CommandText); + config.InterceptDbCommand = (dbCommand, temporaryTables) => + { + // Log the command text + Console.WriteLine("Executing SQL Command: " + dbCommand.CommandText); - // Modify the command text if needed - dbCommand.CommandText += " OPTION (RECOMPILE)"; + // Modify the command text if needed + dbCommand.CommandText += " OPTION (RECOMPILE)"; - // Apply additional configuration if needed - dbCommand.CommandTimeout = 60; -}; + // Apply additional configuration if needed + dbCommand.CommandTimeout = 60; + }; +}); ``` See [DbCommandLogger](https://github.com/rent-a-developer/DbConnectionPlus/blob/main/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs) @@ -441,6 +456,41 @@ for an example of logging executed commands. #### Entity Mapping +You can configure how entity types are mapped to database tables and columns using either the fluent API or data +annotation attributes. + +> [!NOTE] +> Mapping configured via the fluent API takes precedence over mapping configured via data annotation attributes. +> When a fluent mapping exist for an entity type, the data annotations on this entity type are ignored. +> When a fluent mapping exists for an entity property, the data annotations on this property are ignored. + +##### Fluent API +You can use the fluent API to configure how entity types are mapped to database tables and columns. + +```csharp +using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; + +DbConnectionExtensions.Configure(config => +{ + config.Entity() + .ToTable("Products"); + + config.Entity() + .Property(a => a.Id) + .HasColumnName("ProductId"); + .IsIdentity() + .IsKey(); + + config.Entity() + .Property(a => a.DiscountedPrice) + .IsComputed(); + + config.Entity() + .Property(a => a.IsOnSale) + .IsIgnored(); +}); +``` + ##### Data annotation attributes You can use the following attributes to configure how entity types are mapped to database tables and columns: @@ -1081,7 +1131,10 @@ Then register your custom database adapter before using DbConnectionPlus: ```csharp using RentADeveloper.DbConnectionPlus.DatabaseAdapters; -DatabaseAdapterRegistry.RegisterAdapter(new MyDatabaseAdapter()); +DbConnectionExtensions.Configure(config => +{ + config.RegisterDatabaseAdapter(new MyDatabaseAdapter()); +}); ``` See [SqlServerDatabaseAdapter](https://github.com/rent-a-developer/DbConnectionPlus/blob/main/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs) diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index afa61d3..06797d5 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -1421,7 +1421,7 @@ public void UpdateEntities_Manually() for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) { - var updatedEntities = Generate.UpdatesFor(this.entitiesInDb); + var updatedEntities = Generate.UpdateFor(this.entitiesInDb); using var command = connection.CreateCommand(); command.CommandText = """ @@ -1547,7 +1547,7 @@ public void UpdateEntities_DbConnectionPlus() for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) { - var updatesEntities = Generate.UpdatesFor(this.entitiesInDb); + var updatesEntities = Generate.UpdateFor(this.entitiesInDb); connection.UpdateEntities(updatesEntities); } diff --git a/docs/DESIGN-DECISIONS.md b/docs/DESIGN-DECISIONS.md index 1e99fa8..28eef03 100644 --- a/docs/DESIGN-DECISIONS.md +++ b/docs/DESIGN-DECISIONS.md @@ -1,6 +1,6 @@ # DbConnectionPlus - Design Decisions Document -**Version:** 1.0.0 +**Version:** 1.1.0 **Last Updated:** January 2026 **Author:** David Liebeherr @@ -579,6 +579,45 @@ public static InterpolatedParameter Parameter( ## Entity Mapping Strategy +### Fluent API-based Configuration + +**Decision:** Provide optional fluent API for entity configuration. + +**Example:** +```csharp +DbConnectionExtensions.Configure(config => + { + // Table name mapping: + config.Entity() + .ToTable("Products"); + + // Column name mapping: + config.Entity() + .Property(a => a.Name) + .HasColumnName("ProductName"); + + // Key column mapping: + config.Entity() + .Property(a => a.Id) + .IsKey(); + + // Database generated column mapping: + config.Entity() + .Property(a => a.DiscountedPrice) + .IsDatabaseGenerated(); + + // Ignored property mapping: + config.Entity() + .Property(a => a.IsOnSale) + .Ignore(); + } +); +``` + +**Benefits:** +- **Mostly EF Core compatible**: Similar API as of EF core +- **Convenient**: Provides convinient way to configure entities without attributes + ### Attribute-Based Configuration **Decision:** Use standard .NET data annotations for entity metadata. @@ -631,13 +670,15 @@ public static class EntityHelper ``` **Cached Information:** -- Table name (from `[Table]` attribute or type name) +- Table name (from `[Table]` attribute, fluent API config or type name) - Metadata of properties: - - Mapped properties (excluding `[NotMapped]`) - - Key properties (marked with `[Key]`) + - Mapped properties (excluding ignored properties) + - Key properties + - Computed properties + - Identity property + - Database generated properties - Insert properties (properties to be included when inserting an entity) - Update properties (properties to be included when updating an entity) - - Database generated properties (marked with `[DatabaseGenerated(DatabaseGeneratedOption.Identity)]` or `[DatabaseGenerated(DatabaseGeneratedOption.Computed)]`) **Performance Impact:** - First entity operation: few ms for metadata extraction @@ -996,22 +1037,45 @@ public List Query_Entities_DbConnectionPlus() ### Global Configuration -**Decision:** Use static properties for global settings that rarely change. +**Decision:** Provide a config method for configuring global settings. -**Current Settings:** +**Best Practice:** +Set during application startup before any database operations: ```csharp -public static class DbConnectionExtensions +// In Program.cs or Startup.cs + +DbConnectionExtensions.Configure(config => { - public static EnumSerializationMode EnumSerializationMode { get; set; } - = EnumSerializationMode.Strings; -} + config.EnumSerializationMode = EnumSerializationMode.Integers; +}); ``` -**Best Practice:** -Set during application startup before any database operations: +### Entity type mapping configuration + +**Decision:** Provide a Fluent API to configure entity type mapping. + ```csharp -// In Program.cs or Startup.cs -DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; +using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; + +DbConnectionExtensions.Configure(config => +{ + config.Entity() + .ToTable("Products"); + + config.Entity() + .Property(a => a.Id) + .HasColumnName("ProductId"); + .IsIdentity() + .IsKey(); + + config.Entity() + .Property(a => a.DiscountedPrice) + .IsComputed(); + + config.Entity() + .Property(a => a.IsOnSale) + .IsIgnored(); +}); ``` --- @@ -1036,7 +1100,10 @@ public class MyCustomDatabaseAdapter : IDatabaseAdapter } // Register adapter -DatabaseAdapterRegistry.RegisterAdapter(new MyCustomDatabaseAdapter()); +DbConnectionExtensions.Configure(config => +{ + config.RegisterDatabaseAdapter(new MyCustomDatabaseAdapter()); +}); ``` **Use Cases:** diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs new file mode 100644 index 0000000..ad2de3e --- /dev/null +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -0,0 +1,199 @@ +using Microsoft.Data.Sqlite; +using MySqlConnector; +using Npgsql; +using Oracle.ManagedDataAccess.Client; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; + +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// The configuration for DbConnectionPlus. +/// +public sealed class DbConnectionPlusConfiguration : IFreezable +{ + /// + /// Initializes a new instance of the class. + /// + internal DbConnectionPlusConfiguration() + { + this.databaseAdapters.TryAdd(typeof(MySqlConnection), new MySqlDatabaseAdapter()); + this.databaseAdapters.TryAdd(typeof(OracleConnection), new OracleDatabaseAdapter()); + this.databaseAdapters.TryAdd(typeof(NpgsqlConnection), new PostgreSqlDatabaseAdapter()); + this.databaseAdapters.TryAdd(typeof(SqliteConnection), new SqliteDatabaseAdapter()); + this.databaseAdapters.TryAdd(typeof(SqlConnection), new SqlServerDatabaseAdapter()); + } + + /// + /// + /// Controls how values are serialized when they are sent to a database using one of the + /// following methods: + /// + /// + /// 1. When an entity containing an enum property is inserted via + /// , + /// , + /// or + /// . + /// + /// + /// 2. When an entity containing an enum property is updated via + /// , + /// , + /// or + /// . + /// + /// + /// 3. When an enum value is passed as a parameter to an SQL statement via + /// . + /// + /// + /// 4. When a sequence of enum values is passed as a temporary table to an SQL statement via + /// . + /// + /// + /// 5. When objects containing an enum property are passed as a temporary table to an SQL statement via + /// . + /// + /// The default is . + /// + /// + /// An attempt was made to modify this property and the configuration of DbConnectionPlus is already frozen and + /// can no longer be modified. + /// + public EnumSerializationMode EnumSerializationMode + { + get; + set + { + this.EnsureNotFrozen(); + field = value; + } + } = EnumSerializationMode.Strings; + + /// + /// A function that can be used to intercept database commands executed via DbConnectionPlus. + /// Can be used for logging or modifying commands before execution. + /// + /// + /// An attempt was made to modify this property and the configuration of DbConnectionPlus is already frozen and + /// can no longer be modified. + /// + public InterceptDbCommand? InterceptDbCommand + { + get; + set + { + this.EnsureNotFrozen(); + field = value; + } + } + + /// + /// Gets a builder for configuring the entity type . + /// + /// The type of the entity to configure. + /// A builder to configure the entity type. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityTypeBuilder Entity() + { + this.EnsureNotFrozen(); + + return (EntityTypeBuilder)this.entityTypeBuilders.GetOrAdd( + typeof(TEntity), + _ => new EntityTypeBuilder() + ); + } + + /// + /// Registers a database adapter for the connection type . + /// If an adapter is already registered for the connection type , the existing + /// adapter is replaced. + /// + /// + /// The type of database connection for which is being registered. + /// + /// + /// The database adapter to associate with the connection type . + /// + /// is . + public void RegisterDatabaseAdapter(IDatabaseAdapter adapter) + where TConnection : DbConnection + { + ArgumentNullException.ThrowIfNull(adapter); + + this.databaseAdapters.AddOrUpdate(typeof(TConnection), adapter, (_, _) => adapter); + } + + /// + void IFreezable.Freeze() + { + this.isFrozen = true; + + foreach (var entityTypeBuilder in this.entityTypeBuilders.Values) + { + entityTypeBuilder.Freeze(); + } + } + + /// + /// The singleton instance of . + /// + public static DbConnectionPlusConfiguration Instance { get; internal set; } = new(); + + /// + /// Retrieves the database adapter associated with the connection type . + /// + /// + /// The type of the database connection for which to retrieve the adapter. + /// + /// + /// An instance that supports database connections of the type + /// . + /// + /// + /// is . + /// + /// + /// No adapter is registered for the connection type . + /// + internal IDatabaseAdapter GetDatabaseAdapter(Type connectionType) + { + ArgumentNullException.ThrowIfNull(connectionType); + + return this.databaseAdapters.TryGetValue(connectionType, out var adapter) + ? adapter + : throw new InvalidOperationException( + $"No database adapter is registered for the database connection of the type {connectionType}. " + + $"Please call {nameof(DbConnectionExtensions)}.{nameof(DbConnectionExtensions.Configure)} to " + + "register an adapter for that connection type." + ); + } + + /// + /// Gets the configured entity type builders. + /// + /// The configured entity type builders. + internal IReadOnlyDictionary GetEntityTypeBuilders() => this.entityTypeBuilders; + + /// + /// Ensures this instance is not frozen. + /// + /// This object is already frozen. + private void EnsureNotFrozen() + { + if (this.isFrozen) + { + ThrowHelper.ThrowConfigurationIsFrozenException(); + } + } + + private readonly ConcurrentDictionary databaseAdapters = []; + private readonly ConcurrentDictionary entityTypeBuilders = new(); + private Boolean isFrozen; +} diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs new file mode 100644 index 0000000..f2e18a6 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -0,0 +1,159 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// A builder for configuring an entity property. +/// +public sealed class EntityPropertyBuilder : IEntityPropertyBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The entity type builder this property builder belongs to. + /// The name of the property being configured. + internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String propertyName) + { + this.entityTypeBuilder = entityTypeBuilder; + this.propertyName = propertyName; + } + + /// + /// Sets the name of the column to map the property to. + /// + /// The name of the column to map the property to. + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + // ReSharper disable once ParameterHidesMember + public EntityPropertyBuilder HasColumnName(String columnName) + { + this.EnsureNotFrozen(); + + this.columnName = columnName; + return this; + } + + /// + /// Marks the property as mapped to a computed database column. + /// Such properties will be ignored during insert and update operations. + /// Their values will be read back from the database after an insert or update and populated on the entity. + /// + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder IsComputed() + { + this.EnsureNotFrozen(); + + this.isComputed = true; + return this; + } + + /// + /// Marks the property as mapped to an identity database column. + /// Such properties will be ignored during insert and update operations. + /// Their values will be read back from the database after an insert or update and populated on the entity. + /// + /// + /// Another property is already marked as an identity property for the entity type. + /// + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder IsIdentity() + { + this.EnsureNotFrozen(); + + var otherIdentityProperty = + this.entityTypeBuilder.PropertyBuilders.Values.FirstOrDefault(a => + a.PropertyName != this.propertyName && a.IsIdentity + ); + + if (otherIdentityProperty is not null) + { + throw new InvalidOperationException( + $"There is already the property '{otherIdentityProperty.PropertyName}' marked as an identity " + + $"property for the entity type {this.entityTypeBuilder.EntityType}. Only one property can be marked " + + "as identity property per entity type." + ); + } + + this.isIdentity = true; + return this; + } + + /// + /// Marks the property to not be mapped to a database column. + /// + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder IsIgnored() + { + this.EnsureNotFrozen(); + + this.isIgnored = true; + return this; + } + + /// + /// Marks the property as a key property. + /// + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder IsKey() + { + this.EnsureNotFrozen(); + + this.isKey = true; + return this; + } + + /// + String? IEntityPropertyBuilder.ColumnName => this.columnName; + + /// + void IFreezable.Freeze() => this.isFrozen = true; + + /// + Boolean IEntityPropertyBuilder.IsComputed => this.isComputed; + + /// + Boolean IEntityPropertyBuilder.IsIdentity => this.isIdentity; + + /// + Boolean IEntityPropertyBuilder.IsIgnored => this.isIgnored; + + /// + Boolean IEntityPropertyBuilder.IsKey => this.isKey; + + /// + String IEntityPropertyBuilder.PropertyName => this.propertyName; + + /// + /// Ensures this instance is not frozen. + /// + /// This object is already frozen. + private void EnsureNotFrozen() + { + if (this.isFrozen) + { + ThrowHelper.ThrowConfigurationIsFrozenException(); + } + } + + private readonly IEntityTypeBuilder entityTypeBuilder; + private readonly String propertyName; + + private String? columnName; + private Boolean isComputed; + private Boolean isFrozen; + private Boolean isIdentity; + private Boolean isIgnored; + private Boolean isKey; +} diff --git a/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs new file mode 100644 index 0000000..d81b4cb --- /dev/null +++ b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs @@ -0,0 +1,117 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// A builder for configuring an entity type. +/// +/// The type of the entity being configured. +public sealed class EntityTypeBuilder : IEntityTypeBuilder +{ + /// + /// Gets a builder for configuring the specified property. + /// + /// The property type of the property to configure. + /// + /// A lambda expression representing the property to be configured (blog => blog.Url). + /// + /// The builder for the specified property. + /// + /// is not a valid property access expression. + /// + /// + /// is . + /// + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder Property(Expression> propertyExpression) + { + ArgumentNullException.ThrowIfNull(propertyExpression); + + this.EnsureNotFrozen(); + + var propertyName = GetPropertyNameFromPropertyExpression(propertyExpression); + + return (EntityPropertyBuilder)this.propertyBuilders.GetOrAdd( + propertyName, + static (propertyName2, self) => new EntityPropertyBuilder(self, propertyName2), + this + ); + } + + /// + /// Maps the entity to the specified table name. + /// + /// The name of the table to map the entity to. + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + // ReSharper disable once ParameterHidesMember + public EntityTypeBuilder ToTable(String tableName) + { + this.EnsureNotFrozen(); + + this.tableName = tableName; + + return this; + } + + /// + Type IEntityTypeBuilder.EntityType => typeof(TEntity); + + /// + void IFreezable.Freeze() + { + this.isFrozen = true; + + foreach (var propertyBuilder in this.propertyBuilders.Values) + { + propertyBuilder.Freeze(); + } + } + + /// + IReadOnlyDictionary IEntityTypeBuilder.PropertyBuilders => + this.propertyBuilders; + + /// + String? IEntityTypeBuilder.TableName => this.tableName; + + /// + /// Ensures this instance is not frozen. + /// + /// This object is already frozen. + private void EnsureNotFrozen() + { + if (this.isFrozen) + { + ThrowHelper.ThrowConfigurationIsFrozenException(); + } + } + + /// + /// Gets the name of the property accessed in the specified property access expression. + /// + /// The property access expression to get the property name from. + /// The name of the property accessed in . + /// + /// is not a valid property access expression. + /// + private static String GetPropertyNameFromPropertyExpression(LambdaExpression propertyExpression) + { + if (propertyExpression.Body is MemberExpression { Member: PropertyInfo propertyInfo }) return propertyInfo.Name; + + throw new ArgumentException( + $"The expression '{propertyExpression}' is not a valid property access expression. The expression should " + + "represent a simple property access: 'a => a.MyProperty'.", + nameof(propertyExpression) + ); + } + + private readonly ConcurrentDictionary propertyBuilders = new(); + private Boolean isFrozen; + private String? tableName; +} diff --git a/src/DbConnectionPlus/Configuration/Freezable.cs b/src/DbConnectionPlus/Configuration/Freezable.cs new file mode 100644 index 0000000..613ee94 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/Freezable.cs @@ -0,0 +1,12 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// Represents an object that can be frozen to prevent further modifications. +/// +public interface IFreezable +{ + /// + /// Freezes the object, preventing any further modifications. + /// + public void Freeze(); +} diff --git a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs new file mode 100644 index 0000000..c0c6b79 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs @@ -0,0 +1,37 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// Represents a builder for configuring an entity property. +/// +internal interface IEntityPropertyBuilder : IFreezable +{ + /// + /// The name of the column the property is mapped to. + /// + internal String? ColumnName { get; } + + /// + /// Determines whether the property is mapped to a computed database column. + /// + internal Boolean IsComputed { get; } + + /// + /// Determines whether the property is mapped to an identity database column. + /// + internal Boolean IsIdentity { get; } + + /// + /// Determines whether the property is not mapped to a database column. + /// + internal Boolean IsIgnored { get; } + + /// + /// Determines whether the property is mapped to a key database column. + /// + internal Boolean IsKey { get; } + + /// + /// The name of the property being configured. + /// + internal String PropertyName { get; } +} diff --git a/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs new file mode 100644 index 0000000..eccb83a --- /dev/null +++ b/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs @@ -0,0 +1,22 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// Represents a builder for configuring an entity type. +/// +internal interface IEntityTypeBuilder : IFreezable +{ + /// + /// The entity type being configured. + /// + internal Type EntityType { get; } + + /// + /// The property builders associated with the entity type. + /// + internal IReadOnlyDictionary PropertyBuilders { get; } + + /// + /// The name of the table the entity type is mapped to. + /// + internal String? TableName { get; } +} diff --git a/src/DbConnectionPlus/InterceptDbCommand.cs b/src/DbConnectionPlus/Configuration/InterceptDbCommand.cs similarity index 91% rename from src/DbConnectionPlus/InterceptDbCommand.cs rename to src/DbConnectionPlus/Configuration/InterceptDbCommand.cs index ee1e012..252f14f 100644 --- a/src/DbConnectionPlus/InterceptDbCommand.cs +++ b/src/DbConnectionPlus/Configuration/InterceptDbCommand.cs @@ -3,7 +3,7 @@ using RentADeveloper.DbConnectionPlus.SqlStatements; -namespace RentADeveloper.DbConnectionPlus; +namespace RentADeveloper.DbConnectionPlus.Configuration; /// /// A delegate for intercepting database commands executed via DbConnectionPlus. diff --git a/src/DbConnectionPlus/Converters/EnumConverter.cs b/src/DbConnectionPlus/Converters/EnumConverter.cs index 6decb3f..7833d80 100644 --- a/src/DbConnectionPlus/Converters/EnumConverter.cs +++ b/src/DbConnectionPlus/Converters/EnumConverter.cs @@ -132,14 +132,156 @@ internal static class EnumConverter ThrowCouldNotConvertNumericValueToEnumType(value, targetType); } - return (TTarget?)Enum.ToObject(effectiveTargetType, valueConvertedToEnumUnderlyingType); + return (TTarget?)Enum.ToObject( + effectiveTargetType, + valueConvertedToEnumUnderlyingType + ); default: - ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(value, targetType); + ThrowValueIsNeitherEnumValueNorStringNorNumericValueException( + value, + targetType + ); return default; // Just to satisfy the compiler. } } + /// + /// Converts to an enum member of the type . + /// + /// + /// The value to convert to an enum member of the type . + /// + /// The type to convert to. + /// + /// converted to an enum member of the type . + /// + /// + /// must either be a string representing the name of an enum member (case-insensitive) + /// of the type or a numeric value representing the value of an enum member of + /// the type . + /// + /// + /// is not an enum type nor a nullable enum type. + /// + /// is . + /// + /// + /// + /// + /// could not be converted to the type + /// , because is and the type + /// is non-nullable. + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is an empty string. + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is a string that consists only of white-space + /// characters. + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is a string that does not match + /// the name of any enum member of the type . + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is of a type that could not be converted to + /// the underlying type of the type . + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is a numeric value that does not match the + /// value of any enum member of the type . + /// + /// + /// + /// + /// could not be converted to an enum member of the type + /// , because is neither an enum member of that type + /// nor a string nor a numeric value. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Object? ConvertValueToEnumMember(Object? value, Type targetType) + { + ArgumentNullException.ThrowIfNull(targetType); + + // Unwrap Nullable types: + var effectiveTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (!effectiveTargetType.IsEnum) + { + ThrowTypeIsNeitherEnumNorNullableEnumTypeException(value, targetType); + } + + switch (value) + { + case null or DBNull when targetType.IsReferenceTypeOrNullableType(): + return null; + + case null or DBNull when !targetType.IsReferenceTypeOrNullableType(): + ThrowCouldNotConvertNullToNonNullableEnumTypeException(targetType); + return null!; // Just to satisfy the compiler. + + case not null when value.GetType().IsAssignableTo(effectiveTargetType): + return value; + + case String stringValue when String.IsNullOrWhiteSpace(stringValue): + ThrowCouldNotConvertEmptyOrWhitespaceStringToEnumTypeException(targetType); + return null!; // Just to satisfy the compiler. + + case String stringValue: + if (!Enum.TryParse(effectiveTargetType, stringValue, true, out var result)) + { + ThrowCouldNotConvertStringToEnumTypeException(stringValue, targetType); + } + + return result; + + case Byte or SByte or Int16 or UInt16 or Int32 or UInt32 or Int64 or UInt64 or Double or Single or Decimal: + var enumUnderlyingType = Enum.GetUnderlyingType(effectiveTargetType); + + var valueConvertedToEnumUnderlyingType = Convert.ChangeType( + value, + enumUnderlyingType, + CultureInfo.InvariantCulture + ); + + if (!Enum.IsDefined(effectiveTargetType, valueConvertedToEnumUnderlyingType)) + { + ThrowCouldNotConvertNumericValueToEnumType(value, targetType); + } + + return Enum.ToObject( + effectiveTargetType, + valueConvertedToEnumUnderlyingType + ); + + default: + ThrowValueIsNeitherEnumValueNorStringNorNumericValueException( + value, + targetType + ); + return null!; // Just to satisfy the compiler. + } + } + [MethodImpl(MethodImplOptions.NoInlining)] [DoesNotReturn] private static void ThrowCouldNotConvertEmptyOrWhitespaceStringToEnumTypeException(Type enumType) => diff --git a/src/DbConnectionPlus/Converters/ValueConverter.cs b/src/DbConnectionPlus/Converters/ValueConverter.cs index bf795f1..f13ac7f 100644 --- a/src/DbConnectionPlus/Converters/ValueConverter.cs +++ b/src/DbConnectionPlus/Converters/ValueConverter.cs @@ -134,7 +134,10 @@ internal static Boolean CanConvert(Type sourceType, Type targetType) case String stringValue when effectiveTargetType == typeof(Char): if (stringValue.Length != 1) { - ThrowCouldNotConvertNonSingleCharStringToCharException(stringValue, targetType); + ThrowCouldNotConvertNonSingleCharStringToCharException( + stringValue, + targetType + ); } return (TTarget)(Object)stringValue[0]; @@ -201,18 +204,182 @@ internal static Boolean CanConvert(Type sourceType, Type targetType) try { - return (TTarget?)Convert.ChangeType(value, effectiveTargetType, CultureInfo.InvariantCulture); + return (TTarget?)Convert.ChangeType( + value, + effectiveTargetType, + CultureInfo.InvariantCulture + ); } catch (Exception exception) when ( exception is ArgumentException or InvalidCastException or FormatException or OverflowException ) { - ThrowCouldNotConvertValueToTargetTypeException(value, targetType, exception); + ThrowCouldNotConvertValueToTargetTypeException( + value, + targetType, + exception + ); return default; // Just to satisfy the compiler } } } + /// + /// Converts to the type . + /// + /// The value to convert to the type . + /// The type to convert to. + /// converted to the type . + /// is . + /// + /// + /// + /// + /// is or a value, but + /// the type is non-nullable. + /// + /// + /// + /// + /// could not be converted to the type , + /// because that conversion is not supported. + /// + /// + /// + /// + /// is or and + /// is a string that has a length other than 1. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Object? ConvertValueToType(Object? value, Type targetType) + { + ArgumentNullException.ThrowIfNull(targetType); + + // Unwrap Nullable types: + var effectiveTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + switch (value) + { + // Cases are ordered by frequency of use: + + case null or DBNull when targetType.IsReferenceTypeOrNullableType(): + return null; + + case null or DBNull when !targetType.IsReferenceTypeOrNullableType(): + ThrowCouldNotConvertNullOrDbNullToNonNullableTargetTypeException(value, targetType); + return null!; // Just to satisfy the compiler. + + case not null when value.GetType().IsAssignableTo(effectiveTargetType): + return value; + + case String stringValue when effectiveTargetType == typeof(Guid): + if (!Guid.TryParse(stringValue, out var guidResult)) + { + ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType); + } + + return guidResult; + + case String stringValue when effectiveTargetType == typeof(TimeSpan): + if (!TimeSpan.TryParse(stringValue, out var timeSpanResult)) + { + ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType); + } + + return timeSpanResult; + + case String stringValue when effectiveTargetType == typeof(Char): + if (stringValue.Length != 1) + { + ThrowCouldNotConvertNonSingleCharStringToCharException( + stringValue, + targetType + ); + } + + return stringValue[0]; + + case String stringValue when effectiveTargetType == typeof(DateTimeOffset): + if (!DateTimeOffset.TryParse(stringValue, out var dateTimeOffsetResult)) + { + ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType); + } + + return dateTimeOffsetResult; + + case String stringValue when effectiveTargetType == typeof(DateOnly): + if (!DateOnly.TryParse(stringValue, out var dateOnlyResult)) + { + ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType); + } + + return dateOnlyResult; + + case String stringValue when effectiveTargetType == typeof(TimeOnly): + if (!TimeOnly.TryParse(stringValue, out var timeOnlyResult)) + { + ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType); + } + + return timeOnlyResult; + + case Guid guid when targetType == typeof(String): + return guid.ToString("D"); + + case Guid guid when targetType == typeof(Byte[]): + return guid.ToByteArray(); + + case DateTime dateTime when targetType == typeof(String): + return dateTime.ToString("O", CultureInfo.InvariantCulture); + + case DateTime dateTime when effectiveTargetType == typeof(DateOnly): + return DateOnly.FromDateTime(dateTime); + + case TimeSpan timeSpan when targetType == typeof(String): + return timeSpan.ToString("g", CultureInfo.InvariantCulture); + + case TimeSpan timeSpan when effectiveTargetType == typeof(TimeOnly): + return TimeOnly.FromTimeSpan(timeSpan); + + case Byte[] bytes when effectiveTargetType == typeof(Guid): + return new Guid(bytes); + + case DateTimeOffset dateTimeOffset when targetType == typeof(String): + return dateTimeOffset.ToString("O", CultureInfo.InvariantCulture); + + case DateOnly dateOnly when targetType == typeof(String): + return dateOnly.ToString("O", CultureInfo.InvariantCulture); + + case TimeOnly timeOnly when targetType == typeof(String): + return timeOnly.ToString("O", CultureInfo.InvariantCulture); + + default: + if (effectiveTargetType.IsEnum) + { + return EnumConverter.ConvertValueToEnumMember(value, targetType); + } + + try + { + return Convert.ChangeType( + value, + effectiveTargetType, + CultureInfo.InvariantCulture + ); + } + catch (Exception exception) when ( + exception is ArgumentException or InvalidCastException or FormatException or OverflowException + ) + { + ThrowCouldNotConvertValueToTargetTypeException(value, targetType, exception); + return null!; // Just to satisfy the compiler + } + } + } + /// /// Determines whether is a type that can be converted to an enum type or a type that an /// enum can be converted to. diff --git a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs b/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs deleted file mode 100644 index 06ce2ad..0000000 --- a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 David Liebeherr -// Licensed under the MIT License. See LICENSE.md in the project root for more information. - -using Microsoft.Data.Sqlite; -using MySqlConnector; -using Npgsql; -using Oracle.ManagedDataAccess.Client; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; - -namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters; - -/// -/// A registry for database adapters that adopt DbConnectionPlus to specific database systems. -/// -public static class DatabaseAdapterRegistry -{ - /// - /// Initializes the class by registering the default database adapters. - /// - static DatabaseAdapterRegistry() - { - adapters.TryAdd(typeof(MySqlConnection), new MySqlDatabaseAdapter()); - adapters.TryAdd(typeof(OracleConnection), new OracleDatabaseAdapter()); - adapters.TryAdd(typeof(NpgsqlConnection), new PostgreSqlDatabaseAdapter()); - adapters.TryAdd(typeof(SqliteConnection), new SqliteDatabaseAdapter()); - adapters.TryAdd(typeof(SqlConnection), new SqlServerDatabaseAdapter()); - } - - /// - /// Registers a database adapter for the connection type . - /// If an adapter is already registered for the connection type , the existing - /// adapter is replaced. - /// - /// - /// The type of database connection for which is being registered. - /// - /// - /// The database adapter to associate with the connection type . - /// - /// is . - public static void RegisterAdapter(IDatabaseAdapter adapter) - where TConnection : DbConnection - { - ArgumentNullException.ThrowIfNull(adapter); - - adapters.AddOrUpdate(typeof(TConnection), adapter, (_, _) => adapter); - } - - /// - /// Retrieves the database adapter associated with the connection type . - /// - /// - /// The type of the database connection for which to retrieve the adapter. - /// - /// - /// An instance that supports database connections of the type - /// . - /// - /// - /// is . - /// - /// - /// No adapter is registered for the connection type . - /// - internal static IDatabaseAdapter GetAdapter(Type connectionType) - { - ArgumentNullException.ThrowIfNull(connectionType); - - return adapters.TryGetValue(connectionType, out var adapter) - ? adapter - : throw new InvalidOperationException( - $"No database adapter is registered for the database connection of the type {connectionType}. " + - $"Please call {nameof(DatabaseAdapterRegistry)}.{nameof(RegisterAdapter)} to register an adapter " + - "for that connection type." - ); - } - - private static readonly ConcurrentDictionary adapters = []; -} diff --git a/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs index c1896a5..8fbcf24 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs @@ -24,7 +24,7 @@ public interface IDatabaseAdapter /// /// /// If is an value, it is serialized according to the setting - /// before being assigned to the parameter. + /// before being assigned to the parameter. /// /// /// The parameter to bind to. diff --git a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs index 0eaa4fb..a7f800a 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs @@ -34,20 +34,20 @@ public interface IEntityManipulator /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entities will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entities will be deleted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// public Int32 DeleteEntities( @@ -85,20 +85,20 @@ CancellationToken cancellationToken /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entities will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entities will be deleted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// public Task DeleteEntitiesAsync( @@ -132,20 +132,20 @@ CancellationToken cancellationToken /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entity will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entity will be deleted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// public Int32 DeleteEntity( @@ -183,20 +183,20 @@ CancellationToken cancellationToken /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entity will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entity will be deleted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// public Task DeleteEntityAsync( @@ -234,23 +234,27 @@ CancellationToken cancellationToken /// /// /// - /// The table into which the entities will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entities will be inserted can be configured via or + /// . Per default, the singular name of the type + /// is used + /// as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -294,23 +298,26 @@ CancellationToken cancellationToken /// /// /// - /// The table into which the entities will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entities will be inserted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -350,23 +357,26 @@ CancellationToken cancellationToken /// /// /// - /// The table into which the entity will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entity will be inserted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -410,23 +420,26 @@ CancellationToken cancellationToken /// /// /// - /// The table into which the entity will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entity will be inserted can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -448,7 +461,7 @@ CancellationToken cancellationToken /// A token that can be used to cancel the operation. /// The number of rows that were affected by the update operation. /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// @@ -469,27 +482,30 @@ CancellationToken cancellationToken /// /// /// - /// The table where the entities will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entities will be updated can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -537,27 +553,30 @@ CancellationToken cancellationToken /// /// /// - /// The table where the entities will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entities will be updated can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -593,34 +612,37 @@ CancellationToken cancellationToken /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table where the entity will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entity will be updated can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -660,34 +682,37 @@ CancellationToken cancellationToken /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table where the entity will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entity will be updated can be configured via or + /// . Per default, the singular name of the type + /// is used as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or + /// ) are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs index 1fe4dc9..e88a131 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; @@ -34,7 +34,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { case Enum enumValue: - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -44,11 +44,14 @@ public void BindParameterValue(DbParameter parameter, Object? value) _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; - parameter.Value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case DateTime: diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index 18159dd..cba7f92 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -3,6 +3,7 @@ using LinkDotNet.StringBuilder; using MySqlConnector; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -899,6 +900,58 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } + /// + /// Creates the SQL code to create a temporary table for the keys of the provided entity type. + /// + /// The name of the table to create. + /// The metadata for the entity type to create the table for. + /// The SQL code to create the temporary table. + private String CreateEntityKeysTemporaryTableSqlCode( + String tableName, + EntityTypeMetadata entityTypeMetadata + ) + { + if (entityTypeMetadata.KeyProperties.Count == 0) + { + ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); + } + + using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); + + createKeysTableSqlBuilder.Append("CREATE TEMPORARY TABLE `"); + createKeysTableSqlBuilder.Append(tableName); + createKeysTableSqlBuilder.AppendLine("`"); + + createKeysTableSqlBuilder.Append(Constants.Indent); + createKeysTableSqlBuilder.Append("("); + + var prependSeparator = false; + + foreach (var property in entityTypeMetadata.KeyProperties) + { + if (prependSeparator) + { + createKeysTableSqlBuilder.Append(", "); + } + + createKeysTableSqlBuilder.Append('`'); + createKeysTableSqlBuilder.Append(property.PropertyName); + createKeysTableSqlBuilder.Append("` "); + createKeysTableSqlBuilder.Append( + this.databaseAdapter.GetDataType( + property.PropertyType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) + ); + + prependSeparator = true; + } + + createKeysTableSqlBuilder.AppendLine(")"); + + return createKeysTableSqlBuilder.ToString(); + } + /// /// Creates a command to insert an entity. /// @@ -973,58 +1026,6 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } - /// - /// Creates the SQL code to create a temporary table for the keys of the provided entity type. - /// - /// The name of the table to create. - /// The metadata for the entity type to create the table for. - /// The SQL code to create the temporary table. - private String CreateEntityKeysTemporaryTableSqlCode( - String tableName, - EntityTypeMetadata entityTypeMetadata - ) - { - if (entityTypeMetadata.KeyProperties.Count == 0) - { - ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); - } - - using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); - - createKeysTableSqlBuilder.Append("CREATE TEMPORARY TABLE `"); - createKeysTableSqlBuilder.Append(tableName); - createKeysTableSqlBuilder.AppendLine("`"); - - createKeysTableSqlBuilder.Append(Constants.Indent); - createKeysTableSqlBuilder.Append("("); - - var prependSeparator = false; - - foreach (var property in entityTypeMetadata.KeyProperties) - { - if (prependSeparator) - { - createKeysTableSqlBuilder.Append(", "); - } - - createKeysTableSqlBuilder.Append('`'); - createKeysTableSqlBuilder.Append(property.PropertyName); - createKeysTableSqlBuilder.Append("` "); - createKeysTableSqlBuilder.Append( - this.databaseAdapter.GetDataType( - property.PropertyType, - DbConnectionExtensions.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Gets the SQL code to delete an entity of the provided entity type. /// @@ -1172,14 +1173,10 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => sqlBuilder.Append(Constants.Indent); - var identityProperty = entityTypeMetadata.DatabaseGeneratedProperties.FirstOrDefault(a => - a.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity - ); - - if (identityProperty is not null) + if (entityTypeMetadata.IdentityProperty is not null) { sqlBuilder.Append('`'); - sqlBuilder.Append(identityProperty.ColumnName); + sqlBuilder.Append(entityTypeMetadata.IdentityProperty.ColumnName); sqlBuilder.Append("` = LAST_INSERT_ID()"); } else @@ -1393,6 +1390,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1430,6 +1429,8 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs index 6eaa071..a9f51bf 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; @@ -65,7 +65,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -84,7 +84,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -167,7 +167,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -190,7 +190,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -351,7 +351,10 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu if (value is Enum enumValue) { enumValues.Add( - EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode) + EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) ); } else @@ -360,7 +363,7 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu } } - var newValuesType = DbConnectionExtensions.EnumSerializationMode switch + var newValuesType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => typeof(Int32?), @@ -370,7 +373,7 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs index 441888b..77677d7 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using Oracle.ManagedDataAccess.Client; @@ -45,7 +45,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { case Enum enumValue: - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -55,11 +55,14 @@ public void BindParameterValue(DbParameter parameter, Object? value) _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; - parameter.Value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case Guid guid: diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs index ae443e2..a1bf7d1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -640,7 +641,7 @@ EntityTypeMetadata entityTypeMetadata parameter.ParameterName = "return_" + property.ColumnName; parameter.DbType = this.databaseAdapter.GetDbType( property.PropertyType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); parameter.Direction = ParameterDirection.Output; parameters.Add(parameter); @@ -691,7 +692,7 @@ EntityTypeMetadata entityTypeMetadata parameter.ParameterName = "return_" + property.ColumnName; parameter.DbType = this.databaseAdapter.GetDbType( property.PropertyType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); parameter.Direction = ParameterDirection.Output; parameters.Add(parameter); @@ -1025,6 +1026,9 @@ Object entity } var value = outputParameters[i].Value; + + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs index dc2ae73..a7c35dd 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using FastMember; @@ -76,7 +76,7 @@ public TemporaryTableDisposer BuildTemporaryTable( // ReSharper disable once PossibleMultipleEnumeration values, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -95,7 +95,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateMultiColumnTemporaryTableSqlCode( quotedTableName, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -111,7 +111,14 @@ public TemporaryTableDisposer BuildTemporaryTable( // ReSharper disable once PossibleMultipleEnumeration using var reader = CreateValuesDataReader(values, valuesType); - this.PopulateTemporaryTable(oracleConnection, oracleTransaction, quotedTableName, valuesType, reader, cancellationToken); + this.PopulateTemporaryTable( + oracleConnection, + oracleTransaction, + quotedTableName, + valuesType, + reader, + cancellationToken + ); return new( () => DropTemporaryTable(quotedTableName, oracleConnection, oracleTransaction), @@ -166,7 +173,7 @@ public async Task BuildTemporaryTableAsync( // ReSharper disable once PossibleMultipleEnumeration values, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -187,7 +194,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateMultiColumnTemporaryTableSqlCode( quotedTableName, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -469,7 +476,8 @@ DbDataReader dataReader } else { - var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead).ToList(); + var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead) + .ToList(); for (var i = 0; i < properties.Count; i++) { diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs index 49d64b3..275adaa 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using NpgsqlTypes; @@ -35,7 +35,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { case Enum enumValue: - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -45,11 +45,14 @@ public void BindParameterValue(DbParameter parameter, Object? value) _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; - parameter.Value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case DateTime: diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index c7c0bc3..9567f39 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -1,8 +1,9 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; using Npgsql; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -771,7 +772,11 @@ CancellationToken cancellationToken var npgsqlDbTypes = entityTypeMetadata .KeyProperties - .Select(p => this.databaseAdapter.GetDbType(p.PropertyType, DbConnectionExtensions.EnumSerializationMode)) + .Select(p => this.databaseAdapter.GetDbType( + p.PropertyType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) + ) .ToArray(); using var importer = connection.BeginBinaryImport($"COPY \"{keysTableName}\" FROM STDIN (FORMAT BINARY)"); @@ -835,7 +840,11 @@ await connection.ExecuteNonQueryAsync( var npgsqlDbTypes = entityTypeMetadata .KeyProperties - .Select(p => this.databaseAdapter.GetDbType(p.PropertyType, DbConnectionExtensions.EnumSerializationMode)) + .Select(p => this.databaseAdapter.GetDbType( + p.PropertyType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) + ) .ToArray(); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task @@ -913,6 +922,58 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } + /// + /// Creates the SQL code to create a temporary table for the keys of the provided entity type. + /// + /// The name of the table to create. + /// The metadata for the entity type to create the table for. + /// The SQL code to create the temporary table. + private String CreateEntityKeysTemporaryTableSqlCode( + String tableName, + EntityTypeMetadata entityTypeMetadata + ) + { + if (entityTypeMetadata.KeyProperties.Count == 0) + { + ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); + } + + using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); + + createKeysTableSqlBuilder.Append("CREATE TEMP TABLE \""); + createKeysTableSqlBuilder.Append(tableName); + createKeysTableSqlBuilder.AppendLine("\""); + + createKeysTableSqlBuilder.Append(Constants.Indent); + createKeysTableSqlBuilder.Append("("); + + var prependSeparator = false; + + foreach (var property in entityTypeMetadata.KeyProperties) + { + if (prependSeparator) + { + createKeysTableSqlBuilder.Append(", "); + } + + createKeysTableSqlBuilder.Append('"'); + createKeysTableSqlBuilder.Append(property.PropertyName); + createKeysTableSqlBuilder.Append("\" "); + createKeysTableSqlBuilder.Append( + this.databaseAdapter.GetDataType( + property.PropertyType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) + ); + + prependSeparator = true; + } + + createKeysTableSqlBuilder.AppendLine(")"); + + return createKeysTableSqlBuilder.ToString(); + } + /// /// Creates a command to insert an entity. /// @@ -987,58 +1048,6 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } - /// - /// Creates the SQL code to create a temporary table for the keys of the provided entity type. - /// - /// The name of the table to create. - /// The metadata for the entity type to create the table for. - /// The SQL code to create the temporary table. - private String CreateEntityKeysTemporaryTableSqlCode( - String tableName, - EntityTypeMetadata entityTypeMetadata - ) - { - if (entityTypeMetadata.KeyProperties.Count == 0) - { - ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); - } - - using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); - - createKeysTableSqlBuilder.Append("CREATE TEMP TABLE \""); - createKeysTableSqlBuilder.Append(tableName); - createKeysTableSqlBuilder.AppendLine("\""); - - createKeysTableSqlBuilder.Append(Constants.Indent); - createKeysTableSqlBuilder.Append("("); - - var prependSeparator = false; - - foreach (var property in entityTypeMetadata.KeyProperties) - { - if (prependSeparator) - { - createKeysTableSqlBuilder.Append(", "); - } - - createKeysTableSqlBuilder.Append('"'); - createKeysTableSqlBuilder.Append(property.PropertyName); - createKeysTableSqlBuilder.Append("\" "); - createKeysTableSqlBuilder.Append( - this.databaseAdapter.GetDataType( - property.PropertyType, - DbConnectionExtensions.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Gets the SQL code to delete an entity of the provided entity type. /// @@ -1328,6 +1337,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1365,6 +1376,8 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs index d8f572a..aa78493 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using FastMember; @@ -65,7 +65,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -84,7 +84,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -142,7 +142,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -163,7 +163,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -288,7 +288,9 @@ CancellationToken cancellationToken var npgsqlDbTypes = dataReader .GetFieldTypes() - .Select(t => this.databaseAdapter.GetDbType(t, DbConnectionExtensions.EnumSerializationMode)) + .Select(t => + this.databaseAdapter.GetDbType(t, DbConnectionPlusConfiguration.Instance.EnumSerializationMode) + ) .ToArray(); while (dataReader.Read()) @@ -309,7 +311,10 @@ CancellationToken cancellationToken if (value is Enum enumValue) { - value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); } importer.Write(value, npgsqlDbTypes[i]); @@ -344,7 +349,9 @@ CancellationToken cancellationToken var npgsqlDbTypes = dataReader .GetFieldTypes() - .Select(a => this.databaseAdapter.GetDbType(a, DbConnectionExtensions.EnumSerializationMode)) + .Select(a => + this.databaseAdapter.GetDbType(a, DbConnectionPlusConfiguration.Instance.EnumSerializationMode) + ) .ToArray(); while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false)) @@ -365,7 +372,10 @@ CancellationToken cancellationToken if (value is Enum enumValue) { - value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); } await importer.WriteAsync(value, npgsqlDbTypes[i], cancellationToken).ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs index 9faa11f..554a70c 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; @@ -34,7 +34,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { case Enum enumValue: - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -44,11 +44,14 @@ public void BindParameterValue(DbParameter parameter, Object? value) _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; - parameter.Value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case DateTime: diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index 5f6eab2..cbd2924 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -890,6 +891,58 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } + /// + /// Creates the SQL code to create a temporary table for the keys of the provided entity type. + /// + /// The name of the table to create. + /// The metadata for the entity type to create the table for. + /// The SQL code to create the temporary table. + private String CreateEntityKeysTemporaryTableSqlCode( + String tableName, + EntityTypeMetadata entityTypeMetadata + ) + { + if (entityTypeMetadata.KeyProperties.Count == 0) + { + ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); + } + + using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); + + createKeysTableSqlBuilder.Append("CREATE TABLE [#"); + createKeysTableSqlBuilder.Append(tableName); + createKeysTableSqlBuilder.AppendLine("]"); + + createKeysTableSqlBuilder.Append(Constants.Indent); + createKeysTableSqlBuilder.Append("("); + + var prependSeparator = false; + + foreach (var property in entityTypeMetadata.KeyProperties) + { + if (prependSeparator) + { + createKeysTableSqlBuilder.Append(", "); + } + + createKeysTableSqlBuilder.Append('['); + createKeysTableSqlBuilder.Append(property.PropertyName); + createKeysTableSqlBuilder.Append("] "); + createKeysTableSqlBuilder.Append( + this.databaseAdapter.GetDataType( + property.PropertyType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ) + ); + + prependSeparator = true; + } + + createKeysTableSqlBuilder.AppendLine(")"); + + return createKeysTableSqlBuilder.ToString(); + } + /// /// Creates a command to insert an entity. /// @@ -964,58 +1017,6 @@ EntityTypeMetadata entityTypeMetadata return (command, parameters); } - /// - /// Creates the SQL code to create a temporary table for the keys of the provided entity type. - /// - /// The name of the table to create. - /// The metadata for the entity type to create the table for. - /// The SQL code to create the temporary table. - private String CreateEntityKeysTemporaryTableSqlCode( - String tableName, - EntityTypeMetadata entityTypeMetadata - ) - { - if (entityTypeMetadata.KeyProperties.Count == 0) - { - ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType); - } - - using var createKeysTableSqlBuilder = new ValueStringBuilder(stackalloc Char[200]); - - createKeysTableSqlBuilder.Append("CREATE TABLE [#"); - createKeysTableSqlBuilder.Append(tableName); - createKeysTableSqlBuilder.AppendLine("]"); - - createKeysTableSqlBuilder.Append(Constants.Indent); - createKeysTableSqlBuilder.Append("("); - - var prependSeparator = false; - - foreach (var property in entityTypeMetadata.KeyProperties) - { - if (prependSeparator) - { - createKeysTableSqlBuilder.Append(", "); - } - - createKeysTableSqlBuilder.Append('['); - createKeysTableSqlBuilder.Append(property.PropertyName); - createKeysTableSqlBuilder.Append("] "); - createKeysTableSqlBuilder.Append( - this.databaseAdapter.GetDataType( - property.PropertyType, - DbConnectionExtensions.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Gets the SQL code to delete an entity of the provided entity type. /// @@ -1305,6 +1306,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1342,6 +1345,8 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs index 072788e..260cd00 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using FastMember; @@ -72,7 +72,7 @@ public TemporaryTableDisposer BuildTemporaryTable( values, valuesType, databaseCollation, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -92,7 +92,7 @@ public TemporaryTableDisposer BuildTemporaryTable( name, valuesType, databaseCollation, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -118,12 +118,15 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - sqlBulkCopy.ColumnMappings.Add(Constants.SingleColumnTemporaryTableColumnName, Constants.SingleColumnTemporaryTableColumnName); + sqlBulkCopy.ColumnMappings.Add( + Constants.SingleColumnTemporaryTableColumnName, + Constants.SingleColumnTemporaryTableColumnName + ); } else { var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead); - + foreach (var property in properties) { sqlBulkCopy.ColumnMappings.Add(property.PropertyName, property.ColumnName); @@ -182,7 +185,7 @@ public async Task BuildTemporaryTableAsync( values, valuesType, databaseCollation, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -204,7 +207,7 @@ public async Task BuildTemporaryTableAsync( name, valuesType, databaseCollation, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -233,7 +236,10 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - sqlBulkCopy.ColumnMappings.Add(Constants.SingleColumnTemporaryTableColumnName, Constants.SingleColumnTemporaryTableColumnName); + sqlBulkCopy.ColumnMappings.Add( + Constants.SingleColumnTemporaryTableColumnName, + Constants.SingleColumnTemporaryTableColumnName + ); } else { diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs index 125e456..3341b13 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; @@ -34,7 +34,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { case Enum enumValue: - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -44,11 +44,14 @@ public void BindParameterValue(DbParameter parameter, Object? value) _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; - parameter.Value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case DateTime: diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 0592aa2..1519eda 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -832,14 +833,10 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => sqlBuilder.Append(Constants.Indent); - var identityProperty = entityTypeMetadata.DatabaseGeneratedProperties.FirstOrDefault(a => - a.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity - ); - - if (identityProperty is not null) + if (entityTypeMetadata.IdentityProperty is not null) { sqlBuilder.Append('"'); - sqlBuilder.Append(identityProperty.ColumnName); + sqlBuilder.Append(entityTypeMetadata.IdentityProperty.ColumnName); sqlBuilder.Append("\" = last_insert_rowid()"); } else @@ -1056,6 +1053,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1093,6 +1092,8 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs index d412150..501793f 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using FastMember; @@ -65,7 +65,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -84,7 +84,7 @@ public TemporaryTableDisposer BuildTemporaryTable( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -142,7 +142,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateSingleColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -163,7 +163,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -181,7 +181,14 @@ public async Task BuildTemporaryTableAsync( await using var reader = CreateValuesDataReader(values, valuesType); #pragma warning restore CA2007 - await PopulateTemporaryTableAsync(sqliteConnection, sqliteTransaction, name, valuesType, reader, cancellationToken) + await PopulateTemporaryTableAsync( + sqliteConnection, + sqliteTransaction, + name, + valuesType, + reader, + cancellationToken + ) .ConfigureAwait(false); return new( @@ -306,7 +313,8 @@ DbDataReader dataReader } else { - var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead).ToList(); + var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead) + .ToList(); for (var i = 0; i < properties.Count; i++) { @@ -457,7 +465,10 @@ CancellationToken cancellationToken if (value is Enum enumValue) { - value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); } parameters[i].Value = value; @@ -510,7 +521,10 @@ CancellationToken cancellationToken if (value is Enum enumValue) { - value = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); } parameters[i].Value = value; diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index 3fdf9fa..a079b78 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; @@ -203,7 +203,7 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist { parameterValue = EnumSerializer.SerializeEnum( enumValue, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); } @@ -241,7 +241,7 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist { parameterValue = EnumSerializer.SerializeEnum( enumValue, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); } diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs new file mode 100644 index 0000000..9d68a24 --- /dev/null +++ b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2026 David Liebeherr +// Licensed under the MIT License. See LICENSE.md in the project root for more information. + +using RentADeveloper.DbConnectionPlus.DbCommands; +using RentADeveloper.DbConnectionPlus.Entities; +using RentADeveloper.DbConnectionPlus.SqlStatements; + +namespace RentADeveloper.DbConnectionPlus; + +/// +/// Provides extension members for the type . +/// +public static partial class DbConnectionExtensions +{ + /// + /// Configures DbConnectionPlus. + /// + /// The action that configures DbConnectionPlus. + public static void Configure(Action configureAction) + { + ArgumentNullException.ThrowIfNull(configureAction); + + configureAction(DbConnectionPlusConfiguration.Instance); + + ((IFreezable)DbConnectionPlusConfiguration.Instance).Freeze(); + + // We need to reset the entity type metadata cache, because the configuration may have changed how entities + // are mapped that were previously mapped via data annotation attributes or conventions. + EntityHelper.ResetEntityTypeMetadataCache(); + } + + /// + /// The factory to use to create instances of . + /// + /// + /// This property is mainly used to test the cancellation of SQL statements in integration tests. + /// + internal static IDbCommandFactory DbCommandFactory { get; set; } = new DefaultDbCommandFactory(); + + /// + /// A function to be called before executing a database command via DbConnectionPlus. + /// + /// The database command being executed. + /// The temporary tables created for the command. + internal static void OnBeforeExecutingCommand( + DbCommand command, + IReadOnlyList temporaryTables + ) => + DbConnectionPlusConfiguration.Instance.InterceptDbCommand?.Invoke(command, temporaryTables); +} diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index fa415a9..751fc3e 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -32,20 +32,20 @@ public static partial class DbConnectionExtensions /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entities will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entities will be deleted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// /// @@ -73,7 +73,7 @@ public static Int32 DeleteEntities( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.DeleteEntities( connection, @@ -110,20 +110,20 @@ public static Int32 DeleteEntities( /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entities will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entities will be deleted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// /// @@ -151,7 +151,7 @@ public static Task DeleteEntitiesAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator .DeleteEntitiesAsync(connection, entities, transaction, cancellationToken); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs index 9922c3e..289d038 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs @@ -32,20 +32,20 @@ public static partial class DbConnectionExtensions /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entity will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entity will be deleted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// /// @@ -76,7 +76,7 @@ public static Int32 DeleteEntity( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.DeleteEntity( connection, @@ -113,20 +113,20 @@ public static Int32 DeleteEntity( /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table from which the entity will be deleted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table from which the entity will be deleted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// /// @@ -157,7 +157,7 @@ public static Task DeleteEntityAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.DeleteEntityAsync( connection, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs index cd209f0..9b99813 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.SqlStatements; @@ -51,7 +51,7 @@ public static Int32 ExecuteNonQuery( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -123,7 +123,7 @@ public static async Task ExecuteNonQueryAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs index 704f17d..4e8cc70 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Readers; @@ -60,7 +60,7 @@ public static DbDataReader ExecuteReader( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -155,7 +155,7 @@ public static async Task ExecuteReaderAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs index 969201b..8739339 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; @@ -67,7 +67,7 @@ public static TTarget ExecuteScalar( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -159,7 +159,7 @@ public static async Task ExecuteScalarAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs index 51a9ee6..5d6cbb2 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs @@ -58,7 +58,7 @@ public static Boolean Exists( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -136,7 +136,7 @@ public static async Task ExistsAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs index 5938987..a22f16d 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs @@ -38,25 +38,26 @@ public static partial class DbConnectionExtensions /// /// /// - /// The table into which the entities will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entities will be inserted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -67,7 +68,6 @@ public static partial class DbConnectionExtensions /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -91,7 +91,7 @@ public static Int32 InsertEntities( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.InsertEntities( connection, @@ -132,25 +132,26 @@ public static Int32 InsertEntities( /// /// /// - /// The table into which the entities will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entities will be inserted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -161,7 +162,6 @@ public static Int32 InsertEntities( /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -185,9 +185,14 @@ public static Task InsertEntitiesAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator - .InsertEntitiesAsync(connection, entities, transaction, cancellationToken); + .InsertEntitiesAsync( + connection, + entities, + transaction, + cancellationToken + ); } } diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs index e1424d2..b73962f 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs @@ -38,25 +38,26 @@ public static partial class DbConnectionExtensions /// /// /// - /// The table into which the entity will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entity will be inserted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -67,7 +68,6 @@ public static partial class DbConnectionExtensions /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -91,7 +91,7 @@ public static Int32 InsertEntity( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.InsertEntity( connection, @@ -132,25 +132,26 @@ public static Int32 InsertEntity( /// /// /// - /// The table into which the entity will be inserted is determined by the - /// applied to the type . - /// If this attribute is not present, the singular name of the type is used. + /// The table into which the entity will be inserted can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not inserted. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not inserted. /// Once an entity is inserted, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -161,7 +162,6 @@ public static Int32 InsertEntity( /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -185,7 +185,7 @@ public static Task InsertEntityAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.InsertEntityAsync( connection, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs index 86ac1e2..e343b8e 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs @@ -62,7 +62,7 @@ public static partial class DbConnectionExtensions /// /// /// If you pass an value as a parameter, the enum value is serialized according to the setting - /// . + /// . /// /// public static InterpolatedParameter Parameter( diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Query.cs b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs index 1eee4f9..12079ce 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Query.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Materializers; @@ -60,7 +60,7 @@ public static IEnumerable Query( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -166,7 +166,7 @@ public static async IAsyncEnumerable QueryAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs index d992b53..0e349a8 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Materializers; @@ -57,7 +57,7 @@ public static dynamic QueryFirst( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -142,7 +142,7 @@ public static async Task QueryFirstAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs index b1951f3..22118ef 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; @@ -126,7 +126,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -187,7 +188,7 @@ public static T QueryFirst( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -372,7 +373,8 @@ public static T QueryFirst( /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -433,7 +435,7 @@ public static async Task QueryFirstAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs index 4b651b3..08b6d3d 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs @@ -60,7 +60,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -147,7 +147,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs index 145f093..30cadc3 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs @@ -126,7 +126,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -187,7 +188,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -374,7 +375,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -437,7 +439,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs index 3cb1678..7e9785c 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs @@ -123,7 +123,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -180,7 +181,7 @@ public static IEnumerable Query( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -384,7 +385,8 @@ public static IEnumerable Query( /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -441,7 +443,7 @@ public static async IAsyncEnumerable QueryAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs index b2fa352..e1537ec 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs @@ -70,7 +70,7 @@ public static dynamic QuerySingle( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -175,7 +175,7 @@ public static async Task QuerySingleAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs index e58403c..1a30664 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs @@ -139,7 +139,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -200,7 +201,7 @@ public static T QuerySingle( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -408,7 +409,8 @@ public static T QuerySingle( /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -469,7 +471,7 @@ public static async Task QuerySingleAsync( { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs index 43d92b1..c5f488d 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs @@ -61,7 +61,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -156,7 +156,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs index 1e05f91..a01c440 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs @@ -127,7 +127,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -188,7 +189,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = DbCommandBuilder.BuildDbCommand( statement, @@ -383,7 +384,8 @@ public static partial class DbConnectionExtensions /// 2. Have a parameterless constructor and properties (with public setters) that match the columns of /// the result set returned by the statement. /// - /// The names of the properties must match the names of the columns (case-insensitive). + /// Per default, the names of the properties must match the names of the columns (case-insensitive). + /// This can be configured via or . /// /// The types of the properties must be compatible with the data types of the columns. /// The compatibility is determined using . @@ -446,7 +448,7 @@ public static partial class DbConnectionExtensions { ArgumentNullException.ThrowIfNull(connection); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( statement, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Settings.cs b/src/DbConnectionPlus/DbConnectionExtensions.Settings.cs deleted file mode 100644 index 7738fc9..0000000 --- a/src/DbConnectionPlus/DbConnectionExtensions.Settings.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2026 David Liebeherr -// Licensed under the MIT License. See LICENSE.md in the project root for more information. - -using RentADeveloper.DbConnectionPlus.DbCommands; -using RentADeveloper.DbConnectionPlus.SqlStatements; - -namespace RentADeveloper.DbConnectionPlus; - -/// -/// Provides extension members for the type . -/// -public static partial class DbConnectionExtensions -{ - /// - /// - /// Controls how values are serialized when they are sent to a database using one of the - /// following methods: - /// - /// - /// 1. When an entity containing an enum property is inserted via - /// , , - /// or . - /// - /// - /// 2. When an entity containing an enum property is updated via - /// , , - /// or . - /// - /// 3. When an enum value is passed as a parameter to an SQL statement via . - /// - /// 4. When a sequence of enum values is passed as a temporary table to an SQL statement via - /// . - /// - /// - /// 5. When objects containing an enum property are passed as a temporary table to an SQL statement via - /// . - /// - /// The default is . - /// - /// - /// Thread Safety: - /// This is a static mutable property. To avoid race conditions in multi-threaded applications, set this property - /// during application initialization before any database operations are performed, and do not change it afterward. - /// Changing this value while database operations are in progress from multiple threads may lead to inconsistent - /// behavior. - /// - public static EnumSerializationMode EnumSerializationMode { get; set; } = EnumSerializationMode.Strings; - - /// - /// A function that can be used to intercept database commands executed via DbConnectionPlus. - /// Can be used for logging or modifying commands before execution. - /// - /// - /// Thread Safety: - /// This is a static mutable property. To avoid race conditions in multi-threaded applications, set this property - /// during application initialization before any database operations are performed, and do not change it afterward. - /// Changing this value while database operations are in progress from multiple threads may lead to inconsistent - /// behavior. - /// - public static InterceptDbCommand? InterceptDbCommand { get; set; } - - /// - /// The factory to use to create instances of . - /// - /// - /// This property is mainly used to test the cancellation of SQL statements in integration tests. - /// - internal static IDbCommandFactory DbCommandFactory { get; set; } = new DefaultDbCommandFactory(); - - /// - /// A function to be called before executing a database command via DbConnectionPlus. - /// - /// The database command being executed. - /// The temporary tables created for the command. - internal static void OnBeforeExecutingCommand( - DbCommand command, - IReadOnlyList temporaryTables - ) => - InterceptDbCommand?.Invoke(command, temporaryTables); -} diff --git a/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs b/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs index 8879835..5beb473 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs @@ -129,7 +129,7 @@ public static partial class DbConnectionExtensions /// /// /// If you pass enum values or objects containing enum properties, the enum values are serialized according to the - /// setting . + /// setting . /// /// public static InterpolatedTemporaryTable TemporaryTable( diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs index 4992be9..f736f63 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs @@ -20,7 +20,7 @@ public static partial class DbConnectionExtensions /// A token that can be used to cancel the operation. /// The number of rows that were affected by the update operation. /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// @@ -41,29 +41,30 @@ public static partial class DbConnectionExtensions /// /// /// - /// The table where the entities will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entities will be updated can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -109,7 +110,7 @@ public static Int32 UpdateEntities( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.UpdateEntities( connection, @@ -132,7 +133,7 @@ public static Int32 UpdateEntities( /// will contain the number of rows that were affected by the update operation. /// /// - /// No instance property (with a public getter) of the type is denoted with a + /// No instance property of the type is configured as a key property. /// . /// /// @@ -154,29 +155,30 @@ public static Int32 UpdateEntities( /// /// /// - /// The table where the entities will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entities will be updated can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -222,7 +224,7 @@ public static Task UpdateEntitiesAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator .UpdateEntitiesAsync(connection, entities, transaction, cancellationToken); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs index daebb17..8114990 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs @@ -34,36 +34,37 @@ public static partial class DbConnectionExtensions /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table where the entity will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entity will be updated can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -100,7 +101,7 @@ public static Int32 UpdateEntity( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.UpdateEntity( connection, @@ -137,36 +138,37 @@ public static Int32 UpdateEntity( /// /// /// - /// No instance property of the type is denoted with a . + /// No instance property of the type is configured as a key property. /// /// /// The operation was cancelled via . /// /// /// - /// The table where the entity will be updated is determined by the applied to the - /// type . - /// If this attribute is not present, the singular name of the type is used. + /// The table in which the entity will be updated can be configured via or + /// . Per default, the singular name of the type is used + /// as the table name. /// /// - /// The type must have at least one instance property denoted with a - /// . + /// The type must have at least one instance property configured as key property. + /// Use or to configure key properties. /// /// - /// Each instance property of the type is mapped to a column with the same name - /// (case-sensitive) in the table. - /// If a property is denoted with the , the name specified in the attribute is used - /// as the column name. + /// Per default, each instance property of the type is mapped to a column with the + /// same name (case-sensitive) in the table. This can be configured via or + /// . /// /// /// The columns must have data types that are compatible with the property types of the corresponding properties. /// The compatibility is determined using . /// - /// Properties denoted with the are ignored. /// - /// Properties denoted with a where the - /// is set to or - /// are also ignored. + /// Properties configured as ignored properties (via or ) + /// are not updated. + /// + /// + /// Properties configured as identity or computed properties (via or + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// @@ -203,7 +205,7 @@ public static Task UpdateEntityAsync( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entity); - var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); + var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType()); return databaseAdapter.EntityManipulator.UpdateEntityAsync( connection, diff --git a/src/DbConnectionPlus/Entities/EntityHelper.cs b/src/DbConnectionPlus/Entities/EntityHelper.cs index afd9451..bc364d7 100644 --- a/src/DbConnectionPlus/Entities/EntityHelper.cs +++ b/src/DbConnectionPlus/Entities/EntityHelper.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using System.Reflection; @@ -112,6 +112,9 @@ public static class EntityHelper /// /// is . /// + /// + /// There is more than one identity property defined for the entity type . + /// public static EntityTypeMetadata GetEntityTypeMetadata(Type entityType) { ArgumentNullException.ThrowIfNull(entityType); @@ -122,6 +125,12 @@ public static EntityTypeMetadata GetEntityTypeMetadata(Type entityType) ); } + /// + /// Resets the cached entity types metadata. + /// + internal static void ResetEntityTypeMetadataCache() => + entityTypeMetadataPerEntityType.Clear(); + /// /// Creates the metadata for the entity type . /// @@ -129,9 +138,29 @@ public static EntityTypeMetadata GetEntityTypeMetadata(Type entityType) /// /// An instance of containing the created metadata. /// + /// + /// There is more than one identity property defined for the entity type . + /// private static EntityTypeMetadata CreateEntityTypeMetadata(Type entityType) { - var tableName = entityType.GetCustomAttribute()?.Name ?? entityType.Name; + String tableName; + + DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders() + .TryGetValue(entityType, out var entityTypeBuilder); + + if (entityTypeBuilder is not null) + { + tableName = !String.IsNullOrWhiteSpace(entityTypeBuilder.TableName) + ? entityTypeBuilder.TableName + : entityType.Name; + } + else + { + tableName = !String.IsNullOrWhiteSpace(entityType.GetCustomAttribute()?.Name) + ? entityType.GetCustomAttribute()?.Name! + : entityType.Name; + } + var properties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance); var propertiesMetadata = new EntityPropertyMetadata[properties.Length]; @@ -139,61 +168,75 @@ private static EntityTypeMetadata CreateEntityTypeMetadata(Type entityType) { var property = properties[i]; - propertiesMetadata[i] = new( - property.GetCustomAttribute()?.Name ?? property.Name, - property.Name, - property.PropertyType, - property, - property.GetCustomAttribute() is not null, - property.GetCustomAttribute() is not null, - property.CanRead, - property.CanWrite, - property.CanRead ? Reflect.PropertyGetter(property) : null, - property.CanWrite ? Reflect.PropertySetter(property) : null, - property.GetCustomAttribute()?.DatabaseGeneratedOption ?? - DatabaseGeneratedOption.None + if ( + entityTypeBuilder is not null && + entityTypeBuilder.PropertyBuilders.TryGetValue(property.Name, out var propertyBuilder) + ) + { + propertiesMetadata[i] = new( + !String.IsNullOrWhiteSpace(propertyBuilder.ColumnName) + ? propertyBuilder.ColumnName + : property.Name, + property.Name, + property.PropertyType, + property, + propertyBuilder.IsIgnored, + propertyBuilder.IsKey, + propertyBuilder.IsComputed, + propertyBuilder.IsIdentity, + property.CanRead, + property.CanWrite, + property.CanRead ? Reflect.PropertyGetter(property) : null, + property.CanWrite ? Reflect.PropertySetter(property) : null + ); + } + else + { + propertiesMetadata[i] = new( + property.GetCustomAttribute()?.Name ?? property.Name, + property.Name, + property.PropertyType, + property, + property.GetCustomAttribute() is not null, + property.GetCustomAttribute() is not null, + property.GetCustomAttribute()?.DatabaseGeneratedOption is + DatabaseGeneratedOption.Computed, + property.GetCustomAttribute()?.DatabaseGeneratedOption is + DatabaseGeneratedOption.Identity, + property.CanRead, + property.CanWrite, + property.CanRead ? Reflect.PropertyGetter(property) : null, + property.CanWrite ? Reflect.PropertySetter(property) : null + ); + } + } + + var identityProperties = propertiesMetadata.Where(a => a.IsIdentity).ToList(); + + if (identityProperties.Count > 1) + { + throw new InvalidOperationException( + $"There are multiple identity properties defined for the entity type {entityType}. Only one property " + + "can be marked as an identity property per entity type." ); } return new( entityType, tableName, - AllProperties: propertiesMetadata, - AllPropertiesByPropertyName: propertiesMetadata - .ToDictionary(p => p.PropertyName), - MappedProperties: propertiesMetadata - .Where(p => !p.IsNotMapped) - .ToList(), - KeyProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - IsKeyProperty: true - }) - .ToList(), - InsertProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.None - }) - .ToList(), - UpdateProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - IsKeyProperty: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.None - }) - .ToList(), - DatabaseGeneratedProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.Identity or DatabaseGeneratedOption.Computed - } + propertiesMetadata, + propertiesMetadata.ToDictionary(p => p.PropertyName), + [.. propertiesMetadata.Where(p => !p.IsIgnored)], + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsKey: true })], + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: true })], + identityProperties.FirstOrDefault(), + [.. propertiesMetadata.Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity))], + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: false, IsIdentity: false })], + [ + .. propertiesMetadata.Where(p => p is + { IsIgnored: false, IsKey: false, IsComputed: false, IsIdentity: false } ) - .ToList() + ] ); } diff --git a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs index ad93215..adfc45d 100644 --- a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs @@ -13,8 +13,10 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// The name of the property. /// The property type of the property. /// The property info of the property. -/// Determines whether the property is not mapped to a database column. -/// Determines whether the property is a key property. +/// Determines whether the property is ignored and not mapped to a database column. +/// Determines whether the property is a key property. +/// Determines whether the property is a computed property. +/// Determines whether the property is an identity property. /// Determines whether the property can be read. /// Determines whether the property can be written to. /// @@ -25,17 +27,17 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// The setter function for the property. /// This is if the property has no setter. /// -/// The database generated option for the property. public sealed record EntityPropertyMetadata( String ColumnName, String PropertyName, Type PropertyType, PropertyInfo PropertyInfo, - Boolean IsNotMapped, - Boolean IsKeyProperty, + Boolean IsIgnored, + Boolean IsKey, + Boolean IsComputed, + Boolean IsIdentity, Boolean CanRead, Boolean CanWrite, MemberGetter? PropertyGetter, - MemberSetter? PropertySetter, - DatabaseGeneratedOption DatabaseGeneratedOption + MemberSetter? PropertySetter ); diff --git a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs index 7a4a296..3b55e8a 100644 --- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs @@ -20,15 +20,22 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// /// The metadata of the key properties of the entity type. /// +/// +/// The metadata of the computed properties of the entity type. +/// +/// +/// The metadata of the identity property of the entity type. +/// This is if the entity type does not have an identity property. +/// +/// +/// The metadata of the database-generated properties of the entity type. +/// /// /// The metadata of the properties needed to insert an entity of the entity type into the database. /// /// /// The metadata of the properties needed to update an entity of the entity type in the database. /// -/// -/// The metadata of the database generated properties of the entity type. -/// public sealed record EntityTypeMetadata( Type EntityType, String TableName, @@ -36,7 +43,9 @@ public sealed record EntityTypeMetadata( IReadOnlyDictionary AllPropertiesByPropertyName, IReadOnlyList MappedProperties, IReadOnlyList KeyProperties, + IReadOnlyList ComputedProperties, + EntityPropertyMetadata? IdentityProperty, + IReadOnlyList DatabaseGeneratedProperties, IReadOnlyList InsertProperties, - IReadOnlyList UpdateProperties, - IReadOnlyList DatabaseGeneratedProperties + IReadOnlyList UpdateProperties ); diff --git a/src/DbConnectionPlus/GlobalUsings.cs b/src/DbConnectionPlus/GlobalUsings.cs index f49bbc9..a5a1db0 100644 --- a/src/DbConnectionPlus/GlobalUsings.cs +++ b/src/DbConnectionPlus/GlobalUsings.cs @@ -7,4 +7,5 @@ global using System.Globalization; global using System.Runtime.CompilerServices; global using Microsoft.Data.SqlClient; +global using RentADeveloper.DbConnectionPlus.Configuration; global using RentADeveloper.DbConnectionPlus.DatabaseAdapters; diff --git a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs index 2e7c22f..14c6fe0 100644 --- a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs +++ b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs @@ -26,12 +26,6 @@ internal static class MaterializerFactoryHelper internal static MethodInfo DbDataReaderIsDBNullMethod { get; } = typeof(DbDataReader) .GetMethod(nameof(DbDataReader.IsDBNull))!; - /// - /// The method. - /// - internal static MethodInfo EnumConverterConvertValueToEnumMemberMethod { get; } = typeof(EnumConverter) - .GetMethod(nameof(EnumConverter.ConvertValueToEnumMember), BindingFlags.Static | BindingFlags.NonPublic)!; - /// /// The 'Chars' property of the type. /// @@ -54,7 +48,8 @@ internal static class MaterializerFactoryHelper /// The method. /// internal static MethodInfo ValueConverterConvertValueToTypeMethod { get; } = typeof(ValueConverter) - .GetMethod(nameof(ValueConverter.ConvertValueToType), BindingFlags.Static | BindingFlags.NonPublic)!; + .GetMethods(BindingFlags.Static | BindingFlags.NonPublic) + .First(m => m is { Name: nameof(ValueConverter.ConvertValueToType), IsGenericMethod: true }); /// /// Creates an that gets the value of a field of the specified field type from a diff --git a/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs b/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs index defa147..4d35599 100644 --- a/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs +++ b/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs @@ -61,7 +61,9 @@ internal static class ValueTupleMaterializerFactory /// /// /// - /// The order of the fields in the value tuple must match the order of the fields in . + /// + /// The order of the fields in the value tuple must match the order of the fields in . + /// /// /// The field types of the fields in must be compatible with the field types of the /// fields in . diff --git a/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs b/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs index 3e7eed6..419ad41 100644 --- a/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs +++ b/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using System.Collections.ObjectModel; diff --git a/src/DbConnectionPlus/Readers/EnumHandlingObjectReader.cs b/src/DbConnectionPlus/Readers/EnumHandlingObjectReader.cs index f69d71b..d177b2a 100644 --- a/src/DbConnectionPlus/Readers/EnumHandlingObjectReader.cs +++ b/src/DbConnectionPlus/Readers/EnumHandlingObjectReader.cs @@ -10,7 +10,7 @@ namespace RentADeveloper.DbConnectionPlus.Readers; /// /// /// A version of that handles enum serialization according to the setting -/// . +/// . /// /// /// For Enum fields returns the type that corresponds to the enum serialization mode. @@ -34,7 +34,7 @@ public EnumHandlingObjectReader(Type type, IEnumerable source, params String[] m if (fieldType?.IsEnumOrNullableEnumType() == true) { - return DbConnectionExtensions.EnumSerializationMode switch + return DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Strings => typeof(String), @@ -42,7 +42,7 @@ public EnumHandlingObjectReader(Type type, IEnumerable source, params String[] m typeof(Int32), _ => ThrowHelper.ThrowInvalidEnumSerializationModeException( - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ) }; } @@ -101,7 +101,10 @@ public override Int32 GetValues(Object[] values) switch (values[i]) { case Enum enumValue: - values[i] = EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + values[i] = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); break; case Char charValue: diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs index 77feb75..9c24c8d 100644 --- a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs +++ b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2026 David Liebeherr +// Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. using System.ComponentModel; @@ -64,7 +64,7 @@ public InterpolatedSqlStatement(Int32 literalLength, Int32 formattedCount) /// contains a duplicate parameter. /// /// If a parameter value is an , it is serialized according to - /// . + /// . /// public InterpolatedSqlStatement(String code, params (String Name, Object? Value)[] parameters) { diff --git a/src/DbConnectionPlus/ThrowHelper.cs b/src/DbConnectionPlus/ThrowHelper.cs index aa747f0..a25c5ac 100644 --- a/src/DbConnectionPlus/ThrowHelper.cs +++ b/src/DbConnectionPlus/ThrowHelper.cs @@ -11,6 +11,18 @@ namespace RentADeveloper.DbConnectionPlus; /// public static class ThrowHelper { + /// + /// Throws an indicating that the configuration of DbConnectionPlus is + /// frozen and can no longer be modified. + /// + /// Always thrown. + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + public static void ThrowConfigurationIsFrozenException() => + throw new InvalidOperationException( + "The configuration of DbConnectionPlus is frozen and can no longer be modified." + ); + /// /// Throws an indicating that an attempt was made to use the temporary tables /// feature of DbConnectionPlus, but the database adapter for the current database system does not support @@ -38,8 +50,8 @@ public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(ID [DoesNotReturn] public static void ThrowEntityTypeHasNoKeyPropertyException(Type entityType) => throw new ArgumentException( - $"Could not get the key property / properties of the type {entityType}. Make sure that at least one " + - $"instance property of that type is denoted with a {typeof(KeyAttribute)}." + $"No property of the type {entityType} is configured as a key property. Make sure that at least one " + + "instance property of that type is configured as key property." ); /// diff --git a/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs b/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs index 4f62a60..716e7b3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs +++ b/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.Converters; +using RentADeveloper.DbConnectionPlus.Converters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 5108423..8ba0bea 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,101 +31,59 @@ public abstract class EntityManipulator_DeleteEntitiesTests protected EntityManipulator_DeleteEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - var entitiesToDelete = this.CreateEntitiesInDb(); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => this.manipulator.DeleteEntities( + await Invoking(() => this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, cancellationToken ) ) - .Should().Throw() + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); - foreach (var entity in entitiesToDelete) - { - // Since the operation was cancelled, the entities should still exist. - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void DeleteEntities_EntitiesHaveNoKeyProperty_ShouldThrow() - { - var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - - Invoking(() => this.manipulator.DeleteEntities( - this.Connection, - [entityWithoutKeyProperty], - null, - TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. " + - $"Make sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." - ); - } - - [Fact] - public void DeleteEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entitiesToDelete = this.CreateEntitiesInDb(); - - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void DeleteEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = this.CreateEntitiesInDb(); - - this.manipulator.DeleteEntities( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - foreach (var entity in entities) { + // Since the operation was cancelled, all entities should still exist. this.ExistsEntityInDb(entity) - .Should().BeFalse(); + .Should().BeTrue(); } } - [Fact] - public void DeleteEntities_MoreThan10Entities_ShouldBatchDeleteIfPossible() + [Theory] + [InlineData(false, 10)] + [InlineData(true, 10)] + // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need + // to test that as well. + [InlineData(false, 30)] + [InlineData(true, 30)] + public async Task DeleteEntities_Mapping_Attributes_ShouldUseAttributesMapping( + Boolean useAsyncApi, + Int32 numberOfEntities + ) { - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); - var entitiesToDelete = this.CreateEntitiesInDb(20); - - this.manipulator.DeleteEntities( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -136,40 +95,34 @@ public void DeleteEntities_MoreThan10Entities_ShouldBatchDeleteIfPossible() this.ExistsEntityInDb(entity) .Should().BeFalse(); } - } - - [Fact] - public void DeleteEntities_MoreThan10Entities_ShouldUseConfiguredColumnNames() - { - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. - - var entities = this.CreateEntitiesInDb(20); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - this.manipulator.DeleteEntities( - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) + foreach (var entity in entitiesToKeep) { this.ExistsEntityInDb(entity) - .Should().BeFalse(); + .Should().BeTrue(); } } - [Fact] - public void DeleteEntities_MoreThan10EntitiesWithCompositeKey_ShouldBatchDeleteIfPossible() + [Theory] + [InlineData(false, 10)] + [InlineData(true, 10)] + // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need + // to test that as well. + [InlineData(false, 30)] + [InlineData(true, 30)] + public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping( + Boolean useAsyncApi, + Int32 numberOfEntities + ) { - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. + MappingTestEntityFluentApi.Configure(); - var entitiesToDelete = this.CreateEntitiesInDb(20); + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); - this.manipulator.DeleteEntities( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -181,134 +134,23 @@ public void DeleteEntities_MoreThan10EntitiesWithCompositeKey_ShouldBatchDeleteI this.ExistsEntityInDb(entity) .Should().BeFalse(); } - } - - [Fact] - public void DeleteEntities_ShouldHandleEntityWithCompositeKey() - { - var entities = this.CreateEntitiesInDb(); - - this.manipulator.DeleteEntities( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void DeleteEntities_ShouldReturnNumberOfAffectedRows() - { - var entitiesToDelete = this.CreateEntitiesInDb(); - - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ) - .Should().Be(entitiesToDelete.Count); - - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ) - .Should().Be(0); - } - - [Fact] - public void DeleteEntities_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - this.manipulator.DeleteEntities( - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) + foreach (var entity in entitiesToKeep) { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void DeleteEntities_Transaction_ShouldUseTransaction() - { - var entitiesToDelete = this.CreateEntitiesInDb(); - - using (var transaction = this.Connection.BeginTransaction()) - { - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - transaction, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity, transaction) - .Should().BeFalse(); - } - - transaction.Rollback(); - } - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public async Task DeleteEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entitiesToDelete = this.CreateEntitiesInDb(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => this.manipulator.DeleteEntitiesAsync( - this.Connection, - entitiesToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); - - foreach (var entity in entitiesToDelete) - { - // Since the operation was cancelled, the entities should still exist. this.ExistsEntityInDb(entity) .Should().BeTrue(); } } - [Fact] - public Task DeleteEntitiesAsync_EntitiesHaveNoKeyProperty_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.manipulator.DeleteEntitiesAsync( + return Invoking(() => this.CallApi( + useAsyncApi, this.Connection, [entityWithoutKeyProperty], null, @@ -317,58 +159,29 @@ public Task DeleteEntitiesAsync_EntitiesHaveNoKeyProperty_ShouldThrow() ) .Should().ThrowAsync() .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. " + - $"Make sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." + $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " + + "sure that at least one instance property of that type is configured as key property." ); } - [Fact] - public async Task DeleteEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entitiesToDelete = this.CreateEntitiesInDb(); - - await this.manipulator.DeleteEntitiesAsync( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task DeleteEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = this.CreateEntitiesInDb(); - - await this.manipulator.DeleteEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldBatchDeleteIfPossible() + [Theory] + [InlineData(false, 10)] + [InlineData(true, 10)] + // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need + // to test that as well. + [InlineData(false, 30)] + [InlineData(true, 30)] + public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( + Boolean useAsyncApi, + Int32 numberOfEntities + ) { - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); - var entitiesToDelete = this.CreateEntitiesInDb(20); - - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -380,56 +193,23 @@ await this.manipulator.DeleteEntitiesAsync( this.ExistsEntityInDb(entity) .Should().BeFalse(); } - } - - [Fact] - public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldUseConfiguredColumnNames() - { - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. - - var entities = this.CreateEntitiesInDb(20); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - await this.manipulator.DeleteEntitiesAsync( - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task DeleteEntitiesAsync_ShouldHandleEntityWithCompositeKey() - { - var entities = this.CreateEntitiesInDb(); - - await this.manipulator.DeleteEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - foreach (var entity in entities) + foreach (var entity in entitiesToKeep) { this.ExistsEntityInDb(entity) - .Should().BeFalse(); + .Should().BeTrue(); } } - [Fact] - public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entitiesToDelete = this.CreateEntitiesInDb(); - (await this.manipulator.DeleteEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -437,7 +217,8 @@ public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() )) .Should().Be(entitiesToDelete.Count); - (await this.manipulator.DeleteEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -446,34 +227,17 @@ public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task DeleteEntitiesAsync_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - await this.manipulator.DeleteEntitiesAsync( - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task DeleteEntitiesAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entitiesToDelete = this.CreateEntitiesInDb(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, transaction, @@ -496,5 +260,31 @@ await this.manipulator.DeleteEntitiesAsync( } } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + IEnumerable entities, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.DeleteEntitiesAsync(connection, entities, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.DeleteEntities(connection, entities, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 64b57a9..974d820 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,8 +31,10 @@ public abstract class EntityManipulator_DeleteEntityTests protected EntityManipulator_DeleteEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,14 +44,15 @@ public void DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIs this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => this.manipulator.DeleteEntity( + await Invoking(() => this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, cancellationToken ) ) - .Should().Throw() + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entity should still exist. @@ -56,32 +60,17 @@ public void DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIs .Should().BeTrue(); } - [Fact] - public void DeleteEntity_EntityHasNoKeyProperty_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; - Invoking(() => this.manipulator.DeleteEntity( - this.Connection, - entityWithoutKeyProperty, - null, - TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. " + - "Make sure that at least one instance property of that type is denoted with a " + - $"{typeof(KeyAttribute)}." - ); - } - - [Fact] - public void DeleteEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entityToDelete = this.CreateEntityInDb(); - - this.manipulator.DeleteEntity( + await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -90,135 +79,46 @@ public void DeleteEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTa this.ExistsEntityInDb(entityToDelete) .Should().BeFalse(); - } - - [Fact] - public void DeleteEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entity = this.CreateEntityInDb(); - - this.manipulator.DeleteEntity( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - [Fact] - public void DeleteEntity_ShouldHandleEntityWithCompositeKey() - { - var entity = this.CreateEntityInDb(); - - this.manipulator.DeleteEntity( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entityToKeep) + .Should().BeTrue(); } - [Fact] - public void DeleteEntity_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entityToDelete = this.CreateEntityInDb(); - - this.manipulator.DeleteEntity( - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ) - .Should().Be(1); - - this.manipulator.DeleteEntity( - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ) - .Should().Be(0); - } + MappingTestEntityFluentApi.Configure(); - [Fact] - public void DeleteEntity_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; - this.manipulator.DeleteEntity( + await this.CallApi( + useAsyncApi, this.Connection, - entityWithColumnAttributes, + entityToDelete, null, TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public void DeleteEntity_Transaction_ShouldUseTransaction() - { - var entityToDelete = this.CreateEntityInDb(); - - using (var transaction = this.Connection.BeginTransaction()) - { - this.manipulator.DeleteEntity( - this.Connection, - entityToDelete, - transaction, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entityToDelete, transaction) - .Should().BeFalse(); - - transaction.Rollback(); - } - this.ExistsEntityInDb(entityToDelete) - .Should().BeTrue(); - } - - [Fact] - public async Task DeleteEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entityToDelete = this.CreateEntityInDb(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => this.manipulator.DeleteEntityAsync( - this.Connection, - entityToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + .Should().BeFalse(); - // Since the operation was cancelled, the entity should still exist. - this.ExistsEntityInDb(entityToDelete) + this.ExistsEntityInDb(entityToKeep) .Should().BeTrue(); } - [Fact] - public Task DeleteEntityAsync_EntityHasNoKeyProperty_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.manipulator.DeleteEntityAsync( + return Invoking(() => this.CallApi( + useAsyncApi, this.Connection, entityWithoutKeyProperty, null, @@ -227,18 +127,22 @@ public Task DeleteEntityAsync_EntityHasNoKeyProperty_ShouldThrow() ) .Should().ThrowAsync() .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. " + - "Make sure that at least one instance property of that type is denoted with a " + - $"{typeof(KeyAttribute)}." + $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " + + "sure that at least one instance property of that type is configured as key property." ); } - [Fact] - public async Task DeleteEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - var entityToDelete = this.CreateEntityInDb(); + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; - await this.manipulator.DeleteEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -247,46 +151,20 @@ await this.manipulator.DeleteEntityAsync( this.ExistsEntityInDb(entityToDelete) .Should().BeFalse(); - } - - [Fact] - public async Task DeleteEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entity = this.CreateEntityInDb(); - - await this.manipulator.DeleteEntityAsync( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - [Fact] - public async Task DeleteEntityAsync_ShouldHandleEntityWithCompositeKey() - { - var entity = this.CreateEntityInDb(); - - await this.manipulator.DeleteEntityAsync( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entityToKeep) + .Should().BeTrue(); } - [Fact] - public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entityToDelete = this.CreateEntityInDb(); - (await this.manipulator.DeleteEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -294,7 +172,8 @@ public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() )) .Should().Be(1); - (await this.manipulator.DeleteEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -303,31 +182,17 @@ public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task DeleteEntityAsync_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - await this.manipulator.DeleteEntityAsync( - this.Connection, - entityWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public async Task DeleteEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entityToDelete = this.CreateEntityInDb(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - await this.manipulator.DeleteEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, transaction, @@ -344,5 +209,31 @@ await this.manipulator.DeleteEntityAsync( .Should().BeTrue(); } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + TEntity entity, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.DeleteEntityAsync(connection, entity, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.DeleteEntity(connection, entity, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 8b4f3d7..85b00e1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,8 +31,12 @@ public abstract class EntityManipulator_InsertEntitiesTests protected EntityManipulator_InsertEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,383 +46,160 @@ public void InsertEntities_CancellationToken_ShouldCancelOperationIfCancellation this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => this.manipulator.InsertEntities(this.Connection, entities, null, cancellationToken)) - .Should().Throw() + await Invoking(() => + this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) + ) + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entities should not have been inserted. - foreach (var entity in entities) + foreach (var entityToInsert in entities) { - this.ExistsEntityInDb(entity) + this.ExistsEntityInDb(entityToInsert) .Should().BeFalse(); } } - [Fact] - public void InsertEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - [Fact] - public void InsertEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); - this.manipulator.InsertEntities( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken ); - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( + (await this.Connection.QueryAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", cancellationToken: TestContext.Current.CancellationToken - ) + ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); } - [Fact] - public void InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); - } - - [Fact] - public void InsertEntities_ShouldHandleIdentityAndComputedColumns() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - entities - .Should().BeEquivalentTo( - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ); - } - - [Fact] - public void InsertEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() - { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - - this.manipulator.InsertEntities( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (reader.Read()) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } - } - - [Fact] - public void InsertEntities_ShouldInsertEntities() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void InsertEntities_ShouldReturnNumberOfAffectedRows() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken) - .Should().Be(entities.Count); - - this.manipulator.InsertEntities( - this.Connection, - Array.Empty(), - null, - TestContext.Current.CancellationToken - ) - .Should().Be(0); - } - - [Fact] - public void InsertEntities_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void InsertEntities_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken ); - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", + (await this.Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); + ).ToListAsync(TestContext.Current.CancellationToken)) + .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); } - [Fact] - public void InsertEntities_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entities = Generate.Multiple(); - - using (var transaction = this.Connection.BeginTransaction()) - { - this.manipulator.InsertEntities( - this.Connection, - entities, - transaction, - TestContext.Current.CancellationToken - ) - .Should().Be(entities.Count); - - foreach (var entity in entities) + var entities = Generate.Multiple(); + entities.ForEach(a => { - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; } - - transaction.Rollback(); - } - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task InsertEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entities = Generate.Multiple(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => - this.manipulator.InsertEntitiesAsync(this.Connection, entities, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entities should not have been inserted. - foreach (var entityToInsert in entities) - { - this.ExistsEntityInDb(entityToInsert) - .Should().BeFalse(); - } - } - - [Fact] - public async Task InsertEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken ); - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public async Task InsertEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken ); - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public async Task InsertEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } - [Fact] - public async Task InsertEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + MappingTestEntityFluentApi.Configure(); - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken + var entities = Generate.Multiple(); + entities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } ); - (await this.Connection.QueryAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); - } - - [Fact] - public async Task InsertEntitiesAsync_ShouldHandleIdentityAndComputedColumns() - { - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken ); - entities + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( - await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken) + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); } - [Fact] - public async Task InsertEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); + var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (await reader.ReadAsync(TestContext.Current.CancellationToken)) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(entities); } - [Fact] - public async Task InsertEntitiesAsync_ShouldInsertEntities() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldInsertEntities(Boolean useAsyncApi) { var entities = Generate.Multiple(); - (await this.manipulator.InsertEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -432,12 +214,15 @@ public async Task InsertEntitiesAsync_ShouldInsertEntities() .Should().BeEquivalentTo(entities); } - [Fact] - public async Task InsertEntitiesAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entities = Generate.Multiple(); - (await this.manipulator.InsertEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -445,7 +230,8 @@ public async Task InsertEntitiesAsync_ShouldReturnNumberOfAffectedRows() )) .Should().Be(entities.Count); - (await this.manipulator.InsertEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, Array.Empty(), null, @@ -454,14 +240,17 @@ public async Task InsertEntitiesAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task InsertEntitiesAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -475,33 +264,17 @@ await this.manipulator.InsertEntitiesAsync( .Should().BeEquivalentTo(entities); } - [Fact] - public async Task InsertEntitiesAsync_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(); - - await this.manipulator.InsertEntitiesAsync( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public async Task InsertEntitiesAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entities = Generate.Multiple(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - (await this.manipulator.InsertEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entities, transaction, @@ -525,5 +298,31 @@ public async Task InsertEntitiesAsync_Transaction_ShouldUseTransaction() } } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + IEnumerable entities, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.InsertEntitiesAsync(connection, entities, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.InsertEntities(connection, entities, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index a613aa8..b44664a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,8 +31,10 @@ public abstract class EntityManipulator_InsertEntityTests protected EntityManipulator_InsertEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,8 +44,10 @@ public void InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIs this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => this.manipulator.InsertEntity(this.Connection, entity, null, cancellationToken)) - .Should().Throw() + await Invoking(() => + this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken) + ) + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entity should not have been inserted. @@ -50,325 +55,123 @@ public void InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIs .Should().BeFalse(); } - [Fact] - public void InsertEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public void InsertEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entity = Generate.Single(); - - this.manipulator.InsertEntity( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public void InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - this.Connection.QuerySingle( + (await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", cancellationToken: TestContext.Current.CancellationToken - ) + )) .Should().Be((Int32)entity.Enum); } - [Fact] - public void InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - this.Connection.QuerySingle( + (await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", cancellationToken: TestContext.Current.CancellationToken - ) + )) .Should().BeEquivalentTo(entity.Enum.ToString()); } - [Fact] - public void InsertEntity_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = Generate.Single(); + var entity = Generate.Single(); + entity.ComputedColumn_ = 0; + entity.IdentityColumn_ = 0; + entity.NotMappedColumn = "ShouldNotBePersisted"; - this.manipulator.InsertEntity( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken ); - entity + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); } - [Fact] - public void InsertEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() - { - var entity = Generate.Single(); - entity.NotMappedValue = "ShouldNotBePersisted"; - - this.manipulator.InsertEntity( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (reader.Read()) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } - } - - [Fact] - public void InsertEntity_ShouldInsertEntity() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); + MappingTestEntityFluentApi.Configure(); - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void InsertEntity_ShouldReturnNumberOfAffectedRows() - { - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken) - .Should().Be(1); - } - - [Fact] - public void InsertEntity_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void InsertEntity_ShouldUseConfiguredColumnNames() - { - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void InsertEntity_Transaction_ShouldUseTransaction() - { - var entity = Generate.Single(); - - using (var transaction = this.Connection.BeginTransaction()) - { - this.manipulator.InsertEntity( - this.Connection, - entity, - transaction, - TestContext.Current.CancellationToken - ) - .Should().Be(1); - - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); - - transaction.Rollback(); - } - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } + var entity = Generate.Single(); + entity.ComputedColumn_ = 0; + entity.IdentityColumn_ = 0; + entity.NotMappedColumn = "ShouldNotBePersisted"; - [Fact] - public async Task InsertEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entity = Generate.Single(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => - this.manipulator.InsertEntityAsync(this.Connection, entity, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entity should not have been inserted. - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public async Task InsertEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public async Task InsertEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); - - (await this.Connection.QuerySingleAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((Int32)entity.Enum); - } - - [Fact] - public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); - - (await this.Connection.QuerySingleAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity.Enum.ToString()); - } - - [Fact] - public async Task InsertEntityAsync_ShouldHandleIdentityAndComputedColumns() - { - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken ); - entity + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( - await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); } - [Fact] - public async Task InsertEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - var entity = Generate.Single(); - entity.NotMappedValue = "ShouldNotBePersisted"; + var entity = Generate.Single(); - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (await reader.ReadAsync(TestContext.Current.CancellationToken)) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_ShouldInsertEntity() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_ShouldInsertEntity(Boolean useAsyncApi) { var entity = Generate.Single(); - (await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken)) + (await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken)) .Should().Be(1); (await this.Connection.QuerySingleAsync( @@ -378,23 +181,27 @@ public async Task InsertEntityAsync_ShouldInsertEntity() .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entity = Generate.Single(); - (await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken)) + (await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken)) .Should().Be(1); } - [Fact] - public async Task InsertEntityAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = Generate.Single(); - await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); (await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", @@ -403,28 +210,17 @@ public async Task InsertEntityAsync_ShouldSupportDateTimeOffsetValues() .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames() - { - var entity = Generate.Single(); - - await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public async Task InsertEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = Generate.Single(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - (await this.manipulator.InsertEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entity, transaction, @@ -442,5 +238,31 @@ public async Task InsertEntityAsync_Transaction_ShouldUseTransaction() .Should().BeFalse(); } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + TEntity entity, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.InsertEntityAsync(connection, entity, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.InsertEntity(connection, entity, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 315fe08..558f574 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,328 +31,24 @@ public abstract class EntityManipulator_UpdateEntitiesTests protected EntityManipulator_UpdateEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => this.manipulator.UpdateEntities(this.Connection, updatedEntities, null, cancellationToken)) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entities should not have been updated. - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void UpdateEntities_EntitiesHaveNoKeyProperty_ShouldThrow() => - Invoking(() => - this.manipulator.UpdateEntities( - this.Connection, - [new EntityWithoutKeyProperty()], - null, - TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. Make " + - $"sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." - ); - - [Fact] - public void UpdateEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities(this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - // Make sure the enums are stored as integers: - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); - - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - // Make sure the enums are stored as integers: - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities.Select(a => (Int32)a.Enum)); - } - - [Fact] - public void UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - // Make sure the enums are stored as strings: - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); - - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - // Make sure the enums are stored as strings: - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); - } - - [Fact] - public void UpdateEntities_ShouldHandleEntityWithCompositeKey() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithCompositeKey")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_ShouldHandleIdentityAndComputedColumns() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - updatedEntities - .Should().BeEquivalentTo( - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) - ); - } - - [Fact] - public void UpdateEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() - { - var entities = this.CreateEntitiesInDb(); - - var updatedEntities = Generate.UpdatesFor(entities); - updatedEntities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (reader.Read()) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } - } - - [Fact] - public void UpdateEntities_ShouldReturnNumberOfAffectedRows() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities(this.Connection, updatedEntities, null, TestContext.Current.CancellationToken) - .Should().Be(entities.Count); - - var nonExistentEntities = Generate.Multiple(); - - this.manipulator.UpdateEntities( - this.Connection, - nonExistentEntities, - null, - TestContext.Current.CancellationToken - ) - .Should().Be(0); - } - - [Fact] - public void UpdateEntities_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_ShouldUpdateEntities() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities(this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities(this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public void UpdateEntities_Transaction_ShouldUseTransaction() - { - var entities = this.CreateEntitiesInDb(); - - using (var transaction = this.Connection.BeginTransaction()) - { - var updatedEntities = Generate.UpdatesFor(entities); - - this.manipulator.UpdateEntities( - this.Connection, - updatedEntities, - transaction, - TestContext.Current.CancellationToken - ) - .Should().Be(entities.Count); - - this.Connection.Query($"SELECT * FROM {Q("Entity")}", transaction) - .Should().BeEquivalentTo(updatedEntities); - - transaction.Rollback(); - } - - this.Connection.Query($"SELECT * FROM {Q("Entity")}") - .Should().BeEquivalentTo(entities); - } - - [Fact] - public async Task UpdateEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.manipulator.UpdateEntitiesAsync(this.Connection, updatedEntities, null, cancellationToken) + this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken) ) .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); @@ -364,66 +61,14 @@ await Invoking(() => .Should().BeEquivalentTo(entities); } - [Fact] - public Task UpdateEntitiesAsync_EntitiesHaveNoKeyProperty_ShouldThrow() => - Invoking(() => - this.manipulator.UpdateEntitiesAsync( - this.Connection, - [new EntityWithoutKeyProperty()], - null, - TestContext.Current.CancellationToken - ) - ) - .Should().ThrowAsync() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. Make " + - $"sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." - ); - - [Fact] - public async Task UpdateEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - await this.manipulator.UpdateEntitiesAsync( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public async Task UpdateEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - await this.manipulator.UpdateEntitiesAsync( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public async Task UpdateEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -441,9 +86,10 @@ await this.manipulator.InsertEntitiesAsync( ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -458,10 +104,12 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities.Select(a => (Int32)a.Enum)); } - [Fact] - public async Task UpdateEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -479,9 +127,10 @@ await this.manipulator.InsertEntitiesAsync( ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -496,81 +145,124 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldHandleEntityWithCompositeKey() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var entities = this.CreateEntitiesInDb(); + + var updatedEntities = Generate.UpdateFor(entities); + updatedEntities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithCompositeKey")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + MappingTestEntityFluentApi.Configure(); + + var entities = this.CreateEntitiesInDb(); + + var updatedEntities = Generate.UpdateFor(entities); + updatedEntities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken ); - updatedEntities + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( - await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ).ToListAsync(TestContext.Current.CancellationToken) + updatedEntities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(); + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - var updatedEntities = Generate.UpdatesFor(entities); - updatedEntities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + [entityWithoutKeyProperty], + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " + + "sure that at least one instance property of that type is configured as key property." + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + var updatedEntities = Generate.UpdateFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (await reader.ReadAsync()) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -580,7 +272,8 @@ public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() var nonExistentEntities = Generate.Multiple(); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, nonExistentEntities, null, @@ -589,15 +282,18 @@ public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -611,13 +307,16 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldUpdateEntities() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_ShouldUpdateEntities(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -632,36 +331,19 @@ public async Task UpdateEntitiesAsync_ShouldUpdateEntities() .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - await this.manipulator.UpdateEntitiesAsync( - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync()) - .Should().BeEquivalentTo(updatedEntities); - } - - [Fact] - public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, transaction, @@ -681,5 +363,31 @@ public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction() .Should().BeEquivalentTo(entities); } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + IEnumerable entities, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.UpdateEntitiesAsync(connection, entities, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.UpdateEntities(connection, entities, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 002da97..5d7c4c8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -1,4 +1,5 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,311 +31,10 @@ public abstract class EntityManipulator_UpdateEntityTests protected EntityManipulator_UpdateEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => this.manipulator.UpdateEntity(this.Connection, updatedEntity, null, cancellationToken)) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entity should not have been updated. - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void UpdateEntity_EntityHasNoKeyProperty_ShouldThrow() => - Invoking(() => - this.manipulator.UpdateEntity( - this.Connection, - new EntityWithoutKeyProperty(), - null, - TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. Make " + - $"sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." - ); - - [Fact] - public void UpdateEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity(this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - // Make sure the enum is stored as integer: - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be((Int32)entity.Enum); - - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - // Make sure the enum is stored as integer: - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be((Int32)updatedEntity.Enum); - } - - [Fact] - public void UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - // Make sure the enum is stored as string: - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity.Enum.ToString()); - - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - // Make sure the enum is stored as string: - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); - } - - [Fact] - public void UpdateEntity_ShouldHandleEntityWithCompositeKey() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithCompositeKey")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_ShouldHandleIdentityAndComputedColumns() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - updatedEntity - .Should().BeEquivalentTo( - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) - ); - } - - [Fact] - public void UpdateEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() - { - var entity = this.CreateEntityInDb(); - - var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.NotMappedValue = "ShouldNotBePersisted"; - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (reader.Read()) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } - } - - [Fact] - public void UpdateEntity_ShouldReturnNumberOfAffectedRows() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity(this.Connection, updatedEntity, null, TestContext.Current.CancellationToken) - .Should().Be(1); - - var nonExistentEntity = Generate.Single(); - - this.manipulator.UpdateEntity(this.Connection, nonExistentEntity, null, TestContext.Current.CancellationToken) - .Should().Be(0); - } - - [Fact] - public void UpdateEntity_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_ShouldUpdateEntity() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity(this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity(this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public void UpdateEntity_Transaction_ShouldUseTransaction() - { - var entity = this.CreateEntityInDb(); - - using (var transaction = this.Connection.BeginTransaction()) - { - var updatedEntity = Generate.UpdateFor(entity); - - this.manipulator.UpdateEntity( - this.Connection, - updatedEntity, - transaction, - TestContext.Current.CancellationToken - ) - .Should().Be(1); - - this.Connection.QuerySingle($"SELECT * FROM {Q("Entity")}", transaction) - .Should().BeEquivalentTo(updatedEntity); - - transaction.Rollback(); - } - - this.Connection.QuerySingle($"SELECT * FROM {Q("Entity")}") - .Should().BeEquivalentTo(entity); - } - - [Fact] - public async Task UpdateEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -346,7 +46,7 @@ public async Task UpdateEntityAsync_CancellationToken_ShouldCancelOperationIfCan this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.manipulator.UpdateEntityAsync(this.Connection, updatedEntity, null, cancellationToken) + this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken) ) .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); @@ -359,66 +59,12 @@ await Invoking(() => .Should().BeEquivalentTo(entity); } - [Fact] - public Task UpdateEntityAsync_EntityHasNoKeyProperty_ShouldThrow() => - Invoking(() => - this.manipulator.UpdateEntityAsync( - this.Connection, - new EntityWithoutKeyProperty(), - null, - TestContext.Current.CancellationToken - ) - ) - .Should().ThrowAsync() - .WithMessage( - $"Could not get the key property / properties of the type {typeof(EntityWithoutKeyProperty)}. Make " + - $"sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." - ); - - [Fact] - public async Task UpdateEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.manipulator.UpdateEntityAsync( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public async Task UpdateEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.manipulator.UpdateEntityAsync( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); @@ -433,7 +79,8 @@ public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreE var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -448,10 +95,12 @@ await this.manipulator.UpdateEntityAsync( .Should().Be((Int32)updatedEntity.Enum); } - [Fact] - public async Task UpdateEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); @@ -466,7 +115,8 @@ public async Task UpdateEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEn var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -481,81 +131,116 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); } - [Fact] - public async Task UpdateEntityAsync_ShouldHandleEntityWithCompositeKey() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken ); - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("EntityWithCompositeKey")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } - [Fact] - public async Task UpdateEntityAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken ); - updatedEntity + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( - await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) + updatedEntity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); } - [Fact] - public async Task UpdateEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityWithoutKeyProperty, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " + + "sure that at least one instance property of that type is configured as key property." + ); + } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.NotMappedValue = "ShouldNotBePersisted"; - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Id")}, {Q("NotMappedValue")} FROM {Q("EntityWithNotMappedProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - while (await reader.ReadAsync(TestContext.Current.CancellationToken)) - { - reader.IsDBNull(reader.GetOrdinal("NotMappedValue")) - .Should().BeTrue(); - } + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - (await this.manipulator.UpdateEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -565,7 +250,8 @@ public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() var nonExistentEntity = Generate.Single(); - (await this.manipulator.UpdateEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, nonExistentEntity, null, @@ -574,15 +260,18 @@ public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task UpdateEntityAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -596,13 +285,16 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldUpdateEntity() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_ShouldUpdateEntity(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - (await this.manipulator.UpdateEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -617,28 +309,10 @@ public async Task UpdateEntityAsync_ShouldUpdateEntity() .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.manipulator.UpdateEntityAsync( - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); - } - - [Fact] - public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -646,7 +320,8 @@ public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction() { var updatedEntity = Generate.UpdateFor(entity); - (await this.manipulator.UpdateEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, transaction, @@ -664,5 +339,31 @@ public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction() .Should().BeEquivalentTo(entity); } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + TEntity entity, + DbTransaction? transaction = null, + CancellationToken cancellationToken = default + ) + where TEntity : class + { + if (useAsyncApi) + { + return this.manipulator.UpdateEntityAsync(connection, entity, transaction, cancellationToken); + } + + try + { + return Task.FromResult( + this.manipulator.UpdateEntity(connection, entity, transaction, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs index 7e9a86b..9876357 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs @@ -1,4 +1,4 @@ -// ReSharper disable AccessToDisposedClosure +// ReSharper disable AccessToDisposedClosure using Oracle.ManagedDataAccess.Client; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs index 7087d7f..04800ab 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs @@ -1,4 +1,4 @@ -// ReSharper disable AccessToDisposedClosure +// ReSharper disable AccessToDisposedClosure using Npgsql; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs index 85521dd..88ea1b5 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs @@ -1,4 +1,4 @@ -// ReSharper disable AccessToDisposedClosure +// ReSharper disable AccessToDisposedClosure using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 8cfa523..07dbbaa 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -1,4 +1,6 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using System.Collections; +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.Extensions; using RentADeveloper.DbConnectionPlus.UnitTests.Assertions; @@ -31,14 +33,19 @@ public abstract class TemporaryTableBuilderTests : Integr protected TemporaryTableBuilderTests() => this.builder = this.DatabaseAdapter.TemporaryTableBuilder; - [Fact] - public void BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var items = Generate.Multiple(); - using var tableDisposer = this.builder.BuildTemporaryTable( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -47,21 +54,27 @@ public void BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupp TestContext.Current.CancellationToken ); - this.Connection.Query( + (await this.Connection.QueryAsync( $"SELECT * FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken - ) + ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(items); } - [Fact] - public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); - using var tableDisposer = this.builder.BuildTemporaryTable( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -72,11 +85,11 @@ public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_S if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) { - this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum") - .Should().Be(this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers)); + this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) + .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); } - using var reader = this.Connection.ExecuteReader( + await using var reader = await this.Connection.ExecuteReaderAsync( $"SELECT {Q("Enum")} FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -86,21 +99,27 @@ public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_S foreach (var entity in entities) { - reader.Read(); + await reader.ReadAsync(TestContext.Current.CancellationToken); reader.GetInt32(0) .Should().Be((Int32)entity.Enum); } } - [Fact] - public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); - using var tableDisposer = this.builder.BuildTemporaryTable( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -115,7 +134,7 @@ public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_Sh .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); } - using var reader = this.Connection.ExecuteReader( + await using var reader = await this.Connection.ExecuteReaderAsync( $"SELECT {Q("Enum")} FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -125,22 +144,27 @@ public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_Sh foreach (var entity in entities) { - reader.Read(); + await reader.ReadAsync(TestContext.Current.CancellationToken); reader.GetString(0) .Should().Be(entity.Enum.ToString()); } } - [Fact] - public void - BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + Boolean useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - using var tableDisposer = this.builder.BuildTemporaryTable( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -155,502 +179,116 @@ public void .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] - public void BuildTemporaryTable_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUseAttributesMapping( + Boolean useAsyncApi + ) { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); + var entities = Generate.Multiple(); + entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); - using var tableDisposer = this.builder.BuildTemporaryTable( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", entities, - typeof(EntityWithNotMappedProperty), + typeof(MappingTestEntityAttributes), TestContext.Current.CancellationToken ); - using var reader = this.Connection.ExecuteReader( + var reader = await this.Connection.ExecuteReaderAsync( $"SELECT * FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); reader.GetFieldNames() - .Should().NotContain(nameof(EntityWithNotMappedProperty.NotMappedValue)); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_NullableProperties_ShouldHandleNullValues() - { - var itemsWithNulls = new List { new() }; - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - itemsWithNulls, - typeof(TemporaryTableTestItemWithNullableProperties), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(itemsWithNulls); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTable() - { - var items = Generate.Multiple(); + .Should().NotContain(nameof(MappingTestEntityAttributes.NotMappedColumn)); - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - items, - typeof(TemporaryTableTestItem), - TestContext.Current.CancellationToken - ); + await reader.DisposeAsync(); - this.Connection.Query( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(items); + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } - [Fact] - public void BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping( + Boolean useAsyncApi + ) { - Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - Generate.Multiple(), - typeof(EntityWithStringProperty), - TestContext.Current.CancellationToken - ); - - var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "String"); - - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); - } + MappingTestEntityFluentApi.Configure(); - [Fact] - public void BuildTemporaryTable_ComplexObjects_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); + var entities = Generate.Multiple(); + entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - entitiesWithColumnAttributes, - typeof(EntityWithColumnAttributes), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void BuildTemporaryTable_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - DateTimeOffset[] values = [Generate.Single()]; - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - values, - typeof(DateTimeOffset), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(values); - } - - [Fact] - public void BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var values = Generate.Multiple(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - values, - typeof(TestEnum), - TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) - { - this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); - } - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldType(0) - .Should().BeAnyOf(typeof(Int32), typeof(Int64)); - - foreach (var value in values) - { - reader.Read(); - - reader.GetInt32(0) - .Should().Be((Int32)value); - } - } - - [Fact] - public void BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var values = Generate.Multiple(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - values, - typeof(TestEnum), - TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) - { - this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); - } - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldType(0) - .Should().Be(typeof(String)); - - foreach (var value in values) - { - reader.Read(); - - reader.GetString(0) - .Should().Be(value.ToString()); - } - } - - [Fact] - public void - BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() - { - Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - Generate.Multiple(), - typeof(TestEnum), - TestContext.Current.CancellationToken - ); - - var columnCollation = this.GetCollationOfTemporaryTableColumn("Values", "Value"); - - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); - } - - [Fact] - public void - BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var values = Generate.MultipleNullable(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - values, - typeof(TestEnum?), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(values); - } - - [Fact] - public void BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable() - { - var values = Generate.Multiple(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - values, - typeof(Int32), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(values); - } - - [Fact] - public void BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns() - { - Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - Generate.Multiple(), - typeof(String), - TestContext.Current.CancellationToken - ); - - var columnCollation = this.GetCollationOfTemporaryTableColumn("Values", "Value"); - - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); - } - - [Fact] - public void BuildTemporaryTable_ScalarValuesWithNullValues_ShouldHandleNullValues() - { - var values = Generate.MultipleNullable(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "NullValues", - values, - typeof(Int32?), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT {Q("Value")} FROM {QT("NullValues")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(values); - } - - [Fact] - public void BuildTemporaryTable_ShouldReturnDisposerThatDropsTable() - { - var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Values", - Generate.Multiple(), - typeof(Int32), - TestContext.Current.CancellationToken - ); - - this.ExistsTemporaryTableInDb("Values") - .Should().BeTrue(); - - tableDisposer.Dispose(); - - this.ExistsTemporaryTableInDb("Values") - .Should().BeFalse(); - } - - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var items = Generate.Multiple(); - - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( - this.Connection, - null, - "Objects", - items, - typeof(TemporaryTableTestItemWithDateTimeOffset), - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(items); - } - - [Fact] - public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", entities, - typeof(EntityWithEnumProperty), + typeof(MappingTestEntityFluentApi), TestContext.Current.CancellationToken ); - if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) - { - this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); - } - - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Enum")} FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldType(0) - .Should().BeAnyOf(typeof(Int32), typeof(Int64)); - - foreach (var entity in entities) - { - await reader.ReadAsync(TestContext.Current.CancellationToken); - - reader.GetInt32(0) - .Should().Be((Int32)entity.Enum); - } - } - - [Fact] - public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var entities = Generate.Multiple(); - - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( - this.Connection, - null, - "Objects", - entities, - typeof(EntityWithEnumProperty), - TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) - { - this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); - } - - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT {Q("Enum")} FROM {QT("Objects")}", + var reader = await this.Connection.ExecuteReaderAsync( + $"SELECT * FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldType(0) - .Should().Be(typeof(String)); - - foreach (var entity in entities) - { - await reader.ReadAsync(TestContext.Current.CancellationToken); - - reader.GetString(0) - .Should().Be(entity.Enum.ToString()); - } - } - - [Fact] - public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() - { - Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( - this.Connection, - null, - "Objects", - Generate.Multiple(), - typeof(EntityWithEnumProperty), - TestContext.Current.CancellationToken - ); + reader.GetFieldNames() + .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMappedColumn)); - var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "Enum"); + await reader.DisposeAsync(); - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } - [Fact] - public async Task - BuildTemporaryTableAsync_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( + Boolean useAsyncApi + ) { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); + var entities = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", entities, - typeof(EntityWithNotMappedProperty), + typeof(MappingTestEntity), TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldNames() - .Should().NotContain(nameof(EntityWithNotMappedProperty.NotMappedValue)); + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo(entities); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColumnTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTable(Boolean useAsyncApi) { var items = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -666,12 +304,15 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColum .Should().BeEquivalentTo(items); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -686,34 +327,15 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseCollationOfDa .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( - this.Connection, - null, - "Objects", - entitiesWithColumnAttributes, - typeof(EntityWithColumnAttributes), - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync()) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_WithNullables_ShouldHandleNullValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_WithNullables_ShouldHandleNullValues(Boolean useAsyncApi) { var itemsWithNulls = new List { new() }; - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -729,14 +351,19 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_WithNullables_ShouldHa .Should().BeEquivalentTo(itemsWithNulls); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); DateTimeOffset[] values = [Generate.Single()]; - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -752,15 +379,20 @@ public async Task BuildTemporaryTableAsync_ScalarValues_DateTimeOffsetValues_Sho .Should().BeEquivalentTo(values); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var values = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -792,15 +424,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -832,15 +469,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + Boolean useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -855,15 +497,18 @@ public async Task .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls() + BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls(Boolean useAsyncApi) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.MultipleNullable(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -879,12 +524,15 @@ public async Task .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumnTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable(Boolean useAsyncApi) { var values = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -900,12 +548,15 @@ public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumn .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -920,12 +571,15 @@ public async Task BuildTemporaryTableAsync_ScalarValues_ShouldUseCollationOfData .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValuesWithNullValues_ShouldHandleNullValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ScalarValuesWithNullValues_ShouldHandleNullValues(Boolean useAsyncApi) { var values = Generate.MultipleNullable(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "NullValues", @@ -941,10 +595,13 @@ public async Task BuildTemporaryTableAsync_ScalarValuesWithNullValues_ShouldHand .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ShouldReturnDisposerThatDropsTableAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ShouldReturnDisposerThatDropsTableAsync(Boolean useAsyncApi) { - var disposer = await this.builder.BuildTemporaryTableAsync( + var disposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -962,5 +619,39 @@ public async Task BuildTemporaryTableAsync_ShouldReturnDisposerThatDropsTableAsy .Should().BeFalse(); } + private Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + DbTransaction? transaction, + String name, + IEnumerable values, + Type valuesType, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return this.builder.BuildTemporaryTableAsync( + connection, + transaction, + name, + values, + valuesType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + this.builder.BuildTemporaryTable(connection, transaction, name, values, valuesType, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + private readonly ITemporaryTableBuilder builder; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs index f0512c0..243ad8d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs @@ -1,3 +1,7 @@ +using System.Data.Common; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder; + namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DbCommands; public sealed class @@ -23,8 +27,10 @@ public sealed class public abstract class DbCommandBuilderTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void BuildDbCommand_ShouldCreateTemporaryTables() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldCreateTemporaryTables(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -38,208 +44,7 @@ SELECT Value ON Entities.Id = Ids.Value """; - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); - - var temporaryTables = statement.TemporaryTables; - - command.CommandText - .Should().Be( - $""" - SELECT Value - FROM {QT(temporaryTables[0].Name)} AS Ids - INNER JOIN {QT(temporaryTables[1].Name)} AS Entities - ON Entities.Id = Ids.Value - """ - ); - - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); - - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeTrue(); - - this.Connection.Query($"SELECT {Q("Value")} FROM {QT(temporaryTables[0].Name)}") - .Should().BeEquivalentTo(entityIds); - - this.Connection.Query($"SELECT * FROM {QT(temporaryTables[1].Name)}") - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(); - var entities = Generate.Multiple(); - - InterpolatedSqlStatement statement = $""" - SELECT Value - FROM {TemporaryTable(entityIds)} AS Ids - INNER JOIN {TemporaryTable(entities)} AS Entities - ON Entities.Id = Ids.Value - """; - - var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); - - var temporaryTables = statement.TemporaryTables; - - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); - - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeTrue(); - - commandDisposer.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeFalse(); - - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeFalse(); - } - - [Fact] - public void BuildDbCommand_ShouldSetCommandTimeout() - { - var timeout = Generate.Single(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.DatabaseAdapter, - this.Connection, - null, - timeout - ); - - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); - } - - [Fact] - public void BuildDbCommand_ShouldSetCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "GetEntities", - this.DatabaseAdapter, - this.Connection, - commandType: CommandType.StoredProcedure - ); - - command.CommandType - .Should().Be(CommandType.StoredProcedure); - } - - [Fact] - public void BuildDbCommand_ShouldSetConnection() - { - var (command, _) = DbCommandBuilder.BuildDbCommand("SELECT 1", this.DatabaseAdapter, this.Connection); - - command.Connection - .Should().BeSameAs(this.Connection); - } - - [Fact] - public void BuildDbCommand_ShouldSetParameters() - { - var entityId = Generate.Id(); - var dateTimeValue = DateTime.UtcNow; - var stringValue = Generate.Single(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $""" - SELECT * - FROM Entity - WHERE Id = {Parameter(entityId)} AND - DateTimeValue = {Parameter(dateTimeValue)} AND - StringValue = {Parameter(stringValue)} - """, - this.DatabaseAdapter, - this.Connection - ); - - command.CommandText - .Should().Be( - $""" - SELECT * - FROM Entity - WHERE Id = {P("EntityId")} AND - DateTimeValue = {P("DateTimeValue")} AND - StringValue = {P("StringValue")} - """ - ); - - command.Parameters.Count - .Should().Be(3); - - command.Parameters["EntityId"] - .Value.Should().Be(entityId); - - command.Parameters["DateTimeValue"] - .Value.Should().Be(dateTimeValue); - - command.Parameters["StringValue"] - .Value.Should().Be(stringValue); - } - - [Fact] - public void BuildDbCommand_ShouldSetTransaction() - { - using var transaction = this.Connection.BeginTransaction(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.DatabaseAdapter, - this.Connection, - transaction - ); - - command.Transaction - .Should().BeSameAs(transaction); - } - - [Fact] - public void BuildDbCommand_ShouldUseCancellationToken() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - this.TestDatabaseProvider.DelayTwoSecondsStatement, - this.DatabaseAdapter, - this.Connection, - null, - null, - CommandType.Text, - cancellationToken - ); - - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); - - this.DatabaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - .Should().BeTrue(); - } - - [Fact] - public async Task BuildDbCommandAsync_ShouldCreateTemporaryTables() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(); - var entities = Generate.Multiple(); - - InterpolatedSqlStatement statement = $""" - SELECT Value - FROM {TemporaryTable(entityIds)} AS Ids - INNER JOIN {TemporaryTable(entities)} AS Entities - ON Entities.Id = Ids.Value - """; - - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync(statement, this.DatabaseAdapter, this.Connection); + var (command, _) = await CallApi(useAsyncApi, statement, this.DatabaseAdapter, this.Connection); var temporaryTables = statement.TemporaryTables; @@ -268,8 +73,10 @@ SELECT Value .Should().BeEquivalentTo(entities); } - [Fact] - public async Task BuildDbCommandAsync_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -283,7 +90,7 @@ SELECT Value ON Entities.Id = Ids.Value """; var (_, commandDisposer) = - await DbCommandBuilder.BuildDbCommandAsync(statement, this.DatabaseAdapter, this.Connection); + await CallApi(useAsyncApi, statement, this.DatabaseAdapter, this.Connection); var temporaryTables = statement.TemporaryTables; @@ -305,12 +112,15 @@ SELECT Value .Should().BeFalse(); } - [Fact] - public async Task BuildDbCommandAsync_ShouldSetCommandTimeout() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldSetCommandTimeout(Boolean useAsyncApi) { var timeout = Generate.Single(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection, @@ -322,12 +132,15 @@ public async Task BuildDbCommandAsync_ShouldSetCommandTimeout() .Should().Be((Int32)timeout.TotalSeconds); } - [Fact] - public async Task BuildDbCommandAsync_ShouldSetCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldSetCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "GetEntities", this.DatabaseAdapter, this.Connection, @@ -338,24 +151,29 @@ public async Task BuildDbCommandAsync_ShouldSetCommandType() .Should().Be(CommandType.StoredProcedure); } - [Fact] - public async Task BuildDbCommandAsync_ShouldSetConnection() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldSetConnection(Boolean useAsyncApi) { var (command, _) = - await DbCommandBuilder.BuildDbCommandAsync("SELECT 1", this.DatabaseAdapter, this.Connection); + await CallApi(useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection); command.Connection .Should().BeSameAs(this.Connection); } - [Fact] - public async Task BuildDbCommandAsync_ShouldSetParameters() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldSetParameters(Boolean useAsyncApi) { var entityId = Generate.Id(); var dateTimeValue = DateTime.UtcNow; var stringValue = Generate.Single(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $""" SELECT * FROM Entity @@ -391,12 +209,15 @@ FROM Entity .Should().Be(stringValue); } - [Fact] - public async Task BuildDbCommandAsync_ShouldSetTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldSetTransaction(Boolean useAsyncApi) { await using var transaction = await this.Connection.BeginTransactionAsync(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection, @@ -407,14 +228,17 @@ public async Task BuildDbCommandAsync_ShouldSetTransaction() .Should().BeSameAs(transaction); } - [Fact] - public async Task BuildDbCommandAsync_ShouldUseCancellationToken() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldUseCancellationToken(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, this.TestDatabaseProvider.DelayTwoSecondsStatement, this.DatabaseAdapter, this.Connection, @@ -430,4 +254,48 @@ public async Task BuildDbCommandAsync_ShouldUseCancellationToken() this.DatabaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) .Should().BeTrue(); } + + private static Task<(DbCommand, DbCommandDisposer)> CallApi( + Boolean useAsyncApi, + InterpolatedSqlStatement statement, + IDatabaseAdapter databaseAdapter, + DbConnection connection, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return DbCommandBuilder.BuildDbCommandAsync( + statement, + databaseAdapter, + connection, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + DbCommandBuilder.BuildDbCommand( + statement, + databaseAdapter, + connection, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException<(DbCommand, DbCommandDisposer)>(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs index 8f1583c..86bac48 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs @@ -24,7 +24,7 @@ public abstract class DefaultDbCommandFactoryTests : Inte where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] - public void CreateSqlCommand_NoTimeout_ShouldUseDefaultTimeout() + public void CreateDbCommand_NoTimeout_ShouldUseDefaultTimeout() { var command = this.factory.CreateDbCommand(this.Connection, "SELECT 1"); @@ -33,7 +33,7 @@ public void CreateSqlCommand_NoTimeout_ShouldUseDefaultTimeout() } [Fact] - public void CreateSqlCommand_ShouldCreateSqlCommandWithSpecifiedSettings() + public void CreateDbCommand_ShouldCreateDbCommandWithSpecifiedSettings() { var commandType = this.TestDatabaseProvider.SupportsStoredProcedures ? CommandType.StoredProcedure diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs index 36b42b5..e31a439 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs @@ -1,3 +1,5 @@ +using System.Data.Common; + namespace RentADeveloper.DbConnectionPlus.IntegrationTests; public sealed class @@ -24,246 +26,12 @@ public abstract class DbConnectionExtensions_ExecuteNonQueryTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void ExecuteNonQuery_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entity = this.CreateEntityInDb(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.ExecuteNonQuery( - $"DELETE FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entity should still exist. - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public void ExecuteNonQuery_CommandType_ShouldPassUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteNonQuery( - Q("DeleteAllEntities"), - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(5); - var entitiesToDelete = entities.Take(2).ToList(); - - InterpolatedSqlStatement statement = - $""" - DELETE FROM {Q("Entity")} - WHERE EXISTS ( - SELECT 1 - FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete - WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND - {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND - {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} - ) - """; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.ExecuteNonQuery( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entitiesToDelete.Count); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(5); - var entitiesToDelete = entities.Take(2).ToList(); - - this.Connection.ExecuteNonQuery( - $""" - DELETE FROM {Q("Entity")} - WHERE EXISTS ( - SELECT 1 - FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete - WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND - {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND - {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} - ) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entitiesToDelete.Count); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - foreach (var entity in entities.Except(entitiesToDelete)) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void ExecuteNonQuery_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteNonQuery( - $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteNonQuery_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.ExecuteNonQuery(statement, cancellationToken: TestContext.Current.CancellationToken); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(5); - var entitiesToDelete = entities.Take(2).ToList(); - var idsOfEntitiesToDelete = entitiesToDelete.ConvertAll(a => a.Id); - - InterpolatedSqlStatement statement = - $""" - DELETE FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) - """; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.ExecuteNonQuery( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(idsOfEntitiesToDelete.Count); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(5); - var entitiesToDelete = entities.Take(2).ToList(); - var idsOfEntitiesToDelete = entitiesToDelete.ConvertAll(a => a.Id); - - this.Connection.ExecuteNonQuery( - $""" - DELETE FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) - """, - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - foreach (var entity in entities.Except(entitiesToDelete)) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void ExecuteNonQuery_ShouldReturnNumberOfAffectedRows() - { - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteNonQuery( - $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(1); - - this.Connection.ExecuteNonQuery( - $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(0); - } - - [Fact] - public void ExecuteNonQuery_Transaction_ShouldUseTransaction() - { - var entity = this.CreateEntityInDb(); - - using (var transaction = this.Connection.BeginTransaction()) - { - this.Connection.ExecuteNonQuery( - $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity, transaction) - .Should().BeFalse(); - - transaction.Rollback(); - } - - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - - [Fact] - public async Task ExecuteNonQueryAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -273,8 +41,9 @@ public async Task ExecuteNonQueryAsync_CancellationToken_ShouldCancelOperationIf this.DbCommandFactory.DelayNextDbCommand = true; - await Invoking(() => - this.Connection.ExecuteNonQueryAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"DELETE FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -287,14 +56,18 @@ await Invoking(() => .Should().BeTrue(); } - [Fact] - public async Task ExecuteNonQueryAsync_CommandType_ShouldPassUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_CommandType_ShouldPassUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); var entity = this.CreateEntityInDb(); - await this.Connection.ExecuteNonQueryAsync( + await CallApi( + useAsyncApi, + this.Connection, Q("DeleteAllEntities"), commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -304,8 +77,12 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeFalse(); } - [Fact] - public async Task ExecuteNonQueryAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -326,7 +103,9 @@ SELECT 1 var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExecuteNonQueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -336,16 +115,22 @@ SELECT 1 .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteNonQueryAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(5); var entitiesToDelete = entities.Take(2).ToList(); - await this.Connection.ExecuteNonQueryAsync( + await CallApi( + useAsyncApi, + this.Connection, $""" DELETE FROM {Q("Entity")} WHERE EXISTS ( @@ -372,12 +157,16 @@ SELECT 1 } } - [Fact] - public async Task ExecuteNonQueryAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - await this.Connection.ExecuteNonQueryAsync( + await CallApi( + useAsyncApi, + this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -386,8 +175,10 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeFalse(); } - [Fact] - public async Task ExecuteNonQueryAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -396,14 +187,23 @@ public async Task ExecuteNonQueryAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - await this.Connection.ExecuteNonQueryAsync(statement, cancellationToken: TestContext.Current.CancellationToken); + await CallApi( + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ); this.ExistsEntityInDb(entity) .Should().BeFalse(); } - [Fact] - public async Task ExecuteNonQueryAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -419,7 +219,9 @@ public async Task ExecuteNonQueryAsync_ScalarValuesTemporaryTable_ShouldDropTemp var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExecuteNonQueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -429,9 +231,13 @@ public async Task ExecuteNonQueryAsync_ScalarValuesTemporaryTable_ShouldDropTemp .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteNonQueryAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -439,7 +245,9 @@ public async Task var entitiesToDelete = entities.Take(2).ToList(); var idsOfEntitiesToDelete = entitiesToDelete.ConvertAll(a => a.Id); - await this.Connection.ExecuteNonQueryAsync( + await CallApi( + useAsyncApi, + this.Connection, $""" DELETE FROM {Q("Entity")} WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) @@ -460,32 +268,42 @@ await this.Connection.ExecuteNonQueryAsync( } } - [Fact] - public async Task ExecuteNonQueryAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.ExecuteNonQueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(1); - (await this.Connection.ExecuteNonQueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(0); } - [Fact] - public async Task ExecuteNonQueryAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteNonQuery_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - await this.Connection.ExecuteNonQueryAsync( + await CallApi( + useAsyncApi, + this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -500,4 +318,37 @@ await this.Connection.ExecuteNonQueryAsync( this.ExistsEntityInDb(entity) .Should().BeTrue(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.ExecuteNonQueryAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.ExecuteNonQuery(statement, transaction, commandTimeout, commandType, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs index 133853d..3879b34 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs @@ -1,3 +1,5 @@ +using System.Data.Common; + namespace RentADeveloper.DbConnectionPlus.IntegrationTests; public sealed class @@ -24,289 +26,12 @@ public abstract class DbConnectionExtensions_ExecuteReaderTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void ExecuteReader_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - { - using var reader = this.Connection.ExecuteReader( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ); - } - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void ExecuteReader_CommandBehavior_ShouldPassUseCommandBehavior() - { - var reader = this.Connection.ExecuteReader( - $"SELECT * FROM {Q("Entity")}", - commandBehavior: CommandBehavior.CloseConnection, - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.Dispose(); - - this.Connection.State - .Should().Be(ConnectionState.Closed); - } - - [Fact] - public void ExecuteReader_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(); - - using var reader = this.Connection.ExecuteReader( - Q("GetEntityIdsAndStringValues"), - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - reader.Read() - .Should().BeTrue(); - - reader.GetInt64(0) - .Should().Be(entity.Id); - - reader.GetString(1) - .Should().Be(entity.StringValue); - } - } - - [Fact] - public void ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(); - - InterpolatedSqlStatement statement = $""" - SELECT {Q("Id")} - FROM {TemporaryTable(entities)} - """; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var reader = this.Connection.ExecuteReader( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - reader.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteReader_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(); - - using var reader = this.Connection.ExecuteReader( - $""" - SELECT {Q("Id")}, {Q("StringValue")}, {Q("DecimalValue")} - FROM {TemporaryTable(entities)} - """, - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - reader.Read() - .Should().BeTrue(); - - reader.GetInt64(0) - .Should().Be(entity.Id); - - reader.GetString(1) - .Should().Be(entity.StringValue); - - reader.GetDecimal(2) - .Should().Be(entity.DecimalValue); - } - } - - [Fact] - public void ExecuteReader_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.Read() - .Should().BeTrue(); - - reader.GetString(0) - .Should().Be(entity.StringValue); - } - - [Fact] - public void ExecuteReader_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - using var reader = this.Connection.ExecuteReader( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.Read() - .Should().BeTrue(); - - reader.GetString(0) - .Should().Be(entity.StringValue); - } - - [Fact] - public void ExecuteReader_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var reader = this.Connection.ExecuteReader( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - reader.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteReader_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entityId in entityIds) - { - reader.Read() - .Should().BeTrue(); - - reader.GetInt64(0) - .Should().Be(entityId); - } - } - - [Fact] - public void ExecuteReader_ShouldReturnDataReaderForQueryResult() - { - var entities = this.CreateEntitiesInDb(); - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - reader.Read() - .Should().BeTrue(); - - reader.GetInt64(0) - .Should().Be(entity.Id); - - reader.GetString(1) - .Should().Be(entity.StringValue); - } - - reader.Read() - .Should().BeFalse(); - } - - [Fact] - public void ExecuteReader_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entities = this.CreateEntitiesInDb(null, transaction); - - var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.HasRows - .Should().BeTrue(); - - foreach (var entity in entities) - { - reader.Read() - .Should().BeTrue(); - - reader.GetInt64(0) - .Should().Be(entity.Id); - - reader.GetString(1) - .Should().Be(entity.StringValue); - } - - reader.Dispose(); - - transaction.Rollback(); - } - - using var reader2 = this.Connection.ExecuteReader( - $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader2.HasRows - .Should().BeFalse(); - } - - [Fact] - public async Task ExecuteReaderAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -316,7 +41,9 @@ public async Task ExecuteReaderAsync_CancellationToken_ShouldCancelOperationIfCa await Invoking(async () => { - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ); @@ -326,10 +53,14 @@ await Invoking(async () => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task ExecuteReaderAsync_CommandBehavior_ShouldUseCommandBehavior() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_CommandBehavior_ShouldUseCommandBehavior(Boolean useAsyncApi) { - var reader = await this.Connection.ExecuteReaderAsync( + var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", commandBehavior: CommandBehavior.CloseConnection, cancellationToken: TestContext.Current.CancellationToken @@ -341,14 +72,18 @@ public async Task ExecuteReaderAsync_CommandBehavior_ShouldUseCommandBehavior() .Should().Be(ConnectionState.Closed); } - [Fact] - public async Task ExecuteReaderAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, Q("GetEntityIdsAndStringValues"), commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -367,9 +102,11 @@ public async Task ExecuteReaderAsync_CommandType_ShouldUseCommandType() } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteReaderAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal() + ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -381,7 +118,9 @@ public async Task """; var temporaryTableName = statement.TemporaryTables[0].Name; - var reader = await this.Connection.ExecuteReaderAsync( + var reader = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -398,15 +137,21 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteReaderAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + ExecuteReader_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, $""" SELECT {Q("Id")}, {Q("StringValue")}, {Q("DecimalValue")} FROM {TemporaryTable(entities)} @@ -430,12 +175,16 @@ public async Task } } - [Fact] - public async Task ExecuteReaderAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -447,8 +196,10 @@ public async Task ExecuteReaderAsync_InterpolatedParameter_ShouldPassInterpolate .Should().Be(entity.StringValue); } - [Fact] - public async Task ExecuteReaderAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -457,7 +208,9 @@ public async Task ExecuteReaderAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -469,8 +222,12 @@ public async Task ExecuteReaderAsync_Parameter_ShouldPassParameter() .Should().Be(entity.StringValue); } - [Fact] - public async Task ExecuteReaderAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -480,7 +237,9 @@ public async Task ExecuteReaderAsync_ScalarValuesTemporaryTable_ShouldDropTempor var temporaryTableName = statement.TemporaryTables[0].Name; - var reader = await this.Connection.ExecuteReaderAsync( + var reader = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -497,15 +256,21 @@ public async Task ExecuteReaderAsync_ScalarValuesTemporaryTable_ShouldDropTempor .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteReaderAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + ExecuteReader_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -520,12 +285,16 @@ public async Task } } - [Fact] - public async Task ExecuteReaderAsync_ShouldReturnDataReaderForQueryResult() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_ShouldReturnDataReaderForQueryResult(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - await using var reader = await this.Connection.ExecuteReaderAsync( + await using var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -546,14 +315,18 @@ public async Task ExecuteReaderAsync_ShouldReturnDataReaderForQueryResult() .Should().BeFalse(); } - [Fact] - public async Task ExecuteReaderAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteReader_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(null, transaction); - var reader = await this.Connection.ExecuteReaderAsync( + var reader = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -579,10 +352,54 @@ public async Task ExecuteReaderAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.ExecuteReaderAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )).HasRows .Should().BeFalse(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandBehavior commandBehavior = CommandBehavior.Default, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.ExecuteReaderAsync( + statement, + transaction, + commandTimeout, + commandBehavior, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.ExecuteReader( + statement, + transaction, + commandTimeout, + commandBehavior, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs index 6f75eb6..3839540 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs @@ -1,3 +1,5 @@ +using System.Data.Common; + namespace RentADeveloper.DbConnectionPlus.IntegrationTests; public sealed class @@ -24,344 +26,12 @@ public abstract class DbConnectionExtensions_ExecuteScalarTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void ExecuteScalar_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.ExecuteScalar("SELECT 1", cancellationToken: cancellationToken) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void ExecuteScalar_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'A' ({typeof(String)}), which could not be converted to the type {typeof(Int32)}.*" - ); - - [Fact] - public void ExecuteScalar_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteScalar( - "GetFirstEntityId", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.Id); - } - - [Fact] - public void ExecuteScalar_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(1); - - InterpolatedSqlStatement statement = $""" - SELECT {Q("StringValue")} - FROM {TemporaryTable(entities)} - """; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.ExecuteScalar( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0].StringValue); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteScalar_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(1); - - this.Connection.ExecuteScalar( - $""" - SELECT {Q("StringValue")} - FROM {TemporaryTable(entities)} - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0].StringValue); - } - - [Fact] - public void ExecuteScalar_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteScalar( - $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.StringValue); - } - - [Fact] - public void ExecuteScalar_NoResultSet_ShouldReturnDefault() - { - this.Connection.ExecuteScalar( - "SELECT 1 WHERE 0 = 1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - this.Connection.ExecuteScalar( - "SELECT 1 WHERE 0 = 1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(0); - } - - [Fact] - public void ExecuteScalar_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.ExecuteScalar(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().Be(entity.StringValue); - } - - [Fact] - public void ExecuteScalar_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(1); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.ExecuteScalar( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityIds[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void ExecuteScalar_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(1); - - this.Connection.ExecuteScalar( - $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityIds[0]); - } - - [Fact] - public void ExecuteScalar_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.ExecuteScalar( - $""" - SELECT {Q("DateTimeOffsetValue")} - FROM {Q("EntityWithDateTimeOffset")} - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.DateTimeOffsetValue); - } - - [Fact] - public void ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains " + - $"the value '' ({typeof(String)}), which could not be converted to the type {typeof(Char)}. See " + - "inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'ab' ({typeof(String)}), which could not be converted to the type {typeof(Char)}. See inner " + - "exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.ExecuteScalar( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(character); - } - - [Fact] - public void ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInteger_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.ExecuteScalar( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value '999*' (System.*), which could not be converted to the type {typeof(TestEnum)}.*" - ); - - [Fact] - public void ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'NonExistent' ({typeof(String)}), which could not be converted to the type " + - $"{typeof(TestEnum)}.*" - ); - - [Fact] - public void ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.ExecuteScalar( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void ExecuteScalar_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldThrow() => - Invoking(() => - this.Connection.ExecuteScalar( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains a NULL " + - $"value, which could not be converted to the type {typeof(Int32)}.*" - ); - - [Fact] - public void ExecuteScalar_TargetTypeIsNullable_ColumnValueIsNull_ShouldReturnNull() => - this.Connection.ExecuteScalar( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - [Fact] - public void ExecuteScalar_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); - - this.Connection.ExecuteScalar( - $"SELECT {Q("StringValue")} FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.StringValue); - - transaction.Rollback(); - } - - this.Connection.ExecuteScalar( - $"SELECT {Q("StringValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - } - - [Fact] - public async Task ExecuteScalarAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -370,7 +40,9 @@ public async Task ExecuteScalarAsync_CancellationToken_ShouldCancelOperationIfCa this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: cancellationToken ) @@ -379,10 +51,14 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public Task ExecuteScalarAsync_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task ExecuteScalar_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) @@ -393,14 +69,18 @@ public Task ExecuteScalarAsync_ColumnValueCannotBeConvertedToTargetType_ShouldTh $"value 'A' ({typeof(String)}), which could not be converted to the type {typeof(Int32)}.*" ); - [Fact] - public async Task ExecuteScalarAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntityId", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -408,8 +88,12 @@ public async Task ExecuteScalarAsync_CommandType_ShouldUseCommandType() .Should().Be(entity.Id); } - [Fact] - public async Task ExecuteScalarAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -422,7 +106,9 @@ public async Task ExecuteScalarAsync_ComplexObjectsTemporaryTable_ShouldDropTemp var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -432,15 +118,21 @@ public async Task ExecuteScalarAsync_ComplexObjectsTemporaryTable_ShouldDropTemp .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteScalarAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + ExecuteScalar_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(1); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT {Q("StringValue")} FROM {TemporaryTable(entities)} @@ -450,36 +142,48 @@ public async Task .Should().Be(entities[0].StringValue); } - [Fact] - public async Task ExecuteScalarAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entity.StringValue); } - [Fact] - public async Task ExecuteScalarAsync_NoResultSet_ShouldReturnDefault() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_NoResultSet_ShouldReturnDefault(Boolean useAsyncApi) { - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, "SELECT 1 WHERE 0 = 1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, "SELECT 1 WHERE 0 = 1", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(0); } - [Fact] - public async Task ExecuteScalarAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -488,15 +192,21 @@ public async Task ExecuteScalarAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entity.StringValue); } - [Fact] - public async Task ExecuteScalarAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -506,7 +216,9 @@ public async Task ExecuteScalarAsync_ScalarValuesTemporaryTable_ShouldDropTempor var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -516,29 +228,39 @@ public async Task ExecuteScalarAsync_ScalarValuesTemporaryTable_ShouldDropTempor .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteScalarAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + ExecuteScalar_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(1); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entityIds[0]); } - [Fact] - public async Task ExecuteScalarAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")} @@ -548,15 +270,21 @@ public async Task ExecuteScalarAsync_ShouldSupportDateTimeOffsetValues() .Should().Be(entity.DateTimeOffsetValue); } - [Fact] - public async Task ExecuteScalarAsync_TargetTypeIsChar_ColumnValueIsStringWithLengthNotOne_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) @@ -575,7 +303,9 @@ public async Task ExecuteScalarAsync_TargetTypeIsChar_ColumnValueIsStringWithLen } (await Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -593,35 +323,49 @@ public async Task ExecuteScalarAsync_TargetTypeIsChar_ColumnValueIsStringWithLen ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExecuteScalarAsync_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter() + ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter(Boolean useAsyncApi) { var character = Generate.Single(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(character); } - [Fact] - public async Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsInteger_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInteger_ShouldConvertIntegerToEnum( + Boolean useAsyncApi + ) { var enumValue = Generate.Single(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ) @@ -632,10 +376,14 @@ public Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsInvalidInteger_Shou $"value '999*' (System.*), which could not be converted to the type {typeof(TestEnum)}.*" ); - [Fact] - public Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ) @@ -647,22 +395,30 @@ public Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsInvalidString_Shoul $"{typeof(TestEnum)}.*" ); - [Fact] - public async Task ExecuteScalarAsync_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task ExecuteScalarAsync_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task ExecuteScalar_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.ExecuteScalarAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) @@ -673,22 +429,30 @@ public Task ExecuteScalarAsync_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldT $"value, which could not be converted to the type {typeof(Int32)}.*" ); - [Fact] - public async Task ExecuteScalarAsync_TargetTypeIsNullable_ColumnValueIsNull_ShouldReturnNull() => - (await this.Connection.ExecuteScalarAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_TargetTypeIsNullable_ColumnValueIsNull_ShouldReturnNull(Boolean useAsyncApi) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task ExecuteScalarAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteScalar_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -698,10 +462,51 @@ public async Task ExecuteScalarAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.ExecuteScalarAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.ExecuteScalarAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.ExecuteScalar( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs index 2ee10ed..dcb1211 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs @@ -1,3 +1,5 @@ +using System.Data.Common; + namespace RentADeveloper.DbConnectionPlus.IntegrationTests; public sealed class @@ -24,178 +26,10 @@ public abstract class DbConnectionExtensions_ExistsTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void Exists_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => this.Connection.Exists("SELECT 1", cancellationToken: cancellationToken)) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void Exists_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - this.CreateEntitiesInDb(1); - - this.Connection.Exists( - "GetFirstEntityId", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - } - - [Fact] - public void Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(1); - - InterpolatedSqlStatement statement = $""" - SELECT 1 - FROM {TemporaryTable(entities)} - WHERE {Q("Id")} = {Parameter(entities[0].Id)} - """; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.Exists(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().BeTrue(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Exists_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(1); - - this.Connection.Exists( - $""" - SELECT 1 - FROM {TemporaryTable(entities)} - WHERE {Q("Id")} = {Parameter(entities[0].Id)} - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - } - - [Fact] - public void Exists_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.Exists( - $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - } - - [Fact] - public void Exists_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.Exists(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().BeTrue(); - } - - [Fact] - public void Exists_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = - $"SELECT 1 FROM {TemporaryTable(entityIds)} WHERE {Q("Value")} = {Parameter(entityIds[0])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.Exists(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().BeTrue(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Exists_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - this.Connection.Exists( - $"SELECT 1 FROM {TemporaryTable(entityIds)} WHERE {Q("Value")} = {Parameter(entityIds[0])}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - } - - [Fact] - public void Exists_ShouldReturnBooleanIndicatingWhetherQueryReturnedAtLeastOneRow() - { - var entity = this.CreateEntityInDb(); - - this.Connection.Exists( - $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - - this.Connection.Exists( - $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeFalse(); - } - - [Fact] - public void Exists_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); - - this.Connection.Exists( - $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeTrue(); - - transaction.Rollback(); - } - - this.Connection.Exists( - $"SELECT 1 FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeFalse(); - } - - [Fact] - public async Task ExistsAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -203,19 +37,23 @@ public async Task ExistsAsync_CancellationToken_ShouldCancelOperationIfCancellat this.DbCommandFactory.DelayNextDbCommand = true; - await Invoking(() => this.Connection.ExistsAsync("SELECT 1", cancellationToken: cancellationToken)) + await Invoking(() => CallApi(useAsyncApi, this.Connection, "SELECT 1", cancellationToken: cancellationToken)) .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task ExistsAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); this.CreateEntitiesInDb(1); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntityId", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -223,8 +61,10 @@ public async Task ExistsAsync_CommandType_ShouldUseCommandType() .Should().BeTrue(); } - [Fact] - public async Task ExistsAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -238,7 +78,9 @@ SELECT 1 var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -248,15 +90,21 @@ SELECT 1 .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - ExistsAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + Exists_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(1); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT 1 FROM {TemporaryTable(entities)} @@ -267,20 +115,26 @@ SELECT 1 .Should().BeTrue(); } - [Fact] - public async Task ExistsAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeTrue(); } - [Fact] - public async Task ExistsAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -289,12 +143,19 @@ public async Task ExistsAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - (await this.Connection.ExistsAsync(statement, cancellationToken: TestContext.Current.CancellationToken)) + (await CallApi( + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + )) .Should().BeTrue(); } - [Fact] - public async Task ExistsAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -305,7 +166,9 @@ public async Task ExistsAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTabl var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -315,46 +178,62 @@ public async Task ExistsAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTabl .Should().BeFalse(); } - [Fact] - public async Task ExistsAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {TemporaryTable(entityIds)} WHERE {Q("Value")} = {Parameter(entityIds[0])}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeTrue(); } - [Fact] - public async Task ExistsAsync_ShouldReturnBooleanIndicatingWhetherQueryReturnedAtLeastOneRow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_ShouldReturnBooleanIndicatingWhetherQueryReturnedAtLeastOneRow(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeTrue(); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeFalse(); } - [Fact] - public async Task ExistsAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Exists_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -364,10 +243,45 @@ public async Task ExistsAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.ExistsAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeFalse(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.ExistsAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.Exists(statement, transaction, commandTimeout, commandType, cancellationToken) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs index 98f1ea6..7d32fe8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs @@ -27,7 +27,7 @@ public abstract class [Fact] public void Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; // The variable name must be different from the variable name used in // Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString. @@ -46,7 +46,7 @@ public void Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeE [Fact] public void Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; // The variable name must be different from the variable name used in // Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger. diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 77ab4d6..ac1fc12 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -1,948 +1,46 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_MySql : - DbConnectionExtensions_QueryFirstOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_Oracle : - DbConnectionExtensions_QueryFirstOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_PostgreSql : - DbConnectionExtensions_QueryFirstOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_Sqlite : - DbConnectionExtensions_QueryFirstOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_SqlServer : - DbConnectionExtensions_QueryFirstOfTTests; - -public abstract class - DbConnectionExtensions_QueryFirstOfTTests : IntegrationTestsBase - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirst( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirst( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - [Fact] - public void QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(character); - } - - [Fact] - public void QueryFirst_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" - ); - - [Fact] - public void QueryFirst_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirst_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirst_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirst_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - this.Connection.QueryFirst( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - [Fact] - public void QueryFirst_BuiltInType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0].DateTimeOffsetValue); - } - - [Fact] - public void QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QueryFirst_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QueryFirst( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirst( - $"SELECT '' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirst( - $"SELECT 'ab' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT '{character}' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); - } - - [Fact] - public void QueryFirst_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst( - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" - ); - - [Fact] - public void QueryFirst_EntityType_ColumnHasNoName_ShouldThrow() - { - InterpolatedSqlStatement statement = this.TestDatabaseProvider switch - { - SqlServerTestDatabaseProvider => - "SELECT 1", - - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", - - _ => - "SELECT 1 AS ''" - }; - - Invoking(() => - this.Connection.QueryFirst( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" - ); - } - - [Fact] - public void QueryFirst_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void QueryFirst_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() - { - var entity = Invoking(() => - this.Connection.QueryFirst( - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().NotThrow().Subject; - - entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); - } - - [Fact] - public void QueryFirst_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() - { - var entities = this.CreateEntitiesInDb(2); - var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entitiesWithDifferentCasingProperties[0]); - } - - [Fact] - public void QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => this.Connection.QueryFirst( - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => this.Connection.QueryFirst( - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QueryFirst_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirst_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirst_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst($"SELECT 1 AS {Q("NonExistent")}") - ) - .Should().Throw() - .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. The type " + - "either needs to have a parameterless constructor or a constructor whose parameters match the " + - "columns returned by the SQL statement, e.g. a constructor that has the following " + - $"signature:{Environment.NewLine}" + - "(* NonExistent).*" - ); - - [Fact] - public void - QueryFirst_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void - QueryFirst_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QueryFirst( - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a " + - $"NULL value, but the corresponding property of the type {typeof(EntityWithNonNullableProperty)} " + - "is non-nullable.*" - ); - } - - [Fact] - public void QueryFirst_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); - } - - [Fact] - public void QueryFirst_EntityType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QueryFirst( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - - [Fact] - public void QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public void QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); - - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); - - Invoking(() => - this.Connection.QueryFirst( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } - - [Fact] - public void QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_Parameter_ShouldPassParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entities[0].Id) - ); - - this.Connection.QueryFirst(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did not return any rows." - ); - - [Fact] - public void QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS Id FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QueryFirst( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityIds[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(2); - var entityIds = entities.Select(a => a.Id); - - this.Connection.QueryFirst( - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirst_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entities = this.CreateEntitiesInDb(2, transaction); - - this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - - transaction.Rollback(); - } - - Invoking(() => this.Connection.QueryFirst( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw(); - } - - [Fact] - public void QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirst>( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(character)); - } - - [Fact] - public void QueryFirst_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" - ); - - [Fact] - public void QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst>( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirst>( - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" - ); - } - - [Fact] - public void QueryFirst_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QueryFirst>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(new(null)); - } - - [Fact] - public void QueryFirst_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirst<(Int32, Int32)>( - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. Make sure that the SQL statement returns the same number of columns as the number of " + - "fields in the value tuple type.*" - ); - - [Fact] - public void QueryFirst_ValueTupleType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QueryFirst>( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); - } +using System.Data.Common; - [Fact] - public void QueryFirst_ValueTupleType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); +namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - var entities = this.CreateEntitiesInDb(2); +public sealed class + DbConnectionExtensions_QueryFirstOfTTests_MySql : + DbConnectionExtensions_QueryFirstOfTTests; - this.Connection.QueryFirst<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); - } +public sealed class + DbConnectionExtensions_QueryFirstOfTTests_Oracle : + DbConnectionExtensions_QueryFirstOfTTests; - [Fact] - public void QueryFirst_ValueTupleType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); +public sealed class + DbConnectionExtensions_QueryFirstOfTTests_PostgreSql : + DbConnectionExtensions_QueryFirstOfTTests; - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); +public sealed class + DbConnectionExtensions_QueryFirstOfTTests_Sqlite : + DbConnectionExtensions_QueryFirstOfTTests; - Invoking(() => - this.Connection.QueryFirst>( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } +public sealed class + DbConnectionExtensions_QueryFirstOfTTests_SqlServer : + DbConnectionExtensions_QueryFirstOfTTests; - [Fact] - public async Task QueryFirstAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() +public abstract class + DbConnectionExtensions_QueryFirstOfTTests : IntegrationTestsBase + where TTestDatabaseProvider : ITestDatabaseProvider, new() +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) @@ -960,7 +58,9 @@ public async Task QueryFirstAsync_BuiltInType_CharTargetType_ColumnContainsStrin } (await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -977,23 +77,33 @@ public async Task QueryFirstAsync_BuiltInType_CharTargetType_ColumnContainsStrin ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(character); } - [Fact] - public Task QueryFirstAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1004,10 +114,14 @@ public Task QueryFirstAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public Task QueryFirstAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ) @@ -1018,10 +132,14 @@ public Task QueryFirstAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInte $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); - [Fact] - public Task QueryFirstAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1033,34 +151,46 @@ public Task QueryFirstAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidStri "exception for details.*" ); - [Fact] - public async Task QueryFirstAsync_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public async Task QueryFirstAsync_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task QueryFirstAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) @@ -1071,30 +201,42 @@ public Task QueryFirstAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull $"to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public async Task QueryFirstAsync_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - (await this.Connection.QueryFirstAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task QueryFirstAsync_BuiltInType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0].DateTimeOffsetValue); } - [Fact] - public async Task QueryFirstAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -1103,7 +245,9 @@ public async Task QueryFirstAsync_CancellationToken_ShouldCancelOperationIfCance this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -1112,14 +256,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QueryFirstAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -1127,9 +275,11 @@ public async Task QueryFirstAsync_CommandType_ShouldUseCommandType() .Should().Be(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1139,7 +289,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1149,31 +301,41 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1192,7 +354,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1210,23 +374,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); } - [Fact] - public Task QueryFirstAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("TimeSpanValue")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1238,8 +414,10 @@ public Task QueryFirstAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityProp $"{typeof(Entity)}.*" ); - [Fact] - public async Task QueryFirstAsync_EntityType_ColumnHasNoName_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { @@ -1254,7 +432,9 @@ public async Task QueryFirstAsync_EntityType_ColumnHasNoName_ShouldThrow() }; await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1266,35 +446,51 @@ await Invoking(() => ); } - [Fact] - public async Task QueryFirstAsync_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] - public async Task QueryFirstAsync_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] - public async Task QueryFirstAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + Boolean useAsyncApi + ) { var entity = (await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1305,22 +501,34 @@ public async Task QueryFirstAsync_EntityType_EntityTypeHasNoCorrespondingPropert .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); } - [Fact] - public async Task QueryFirstAsync_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entitiesWithDifferentCasingProperties[0]); } - [Fact] - public async Task QueryFirstAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - await Invoking(() => this.Connection.QueryFirstAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1337,9 +545,15 @@ await Invoking(() => this.Connection.QueryFirstAsync - await Invoking(() => this.Connection.QueryFirstAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1356,12 +570,16 @@ await Invoking(() => this.Connection.QueryFirstAsync(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken )) @@ -1369,12 +587,16 @@ public async Task QueryFirstAsync_EntityType_EnumEntityProperty_ShouldConvertInt .Should().Be(enumValue); } - [Fact] - public async Task QueryFirstAsync_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken )) @@ -1382,10 +604,56 @@ public async Task QueryFirstAsync_EntityType_EnumEntityProperty_ShouldConvertStr .Should().Be(enumValue); } - [Fact] - public Task QueryFirstAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync($"SELECT 1 AS {Q("NonExistent")}") + CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) .Should().ThrowAsync() .WithMessage( @@ -1396,41 +664,73 @@ public Task QueryFirstAsync_EntityType_NoCompatibleConstructor_NoParameterlessCo "(* NonExistent).*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + QueryFirst_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + QueryFirst_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public Task QueryFirstAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1442,68 +742,73 @@ public Task QueryFirstAsync_EntityType_NonNullableEntityProperty_ColumnContainsN ); } - [Fact] - public async Task QueryFirstAsync_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Fact] - public async Task QueryFirstAsync_EntityType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); } - [Fact] - public async Task QueryFirstAsync_EntityType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstAsync_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - (await this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public Task QueryFirstAsync_EntityType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1514,20 +819,26 @@ public Task QueryFirstAsync_EntityType_UnsupportedFieldType_ShouldThrow() ); } - [Fact] - public async Task QueryFirstAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -1536,16 +847,22 @@ public async Task QueryFirstAsync_Parameter_ShouldPassParameter() ("Id", entities[0].Id) ); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public Task QueryFirstAsync_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QueryFirstAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1555,9 +872,11 @@ public Task QueryFirstAsync_QueryReturnedNoRows_ShouldThrow() => "The SQL statement did not return any rows." ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1568,7 +887,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1578,16 +899,22 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(2); var entityIds = entities.ConvertAll(a => a.Id); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT * FROM {Q("Entity")} @@ -1598,14 +925,18 @@ public async Task .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -1615,7 +946,9 @@ public async Task QueryFirstAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - await Invoking(() => this.Connection.QueryFirstAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1623,16 +956,22 @@ await Invoking(() => this.Connection.QueryFirstAsync( .Should().ThrowAsync(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1651,7 +990,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1669,23 +1010,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(character)); } - [Fact] - public Task QueryFirstAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1697,10 +1050,16 @@ public Task QueryFirstAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueT $"{typeof(ValueTuple)}.*" ); - [Fact] - public Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 999 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1717,10 +1076,16 @@ public Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInv $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] - public Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'NonExistent' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1737,39 +1102,51 @@ public Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInv "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public async Task QueryFirstAsync_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public Task QueryFirstAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1781,24 +1158,36 @@ public Task QueryFirstAsync_ValueTupleType_NonNullableValueTupleField_ColumnCont ); } - [Fact] - public async Task QueryFirstAsync_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryFirstAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); } - [Fact] - public Task QueryFirstAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstAsync<(Int32, Int32)>( + CallApi<(Int32, Int32)>( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1810,41 +1199,53 @@ public Task QueryFirstAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfVa "fields in the value tuple type.*" ); - [Fact] - public async Task QueryFirstAsync_ValueTupleType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryFirstAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(ValueTuple.Create(bytes)); } - [Fact] - public async Task QueryFirstAsync_ValueTupleType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstAsync<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); } - [Fact] - public Task QueryFirstAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1854,4 +1255,43 @@ public Task QueryFirstAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QueryFirstAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QueryFirst( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index a5ba3d7..a2b7538 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -1,964 +1,48 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_MySql : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Oracle : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_PostgreSql : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Sqlite : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_SqlServer : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - -public abstract class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests : IntegrationTestsBase< - TTestDatabaseProvider> - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - [Fact] - public void - QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(character); - } - - [Fact] - public void QueryFirstOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" - ); - - [Fact] - public void QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirstOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QueryFirstOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - this.Connection.QueryFirstOrDefault( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - [Fact] - public void QueryFirstOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0].DateTimeOffsetValue); - } - - [Fact] - public void QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QueryFirstOrDefault_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QueryFirstOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT '' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT 'ab' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT '{character}' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" - ); - - [Fact] - public void QueryFirstOrDefault_EntityType_ColumnHasNoName_ShouldThrow() - { - InterpolatedSqlStatement statement = this.TestDatabaseProvider switch - { - SqlServerTestDatabaseProvider => - "SELECT 1", - - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", - - _ => - "SELECT 1 AS ''" - }; - - Invoking(() => - this.Connection.QueryFirstOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" - ); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() - { - var entity = Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().NotThrow().Subject; - - entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() - { - var entities = this.CreateEntitiesInDb(2); - var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entitiesWithDifferentCasingProperties[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => this.Connection.QueryFirstOrDefault( - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => this.Connection.QueryFirstOrDefault( - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - )! - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - )! - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault($"SELECT 1 AS {Q("NonExistent")}") - ) - .Should().Throw() - .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. The type " + - "either needs to have a parameterless constructor or a constructor whose parameters match the " + - "columns returned by the SQL statement, e.g. a constructor that has the following " + - $"signature:{Environment.NewLine}" + - "(* NonExistent).*" - ); - - [Fact] - public void - QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities[0]); - } - - [Fact] - public void - QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a " + - $"NULL value, but the corresponding property of the type {typeof(EntityWithNonNullableProperty)} " + - "is non-nullable.*" - ); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QueryFirstOrDefault( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public void QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); - - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); - - Invoking(() => - this.Connection.QueryFirstOrDefault( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } - - [Fact] - public void QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entities = this.CreateEntitiesInDb(2); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_Parameter_ShouldPassParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entities[0].Id) - ); - - this.Connection.QueryFirstOrDefault(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnDefault() - { - this.Connection.QueryFirstOrDefault( - $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(0); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - this.Connection.QueryFirstOrDefault<(Int64, String)>( - $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(default); - } - - [Fact] - public void QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS Id FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QueryFirstOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityIds[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(2); - var entityIds = entities.Select(a => a.Id); - - this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - } - - [Fact] - public void QueryFirstOrDefault_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entities = this.CreateEntitiesInDb(2, transaction); - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entities[0]); - - transaction.Rollback(); - } - - this.Connection.QueryFirstOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - } - - [Fact] - public void - QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QueryFirstOrDefault>( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(character)); - } - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" - ); - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault>( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QueryFirstOrDefault>( - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" - ); - } - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QueryFirstOrDefault>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(new(null)); - } - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => - Invoking(() => - this.Connection.QueryFirstOrDefault<(Int32, Int32)>( - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. Make sure that the SQL statement returns the same number of columns as the number of " + - "fields in the value tuple type.*" - ); - - [Fact] - public void QueryFirstOrDefault_ValueTupleType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QueryFirstOrDefault>( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); - } +using System.Data.Common; - [Fact] - public void QueryFirstOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); +namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - var entities = this.CreateEntitiesInDb(2); +public sealed class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests_MySql : + DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - this.Connection.QueryFirstOrDefault<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); - } +public sealed class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Oracle : + DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - [Fact] - public void QueryFirstOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); +public sealed class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests_PostgreSql : + DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); +public sealed class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Sqlite : + DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - Invoking(() => - this.Connection.QueryFirstOrDefault>( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } +public sealed class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests_SqlServer : + DbConnectionExtensions_QueryFirstOrDefaultOfTTests; - [Fact] +public abstract class + DbConnectionExtensions_QueryFirstOrDefaultOfTTests : IntegrationTestsBase< + TTestDatabaseProvider> + where TTestDatabaseProvider : ITestDatabaseProvider, new() +{ + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) @@ -976,7 +60,9 @@ public async Task } (await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -993,23 +79,35 @@ public async Task ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(character); } - [Fact] - public Task QueryFirstOrDefaultAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1020,10 +118,16 @@ public Task QueryFirstOrDefaultAsync_BuiltInType_ColumnValueCannotBeConvertedToT $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ) @@ -1034,10 +138,16 @@ public Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsIn $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); - [Fact] - public Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1049,34 +159,48 @@ public Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsIn "exception for details.*" ); - [Fact] - public async Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public async Task QueryFirstOrDefaultAsync_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task QueryFirstOrDefaultAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) @@ -1087,30 +211,44 @@ public Task QueryFirstOrDefaultAsync_BuiltInType_NonNullableTargetType_ColumnCon $"to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public async Task QueryFirstOrDefaultAsync_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - (await this.Connection.QueryFirstOrDefaultAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task QueryFirstOrDefaultAsync_BuiltInType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0].DateTimeOffsetValue); } - [Fact] - public async Task QueryFirstOrDefaultAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -1119,7 +257,9 @@ public async Task QueryFirstOrDefaultAsync_CancellationToken_ShouldCancelOperati this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -1128,14 +268,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QueryFirstOrDefaultAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -1143,9 +287,11 @@ public async Task QueryFirstOrDefaultAsync_CommandType_ShouldUseCommandType() .Should().Be(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1155,7 +301,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1165,31 +313,43 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1208,7 +368,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1226,23 +388,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); } - [Fact] - public Task QueryFirstOrDefaultAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("TimeSpanValue")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1254,8 +428,10 @@ public Task QueryFirstOrDefaultAsync_EntityType_ColumnDataTypeNotCompatibleWithE $"{typeof(Entity)}.*" ); - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { @@ -1270,7 +446,9 @@ public async Task QueryFirstOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThro }; await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1282,36 +460,54 @@ await Invoking(() => ); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() + QueryFirstOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + Boolean useAsyncApi + ) { var entity = (await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1322,24 +518,36 @@ public async Task .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() + QueryFirstOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entitiesWithDifferentCasingProperties[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - await Invoking(() => this.Connection.QueryFirstOrDefaultAsync( + QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1356,10 +564,16 @@ await Invoking(() => this.Connection.QueryFirstOrDefaultAsync - await Invoking(() => this.Connection.QueryFirstOrDefaultAsync( + QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1376,12 +590,16 @@ await Invoking(() => this.Connection.QueryFirstOrDefaultAsync(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ))! @@ -1389,12 +607,16 @@ public async Task QueryFirstOrDefaultAsync_EntityType_EnumEntityProperty_ShouldC .Should().Be(enumValue); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ))! @@ -1402,10 +624,56 @@ public async Task QueryFirstOrDefaultAsync_EntityType_EnumEntityProperty_ShouldC .Should().Be(enumValue); } - [Fact] - public Task QueryFirstOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync($"SELECT 1 AS {Q("NonExistent")}") + CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) .Should().ThrowAsync() .WithMessage( @@ -1416,41 +684,77 @@ public Task QueryFirstOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParame "(* NonExistent).*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entities[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public Task QueryFirstOrDefaultAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( + Boolean useAsyncApi + ) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1462,68 +766,73 @@ public Task QueryFirstOrDefaultAsync_EntityType_NonNullableEntityProperty_Column ); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstOrDefaultAsync_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - (await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public Task QueryFirstOrDefaultAsync_EntityType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1534,20 +843,26 @@ public Task QueryFirstOrDefaultAsync_EntityType_UnsupportedFieldType_ShouldThrow ); } - [Fact] - public async Task QueryFirstOrDefaultAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstOrDefaultAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -1556,38 +871,50 @@ public async Task QueryFirstOrDefaultAsync_Parameter_ShouldPassParameter() ("Id", entities[0].Id) ); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstOrDefaultAsync_QueryReturnedNoRows_ShouldReturnDefault() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnDefault(Boolean useAsyncApi) { - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(0); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - (await this.Connection.QueryFirstOrDefaultAsync<(Int64, String)>( + (await CallApi<(Int64, String)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(default); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1598,7 +925,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1608,16 +937,22 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(2); var entityIds = entities.ConvertAll(a => a.Id); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT * FROM {Q("Entity")} @@ -1628,14 +963,18 @@ public async Task .Should().Be(entities[0]); } - [Fact] - public async Task QueryFirstOrDefaultAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -1645,23 +984,31 @@ public async Task QueryFirstOrDefaultAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1680,7 +1027,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1698,24 +1047,36 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(character)); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QueryFirstOrDefaultAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => + QueryFirstOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1727,11 +1088,17 @@ public Task $"{typeof(ValueTuple)}.*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QueryFirstOrDefaultAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => + QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 999 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1748,10 +1115,16 @@ public Task $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] - public Task QueryFirstOrDefaultAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'NonExistent' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1768,39 +1141,57 @@ public Task QueryFirstOrDefaultAsync_ValueTupleType_EnumValueTupleField_ColumnCo "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QueryFirstOrDefaultAsync_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum( + Boolean useAsyncApi + ) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public async Task QueryFirstOrDefaultAsync_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum( + Boolean useAsyncApi + ) { var enumValue = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public Task QueryFirstOrDefaultAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1812,26 +1203,38 @@ public Task QueryFirstOrDefaultAsync_ValueTupleType_NonNullableValueTupleField_C ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryFirstOrDefaultAsync_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() + QueryFirstOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryFirstOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QueryFirstOrDefaultAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => + QueryFirstOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryFirstOrDefaultAsync<(Int32, Int32)>( + CallApi<(Int32, Int32)>( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1843,41 +1246,53 @@ public Task "fields in the value tuple type.*" ); - [Fact] - public async Task QueryFirstOrDefaultAsync_ValueTupleType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryFirstOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(ValueTuple.Create(bytes)); } - [Fact] - public async Task QueryFirstOrDefaultAsync_ValueTupleType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await this.Connection.QueryFirstOrDefaultAsync<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); } - [Fact] - public Task QueryFirstOrDefaultAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirstOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1887,4 +1302,43 @@ public Task QueryFirstOrDefaultAsync_ValueTupleType_UnsupportedFieldType_ShouldT "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QueryFirstOrDefaultAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QueryFirstOrDefault( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs index d14d6b6..c4e4101 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; @@ -27,8 +28,12 @@ public abstract class DbConnectionExtensions_QueryFirstOrDefaultTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -36,24 +41,30 @@ public void QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancell this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => - this.Connection.QueryFirstOrDefault( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) ) - .Should().Throw() + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public void QueryFirstOrDefault_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -62,8 +73,12 @@ public void QueryFirstOrDefault_CommandType_ShouldUseCommandType() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -73,7 +88,9 @@ public void QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporary var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -84,15 +101,21 @@ public void QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporary .Should().BeFalse(); } - [Fact] - public void - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -100,12 +123,16 @@ public void EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -113,8 +140,10 @@ public void QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedPara EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirstOrDefault_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -123,7 +152,9 @@ public void QueryFirstOrDefault_Parameter_ShouldPassParameter() ("Id", entities[0].Id) ); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -131,16 +162,25 @@ public void QueryFirstOrDefault_Parameter_ShouldPassParameter() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnNull() => - ((Object?)this.Connection.QueryFirstOrDefault( + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnNull(Boolean useAsyncApi) => + ((Object?)await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public void QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -150,7 +190,9 @@ public void QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTa var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -165,15 +207,21 @@ public void QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTa .Should().BeFalse(); } - [Fact] - public void - QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -182,12 +230,16 @@ public void .Should().Be(entityIds[0]); } - [Fact] - public void QueryFirstOrDefault_ShouldReturnDynamicObjectForFirstRow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_ShouldReturnDynamicObjectForFirstRow(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -195,14 +247,18 @@ public void QueryFirstOrDefault_ShouldReturnDynamicObjectForFirstRow() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirstOrDefault_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { - using (var transaction = this.Connection.BeginTransaction()) + await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - var dynamicObject = this.Connection.QueryFirstOrDefault( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -210,207 +266,54 @@ public void QueryFirstOrDefault_Transaction_ShouldUseTransaction() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - transaction.Rollback(); + await transaction.RollbackAsync(); } - ((Object?)this.Connection.QueryFirstOrDefault( + ((Object?)await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); } - [Fact] - public async Task QueryFirstOrDefaultAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_CommandType_ShouldUseCommandType() + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public async Task - QueryFirstOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_Parameter_ShouldPassParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entities[0].Id) - ); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - - [Fact] - public async Task QueryFirstOrDefaultAsync_QueryReturnedNoRows_ShouldReturnNull() => - ((Object?)await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); - - [Fact] - public async Task QueryFirstOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - ((Object?)dynamicObject) - .Should().NotBeNull(); - - ((Object?)dynamicObject.Id) - .Should().Be(entityIds[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public async Task - QueryFirstOrDefaultAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - ValueConverter.ConvertValueToType((Object)dynamicObject!.Id) - .Should().Be(entityIds[0]); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_ShouldReturnDynamicObjectForFirstRow() - { - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstOrDefaultAsync_Transaction_ShouldUseTransaction() - { - await using (var transaction = await this.Connection.BeginTransactionAsync()) + if (useAsyncApi) { - var entities = this.CreateEntitiesInDb(2, transaction); - - var dynamicObject = await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", + return connection.QueryFirstOrDefaultAsync( + statement, transaction, - cancellationToken: TestContext.Current.CancellationToken + commandTimeout, + commandType, + cancellationToken ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - - await transaction.RollbackAsync(); } - ((Object?)await this.Connection.QueryFirstOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + try + { + return Task.FromResult( + connection.QueryFirstOrDefault( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs index b358d40..17a2a2c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; @@ -27,8 +28,10 @@ public abstract class DbConnectionExtensions_QueryFirstTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -36,24 +39,30 @@ public void QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRe this.DbCommandFactory.DelayNextDbCommand = true; - Invoking(() => - this.Connection.QueryFirst( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) ) - .Should().Throw() + .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public void QueryFirst_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -62,8 +71,12 @@ public void QueryFirst_CommandType_ShouldUseCommandType() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -73,7 +86,9 @@ public void QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfte var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -84,15 +99,21 @@ public void QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfte .Should().BeFalse(); } - [Fact] - public void - QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -100,12 +121,16 @@ public void EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -113,8 +138,10 @@ public void QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirst_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -123,7 +150,9 @@ public void QueryFirst_Parameter_ShouldPassParameter() ("Id", entities[0].Id) ); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -131,20 +160,27 @@ public void QueryFirst_Parameter_ShouldPassParameter() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirst_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QueryFirst( + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().Throw() + .Should().ThrowAsync() .WithMessage( "The SQL statement did not return any rows." ); - [Fact] - public void QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -154,7 +190,9 @@ public void QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterE var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -169,15 +207,21 @@ public void QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterE .Should().BeFalse(); } - [Fact] - public void - QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -186,12 +230,16 @@ public void .Should().Be(entityIds[0]); } - [Fact] - public void QueryFirst_ShouldReturnDynamicObjectForFirstRow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_ShouldReturnDynamicObjectForFirstRow(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -199,14 +247,18 @@ public void QueryFirst_ShouldReturnDynamicObjectForFirstRow() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public void QueryFirst_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { - using (var transaction = this.Connection.BeginTransaction()) + await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - var dynamicObject = this.Connection.QueryFirst( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -214,219 +266,58 @@ public void QueryFirst_Transaction_ShouldUseTransaction() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - transaction.Rollback(); + await transaction.RollbackAsync(); } - Invoking(() => this.Connection.QueryFirst( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().Throw() - .WithMessage( - "The SQL statement did not return any rows." - ); - } - - [Fact] - public async Task QueryFirstAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => - this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public async Task QueryFirstAsync_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstAsync( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = await this.Connection.QueryFirstAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public async Task - QueryFirstAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - var dynamicObject = await this.Connection.QueryFirstAsync( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - [Fact] - public async Task QueryFirstAsync_Parameter_ShouldPassParameter() - { - var entities = this.CreateEntitiesInDb(2); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entities[0].Id) - ); - - var dynamicObject = await this.Connection.QueryFirstAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - } - - - [Fact] - public Task QueryFirstAsync_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) .Should().ThrowAsync() .WithMessage( "The SQL statement did not return any rows." ); - - [Fact] - public async Task QueryFirstAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = await this.Connection.QueryFirstAsync( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - ((Object?)dynamicObject) - .Should().NotBeNull(); - - ((Object?)dynamicObject.Id) - .Should().Be(entityIds[0]); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public async Task - QueryFirstAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - var dynamicObject = await this.Connection.QueryFirstAsync( - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - ValueConverter.ConvertValueToType((Object)dynamicObject.Id) - .Should().Be(entityIds[0]); - } - - [Fact] - public async Task QueryFirstAsync_ShouldReturnDynamicObjectForFirstRow() - { - var entities = this.CreateEntitiesInDb(2); - - var dynamicObject = await this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); } - [Fact] - public async Task QueryFirstAsync_Transaction_ShouldUseTransaction() + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) { - await using (var transaction = await this.Connection.BeginTransactionAsync()) + if (useAsyncApi) { - var entities = this.CreateEntitiesInDb(2, transaction); - - var dynamicObject = await this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")}", + return connection.QueryFirstAsync( + statement, transaction, - cancellationToken: TestContext.Current.CancellationToken + commandTimeout, + commandType, + cancellationToken ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entities[0]); - - await transaction.RollbackAsync(); } - await Invoking(() => this.Connection.QueryFirstAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken + try + { + return Task.FromResult( + connection.QueryFirst( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken ) - ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index f62cc72..e6191c2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -1,968 +1,46 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - -public sealed class - DbConnectionExtensions_QueryOfTTests_MySql : - DbConnectionExtensions_QueryOfTTests; - -public sealed class - DbConnectionExtensions_QueryOfTTests_Oracle : - DbConnectionExtensions_QueryOfTTests; - -public sealed class - DbConnectionExtensions_QueryOfTTests_PostgreSql : - DbConnectionExtensions_QueryOfTTests; - -public sealed class - DbConnectionExtensions_QueryOfTTests_Sqlite : - DbConnectionExtensions_QueryOfTTests; - -public sealed class - DbConnectionExtensions_QueryOfTTests_SqlServer : - DbConnectionExtensions_QueryOfTTests; - -public abstract class - DbConnectionExtensions_QueryOfTTests : IntegrationTestsBase - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.Query( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - Invoking(() => - this.Connection.Query( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - [Fact] - public void Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.Query( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([character]); - } - - [Fact] - public void Query_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.Query( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void Query_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.Query( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" - ); - - [Fact] - public void Query_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.Query( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" - ); - - [Fact] - public void Query_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([enumValue]); - } - - [Fact] - public void Query_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([enumValue]); - } - - [Fact] - public void Query_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => - Invoking(() => - this.Connection.Query( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void Query_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - this.Connection.Query( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new Int32?[] { null }); - - [Fact] - public void Query_BuiltInType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(e => e.DateTimeOffsetValue)); - } - - [Fact] - public void Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ).ToList() - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void Query_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var enumerator = this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetEnumerator(); - - enumerator.MoveNext() - .Should().BeTrue(); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - enumerator.MoveNext() - .Should().BeTrue(); - - enumerator.MoveNext() - .Should().BeFalse(); - - enumerator.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(); - - this.Connection.Query( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.Query( - $"SELECT '' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.Query( - $"SELECT 'ab' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.Query( - $"SELECT '{character}' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([new EntityWithCharProperty { Char = character }]); - } - - [Fact] - public void Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => - Invoking(() => - this.Connection.Query( - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" - ); - - [Fact] - public void Query_EntityType_ColumnHasNoName_ShouldThrow() - { - InterpolatedSqlStatement statement = this.TestDatabaseProvider switch - { - SqlServerTestDatabaseProvider => - "SELECT 1", - - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", - - _ => - "SELECT 1 AS ''" - }; - - Invoking(() => - this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" - ); - } - - [Fact] - public void Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() - { - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() - { - var entities = Invoking(() => - this.Connection.Query( - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().NotThrow().Subject; - - entities - .Should().BeEquivalentTo([new EntityWithNonNullableProperty { Id = 1, Value = 2 }]); - } - - [Fact] - public void Query_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties); - } - - [Fact] - public void Query_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => this.Connection.Query( - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void Query_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => this.Connection.Query( - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void Query_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query( - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Single().Enum - .Should().Be(enumValue); - } - - [Fact] - public void Query_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query( - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Single().Enum - .Should().Be(enumValue); - } - - [Fact] - public void Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => - Invoking(() => - this.Connection.Query($"SELECT 1 AS {Q("NonExistent")}") - .ToList() - ) - .Should().Throw() - .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. The type " + - "either needs to have a parameterless constructor or a constructor whose parameters match the " + - "columns returned by the SQL statement, e.g. a constructor that has the following " + - $"signature:{Environment.NewLine}" + - "(* NonExistent).*" - ); - - [Fact] - public void - Query_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void - Query_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() - { - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a " + - $"NULL value, but the corresponding property of the type {typeof(EntityWithNonNullableProperty)} " + - "is non-nullable.*" - ); - } - - [Fact] - public void Query_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([new EntityWithNullableProperty { Id = 1, Value = null }]); - } - - [Fact] - public void Query_EntityType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.Query( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([new EntityWithBinaryProperty { BinaryData = bytes }]); - } - - [Fact] - public void Query_EntityType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entities = this.CreateEntitiesInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - } - - [Fact] - public void Query_EntityType_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entitiesWithColumnAttributes); - } - - [Fact] - public void Query_EntityType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); - - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); - - Invoking(() => - this.Connection.Query( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } - - [Fact] - public void Query_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([entity]); - } - - [Fact] - public void Query_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.Query(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().BeEquivalentTo([entity]); - } - - [Fact] - public void Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS Id FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var enumerator = this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetEnumerator(); - - enumerator.MoveNext() - .Should().BeTrue(); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - enumerator.MoveNext() - .Should().BeTrue(); - - enumerator.MoveNext() - .Should().BeFalse(); - - enumerator.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = this.CreateEntitiesInDb(5); - var entityIds = entities.Take(2).Select(a => a.Id).ToList(); - - this.Connection.Query( - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Take(2)); - } - - [Fact] - public void Query_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entities = this.CreateEntitiesInDb(null, transaction); - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); - - transaction.Rollback(); - } - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEmpty(); - } - - [Fact] - public void Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.Query>( - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.Query>( - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.Query>( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([ValueTuple.Create(character)]); - } - - [Fact] - public void Query_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => - Invoking(() => - this.Connection.Query>( - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" - ); - - [Fact] - public void Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.Query>( - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.Query>( - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void Query_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query>( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); - } - - [Fact] - public void Query_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.Query>( - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); - } - - [Fact] - public void Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.Query>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" - ); - } - - [Fact] - public void Query_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.Query>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([new ValueTuple(null)]); - } - - [Fact] - public void Query_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => - Invoking(() => - this.Connection.Query<(Int32, Int32)>( - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. Make sure that the SQL statement returns the same number of columns as the number of " + - "fields in the value tuple type.*" - ); - - [Fact] - public void Query_ValueTupleType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.Query>( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo([ValueTuple.Create(bytes)]); - } +using System.Data.Common; - [Fact] - public void Query_ValueTupleType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); +namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - var entities = this.CreateEntitiesInDb(); +public sealed class + DbConnectionExtensions_QueryOfTTests_MySql : + DbConnectionExtensions_QueryOfTTests; - this.Connection.Query<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(e => (e.Id, e.DateTimeOffsetValue))); - } +public sealed class + DbConnectionExtensions_QueryOfTTests_Oracle : + DbConnectionExtensions_QueryOfTTests; - [Fact] - public void Query_ValueTupleType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); +public sealed class + DbConnectionExtensions_QueryOfTTests_PostgreSql : + DbConnectionExtensions_QueryOfTTests; - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); +public sealed class + DbConnectionExtensions_QueryOfTTests_Sqlite : + DbConnectionExtensions_QueryOfTTests; - Invoking(() => - this.Connection.Query>( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList() - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } +public sealed class + DbConnectionExtensions_QueryOfTTests_SqlServer : + DbConnectionExtensions_QueryOfTTests; - [Fact] - public async Task QueryAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() +public abstract class + DbConnectionExtensions_QueryOfTTests : IntegrationTestsBase + where TTestDatabaseProvider : ITestDatabaseProvider, new() +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -980,7 +58,9 @@ public async Task QueryAsync_BuiltInType_CharTargetType_ColumnContainsStringWith } (await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -997,22 +77,32 @@ public async Task QueryAsync_BuiltInType_CharTargetType_ColumnContainsStringWith ); } - [Fact] - public async Task QueryAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([character]); } - [Fact] - public Task QueryAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1023,10 +113,14 @@ public Task QueryAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_Shou $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public Task QueryAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1037,10 +131,14 @@ public Task QueryAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_S $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); - [Fact] - public Task QueryAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1052,34 +150,46 @@ public Task QueryAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_Sh "exception for details.*" ); - [Fact] - public async Task QueryAsync_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([enumValue]); } - [Fact] - public async Task QueryAsync_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([enumValue]); } - [Fact] - public Task QueryAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1090,30 +200,41 @@ public Task QueryAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_Shou $"to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public async Task QueryAsync_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - (await this.Connection.QueryAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task + Query_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull(Boolean useAsyncApi) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask()) .Should().BeEquivalentTo(new Int32?[] { null }); - [Fact] - public async Task QueryAsync_BuiltInType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities.Select(e => e.DateTimeOffsetValue)); } - [Fact] - public async Task QueryAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -1122,7 +243,9 @@ public async Task QueryAsync_CancellationToken_ShouldCancelOperationIfCancellati this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ).ToListAsync(cancellationToken).AsTask() @@ -1131,14 +254,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QueryAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -1146,9 +273,11 @@ public async Task QueryAsync_CommandType_ShouldUseCommandType() .Should().BeEquivalentTo(entities); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() + Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1158,7 +287,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - var asyncEnumerator = this.Connection.QueryAsync( + var asyncEnumerator = CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).GetAsyncEnumerator(); @@ -1184,31 +315,39 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() + Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1227,7 +366,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1245,23 +386,33 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([new EntityWithCharProperty { Char = character }]); } - [Fact] - public Task QueryAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("TimeSpanValue")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1273,8 +424,10 @@ public Task QueryAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyT $"{typeof(Entity)}.*" ); - [Fact] - public async Task QueryAsync_EntityType_ColumnHasNoName_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { @@ -1289,7 +442,9 @@ public async Task QueryAsync_EntityType_ColumnHasNoName_ShouldThrow() }; await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1301,35 +456,49 @@ await Invoking(() => ); } - [Fact] - public async Task QueryAsync_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] - public async Task QueryAsync_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] - public async Task QueryAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + Boolean useAsyncApi + ) { var entities = (await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1340,22 +509,34 @@ public async Task QueryAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForC .Should().BeEquivalentTo([new EntityWithNonNullableProperty { Id = 1, Value = 2 }]); } - [Fact] - public async Task QueryAsync_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties); } - [Fact] - public async Task QueryAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - await Invoking(() => this.Connection.QueryAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1372,9 +553,15 @@ await Invoking(() => this.Connection.QueryAsync( $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] - public async Task QueryAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - await Invoking(() => this.Connection.QueryAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1391,12 +578,16 @@ await Invoking(() => this.Connection.QueryAsync( "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QueryAsync_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ).FirstAsync()) @@ -1404,12 +595,16 @@ public async Task QueryAsync_EntityType_EnumEntityProperty_ShouldConvertIntegerT .Should().Be(enumValue); } - [Fact] - public async Task QueryAsync_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ).FirstAsync()) @@ -1417,10 +612,70 @@ public async Task QueryAsync_EntityType_EnumEntityProperty_ShouldConvertStringTo .Should().Be(enumValue); } - [Fact] - public Task QueryAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken)) + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entities = this.CreateEntitiesInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken)) + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken)) + .Should().BeEquivalentTo(entities); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync($"SELECT 1 AS {Q("NonExistent")}") + CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") .ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().ThrowAsync() @@ -1432,41 +687,57 @@ public Task QueryAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstru "(* NonExistent).*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + Query_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + Query_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] - public Task QueryAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1478,68 +749,71 @@ public Task QueryAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_S ); } - [Fact] - public async Task QueryAsync_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull(Boolean useAsyncApi) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([new EntityWithNullableProperty { Id = 1, Value = null }]); } - [Fact] - public async Task QueryAsync_EntityType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([new EntityWithBinaryProperty { BinaryData = bytes }]); } - [Fact] - public async Task QueryAsync_EntityType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities); } - [Fact] - public async Task QueryAsync_EntityType_ShouldUseConfiguredColumnNames() - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entitiesWithColumnAttributes); - } - - [Fact] - public Task QueryAsync_EntityType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1550,20 +824,26 @@ public Task QueryAsync_EntityType_UnsupportedFieldType_ShouldThrow() ); } - [Fact] - public async Task QueryAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([entity]); } - [Fact] - public async Task QueryAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -1572,16 +852,20 @@ public async Task QueryAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([entity]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() + Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1592,7 +876,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - var asyncEnumerator = this.Connection.QueryAsync( + var asyncEnumerator = CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).GetAsyncEnumerator(); @@ -1618,16 +904,20 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(5); var entityIds = entities.Take(2).Select(a => a.Id).ToList(); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT * FROM {Q("Entity")} @@ -1638,14 +928,18 @@ public async Task .Should().BeEquivalentTo(entities.Take(2)); } - [Fact] - public async Task QueryAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(null, transaction); - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -1655,22 +949,30 @@ public async Task QueryAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEmpty(); } - [Fact] - public async Task QueryAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1689,7 +991,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1707,23 +1011,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QueryAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QueryAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([ValueTuple.Create(character)]); } - [Fact] - public Task QueryAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1735,10 +1051,16 @@ public Task QueryAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleF $"{typeof(ValueTuple)}.*" ); - [Fact] - public Task QueryAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 999 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1755,10 +1077,14 @@ public Task QueryAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidI $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] - public Task QueryAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'NonExistent' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1775,39 +1101,51 @@ public Task QueryAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidS "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QueryAsync_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); } - [Fact] - public async Task QueryAsync_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QueryAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); } - [Fact] - public Task QueryAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1819,24 +1157,36 @@ public Task QueryAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsN ); } - [Fact] - public async Task QueryAsync_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QueryAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([new ValueTuple(null)]); } - [Fact] - public Task QueryAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QueryAsync<(Int32, Int32)>( + CallApi<(Int32, Int32)>( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1848,41 +1198,53 @@ public Task QueryAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTu "fields in the value tuple type.*" ); - [Fact] - public async Task QueryAsync_ValueTupleType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QueryAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo([ValueTuple.Create(bytes)]); } - [Fact] - public async Task QueryAsync_ValueTupleType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await this.Connection.QueryAsync<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEquivalentTo(entities.Select(e => (e.Id, e.DateTimeOffsetValue))); } - [Fact] - public Task QueryAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QueryAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -1892,4 +1254,34 @@ public Task QueryAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } + + private static IAsyncEnumerable CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QueryAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + return connection.Query( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ).ToAsyncEnumerable(); + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 92dc296..e776306 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -1,964 +1,46 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_MySql : - DbConnectionExtensions_QuerySingleOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_Oracle : - DbConnectionExtensions_QuerySingleOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_PostgreSql : - DbConnectionExtensions_QuerySingleOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_Sqlite : - DbConnectionExtensions_QuerySingleOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_SqlServer : - DbConnectionExtensions_QuerySingleOfTTests; - -public abstract class - DbConnectionExtensions_QuerySingleOfTTests : IntegrationTestsBase - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingle( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingle( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - [Fact] - public void QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(character); - } - - [Fact] - public void QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" - ); - - [Fact] - public void QuerySingle_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingle_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingle_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - this.Connection.QuerySingle( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - [Fact] - public void QuerySingle_BuiltInType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.DateTimeOffsetValue); - } - - [Fact] - public void QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QuerySingle_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - "GetFirstEntity", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable([entity])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT * FROM {TemporaryTable([entity])}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingle( - $"SELECT '' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingle( - $"SELECT 'ab' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT '{character}' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); - } - - [Fact] - public void QuerySingle_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle( - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" - ); - - [Fact] - public void QuerySingle_EntityType_ColumnHasNoName_ShouldThrow() - { - InterpolatedSqlStatement statement = this.TestDatabaseProvider switch - { - SqlServerTestDatabaseProvider => - "SELECT 1", - - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", - - _ => - "SELECT 1 AS ''" - }; - - Invoking(() => - this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" - ); - } - - [Fact] - public void QuerySingle_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() - { - var entity = Invoking(() => - this.Connection.QuerySingle( - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().NotThrow().Subject; - - entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); - } - - [Fact] - public void QuerySingle_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() - { - var entity = this.CreateEntityInDb(); - var entityWithDifferentCasingProperties = Generate.MapTo(entity); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithDifferentCasingProperties); - } - - [Fact] - public void QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => this.Connection.QuerySingle( - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => this.Connection.QuerySingle( - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QuerySingle_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingle_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingle_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle($"SELECT 1 AS {Q("NonExistent")}") - ) - .Should().Throw() - .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. The type " + - "either needs to have a parameterless constructor or a constructor whose parameters match the " + - "columns returned by the SQL statement, e.g. a constructor that has the following " + - $"signature:{Environment.NewLine}" + - "(* NonExistent).*" - ); - - [Fact] - public void - QuerySingle_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void - QuerySingle_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a " + - $"NULL value, but the corresponding property of the type {typeof(EntityWithNonNullableProperty)} " + - "is non-nullable.*" - ); - } - - [Fact] - public void QuerySingle_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); - } - - [Fact] - public void QuerySingle_EntityType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QuerySingle( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - - [Fact] - public void QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public void QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); - - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); - - Invoking(() => - this.Connection.QuerySingle( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } - - [Fact] - public void QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.QuerySingle(statement, cancellationToken: TestContext.Current.CancellationToken) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow() - { - this.CreateEntitiesInDb(2); - - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did return more than one row." - ); - } - - [Fact] - public void QuerySingle_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did not return any rows." - ); - - [Fact] - public void QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS Id FROM {TemporaryTable([entityId])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityId); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = this.CreateEntityInDb(); - var entityId = entity.Id; - - this.Connection.QuerySingle( - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingle_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); - - this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - - transaction.Rollback(); - } - - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw(); - } - - [Fact] - public void QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingle>( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(character)); - } - - [Fact] - public void QuerySingle_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" - ); - - [Fact] - public void QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle>( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingle>( - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QuerySingle_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); +using System.Data.Common; - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" - ); - } - - [Fact] - public void QuerySingle_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QuerySingle>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(new(null)); - } - - [Fact] - public void QuerySingle_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingle<(Int32, Int32)>( - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. Make sure that the SQL statement returns the same number of columns as the number of " + - "fields in the value tuple type.*" - ); - - [Fact] - public void QuerySingle_ValueTupleType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QuerySingle>( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); - } - - [Fact] - public void QuerySingle_ValueTupleType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); +namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - var entity = this.CreateEntityInDb(); +public sealed class + DbConnectionExtensions_QuerySingleOfTTests_MySql : + DbConnectionExtensions_QuerySingleOfTTests; - this.Connection.QuerySingle<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); - } +public sealed class + DbConnectionExtensions_QuerySingleOfTTests_Oracle : + DbConnectionExtensions_QuerySingleOfTTests; - [Fact] - public void QuerySingle_ValueTupleType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); +public sealed class + DbConnectionExtensions_QuerySingleOfTTests_PostgreSql : + DbConnectionExtensions_QuerySingleOfTTests; - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); +public sealed class + DbConnectionExtensions_QuerySingleOfTTests_Sqlite : + DbConnectionExtensions_QuerySingleOfTTests; - Invoking(() => - this.Connection.QuerySingle>( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } +public sealed class + DbConnectionExtensions_QuerySingleOfTTests_SqlServer : + DbConnectionExtensions_QuerySingleOfTTests; - [Fact] - public async Task QuerySingleAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() +public abstract class + DbConnectionExtensions_QuerySingleOfTTests : IntegrationTestsBase + where TTestDatabaseProvider : ITestDatabaseProvider, new() +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) @@ -976,7 +58,9 @@ public async Task QuerySingleAsync_BuiltInType_CharTargetType_ColumnContainsStri } (await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -993,23 +77,33 @@ public async Task QuerySingleAsync_BuiltInType_CharTargetType_ColumnContainsStri ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(character); } - [Fact] - public Task QuerySingleAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1020,10 +114,14 @@ public Task QuerySingleAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetTyp $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public Task QuerySingleAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ) @@ -1034,10 +132,14 @@ public Task QuerySingleAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInt $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); - [Fact] - public Task QuerySingleAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1049,34 +151,46 @@ public Task QuerySingleAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidStr "exception for details.*" ); - [Fact] - public async Task QuerySingleAsync_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public async Task QuerySingleAsync_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task QuerySingleAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) @@ -1087,30 +201,42 @@ public Task QuerySingleAsync_BuiltInType_NonNullableTargetType_ColumnContainsNul $"to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public async Task QuerySingleAsync_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - (await this.Connection.QuerySingleAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task QuerySingleAsync_BuiltInType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entity.DateTimeOffsetValue); } - [Fact] - public async Task QuerySingleAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -1119,7 +245,9 @@ public async Task QuerySingleAsync_CancellationToken_ShouldCancelOperationIfCanc this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -1128,14 +256,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QuerySingleAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -1143,9 +275,11 @@ public async Task QuerySingleAsync_CommandType_ShouldUseCommandType() .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1155,7 +289,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1165,31 +301,41 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1208,7 +354,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1226,23 +374,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); } - [Fact] - public Task QuerySingleAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("TimeSpanValue")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1254,8 +414,10 @@ public Task QuerySingleAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPro $"{typeof(Entity)}.*" ); - [Fact] - public async Task QuerySingleAsync_EntityType_ColumnHasNoName_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { @@ -1270,7 +432,9 @@ public async Task QuerySingleAsync_EntityType_ColumnHasNoName_ShouldThrow() }; await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1282,35 +446,51 @@ await Invoking(() => ); } - [Fact] - public async Task QuerySingleAsync_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + Boolean useAsyncApi + ) { var entity = (await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1321,23 +501,35 @@ public async Task QuerySingleAsync_EntityType_EntityTypeHasNoCorrespondingProper .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() + QuerySingle_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); var entityWithDifferentCasingProperties = Generate.MapTo(entity); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entityWithDifferentCasingProperties); } - [Fact] - public async Task QuerySingleAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - await Invoking(() => this.Connection.QuerySingleAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1354,9 +546,15 @@ await Invoking(() => this.Connection.QuerySingleAsync - await Invoking(() => this.Connection.QuerySingleAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1373,12 +571,16 @@ await Invoking(() => this.Connection.QuerySingleAsync(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken )) @@ -1386,12 +588,16 @@ public async Task QuerySingleAsync_EntityType_EnumEntityProperty_ShouldConvertIn .Should().Be(enumValue); } - [Fact] - public async Task QuerySingleAsync_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken )) @@ -1399,10 +605,56 @@ public async Task QuerySingleAsync_EntityType_EnumEntityProperty_ShouldConvertSt .Should().Be(enumValue); } - [Fact] - public Task QuerySingleAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync($"SELECT 1 AS {Q("NonExistent")}") + CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) .Should().ThrowAsync() .WithMessage( @@ -1413,41 +665,73 @@ public Task QuerySingleAsync_EntityType_NoCompatibleConstructor_NoParameterlessC "(* NonExistent).*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + QuerySingle_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + QuerySingle_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public Task QuerySingleAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1459,68 +743,73 @@ public Task QuerySingleAsync_EntityType_NonNullableEntityProperty_ColumnContains ); } - [Fact] - public async Task QuerySingleAsync_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Fact] - public async Task QuerySingleAsync_EntityType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); } - [Fact] - public async Task QuerySingleAsync_EntityType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public Task QuerySingleAsync_EntityType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1531,20 +820,26 @@ public Task QuerySingleAsync_EntityType_UnsupportedFieldType_ShouldThrow() ); } - [Fact] - public async Task QuerySingleAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -1553,19 +848,25 @@ public async Task QuerySingleAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_QueryReturnedMoreThanOneRow_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => this.Connection.QuerySingleAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1576,9 +877,13 @@ await Invoking(() => this.Connection.QuerySingleAsync( ); } - [Fact] - public Task QuerySingleAsync_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QuerySingleAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1588,9 +893,11 @@ public Task QuerySingleAsync_QueryReturnedNoRows_ShouldThrow() => "The SQL statement did not return any rows." ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1601,7 +908,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1611,16 +920,22 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = this.CreateEntityInDb(); var entityId = entity.Id; - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT * FROM {Q("Entity")} @@ -1631,14 +946,18 @@ public async Task .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -1648,7 +967,9 @@ public async Task QuerySingleAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - await Invoking(() => this.Connection.QuerySingleAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1656,16 +977,22 @@ await Invoking(() => this.Connection.QuerySingleAsync( .Should().ThrowAsync(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1684,7 +1011,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1702,23 +1031,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(character)); } - [Fact] - public Task QuerySingleAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1730,10 +1071,16 @@ public Task QuerySingleAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValue $"{typeof(ValueTuple)}.*" ); - [Fact] - public Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 999 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1750,10 +1097,16 @@ public Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ColumnContainsIn $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] - public Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'NonExistent' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1770,39 +1123,53 @@ public Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ColumnContainsIn "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public async Task QuerySingleAsync_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public Task QuerySingleAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1814,24 +1181,36 @@ public Task QuerySingleAsync_ValueTupleType_NonNullableValueTupleField_ColumnCon ); } - [Fact] - public async Task QuerySingleAsync_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QuerySingleAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); } - [Fact] - public Task QuerySingleAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleAsync<(Int32, Int32)>( + CallApi<(Int32, Int32)>( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1843,41 +1222,53 @@ public Task QuerySingleAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfV "fields in the value tuple type.*" ); - [Fact] - public async Task QuerySingleAsync_ValueTupleType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QuerySingleAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(ValueTuple.Create(bytes)); } - [Fact] - public async Task QuerySingleAsync_ValueTupleType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleAsync<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); } - [Fact] - public Task QuerySingleAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1887,4 +1278,43 @@ public Task QuerySingleAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QuerySingleAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QuerySingle( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index b601707..15953ad 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -1,983 +1,48 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_MySql : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Oracle : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_PostgreSql : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Sqlite : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_SqlServer : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - -public abstract class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests : IntegrationTestsBase< - TTestDatabaseProvider> - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Char)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." - ); - } - - [Fact] - public void - QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(character); - } - - [Fact] - public void QuerySingleOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" - ); - - [Fact] - public void QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingleOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. See inner exception for details.*" - ); - - [Fact] - public void QuerySingleOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - this.Connection.QuerySingleOrDefault( - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - [Fact] - public void QuerySingleOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entity.DateTimeOffsetValue); - } - - [Fact] - public void QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QuerySingleOrDefault_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - "GetFirstEntity", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable([entity])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {TemporaryTable([entity])}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT '' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT 'ab' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT '{character}' AS {Q("Char")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" - ); - - [Fact] - public void QuerySingleOrDefault_EntityType_ColumnHasNoName_ShouldThrow() - { - InterpolatedSqlStatement statement = this.TestDatabaseProvider switch - { - SqlServerTestDatabaseProvider => - "SELECT 1", - - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", - - _ => - "SELECT 1 AS ''" - }; - - Invoking(() => - this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" - ); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() - { - var entity = Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().NotThrow().Subject; - - entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() - { - var entity = this.CreateEntityInDb(); - var entityWithDifferentCasingProperties = Generate.MapTo(entity); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithDifferentCasingProperties); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => this.Connection.QuerySingleOrDefault( - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => this.Connection.QuerySingleOrDefault( - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" - ) - .WithInnerException(typeof(InvalidCastException)) - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - )! - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - )! - .Enum - .Should().Be(enumValue); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault($"SELECT 1 AS {Q("NonExistent")}") - ) - .Should().Throw() - .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. The type " + - "either needs to have a parameterless constructor or a constructor whose parameters match the " + - "columns returned by the SQL statement, e.g. a constructor that has the following " + - $"signature:{Environment.NewLine}" + - "(* NonExistent).*" - ); - - [Fact] - public void - QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void - QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a " + - $"NULL value, but the corresponding property of the type {typeof(EntityWithNonNullableProperty)} " + - "is non-nullable.*" - ); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QuerySingleOrDefault( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); - - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public void QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); - - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } - - [Fact] - public void QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow() - { - this.CreateEntitiesInDb(2); - - Invoking(() => this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did return more than one row." - ); - } - - [Fact] - public void QuerySingleOrDefault_QueryReturnedNoRows_ShouldReturnDefault() - { - this.Connection.QuerySingleOrDefault( - $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(0); - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - - this.Connection.QuerySingleOrDefault<(Int64, String)>( - $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(default); - } - - [Fact] - public void QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS Id FROM {TemporaryTable([entityId])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(entityId); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = this.CreateEntityInDb(); - var entityId = entity.Id; - - this.Connection.QuerySingleOrDefault( - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) - """, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - } - - [Fact] - public void QuerySingleOrDefault_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); +using System.Data.Common; - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity); - - transaction.Rollback(); - } - - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeNull(); - } - - [Fact] - public void - QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() - { - if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) - { - // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." - ); - } - - [Fact] - public void - QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var character = Generate.Single(); - - this.Connection.QuerySingleOrDefault>( - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(character)); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" - ); - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" - ); - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault>( - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() - { - var enumValue = Generate.Single(); - - this.Connection.QuerySingleOrDefault>( - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(ValueTuple.Create(enumValue)); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The column 'Value' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" - ); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() - { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" - ); - - this.Connection.QuerySingleOrDefault>( - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be(new(null)); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => - Invoking(() => - this.Connection.QuerySingleOrDefault<(Int32, Int32)>( - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. Make sure that the SQL statement returns the same number of columns as the number of " + - "fields in the value tuple type.*" - ); - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_ShouldMaterializeBinaryData() - { - var bytes = Generate.Single(); - - this.Connection.QuerySingleOrDefault>( - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); - } - - [Fact] - public void QuerySingleOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); +namespace RentADeveloper.DbConnectionPlus.IntegrationTests; - var entity = this.CreateEntityInDb(); +public sealed class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests_MySql : + DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - this.Connection.QuerySingleOrDefault<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); - } +public sealed class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Oracle : + DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - [Fact] - public void QuerySingleOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow() - { - Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); +public sealed class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests_PostgreSql : + DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); +public sealed class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Sqlite : + DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - Invoking(() => - this.Connection.QuerySingleOrDefault>( - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" - ); - } +public sealed class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests_SqlServer : + DbConnectionExtensions_QuerySingleOrDefaultOfTTests; - [Fact] +public abstract class + DbConnectionExtensions_QuerySingleOrDefaultOfTTests : IntegrationTestsBase< + TTestDatabaseProvider> + where TTestDatabaseProvider : ITestDatabaseProvider, new() +{ + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. (await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) @@ -995,7 +60,9 @@ public async Task } (await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1012,23 +79,35 @@ public async Task ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(character); } - [Fact] - public Task QuerySingleOrDefaultAsync_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1039,10 +118,16 @@ public Task QuerySingleOrDefaultAsync_BuiltInType_ColumnValueCannotBeConvertedTo $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 999", cancellationToken: TestContext.Current.CancellationToken ) @@ -1053,10 +138,16 @@ public Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsI $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); - [Fact] - public Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'NonExistent'", cancellationToken: TestContext.Current.CancellationToken ) @@ -1068,34 +159,48 @@ public Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ColumnContainsI "exception for details.*" ); - [Fact] - public async Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public async Task QuerySingleOrDefaultAsync_BuiltInType_EnumTargetType_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(enumValue); } - [Fact] - public Task QuerySingleOrDefaultAsync_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) @@ -1106,30 +211,44 @@ public Task QuerySingleOrDefaultAsync_BuiltInType_NonNullableTargetType_ColumnCo $"to the type {typeof(Int32)}. See inner exception for details.*" ); - [Fact] - public async Task QuerySingleOrDefaultAsync_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull() => - (await this.Connection.QuerySingleOrDefaultAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) => + (await CallApi( + useAsyncApi, + this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task QuerySingleOrDefaultAsync_BuiltInType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(entity.DateTimeOffsetValue); } - [Fact] - public async Task QuerySingleOrDefaultAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -1138,7 +257,9 @@ public async Task QuerySingleOrDefaultAsync_CancellationToken_ShouldCancelOperat this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -1147,14 +268,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QuerySingleOrDefaultAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -1162,9 +287,11 @@ public async Task QuerySingleOrDefaultAsync_CommandType_ShouldUseCommandType() .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1174,7 +301,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1184,31 +313,43 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1227,7 +368,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1245,23 +388,35 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT '{character}' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); } - [Fact] - public Task QuerySingleOrDefaultAsync_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("TimeSpanValue")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1273,8 +428,10 @@ public Task QuerySingleOrDefaultAsync_EntityType_ColumnDataTypeNotCompatibleWith $"{typeof(Entity)}.*" ); - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { @@ -1289,7 +446,9 @@ public async Task QuerySingleOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThr }; await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1301,36 +460,54 @@ await Invoking(() => ); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn() + QuerySingleOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + Boolean useAsyncApi + ) { var entity = (await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1341,24 +518,36 @@ public async Task .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities() + QuerySingleOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); var entityWithDifferentCasingProperties = Generate.MapTo(entity); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entityWithDifferentCasingProperties); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow() => - await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( + QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1375,10 +564,16 @@ await Invoking(() => this.Connection.QuerySingleOrDefaultAsync - await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( + QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1395,12 +590,16 @@ await Invoking(() => this.Connection.QuerySingleOrDefaultAsync(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ))! @@ -1408,12 +607,16 @@ public async Task QuerySingleOrDefaultAsync_EntityType_EnumEntityProperty_Should .Should().Be(enumValue); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_EnumEntityProperty_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ))! @@ -1421,10 +624,58 @@ public async Task QuerySingleOrDefaultAsync_EntityType_EnumEntityProperty_Should .Should().Be(enumValue); } - [Fact] - public Task QuerySingleOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 1 AS {Q("NonExistent")}" ) ) @@ -1437,41 +688,77 @@ public Task QuerySingleOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParam "(* NonExistent).*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public Task QuerySingleOrDefaultAsync_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( + Boolean useAsyncApi + ) + { + var entity = this.CreateEntityInDb(); + + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1483,68 +770,73 @@ public Task QuerySingleOrDefaultAsync_EntityType_NonNullableEntityProperty_Colum ); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_EntityType_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - (await this.Connection.QuerySingleOrDefaultAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); - } - - [Fact] - public Task QuerySingleOrDefaultAsync_EntityType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1555,20 +847,26 @@ public Task QuerySingleOrDefaultAsync_EntityType_UnsupportedFieldType_ShouldThro ); } - [Fact] - public async Task QuerySingleOrDefaultAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -1577,19 +875,25 @@ public async Task QuerySingleOrDefaultAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_QueryReturnedMoreThanOneRow_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1600,31 +904,41 @@ await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( ); } - [Fact] - public async Task QuerySingleOrDefaultAsync_QueryReturnedNoRows_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) { - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(0); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - (await this.Connection.QuerySingleOrDefaultAsync<(Int64, String)>( + (await CallApi<(Int64, String)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(default); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1635,7 +949,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1645,16 +961,22 @@ public async Task .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = this.CreateEntityInDb(); var entityId = entity.Id; - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $""" SELECT * FROM {Q("Entity")} @@ -1665,14 +987,18 @@ public async Task .Should().BeEquivalentTo(entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -1682,23 +1008,31 @@ public async Task QuerySingleOrDefaultAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow() + QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + Boolean useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1717,7 +1051,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1735,24 +1071,36 @@ await Invoking(() => ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter() + QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + Boolean useAsyncApi + ) { var character = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(character)); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QuerySingleOrDefaultAsync_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() => + QuerySingleOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 123 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1764,11 +1112,17 @@ public Task $"{typeof(ValueTuple)}.*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QuerySingleOrDefaultAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow() => + QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 999 AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1785,11 +1139,17 @@ public Task $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QuerySingleOrDefaultAsync_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow() => + QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'NonExistent' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1806,39 +1166,57 @@ public Task "That string does not match any of the names of the enum's members.*" ); - [Fact] - public async Task QuerySingleOrDefaultAsync_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum( + Boolean useAsyncApi + ) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {(Int32)enumValue}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public async Task QuerySingleOrDefaultAsync_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum( + Boolean useAsyncApi + ) { var enumValue = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(ValueTuple.Create(enumValue)); } - [Fact] - public Task QuerySingleOrDefaultAsync_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( + Boolean useAsyncApi + ) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); return Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1850,26 +1228,38 @@ public Task QuerySingleOrDefaultAsync_ValueTupleType_NonNullableValueTupleField_ ); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull() + QuerySingleOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + Boolean useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" ); - (await this.Connection.QuerySingleOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public Task - QuerySingleOrDefaultAsync_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow() => + QuerySingleOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + Boolean useAsyncApi + ) => Invoking(() => - this.Connection.QuerySingleOrDefaultAsync<(Int32, Int32)>( + CallApi<(Int32, Int32)>( + useAsyncApi, + this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) @@ -1881,41 +1271,53 @@ public Task "fields in the value tuple type.*" ); - [Fact] - public async Task QuerySingleOrDefaultAsync_ValueTupleType_ShouldMaterializeBinaryData() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) { var bytes = Generate.Single(); - (await this.Connection.QuerySingleOrDefaultAsync>( + (await CallApi>( + useAsyncApi, + this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo(ValueTuple.Create(bytes)); } - [Fact] - public async Task QuerySingleOrDefaultAsync_ValueTupleType_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await this.Connection.QuerySingleOrDefaultAsync<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); } - [Fact] - public Task QuerySingleOrDefaultAsync_ValueTupleType_UnsupportedFieldType_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingleOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1925,4 +1327,43 @@ public Task QuerySingleOrDefaultAsync_ValueTupleType_UnsupportedFieldType_Should "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QuerySingleOrDefaultAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QuerySingleOrDefault( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs index 32f043d..5616799 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; @@ -28,217 +29,12 @@ public abstract class : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QuerySingleOrDefault_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - "GetFirstEntity", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable([entity])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {TemporaryTable([entity])}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingleOrDefault_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow() - { - this.CreateEntitiesInDb(2); - - Invoking(() => this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did return more than one row." - ); - } - - [Fact] - public void QuerySingleOrDefault_QueryReturnedNoRows_ShouldReturnNull() => - ((Object?)this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); - - [Fact] - public void QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = this.Connection.QuerySingleOrDefault( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - ((Object?)dynamicObject) - .Should().NotBeNull(); - - ((Object?)dynamicObject.Id) - .Should().Be(entityId); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void - QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}", - cancellationToken: TestContext.Current.CancellationToken - ); - - ValueConverter.ConvertValueToType((Object)dynamicObject!.Id) - .Should().Be(entityId); - } - - [Fact] - public void QuerySingleOrDefault_ShouldReturnDynamicObjectForFirstRow() - { - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingleOrDefault_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); - - var dynamicObject = this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - - transaction.Rollback(); - } - - ((Object?)this.Connection.QuerySingleOrDefault( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); - } - - [Fact] - public async Task QuerySingleOrDefaultAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -247,7 +43,9 @@ public async Task QuerySingleOrDefaultAsync_CancellationToken_ShouldCancelOperat this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -256,14 +54,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QuerySingleOrDefaultAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -272,8 +74,12 @@ public async Task QuerySingleOrDefaultAsync_CommandType_ShouldUseCommandType() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -283,7 +89,9 @@ public async Task QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldD var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -294,15 +102,21 @@ public async Task QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldD .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken ); @@ -310,12 +124,16 @@ public async Task EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -323,8 +141,10 @@ public async Task QuerySingleOrDefaultAsync_InterpolatedParameter_ShouldPassInte EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -333,7 +153,9 @@ public async Task QuerySingleOrDefaultAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -341,12 +163,16 @@ public async Task QuerySingleOrDefaultAsync_Parameter_ShouldPassParameter() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_QueryReturnedMoreThanOneRow_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -358,16 +184,24 @@ await Invoking(() => this.Connection.QuerySingleOrDefaultAsync( } - [Fact] - public async Task QuerySingleOrDefaultAsync_QueryReturnedNoRows_ShouldReturnNull() => - ((Object?)await this.Connection.QuerySingleOrDefaultAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldReturnNull(Boolean useAsyncApi) => + ((Object?)await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); - [Fact] - public async Task QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -377,7 +211,9 @@ public async Task QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDro var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -392,15 +228,21 @@ public async Task QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldDro .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleOrDefaultAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityId = Generate.Id(); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}", cancellationToken: TestContext.Current.CancellationToken ); @@ -409,12 +251,16 @@ public async Task .Should().Be(entityId); } - [Fact] - public async Task QuerySingleOrDefaultAsync_ShouldReturnDynamicObjectForFirstRow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_ShouldReturnDynamicObjectForFirstRow(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -422,14 +268,18 @@ public async Task QuerySingleOrDefaultAsync_ShouldReturnDynamicObjectForFirstRow EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleOrDefaultAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - var dynamicObject = await this.Connection.QuerySingleOrDefaultAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -440,10 +290,51 @@ public async Task QuerySingleOrDefaultAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - ((Object?)await this.Connection.QuerySingleOrDefaultAsync( + ((Object?)await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().BeNull(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QuerySingleOrDefaultAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QuerySingleOrDefault( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs index 83b3b60..92e873b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; @@ -27,217 +28,10 @@ public abstract class DbConnectionExtensions_QuerySingleTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.QuerySingle($"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken) - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void QuerySingle_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingle( - "GetFirstEntity", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable([entity])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entity = Generate.Single(); - - var dynamicObject = this.Connection.QuerySingle( - $"SELECT * FROM {TemporaryTable([entity])}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingle_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - var dynamicObject = this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow() - { - this.CreateEntitiesInDb(2); - - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did return more than one row." - ); - } - - [Fact] - public void QuerySingle_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw() - .WithMessage( - "The SQL statement did not return any rows." - ); - - [Fact] - public void QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var dynamicObject = this.Connection.QuerySingle( - statement, - cancellationToken: TestContext.Current.CancellationToken - ); - - ((Object?)dynamicObject) - .Should().NotBeNull(); - - ((Object?)dynamicObject.Id) - .Should().Be(entityId); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityId = Generate.Id(); - - var dynamicObject = this.Connection.QuerySingle( - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}", - cancellationToken: TestContext.Current.CancellationToken - ); - - ValueConverter.ConvertValueToType((Object)dynamicObject.Id) - .Should().Be(entityId); - } - - [Fact] - public void QuerySingle_ShouldReturnDynamicObjectForFirstRow() - { - var entity = this.CreateEntityInDb(); - - var dynamicObject = this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - } - - [Fact] - public void QuerySingle_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entity = this.CreateEntityInDb(transaction); - - var dynamicObject = this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); - - transaction.Rollback(); - } - - Invoking(() => this.Connection.QuerySingle( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - ) - .Should().Throw(); - } - - [Fact] - public async Task QuerySingleAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -246,7 +40,9 @@ public async Task QuerySingleAsync_CancellationToken_ShouldCancelOperationIfCanc this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ) @@ -255,14 +51,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QuerySingleAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -271,8 +71,12 @@ public async Task QuerySingleAsync_CommandType_ShouldUseCommandType() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -282,7 +86,9 @@ public async Task QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldDropTempor var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -293,15 +99,21 @@ public async Task QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldDropTempor .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken ); @@ -309,12 +121,16 @@ public async Task EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ); @@ -322,8 +138,10 @@ public async Task QuerySingleAsync_InterpolatedParameter_ShouldPassInterpolatedP EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -332,7 +150,9 @@ public async Task QuerySingleAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -340,12 +160,16 @@ public async Task QuerySingleAsync_Parameter_ShouldPassParameter() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleAsync_QueryReturnedMoreThanOneRow_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => this.Connection.QuerySingleAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -357,9 +181,13 @@ await Invoking(() => this.Connection.QuerySingleAsync( } - [Fact] - public Task QuerySingleAsync_QueryReturnedNoRows_ShouldThrow() => - Invoking(() => this.Connection.QuerySingleAsync( + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) @@ -369,8 +197,10 @@ public Task QuerySingleAsync_QueryReturnedNoRows_ShouldThrow() => "The SQL statement did not return any rows." ); - [Fact] - public async Task QuerySingleAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -380,7 +210,9 @@ public async Task QuerySingleAsync_ScalarValuesTemporaryTable_ShouldDropTemporar var temporaryTableName = statement.TemporaryTables[0].Name; - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ); @@ -395,15 +227,21 @@ public async Task QuerySingleAsync_ScalarValuesTemporaryTable_ShouldDropTemporar .Should().BeFalse(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - QuerySingleAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityId = Generate.Id(); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}", cancellationToken: TestContext.Current.CancellationToken ); @@ -412,12 +250,16 @@ public async Task .Should().Be(entityId); } - [Fact] - public async Task QuerySingleAsync_ShouldReturnDynamicObjectForFirstRow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_ShouldReturnDynamicObjectForFirstRow(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -425,14 +267,18 @@ public async Task QuerySingleAsync_ShouldReturnDynamicObjectForFirstRow() EntityAssertions.AssertDynamicObjectMatchesEntity(dynamicObject, entity); } - [Fact] - public async Task QuerySingleAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - var dynamicObject = await this.Connection.QuerySingleAsync( + var dynamicObject = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -443,11 +289,52 @@ public async Task QuerySingleAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - await Invoking(() => this.Connection.QuerySingleAsync( + await Invoking(() => CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync(); } + + private static Task CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QuerySingleAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + connection.QuerySingle( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs index f5c649f..40d77e0 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.IntegrationTests.Assertions; @@ -27,217 +28,10 @@ public abstract class DbConnectionExtensions_QueryTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { - [Fact] - public void Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => - this.Connection.Query($"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken).ToList() - ) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - } - - [Fact] - public void Query_CommandType_ShouldUseCommandType() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); - - var entities = this.CreateEntitiesInDb(); - - var dynamicObjects = this.Connection.Query( - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); - } - - [Fact] - public void Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(2); - - InterpolatedSqlStatement statement = $"SELECT * FROM {TemporaryTable(entities)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var enumerator = this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetEnumerator(); - - enumerator.MoveNext() - .Should().BeTrue(); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - enumerator.MoveNext() - .Should().BeTrue(); - - enumerator.MoveNext() - .Should().BeFalse(); - - enumerator.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entities = Generate.Multiple(); - - var dynamicObjects = this.Connection.Query( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); - } - - [Fact] - public void Query_InterpolatedParameter_ShouldPassInterpolatedParameter() - { - var entity = this.CreateEntityInDb(); - - var dynamicObjects = this.Connection.Query( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, [entity]); - } - - [Fact] - public void Query_Parameter_ShouldPassParameter() - { - var entity = this.CreateEntityInDb(); - - var statement = new InterpolatedSqlStatement( - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {P("Id")}", - ("Id", entity.Id) - ); - - var dynamicObjects = this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, [entity]); - } - - [Fact] - public void Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(2); - - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; - - var temporaryTableName = statement.TemporaryTables[0].Name; - - var enumerator = this.Connection.Query( - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetEnumerator(); - - enumerator.MoveNext() - .Should().BeTrue(); - - if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) - { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); - } - - enumerator.MoveNext() - .Should().BeTrue(); - - enumerator.MoveNext() - .Should().BeFalse(); - - enumerator.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); - } - - [Fact] - public void Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() - { - Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - - var entityIds = Generate.Ids(); - - var dynamicObjects = this.Connection.Query( - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - for (var i = 0; i < entityIds.Count; i++) - { - ValueConverter.ConvertValueToType((Object)dynamicObjects[i].Id) - .Should().Be(entityIds[i]); - } - } - - [Fact] - public void Query_ShouldReturnDynamicObjectsForQueryResult() - { - var entities = this.CreateEntitiesInDb(); - - var dynamicObjects = this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); - } - - [Fact] - public void Query_Transaction_ShouldUseTransaction() - { - using (var transaction = this.Connection.BeginTransaction()) - { - var entities = this.CreateEntitiesInDb(null, transaction); - - var dynamicObjects = this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ).ToList(); - - EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); - - transaction.Rollback(); - } - - this.Connection.Query( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEmpty(); - } - - [Fact] - public async Task QueryAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -246,7 +40,9 @@ public async Task QueryAsync_CancellationToken_ShouldCancelOperationIfCancellati this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.Connection.QueryAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: cancellationToken ).ToListAsync(cancellationToken).AsTask() @@ -255,14 +51,18 @@ await Invoking(() => .Where(a => a.CancellationToken == cancellationToken); } - [Fact] - public async Task QueryAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken @@ -271,8 +71,12 @@ public async Task QueryAsync_CommandType_ShouldUseCommandType() EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); } - [Fact] - public async Task QueryAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -282,7 +86,9 @@ public async Task QueryAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTab var temporaryTableName = statement.TemporaryTables[0].Name; - var enumerator = this.Connection.QueryAsync( + var enumerator = CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).GetAsyncEnumerator(); @@ -308,14 +114,20 @@ public async Task QueryAsync_ComplexObjectsTemporaryTable_ShouldDropTemporaryTab .Should().BeFalse(); } - [Fact] - public async Task QueryAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken); @@ -323,12 +135,16 @@ public async Task QueryAsync_ComplexObjectsTemporaryTable_ShouldPassInterpolated EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); } - [Fact] - public async Task QueryAsync_InterpolatedParameter_ShouldPassInterpolatedParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken); @@ -336,8 +152,10 @@ public async Task QueryAsync_InterpolatedParameter_ShouldPassInterpolatedParamet EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, [entity]); } - [Fact] - public async Task QueryAsync_Parameter_ShouldPassParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -346,7 +164,9 @@ public async Task QueryAsync_Parameter_ShouldPassParameter() ("Id", entity.Id) ); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken); @@ -354,8 +174,12 @@ public async Task QueryAsync_Parameter_ShouldPassParameter() EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, [entity]); } - [Fact] - public async Task QueryAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -365,7 +189,9 @@ public async Task QueryAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTable var temporaryTableName = statement.TemporaryTables[0].Name; - var enumerator = this.Connection.QueryAsync( + var enumerator = CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ).GetAsyncEnumerator(); @@ -391,14 +217,20 @@ public async Task QueryAsync_ScalarValuesTemporaryTable_ShouldDropTemporaryTable .Should().BeFalse(); } - [Fact] - public async Task QueryAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken); @@ -410,12 +242,16 @@ public async Task QueryAsync_ScalarValuesTemporaryTable_ShouldPassInterpolatedVa } } - [Fact] - public async Task QueryAsync_ShouldReturnDynamicObjectsForQueryResult() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_ShouldReturnDynamicObjectsForQueryResult(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken); @@ -423,14 +259,18 @@ public async Task QueryAsync_ShouldReturnDynamicObjectsForQueryResult() EntityAssertions.AssertDynamicObjectsMatchEntities(dynamicObjects, entities); } - [Fact] - public async Task QueryAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(null, transaction); - var dynamicObjects = await this.Connection.QueryAsync( + var dynamicObjects = await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken @@ -441,10 +281,42 @@ public async Task QueryAsync_Transaction_ShouldUseTransaction() await transaction.RollbackAsync(); } - (await this.Connection.QueryAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) .Should().BeEmpty(); } + + private static IAsyncEnumerable CallApi( + Boolean useAsyncApi, + DbConnection connection, + InterpolatedSqlStatement statement, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return connection.QueryAsync( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + return connection.Query( + statement, + transaction, + commandTimeout, + commandType, + cancellationToken + ).ToAsyncEnumerable(); + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs index ad70f36..1de3f5e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs @@ -26,7 +26,7 @@ public void { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -42,7 +42,7 @@ public void TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsSt { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -72,7 +72,7 @@ public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsIntegers_Sh { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValues = Generate.Multiple(); @@ -89,7 +89,7 @@ public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsStrings_Sho { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValues = Generate.Multiple(); @@ -122,7 +122,7 @@ public async Task { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -139,7 +139,7 @@ public async Task { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -170,7 +170,7 @@ public async Task { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValues = Generate.Multiple(); @@ -188,7 +188,7 @@ public async Task { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValues = Generate.Multiple(); diff --git a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs index 1aa33f1..d350e53 100644 --- a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs @@ -1,8 +1,8 @@ -global using System.ComponentModel.DataAnnotations; global using System.Data; global using Xunit; global using AwesomeAssertions; global using Microsoft.Data.SqlClient; +global using RentADeveloper.DbConnectionPlus.Configuration; global using RentADeveloper.DbConnectionPlus.DbCommands; global using RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; global using RentADeveloper.DbConnectionPlus.IntegrationTests.TestHelpers; diff --git a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs index bc4a49a..7931e13 100644 --- a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs +++ b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs @@ -1,5 +1,6 @@ // ReSharper disable StaticMemberInGenericType // ReSharper disable InconsistentNaming + #pragma warning disable RCS1158 using System.Data.Common; @@ -25,9 +26,6 @@ protected IntegrationTestsBase() Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new("en-US"); - // Reset all settings to defaults before each test. - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - DbCommandLogger.LogCommands = false; this.TestDatabaseProvider = new(); @@ -41,10 +39,18 @@ protected IntegrationTestsBase() this.DbCommandFactory = new(this.TestDatabaseProvider); DbConnectionExtensions.DbCommandFactory = this.DbCommandFactory; - DbConnectionExtensions.InterceptDbCommand = DbCommandLogger.LogDbCommand; + DbCommandLogger.LogCommands = true; OracleDatabaseAdapter.AllowTemporaryTables = true; + + // Reset all settings to defaults before each test. + DbConnectionPlusConfiguration.Instance = new() + { + EnumSerializationMode = EnumSerializationMode.Strings, + InterceptDbCommand = DbCommandLogger.LogDbCommand + }; + EntityHelper.ResetEntityTypeMetadataCache(); } /// @@ -231,7 +237,11 @@ [.. keyProperties.Select(p => $"{Q(p.ColumnName)} = {P(p.PropertyName)}")] /// protected Boolean ExistsTemporaryTableInDb(String tableName, DbTransaction? transaction = null) => ExecuteWithoutDbCommandLogging(() => - this.TestDatabaseProvider.ExistsTemporaryTable(tableName, this.Connection, transaction) + this.TestDatabaseProvider.ExistsTemporaryTable( + tableName, + this.Connection, + transaction + ) ); /// @@ -260,7 +270,11 @@ protected String GetDataTypeOfTemporaryTableColumn( String columnName ) => ExecuteWithoutDbCommandLogging(() => - this.TestDatabaseProvider.GetDataTypeOfTemporaryTableColumn(temporaryTableName, columnName, this.Connection) + this.TestDatabaseProvider.GetDataTypeOfTemporaryTableColumn( + temporaryTableName, + columnName, + this.Connection + ) ); /// diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 8f02976..1401b6f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -208,30 +208,14 @@ CREATE TABLE `EntityWithNullableProperty` ); GO - CREATE TABLE `EntityWithIdentityAndComputedProperties` + CREATE TABLE `MappingTestEntity` ( - `Id` BIGINT NOT NULL, - `IdentityValue` BIGINT AUTO_INCREMENT NOT NULL, - `ComputedValue` BIGINT AS (`BaseValue`+999), - `BaseValue` BIGINT NOT NULL, - PRIMARY KEY (`IdentityValue`) - ); - GO - - CREATE TABLE `EntityWithCompositeKey` - ( - `Key1` BIGINT NOT NULL, - `Key2` BIGINT NOT NULL, - `StringValue` VARCHAR(200) NOT NULL, - PRIMARY KEY (`Key1`, `Key2`) - ); - GO - - CREATE TABLE `EntityWithNotMappedProperty` - ( - `Id` BIGINT NOT NULL PRIMARY KEY, - `MappedValue` VARCHAR(200) NOT NULL, - `NotMappedValue` VARCHAR(200) NULL + `KeyColumn1` BIGINT NOT NULL, + `KeyColumn2` BIGINT NOT NULL, + `ValueColumn` INT NOT NULL, + `ComputedColumn` INT AS (`ValueColumn`+999), + `IdentityColumn` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `NotMappedColumn` TEXT NULL ); GO @@ -291,13 +275,7 @@ CREATE TABLE `EntityWithNotMappedProperty` TRUNCATE TABLE `EntityWithNullableProperty`; GO - TRUNCATE TABLE `EntityWithIdentityAndComputedProperties`; - GO - - TRUNCATE TABLE `EntityWithCompositeKey`; - GO - - TRUNCATE TABLE `EntityWithNotMappedProperty`; + TRUNCATE TABLE `MappingTestEntity`; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 8cffba0..aaa0d65 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -196,29 +196,15 @@ CREATE TABLE "EntityWithNullableProperty" ); GO - CREATE TABLE "EntityWithIdentityAndComputedProperties" + CREATE TABLE "MappingTestEntity" ( - "Id" NUMBER(19) NOT NULL PRIMARY KEY, - "IdentityValue" NUMBER(19) GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1), - "ComputedValue" generated always as (("BaseValue"+999)), - "BaseValue" NUMBER(19) NOT NULL - ); - GO - - CREATE TABLE "EntityWithCompositeKey" - ( - "Key1" NUMBER(19) NOT NULL, - "Key2" NUMBER(19) NOT NULL, - "StringValue" NVARCHAR2(200) NOT NULL, - PRIMARY KEY ("Key1", "Key2") - ); - GO - - CREATE TABLE "EntityWithNotMappedProperty" - ( - "Id" NUMBER(19) NOT NULL PRIMARY KEY, - "MappedValue" NVARCHAR2(200) NOT NULL, - "NotMappedValue" NVARCHAR2(200) NULL + "KeyColumn1" NUMBER(19) NOT NULL, + "KeyColumn2" NUMBER(19) NOT NULL, + "ValueColumn" NUMBER(10) NOT NULL, + "ComputedColumn" GENERATED ALWAYS AS (("ValueColumn"+999)), + "IdentityColumn" NUMBER(10) GENERATED ALWAYS AS IDENTITY(START with 1 INCREMENT by 1), + "NotMappedColumn" CLOB NULL, + PRIMARY KEY ("KeyColumn1", "KeyColumn2") ); GO @@ -250,13 +236,7 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS DROP TABLE IF EXISTS "EntityWithNullableProperty" PURGE; GO - DROP TABLE IF EXISTS "EntityWithIdentityAndComputedProperties" PURGE; - GO - - DROP TABLE IF EXISTS "EntityWithCompositeKey" PURGE; - GO - - DROP TABLE IF EXISTS "EntityWithNotMappedProperty" PURGE; + DROP TABLE IF EXISTS "MappingTestEntity" PURGE; GO DROP PROCEDURE IF EXISTS "DeleteAllEntities"; @@ -283,13 +263,7 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS TRUNCATE TABLE "EntityWithNullableProperty"; GO - TRUNCATE TABLE "EntityWithIdentityAndComputedProperties"; - GO - - TRUNCATE TABLE "EntityWithCompositeKey"; - GO - - TRUNCATE TABLE "EntityWithNotMappedProperty"; + TRUNCATE TABLE "MappingTestEntity"; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index d452114..66bb79b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -183,27 +183,15 @@ CREATE TABLE "EntityWithNullableProperty" "Value" bigint NULL ); - CREATE TABLE "EntityWithIdentityAndComputedProperties" + CREATE TABLE "MappingTestEntity" ( - "Id" bigint NOT NULL PRIMARY KEY, - "IdentityValue" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, - "ComputedValue" bigint GENERATED ALWAYS AS ("BaseValue"+(999)), - "BaseValue" bigint NOT NULL - ); - - CREATE TABLE "EntityWithCompositeKey" - ( - "Key1" bigint NOT NULL, - "Key2" bigint NOT NULL, - "StringValue" text NOT NULL, - PRIMARY KEY ("Key1", "Key2") - ); - - CREATE TABLE "EntityWithNotMappedProperty" - ( - "Id" bigint NOT NULL PRIMARY KEY, - "MappedValue" text NOT NULL, - "NotMappedValue" text NULL + "KeyColumn1" bigint NOT NULL, + "KeyColumn2" bigint NOT NULL, + "ValueColumn" integer NOT NULL, + "ComputedColumn" integer GENERATED ALWAYS AS ("ValueColumn"+(999)), + "IdentityColumn" integer GENERATED ALWAYS AS IDENTITY NOT NULL, + "NotMappedColumn" text NULL, + PRIMARY KEY ("KeyColumn1", "KeyColumn2") ); CREATE PROCEDURE "GetEntities" () @@ -252,9 +240,7 @@ DELETE FROM "Entity" TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; TRUNCATE TABLE "EntityWithNonNullableProperty"; TRUNCATE TABLE "EntityWithNullableProperty"; - TRUNCATE TABLE "EntityWithIdentityAndComputedProperties"; - TRUNCATE TABLE "EntityWithCompositeKey"; - TRUNCATE TABLE "EntityWithNotMappedProperty"; + TRUNCATE TABLE "MappingTestEntity"; """; private static readonly String connectionString; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index f225120..b1f519c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -176,27 +176,14 @@ CREATE TABLE EntityWithNullableProperty Value INTEGER NULL ); - CREATE TABLE EntityWithIdentityAndComputedProperties + CREATE TABLE MappingTestEntity ( - Id INTEGER NOT NULL, - IdentityValue INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - ComputedValue INTEGER GENERATED ALWAYS AS (BaseValue+999) VIRTUAL, - BaseValue INTEGER NOT NULL - ); - - CREATE TABLE EntityWithCompositeKey - ( - Key1 INTEGER NOT NULL, - Key2 INTEGER NOT NULL, - StringValue TEXT NOT NULL, - PRIMARY KEY (Key1, Key2) - ); - - CREATE TABLE EntityWithNotMappedProperty - ( - Id INTEGER NOT NULL PRIMARY KEY, - MappedValue TEXT NOT NULL, - NotMappedValue TEXT NULL + KeyColumn1 INTEGER NOT NULL, + KeyColumn2 INTEGER NOT NULL, + ValueColumn INTEGER NOT NULL, + ComputedColumn INTEGER GENERATED ALWAYS AS (ValueColumn+999) VIRTUAL, + IdentityColumn INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + NotMappedColumn TEXT NULL ); """; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index d08f1cd..f793914 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -220,29 +220,15 @@ Value BIGINT NULL ); GO - CREATE TABLE EntityWithIdentityAndComputedProperties + CREATE TABLE MappingTestEntity ( - Id BIGINT NOT NULL PRIMARY KEY, - IdentityValue BIGINT IDENTITY(1,1) NOT NULL, - ComputedValue AS ([BaseValue]+(999)), - BaseValue BIGINT NOT NULL - ); - GO - - CREATE TABLE EntityWithCompositeKey - ( - Key1 BIGINT NOT NULL, - Key2 BIGINT NOT NULL, - StringValue NVARCHAR(200) NOT NULL, - PRIMARY KEY (Key1, Key2) - ); - GO - - CREATE TABLE EntityWithNotMappedProperty - ( - Id BIGINT NOT NULL PRIMARY KEY, - MappedValue NVARCHAR(200) NOT NULL, - NotMappedValue NVARCHAR(200) NULL + KeyColumn1 BIGINT NOT NULL, + KeyColumn2 BIGINT NOT NULL, + ValueColumn INT NOT NULL, + ComputedColumn AS ([ValueColumn]+(999)), + IdentityColumn INT IDENTITY(1,1) NOT NULL, + NotMappedColumn VARCHAR(200) NULL, + PRIMARY KEY (KeyColumn1, KeyColumn2) ); GO @@ -311,13 +297,7 @@ DELETE FROM Entity TRUNCATE TABLE EntityWithNullableProperty; GO - TRUNCATE TABLE EntityWithIdentityAndComputedProperties; - GO - - TRUNCATE TABLE EntityWithCompositeKey; - GO - - TRUNCATE TABLE EntityWithNotMappedProperty; + TRUNCATE TABLE MappingTestEntity; GO """; diff --git a/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs b/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs index 2108649..5fe303a 100644 --- a/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs +++ b/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs @@ -1,4 +1,4 @@ -#pragma warning disable NS5000, NS1001, NS1000 +#pragma warning disable NS5000, NS1001, NS1000 using System.Reflection; using AutoFixture; diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs new file mode 100644 index 0000000..6c2952c --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -0,0 +1,314 @@ +using Microsoft.Data.Sqlite; +using MySqlConnector; +using Npgsql; +using NSubstitute.DbConnection; +using Oracle.ManagedDataAccess.Client; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; +using RentADeveloper.DbConnectionPlus.SqlStatements; + +namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; + +public class DbConnectionPlusConfigurationTests : UnitTestsBase +{ + [Fact] + public void EnumSerializationMode_Integers_ShouldSerializeEnumAsInteger() + { + var enumValue = Generate.Single(); + + this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); + + DbParameter? interceptedDbParameter = null; + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = + (command, _) => interceptedDbParameter = command.Parameters[0]; + + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; + + this.MockDbConnection.ExecuteNonQuery($"SELECT {Parameter(enumValue)}"); + + interceptedDbParameter + .Should().NotBeNull(); + + interceptedDbParameter.Value + .Should().Be((Int32)enumValue); + } + + [Fact] + public void EnumSerializationMode_Strings_ShouldSerializeEnumAsString() + { + var enumValue = Generate.Single(); + + this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); + + DbParameter? interceptedDbParameter = null; + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = + (command, _) => interceptedDbParameter = command.Parameters[0]; + + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; + + this.MockDbConnection.ExecuteNonQuery($"SELECT {Parameter(enumValue)}"); + + interceptedDbParameter + .Should().NotBeNull(); + + interceptedDbParameter.Value + .Should().Be(enumValue.ToString()); + } + + [Fact] + public void Freeze_ShouldFreezeConfigurationAndEntityTypeBuilders() + { + var configuration = new DbConnectionPlusConfiguration(); + + var entityTypeBuilder = configuration.Entity(); + + entityTypeBuilder.ToTable("Entities"); + + var entityPropertyBuilder = entityTypeBuilder.Property(a => a.Id); + + entityPropertyBuilder.IsKey(); + + ((IFreezable)configuration).Freeze(); + + Invoking(() => configuration.EnumSerializationMode = EnumSerializationMode.Integers) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => configuration.InterceptDbCommand = null) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => configuration.Entity()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => entityTypeBuilder.ToTable("Entities")) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => entityTypeBuilder.Property(a => a.Id)) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => entityPropertyBuilder.IsKey()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + } + + [Fact] + public void GetDatabaseAdapter_NoAdapterRegisteredForConnectionType_ShouldThrow() => + Invoking(() => DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionC))) + .Should().Throw() + .WithMessage( + "No database adapter is registered for the database connection of the type " + + $"{typeof(FakeConnectionC)}. Please call {nameof(DbConnectionExtensions)}." + + $"{nameof(Configure)} to register an adapter for that connection type." + ); + + [Fact] + public void GetDatabaseAdapter_ShouldGetAdapter() + { + var adapterA = Substitute.For(); + var adapterB = Substitute.For(); + + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) + .Should().BeSameAs(adapterA); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)) + .Should().BeSameAs(adapterB); + } + + [Fact] + public void GetDatabaseAdapter_ShouldGetDefaultAdapters() + { + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(MySqlConnection)) + .Should().BeOfType(); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(OracleConnection)) + .Should().BeOfType(); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(NpgsqlConnection)) + .Should().BeOfType(); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(SqliteConnection)) + .Should().BeOfType(); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(SqlConnection)) + .Should().BeOfType(); + } + + [Fact] + public void GetEntityTypeBuilders_ShouldGetConfiguredBuilders() + { + var configuration = new DbConnectionPlusConfiguration(); + + var entityBuilder = configuration.Entity(); + var mappingTestEntityFluentApiBuilder = configuration.Entity(); + + var entityTypeBuilders = configuration.GetEntityTypeBuilders(); + + entityTypeBuilders + .Should().ContainKeys( + typeof(Entity), + typeof(MappingTestEntityFluentApi) + ); + + entityTypeBuilders[typeof(Entity)] + .Should().BeSameAs(entityBuilder); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .Should().BeSameAs(mappingTestEntityFluentApiBuilder); + } + + [Fact] + public void InterceptDbCommand_ShouldInterceptDbCommands() + { + var interceptor = Substitute.For(); + + DbCommand? interceptedDbCommand = null; + IReadOnlyCollection? interceptedTemporaryTables = null; + + interceptor + .WhenForAnyArgs(interceptor2 => + interceptor2.Invoke( + Arg.Any(), + Arg.Any>() + ) + ) + .Do(info => + { + interceptedDbCommand = info.Arg(); + interceptedTemporaryTables = info.Arg>(); + } + ); + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = interceptor; + + this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); + + var entities = Generate.Multiple(); + var entityIds = Generate.Ids(); + var stringValue = entities[0].StringValue; + + InterpolatedSqlStatement statement = + $""" + SELECT Id, StringValue + FROM {TemporaryTable(entities)} TEntity + WHERE TEntity.Id IN ({TemporaryTable(entityIds)}) OR StringValue = {Parameter(stringValue)} + """; + + var temporaryTables = statement.TemporaryTables; + + var transaction = this.MockDbConnection.BeginTransaction(); + var timeout = Generate.Single(); + var cancellationToken = Generate.Single(); + + _ = this.MockDbConnection.Query( + statement, + transaction, + timeout, + CommandType.StoredProcedure, + cancellationToken + ).ToList(); + + interceptor.Received().Invoke( + Arg.Any(), + Arg.Any>() + ); + + interceptedDbCommand + .Should().NotBeNull(); + + interceptedDbCommand.CommandText + .Should().Be( + $""" + SELECT Id, StringValue + FROM [#{temporaryTables[0].Name}] TEntity + WHERE TEntity.Id IN ([#{temporaryTables[1].Name}]) OR StringValue = @StringValue + """ + ); + + interceptedDbCommand.Transaction + .Should().Be(transaction); + + interceptedDbCommand.CommandType + .Should().Be(CommandType.StoredProcedure); + + interceptedDbCommand.CommandTimeout + .Should().Be((Int32)timeout.TotalSeconds); + + interceptedDbCommand.Parameters.Count + .Should().Be(1); + + interceptedDbCommand.Parameters[0].ParameterName + .Should().Be("StringValue"); + + interceptedDbCommand.Parameters[0].Value + .Should().Be(stringValue); + + interceptedTemporaryTables + .Should().NotBeNull(); + + interceptedTemporaryTables + .Should().BeEquivalentTo(temporaryTables); + } + + [Fact] + public void RegisterDatabaseAdapter_ShouldRegisterAdapter() + { + var adapterA = Substitute.For(); + + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) + .Should().BeSameAs(adapterA); + + var adapterB = Substitute.For(); + + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)) + .Should().BeSameAs(adapterB); + } + + [Fact] + public void RegisterDatabaseAdapter_ShouldReplaceRegisteredAdapter() + { + var adapterA = Substitute.For(); + + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) + .Should().BeSameAs(adapterA); + + var adapterB = Substitute.For(); + + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); + + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) + .Should().BeSameAs(adapterB); + } + + [Fact] + public void ShouldGuardAgainstNullArguments() + { + ArgumentNullGuardVerifier.Verify(() => + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(SqlConnection)) + ); + + ArgumentNullGuardVerifier.Verify(() => + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter( + new SqlServerDatabaseAdapter() + ) + ); + } +} diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs new file mode 100644 index 0000000..31eea9e --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -0,0 +1,198 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; + +public class EntityPropertyBuilderTests : UnitTestsBase +{ + [Fact] + public void Freeze_ShouldFreezeBuilder() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + ((IFreezable)builder).Freeze(); + + Invoking(() => builder.HasColumnName("Identifier")) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.IsComputed()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.IsIdentity()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.IsIgnored()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.IsKey()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + } + + [Fact] + public void GetColumnName_Configured_ShouldReturnColumnName() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.HasColumnName("Identifier"); + + ((IEntityPropertyBuilder)builder).ColumnName + .Should().Be("Identifier"); + } + + [Fact] + public void GetColumnName_NotConfigured_ShouldReturnNull() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).ColumnName + .Should().BeNull(); + } + + [Fact] + public void GetIsComputed_Configured_ShouldReturnTrue() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsComputed(); + + ((IEntityPropertyBuilder)builder).IsComputed + .Should().BeTrue(); + } + + [Fact] + public void GetIsComputed_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsComputed + .Should().BeFalse(); + } + + [Fact] + public void GetIsIdentity_Configured_ShouldReturnTrue() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsIdentity(); + + ((IEntityPropertyBuilder)builder).IsIdentity + .Should().BeTrue(); + } + + [Fact] + public void GetIsIdentity_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsIdentity + .Should().BeFalse(); + } + + [Fact] + public void GetIsIgnored_Configured_ShouldReturnTrue() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsIgnored(); + + ((IEntityPropertyBuilder)builder).IsIgnored + .Should().BeTrue(); + } + + [Fact] + public void GetIsIgnored_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsIgnored + .Should().BeFalse(); + } + + [Fact] + public void GetIsKey_Configured_ShouldReturnTrue() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsKey(); + + ((IEntityPropertyBuilder)builder).IsKey + .Should().BeTrue(); + } + + [Fact] + public void GetIsKey_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsKey + .Should().BeFalse(); + } + + [Fact] + public void HasColumnName_ShouldSetColumnName() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.HasColumnName("Identifier"); + + ((IEntityPropertyBuilder)builder).ColumnName + .Should().Be("Identifier"); + } + + [Fact] + public void IsComputed_ShouldMarkPropertyAsComputed() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsComputed(); + + ((IEntityPropertyBuilder)builder).IsComputed + .Should().BeTrue(); + } + + [Fact] + public void IsIdentity_OtherPropertyIsAlreadyMarked_ShouldThrow() + { + var entityTypeBuilder = new EntityTypeBuilder(); + entityTypeBuilder.Property(a => a.Id).IsIdentity(); + + var builder = new EntityPropertyBuilder(entityTypeBuilder, "Property"); + + Invoking(() => builder.IsIdentity()) + .Should().Throw() + .WithMessage( + "There is already the property 'Id' marked as an identity property for the entity type " + + $"{typeof(Entity)}. Only one property can be marked as identity property per entity type." + ); + } + + [Fact] + public void IsIdentity_ShouldMarkPropertyAsIdentity() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsIdentity(); + + ((IEntityPropertyBuilder)builder).IsIdentity + .Should().BeTrue(); + } + + [Fact] + public void IsIgnored_ShouldMarkPropertyAsIgnored() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.IsIgnored(); + + ((IEntityPropertyBuilder)builder).IsIgnored + .Should().BeTrue(); + } + + [Fact] + public void IsKey_ShouldMarkPropertyAsKey() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.IsKey(); + + ((IEntityPropertyBuilder)builder).IsKey + .Should().BeTrue(); + } +} diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs new file mode 100644 index 0000000..b2918b2 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs @@ -0,0 +1,123 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; + +public class EntityTypeBuilderTests : UnitTestsBase +{ + [Fact] + public void Freeze_ShouldFreezeBuilderAndAllPropertyBuilders() + { + var builder = new EntityTypeBuilder(); + + builder.ToTable("Entities"); + builder.Property(a => a.Id).IsKey(); + builder.Property(a => a.StringValue).IsIgnored(); + + ((IFreezable)builder).Freeze(); + + Invoking(() => builder.ToTable("Entities2")) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.Property(a => a.Id).HasColumnName("Identifier")) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.Property(a => a.StringValue).HasColumnName("String")) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + } + + [Fact] + public void Property_InvalidExpression_ShouldThrow() + { + var builder = new EntityTypeBuilder(); + + Invoking(() => builder.Property(a => a.Id.ToString())) + .Should().Throw() + .WithMessage( + "The expression 'a => a.Id.ToString()' is not a valid property access expression. The expression " + + "should represent a simple property access: 'a => a.MyProperty'.*" + ); + } + + [Fact] + public void Property_ShouldGetPropertyBuilder() + { + var builder = new EntityTypeBuilder(); + + var propertyBuilder = builder.Property(a => a.Id); + + propertyBuilder + .Should().NotBeNull(); + + builder.Property(a => a.Id) + .Should().BeSameAs(propertyBuilder); + } + + [Fact] + public void PropertyBuilders_ShouldGetBuildersOfConfiguredProperties() + { + var builder = new EntityTypeBuilder(); + + builder.Property(a => a.Id).IsKey(); + builder.Property(a => a.StringValue).IsComputed(); + builder.Property(a => a.Int64Value).IsIgnored(); + + var propertyBuilders = ((IEntityTypeBuilder)builder).PropertyBuilders; + + propertyBuilders + .Should().HaveCount(3); + + propertyBuilders + .Should().ContainKeys("Id", "StringValue", "Int64Value"); + + propertyBuilders["Id"] + .Should().BeSameAs(builder.Property(a => a.Id)); + + propertyBuilders["StringValue"] + .Should().BeSameAs(builder.Property(a => a.StringValue)); + + propertyBuilders["Int64Value"] + .Should().BeSameAs(builder.Property(a => a.Int64Value)); + } + + [Fact] + public void ShouldGuardAgainstNullArguments() + { + var builder = new EntityTypeBuilder(); + + ArgumentNullGuardVerifier.Verify(() => + builder.Property(a => a.Id) + ); + } + + [Fact] + public void TableName_NotConfigured_ShouldReturnNull() + { + var builder = new EntityTypeBuilder(); + + ((IEntityTypeBuilder)builder).TableName + .Should().BeNull(); + } + + [Fact] + public void ToTable_Configured_ShouldGetTableName() + { + var builder = new EntityTypeBuilder(); + + builder.ToTable("Entities"); + + ((IEntityTypeBuilder)builder).TableName + .Should().Be("Entities"); + } + + [Fact] + public void ToTable_ShouldSetTableName() + { + var builder = new EntityTypeBuilder(); + + builder.ToTable("Entities"); + + ((IEntityTypeBuilder)builder).TableName + .Should().Be("Entities"); + } +} diff --git a/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs index 3a94c0c..5cae7f3 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs @@ -6,7 +6,7 @@ public class EnumConverterTests : UnitTestsBase { [Fact] public void ConvertValueToEnumMember_EmptyStringValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(String.Empty)) + Invoking(() => EnumConverter.ConvertValueToEnumMember(String.Empty, typeof(TestEnum))) .Should().Throw() .WithMessage( "Could not convert an empty string or a string that consists only of white-space characters to an " + @@ -15,6 +15,118 @@ public void ConvertValueToEnumMember_EmptyStringValue_ShouldThrow() => [Fact] public void ConvertValueToEnumMember_NonEnumTargetType_ShouldThrow() + { + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(Int32))) + .Should().Throw() + .WithMessage( + $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + + $"{typeof(Int32)}, because the type {typeof(Int32)} is not an enum type.*" + ); + + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(Int32?))) + .Should().Throw() + .WithMessage( + $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + + $"{typeof(Int32?)}, because the type {typeof(Int32?)} is not an enum type.*" + ); + } + + [Fact] + public void ConvertValueToEnumMember_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + { + Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." + ); + + Invoking(() => EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." + ); + } + + [Fact] + public void ConvertValueToEnumMember_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + { + EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum?)) + .Should().BeNull(); + + EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum?)) + .Should().BeNull(); + } + + [Fact] + public void ConvertValueToEnumMember_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(999, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members." + ); + + [Theory] + [MemberData(nameof(GetConvertValueToEnumMemberTestData))] + public void + ConvertValueToEnumMember_ShouldConvertValueToEnumMember(Object value, TestEnum expectedResult) + { + EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum)) + .Should().Be(expectedResult); + + EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum?)) + .Should().Be(expectedResult); + } + + [Fact] + public void ConvertValueToEnumMember_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent", typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members." + ); + + [Fact] + public void ConvertValueToEnumMember_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the value '{Guid.Empty}' ({typeof(Guid)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." + ); + + [Fact] + public void ConvertValueToEnumMember_ValueIsOfDifferentEnumType_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the value 'Red' ({typeof(ConsoleColor)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." + ); + + [Fact] + public void ConvertValueToEnumMember_WhitespaceStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(" ", typeof(TestEnum))) + .Should().Throw() + .WithMessage( + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." + ); + + [Fact] + public void ConvertValueToEnumMemberOfT_EmptyStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(String.Empty)) + .Should().Throw() + .WithMessage( + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." + ); + + [Fact] + public void ConvertValueToEnumMemberOfT_NonEnumTargetType_ShouldThrow() { Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA")) .Should().Throw() @@ -32,7 +144,7 @@ public void ConvertValueToEnumMember_NonEnumTargetType_ShouldThrow() } [Fact] - public void ConvertValueToEnumMember_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToEnumMemberOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value)) .Should().Throw() @@ -48,7 +160,7 @@ public void ConvertValueToEnumMember_NonNullableTargetType_NullOrDBNullValue_Sho } [Fact] - public void ConvertValueToEnumMember_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToEnumMemberOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { EnumConverter.ConvertValueToEnumMember(DBNull.Value) .Should().BeNull(); @@ -58,7 +170,7 @@ public void ConvertValueToEnumMember_NullableTargetType_NullOrDBNullValue_Should } [Fact] - public void ConvertValueToEnumMember_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => + public void ConvertValueToEnumMemberOfT_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => Invoking(() => EnumConverter.ConvertValueToEnumMember(999)) .Should().Throw() .WithMessage( @@ -69,7 +181,7 @@ public void ConvertValueToEnumMember_NumericValueNotMatchingAnyEnumMemberValue_S [Theory] [MemberData(nameof(GetConvertValueToEnumMemberTestData))] public void - ConvertValueToEnumMember_ShouldConvertValueToEnumMember(Object value, TestEnum expectedResult) + ConvertValueToEnumMemberOfT_ShouldConvertValueToEnumMember(Object value, TestEnum expectedResult) { EnumConverter.ConvertValueToEnumMember(value) .Should().Be(expectedResult); @@ -79,7 +191,7 @@ public void } [Fact] - public void ConvertValueToEnumMember_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => + public void ConvertValueToEnumMemberOfT_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent")) .Should().Throw() .WithMessage( @@ -88,7 +200,7 @@ public void ConvertValueToEnumMember_StringValueNotMatchingAnyEnumMemberName_Sho ); [Fact] - public void ConvertValueToEnumMember_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => + public void ConvertValueToEnumMemberOfT_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty)) .Should().Throw() .WithMessage( @@ -98,7 +210,7 @@ public void ConvertValueToEnumMember_ValueIsNeitherEnumValueNorStringNorNumeric_ ); [Fact] - public void ConvertValueToEnumMember_ValueIsOfDifferentEnumType_ShouldThrow() => + public void ConvertValueToEnumMemberOfT_ValueIsOfDifferentEnumType_ShouldThrow() => Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red)) .Should().Throw() .WithMessage( @@ -108,7 +220,7 @@ public void ConvertValueToEnumMember_ValueIsOfDifferentEnumType_ShouldThrow() => ); [Fact] - public void ConvertValueToEnumMember_WhitespaceStringValue_ShouldThrow() => + public void ConvertValueToEnumMemberOfT_WhitespaceStringValue_ShouldThrow() => Invoking(() => EnumConverter.ConvertValueToEnumMember(" ")) .Should().Throw() .WithMessage( diff --git a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs index 710c25c..b6991ff 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs @@ -1,4 +1,5 @@ -// ReSharper disable SpecifyACultureInStringConversionExplicitly +// ReSharper disable SpecifyACultureInStringConversionExplicitly + #pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. #pragma warning disable IDE0004 @@ -14,7 +15,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Converters; public class ValueConverterTests : UnitTestsBase { [Theory] - [MemberData(nameof(GetCanConvertTestData))] + [MemberData(nameof(GetConvertTestData))] public void CanConvert_NullableSourceType_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, @@ -38,7 +39,7 @@ public void CanConvert_NullableSourceType_ShouldDetermineIfConversionIsPossible( } [Theory] - [MemberData(nameof(GetCanConvertTestData))] + [MemberData(nameof(GetConvertTestData))] public void CanConvert_NullableTargetType_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, @@ -62,25 +63,213 @@ public void CanConvert_NullableTargetType_ShouldDetermineIfConversionIsPossible( } [Theory] - [MemberData(nameof(GetCanConvertTestData))] + [MemberData(nameof(GetConvertTestData))] public void CanConvert_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, Boolean expectedCanConvert, +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters +#pragma warning disable RCS1163 // Unused parameter Object? sourceValue, Object? expectedTargetValue - ) - { +#pragma warning restore RCS1163 // Unused parameter +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + ) => ValueConverter.CanConvert(sourceType, targetType) .Should().Be( expectedCanConvert, $"{sourceType} should {(expectedCanConvert ? "" : "not ")}be convertible to {targetType}" ); + [Fact] + public void ConvertValueToType_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() + { + var character = Generate.Single(); + + ValueConverter.ConvertValueToType(character.ToString(), typeof(Char)) + .Should().Be(character); + + ValueConverter.ConvertValueToType(character.ToString(), typeof(Char?)) + .Should().Be(character); + } + + [Fact] + public void ConvertValueToType_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() + { + Invoking(() => ValueConverter.ConvertValueToType(String.Empty, typeof(Char))) + .Should().Throw() + .WithMessage( + $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly one " + + "character long." + ); + + Invoking(() => ValueConverter.ConvertValueToType(String.Empty, typeof(Char?))) + .Should().Throw() + .WithMessage( + $"Could not convert the string '' to the type {typeof(Char?)}. The string must be exactly one " + + "character long." + ); + + Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(Char))) + .Should().Throw() + .WithMessage( + $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly one " + + "character long." + ); + + Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(Char?))) + .Should().Throw() + .WithMessage( + $"Could not convert the string 'ab' to the type {typeof(Char?)}. The string must be exactly one " + + "character long." + ); + } + + [Fact] + public void + ConvertValueToType_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() + { + Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + ); + + Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum?))) + .Should().Throw() + .WithMessage( + $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + + $"{typeof(TestEnum?)}. That value does not match any of the values of the enum's members.*" + ); + } + + [Fact] + public void ConvertValueToType_EnumTargetType_ShouldConvertToEnumMember() + { + var enumValue = Generate.Single(); + + ValueConverter.ConvertValueToType((Int32)enumValue, typeof(TestEnum)) + .Should().Be(enumValue); + + ValueConverter.ConvertValueToType((Int32)enumValue, typeof(TestEnum?)) + .Should().Be(enumValue); + } + + [Fact] + public void + ConvertValueToType_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() + { + Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum))) + .Should().Throw() + .WithMessage( + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" + ); + + Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum?))) + .Should().Throw() + .WithMessage( + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum?)}. " + + "That string does not match any of the names of the enum's members.*" + ); + } + + [Fact] + public void ConvertValueToType_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + { + Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value, typeof(DateTime))) + .Should().Throw() + .WithMessage( + $"Could not convert the value {{DBNull}} to the type {typeof(DateTime)}, because the " + + "type is non-nullable.*" + ); + + Invoking(() => ValueConverter.ConvertValueToType(null, typeof(DateTime))) + .Should().Throw() + .WithMessage( + $"Could not convert the value {{null}} to the type {typeof(DateTime)}, because the type is " + + "non-nullable.*" + ); + } + + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToType_NullableSourceType_ShouldConvertValueToTargetType( + Type sourceType, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { + Assert.SkipUnless(sourceType.IsValueType, ""); + + sourceType = typeof(Nullable<>).MakeGenericType(sourceType); + sourceValue = Activator.CreateInstance(sourceType, sourceValue); + + this.ConvertValueToType_ShouldConvertValueToType( + sourceType, + targetType, + expectedCanConvert, + sourceValue, + expectedTargetValue + ); + } + + [Fact] + public void ConvertValueToType_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + { + ValueConverter.ConvertValueToType(DBNull.Value, typeof(Object)) + .Should().BeNull(); + + ValueConverter.ConvertValueToType(DBNull.Value, typeof(Int32?)) + .Should().BeNull(); + + ValueConverter.ConvertValueToType(null, typeof(Object)) + .Should().BeNull(); + + ValueConverter.ConvertValueToType(null, typeof(Int32?)) + .Should().BeNull(); + } + + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToType_NullableTargetType_ShouldConvertValueToTargetType( + Type sourceType, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { + Assert.SkipUnless(targetType.IsValueType, ""); + + targetType = typeof(Nullable<>).MakeGenericType(targetType); + expectedTargetValue = Activator.CreateInstance(targetType, expectedTargetValue); + + this.ConvertValueToType_ShouldConvertValueToType( + sourceType, + targetType, + expectedCanConvert, + sourceValue, + expectedTargetValue + ); + } + + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToType_ShouldConvertValueToType( + Type _, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { if (expectedCanConvert) { - var result = MaterializerFactoryHelper.ValueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType) - .Invoke(null, [sourceValue]); + var result = ValueConverter.ConvertValueToType(sourceValue, targetType); if (result is Byte[] resultBytes && expectedTargetValue is Byte[] expectedTargetValueBytes) { @@ -103,12 +292,8 @@ public void CanConvert_ShouldDetermineIfConversionIsPossible( } else { - Invoking(() => - MaterializerFactoryHelper.ValueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType) - .Invoke(null, [sourceValue]) - ) - .Should().Throw() - .WithInnerException() + Invoking(() => ValueConverter.ConvertValueToType(sourceValue, targetType)) + .Should().Throw() .WithMessage( $"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*" ); @@ -116,7 +301,18 @@ public void CanConvert_ShouldDetermineIfConversionIsPossible( } [Fact] - public void ConvertValueToType_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() + public void ConvertValueToType_ValueCannotBeConvertedToTargetType_ShouldThrow() => + Invoking(() => ValueConverter.ConvertValueToType("NotADate", typeof(DateTime))) + .Should().Throw() + .WithMessage( + $"Could not convert the value 'NotADate' ({typeof(String)}) to the type {typeof(DateTime)}. See " + + "inner exception for details.*" + ) + .WithInnerException() + .WithMessage("The string 'NotADate' was not recognized as a valid DateTime.*"); + + [Fact] + public void ConvertValueToTypeOfT_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() { var character = Generate.Single(); @@ -128,7 +324,7 @@ public void ConvertValueToType_CharTargetType_StringWithLengthOneValue_ShouldGet } [Fact] - public void ConvertValueToType_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() + public void ConvertValueToTypeOfT_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() { Invoking(() => ValueConverter.ConvertValueToType(String.Empty)) .Should().Throw() @@ -161,7 +357,7 @@ public void ConvertValueToType_CharTargetType_ValueIsStringWithLengthNotOne_Shou [Fact] public void - ConvertValueToType_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() + ConvertValueToTypeOfT_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() { Invoking(() => ValueConverter.ConvertValueToType(999)) .Should().Throw() @@ -179,7 +375,7 @@ public void } [Fact] - public void ConvertValueToType_EnumTargetType_ShouldConvertToEnumMember() + public void ConvertValueToTypeOfT_EnumTargetType_ShouldConvertToEnumMember() { var enumValue = Generate.Single(); @@ -192,7 +388,7 @@ public void ConvertValueToType_EnumTargetType_ShouldConvertToEnumMember() [Fact] public void - ConvertValueToType_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() + ConvertValueToTypeOfT_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() { Invoking(() => ValueConverter.ConvertValueToType("NonExistent")) .Should().Throw() @@ -210,7 +406,7 @@ public void } [Fact] - public void ConvertValueToType_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToTypeOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value)) .Should().Throw() @@ -227,8 +423,32 @@ public void ConvertValueToType_NonNullableTargetType_NullOrDBNullValue_ShouldThr ); } + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToTypeOfT_NullableSourceType_ShouldConvertValueToTargetType( + Type sourceType, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { + Assert.SkipUnless(sourceType.IsValueType, ""); + + sourceType = typeof(Nullable<>).MakeGenericType(sourceType); + sourceValue = Activator.CreateInstance(sourceType, sourceValue); + + this.ConvertValueToTypeOfT_ShouldConvertValueToType( + sourceType, + targetType, + expectedCanConvert, + sourceValue, + expectedTargetValue + ); + } + [Fact] - public void ConvertValueToType_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToTypeOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { ValueConverter.ConvertValueToType(DBNull.Value) .Should().BeNull(); @@ -243,8 +463,80 @@ public void ConvertValueToType_NullableTargetType_NullOrDBNullValue_ShouldReturn .Should().BeNull(); } + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToTypeOfT_NullableTargetType_ShouldConvertValueToTargetType( + Type sourceType, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { + Assert.SkipUnless(targetType.IsValueType, ""); + + targetType = typeof(Nullable<>).MakeGenericType(targetType); + expectedTargetValue = Activator.CreateInstance(targetType, expectedTargetValue); + + this.ConvertValueToTypeOfT_ShouldConvertValueToType( + sourceType, + targetType, + expectedCanConvert, + sourceValue, + expectedTargetValue + ); + } + + [Theory] + [MemberData(nameof(GetConvertTestData))] + public void ConvertValueToTypeOfT_ShouldConvertValueToType( + Type _, + Type targetType, + Boolean expectedCanConvert, + Object? sourceValue, + Object? expectedTargetValue + ) + { + if (expectedCanConvert) + { + var result = MaterializerFactoryHelper.ValueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType) + .Invoke(null, [sourceValue]); + + if (result is Byte[] resultBytes && expectedTargetValue is Byte[] expectedTargetValueBytes) + { + resultBytes + .Should().BeEquivalentTo( + expectedTargetValueBytes, + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" + ); + } + else + { + result + .Should().Be( + expectedTargetValue, + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" + ); + } + } + else + { + Invoking(() => + MaterializerFactoryHelper.ValueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType) + .Invoke(null, [sourceValue]) + ) + .Should().Throw() + .WithInnerException() + .WithMessage( + $"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*" + ); + } + } + [Fact] - public void ConvertValueToType_ValueCannotBeConvertedToTargetType_ShouldThrow() => + public void ConvertValueToTypeOfT_ValueCannotBeConvertedToTargetType_ShouldThrow() => Invoking(() => ValueConverter.ConvertValueToType("NotADate")) .Should().Throw() .WithMessage( @@ -255,8 +547,11 @@ public void ConvertValueToType_ValueCannotBeConvertedToTargetType_ShouldThrow() .WithMessage("The string 'NotADate' was not recognized as a valid DateTime.*"); [Fact] - public void ShouldGuardAgainstNullArguments() => + public void ShouldGuardAgainstNullArguments() + { ArgumentNullGuardVerifier.Verify(() => ValueConverter.CanConvert(typeof(Int16), typeof(Int32))); + ArgumentNullGuardVerifier.Verify(() => ValueConverter.ConvertValueToType(1, typeof(Int32))); + } public static IEnumerable<( Type SourceType, @@ -265,7 +560,7 @@ public void ShouldGuardAgainstNullArguments() => Object SourceValue, Object ExpectedTargetValue )> - GetCanConvertTestData() + GetConvertTestData() { var faker = new Faker(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/DatabaseAdapterRegistryTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/DatabaseAdapterRegistryTests.cs deleted file mode 100644 index 5fb0e3b..0000000 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/DatabaseAdapterRegistryTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.Data.Sqlite; -using MySqlConnector; -using Npgsql; -using Oracle.ManagedDataAccess.Client; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; - -namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters; - -public class DatabaseAdapterRegistryTests -{ - [Fact] - public void GetAdapter_NoAdapterRegisteredForConnectionType_ShouldThrow() => - Invoking(() => DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionC))) - .Should().Throw() - .WithMessage( - "No database adapter is registered for the database connection of the type " + - $"{typeof(FakeConnectionC)}. Please call " + - $"{nameof(DatabaseAdapterRegistry)}.{nameof(DatabaseAdapterRegistry.RegisterAdapter)} to register an " + - "adapter for that connection type." - ); - - [Fact] - public void GetAdapter_ShouldGetAdapter() - { - var adapterA = Substitute.For(); - var adapterB = Substitute.For(); - - DatabaseAdapterRegistry.RegisterAdapter(adapterA); - DatabaseAdapterRegistry.RegisterAdapter(adapterB); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionB)) - .Should().BeSameAs(adapterB); - } - - [Fact] - public void GetAdapter_ShouldGetDefaultAdapters() - { - DatabaseAdapterRegistry.GetAdapter(typeof(MySqlConnection)) - .Should().BeOfType(); - - DatabaseAdapterRegistry.GetAdapter(typeof(OracleConnection)) - .Should().BeOfType(); - - DatabaseAdapterRegistry.GetAdapter(typeof(NpgsqlConnection)) - .Should().BeOfType(); - - DatabaseAdapterRegistry.GetAdapter(typeof(SqliteConnection)) - .Should().BeOfType(); - - DatabaseAdapterRegistry.GetAdapter(typeof(SqlConnection)) - .Should().BeOfType(); - } - - [Fact] - public void RegisterAdapter_ShouldRegisterAdapter() - { - var adapterA = Substitute.For(); - - DatabaseAdapterRegistry.RegisterAdapter(adapterA); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); - - var adapterB = Substitute.For(); - - DatabaseAdapterRegistry.RegisterAdapter(adapterB); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionB)) - .Should().BeSameAs(adapterB); - } - - [Fact] - public void RegisterAdapter_ShouldReplaceRegisteredAdapter() - { - var adapterA = Substitute.For(); - - DatabaseAdapterRegistry.RegisterAdapter(adapterA); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); - - var adapterB = Substitute.For(); - - DatabaseAdapterRegistry.RegisterAdapter(adapterB); - - DatabaseAdapterRegistry.GetAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterB); - } - - [Fact] - public void ShouldGuardAgainstNullArguments() - { - ArgumentNullGuardVerifier.Verify(() => - DatabaseAdapterRegistry.GetAdapter(typeof(SqlConnection)) - ); - - ArgumentNullGuardVerifier.Verify(() => - DatabaseAdapterRegistry.RegisterAdapter(new SqlServerDatabaseAdapter()) - ); - } -} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs index e6b1eb4..50daddf 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs @@ -39,7 +39,7 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldBindEnumAsInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var parameter = Substitute.For(); @@ -57,7 +57,7 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBindEnumAsString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var parameter = Substitute.For(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs index 3125646..eb7f7f7 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.MySql; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs index 0a35b1c..128f384 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs @@ -62,7 +62,7 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldBindEnumAsInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var parameter = Substitute.For(); @@ -80,7 +80,7 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBindEnumAsString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var parameter = Substitute.For(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs index 833e17c..c478281 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Oracle; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs index 7c2fc5d..9c4064a 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs @@ -40,7 +40,7 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldBindEnumAsInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var parameter = Substitute.For(); @@ -58,7 +58,7 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBindEnumAsString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var parameter = Substitute.For(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs index c28cdb9..d6db9a5 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.PostgreSql; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs index e12e0fe..ef91a29 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs @@ -39,7 +39,7 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldBindEnumAsInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var parameter = Substitute.For(); @@ -57,7 +57,7 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBindEnumAsString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var parameter = Substitute.For(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs index 1cd7629..1f782a2 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.SqlServer; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs index 91afe73..df5c8b9 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs @@ -39,7 +39,7 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldBindEnumAsInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var parameter = Substitute.For(); @@ -57,7 +57,7 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB [Fact] public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBindEnumAsString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var parameter = Substitute.For(); diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs index 24772b9..ef3474e 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Sqlite; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs index 87e6d1a..327df9f 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters; diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs index 0aa1d76..9b84e94 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -1,6 +1,7 @@ -// ReSharper disable UnusedParameter.Local -#pragma warning disable NS1001 +// ReSharper disable UnusedParameter.Local +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.SqlStatements; using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder; @@ -8,647 +9,18 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; public class DbCommandBuilderTests : UnitTestsBase { - [Fact] - public void BuildDbCommand_CancellationToken_ShouldUseCancellationToken() - { - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - - cancellationTokenSource.Cancel(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection, - cancellationToken: cancellationToken - ); - - command.Received().Cancel(); - } - - [Fact] - public void BuildDbCommand_Code_Parameters_ShouldStoreCodeAndParameters() - { - var statement = new InterpolatedSqlStatement( - "Code", - ("Parameter1", "Value1"), - ("Parameter2", "Value2"), - ("Parameter3", "Value3") - ); - - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.MockDatabaseAdapter, this.MockDbConnection); - - command.CommandText - .Should().Be("Code"); - - command.Parameters.Count - .Should().Be(3); - - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); - - command.Parameters[0].Value - .Should().Be("Value1"); - - command.Parameters[1].ParameterName - .Should().Be("Parameter2"); - - command.Parameters[1].Value - .Should().Be("Value2"); - - command.Parameters[2].ParameterName - .Should().Be("Parameter3"); - - command.Parameters[2].Value - .Should().Be("Value3"); - } - - [Fact] - public void BuildDbCommand_CommandTimeout_ShouldUseCommandTimeout() - { - var timeout = Generate.Single(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection, - commandTimeout: timeout - ); - - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); - } - - [Fact] - public void BuildDbCommand_CommandType_ShouldUseCommandType() - { - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection, - commandType: CommandType.StoredProcedure - ); - - command.CommandType - .Should().Be(CommandType.StoredProcedure); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_DuplicateInferredName_ShouldAppendSuffix() - { - var productId = Generate.Id(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(productId)}, {Parameter(productId)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be("SELECT @ProductId, @ProductId2"); - - command.Parameters.Count - .Should().Be(2); - - command.Parameters[0].ParameterName - .Should().Be("ProductId"); - - command.Parameters[1].ParameterName - .Should().Be("ProductId2"); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_DuplicateInferredNameWithDifferentCasing_ShouldAppendSuffix() - { - var productId = Generate.Id(); - var productid = Generate.Id(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(productId)}, {Parameter(productid)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be("SELECT @ProductId, @Productid2"); - - command.Parameters.Count - .Should().Be(2); - - command.Parameters[0].ParameterName - .Should().Be("ProductId"); - - command.Parameters[1].ParameterName - .Should().Be("Productid2"); - } - - [Fact] - public void - BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var enumValue = Generate.Single(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(enumValue)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.Parameters.Count - .Should().Be(1); - - command.Parameters[0].ParameterName - .Should().Be("EnumValue"); - - command.Parameters[0].Value - .Should().Be((Int32)enumValue); - } - - [Fact] - public void - BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var enumValue = Generate.Single(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(enumValue)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.Parameters.Count - .Should().Be(1); - - command.Parameters[0].ParameterName - .Should().Be("EnumValue"); - - command.Parameters[0].Value - .Should().Be(enumValue.ToString()); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_ShouldHandleNullAndNonNullValues() - { - Int64? id1 = Generate.Id(); - Int64? id2 = null; - Object value1 = Generate.Single(); - Object? value2 = null; - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(id1)}, {Parameter(id2)}, {Parameter(value1)}, {Parameter(value2)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.Parameters.Count - .Should().Be(4); - - command.CommandText - .Should().Be("SELECT @Id1, @Id2, @Value1, @Value2"); - - command.Parameters[0].ParameterName - .Should().Be("Id1"); - - command.Parameters[0].Value - .Should().Be(id1); - - command.Parameters[1].ParameterName - .Should().Be("Id2"); - - command.Parameters[1].Value - .Should().Be(DBNull.Value); - - command.Parameters[2].ParameterName - .Should().Be("Value1"); - - command.Parameters[2].Value - .Should().Be(value1); - - command.Parameters[3].ParameterName - .Should().Be("Value2"); - - command.Parameters[3].Value - .Should().Be(DBNull.Value); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_ShouldInferNameFromValueExpressionIfPossible() - { - var productId = Generate.Id(); - static Int64 GetProductId() => Generate.Id(); -#pragma warning disable RCS1163 // Unused parameter - static Int64 GetProductIdByCategory(String category) => Generate.Id(); -#pragma warning restore RCS1163 // Unused parameter - var productIds = Generate.Ids().ToArray(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $""" - SELECT {Parameter(productId)}, - {Parameter(GetProductId())}, - {Parameter(GetProductIdByCategory("Shoes"))}, - {Parameter(productIds[1])}, - {Parameter(this.testProductId)}, - {Parameter(new { })} - """, - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be( - """ - SELECT @ProductId, - @ProductId2, - @ProductIdByCategoryShoes, - @ProductIds1, - @TestProductId, - @Parameter_6 - """ - ); - - command.Parameters.Count - .Should().Be(6); - - command.Parameters[0].ParameterName - .Should().Be("ProductId"); - - command.Parameters[1].ParameterName - .Should().Be("ProductId2"); - - command.Parameters[2].ParameterName - .Should().Be("ProductIdByCategoryShoes"); - - command.Parameters[3].ParameterName - .Should().Be("ProductIds1"); - - command.Parameters[4].ParameterName - .Should().Be("TestProductId"); - - command.Parameters[5].ParameterName - .Should().Be("Parameter_6"); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_ShouldStoreParameter() - { - var value = Generate.ScalarValue(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(value)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be("SELECT @Value"); - - command.Parameters.Count - .Should().Be(1); - - command.Parameters[0].ParameterName - .Should().Be("Value"); - - command.Parameters[0].Value - .Should().Be(value); - } - - [Fact] - public void BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpressions() - { - const Double baseDiscount = 0.1; - var entityIds = Generate.Ids(20); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $""" - SELECT {Parameter(baseDiscount * 5 / 3)}, - {Parameter(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0])} - """, - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be( - """ - SELECT @BaseDiscount53, - @EntityIdsWhereaa5SelectaaToStringToArray0 - """ - ); - - command.Parameters.Count - .Should().Be(2); - - command.Parameters[0].ParameterName - .Should().Be("BaseDiscount53"); - - command.Parameters[0].Value - .Should().Be(baseDiscount * 5 / 3); - - command.Parameters[1].ParameterName - .Should().Be("EntityIdsWhereaa5SelectaaToStringToArray0"); - - command.Parameters[1].Value - .Should().Be(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0]); - } - - [Fact] - public void BuildDbCommand_InterpolatedTemporaryTable_DatabaseAdapterDoesNotSupportTemporaryTables_ShouldThrow() - { - var entityIds = Generate.Ids(); - - this.MockDatabaseAdapter.SupportsTemporaryTables(Arg.Any()).Returns(false); - - Invoking(() => DbCommandBuilder.BuildDbCommand( - $"SELECT Value FROM {TemporaryTable(entityIds)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ) - ) - .Should().Throw() - .WithMessage( - $"The database adapter {this.MockDatabaseAdapter.GetType()} does not support " + - "(local / session-scoped) temporary tables. Therefore the temporary tables feature of " + - "DbConnectionPlus can not be used with this database." - ); - - // No temporary table used - should not throw. - Invoking(() => DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection - ) - ) - .Should().NotThrow(); - } - - [Fact] - public void BuildDbCommand_InterpolatedTemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible() - { - var entityIds = Generate.Ids(); - static List Get() => Generate.Ids(); - static List GetEntityIds() => Generate.Ids(); -#pragma warning disable RCS1163 // Unused parameter - static List GetEntityIdsByCategory(String category) => Generate.Ids(); -#pragma warning restore RCS1163 // Unused parameter - - InterpolatedSqlStatement statement = - $""" - SELECT Value FROM {TemporaryTable(entityIds)} - UNION - SELECT Value FROM {TemporaryTable(GetEntityIds())} - UNION - SELECT Value FROM {TemporaryTable(GetEntityIdsByCategory("Shoes"))} - UNION - SELECT Value FROM {TemporaryTable(this.testEntityIds)} - UNION - SELECT Value FROM {TemporaryTable(Get())} - """; - - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.MockDatabaseAdapter, this.MockDbConnection); - - var temporaryTables = statement.TemporaryTables; - - temporaryTables - .Should().HaveCount(5); - - command.CommandText - .Should().Be( - $""" - SELECT Value FROM [#{temporaryTables[0].Name}] - UNION - SELECT Value FROM [#{temporaryTables[1].Name}] - UNION - SELECT Value FROM [#{temporaryTables[2].Name}] - UNION - SELECT Value FROM [#{temporaryTables[3].Name}] - UNION - SELECT Value FROM [#{temporaryTables[4].Name}] - """ - ); - - temporaryTables[0].Name - .Should().StartWith("EntityIds_"); - - temporaryTables[1].Name - .Should().StartWith("EntityIds_"); - - temporaryTables[2].Name - .Should().StartWith("EntityIdsByCategoryShoes_"); - - temporaryTables[3].Name - .Should().StartWith("TestEntityIds_"); - - temporaryTables[4].Name - .Should().StartWith("Values_"); - } - - [Fact] - public void BuildDbCommand_InterpolatedTemporaryTable_ShouldStoreTemporaryTable() - { - var entities = Generate.Multiple(); - var entityIds = Generate.Ids(); - - InterpolatedSqlStatement statement = - $""" - SELECT Id - FROM {TemporaryTable(entities)} Entities - WHERE Entities.Id IN (SELECT Value FROM {TemporaryTable(entityIds)}) - """; - - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.MockDatabaseAdapter, this.MockDbConnection); - - var temporaryTables = statement.TemporaryTables; - - temporaryTables - .Should().HaveCount(2); - - var table1 = temporaryTables[0]; - - table1.Name - .Should().StartWith("Entities_"); - - table1.Values - .Should().Be(entities); - - table1.ValuesType - .Should().Be(typeof(Entity)); - - var table2 = temporaryTables[1]; - - table2.Name - .Should().StartWith("EntityIds_"); - - table2.Values - .Should().BeEquivalentTo(entityIds); - - table2.ValuesType - .Should().Be(typeof(Int64)); - - command.CommandText - .Should().Be( - $""" - SELECT Id - FROM [#{table1.Name}] Entities - WHERE Entities.Id IN (SELECT Value FROM [#{table2.Name}]) - """ - ); - } - - [Fact] - public void BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParameters() - { - var value1 = Generate.ScalarValue(); - var value2 = Generate.ScalarValue(); - var value3 = Generate.ScalarValue(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {Parameter(value1)}, {Parameter(value2)}, {Parameter(value3)}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should() - .Be("SELECT @Value1, @Value2, @Value3"); - - command.Parameters.Count - .Should().Be(3); - - command.Parameters[0].ParameterName - .Should().Be("Value1"); - - command.Parameters[0].Value - .Should().Be(value1); - - command.Parameters[1].ParameterName - .Should().Be("Value2"); - - command.Parameters[1].Value - .Should().Be(value2); - - command.Parameters[2].ParameterName - .Should().Be("Value3"); - - command.Parameters[2].Value - .Should().Be(value3); - } - - [Fact] - public void BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; - - var enumValue = Generate.Single(); - - var statement = new InterpolatedSqlStatement( - "Code", - ("Parameter1", enumValue) - ); - - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.MockDatabaseAdapter, this.MockDbConnection); - - command.Parameters.Count - .Should().Be(1); - - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); - - command.Parameters[0].Value - .Should().Be((Int32)enumValue); - } - - [Fact] - public void BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() - { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - - var enumValue = Generate.Single(); - - var statement = new InterpolatedSqlStatement( - "Code", - ("Parameter1", enumValue) - ); - - var (command, _) = DbCommandBuilder.BuildDbCommand(statement, this.MockDatabaseAdapter, this.MockDbConnection); - - command.Parameters.Count - .Should().Be(1); - - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); - - command.Parameters[0].Value - .Should().Be(enumValue.ToString()); - } - - [Fact] - public void BuildDbCommand_ShouldFormatAndStoreLiteral() - { - var (command, _) = DbCommandBuilder.BuildDbCommand( - $"SELECT {123.45,10:N2}, {123.45,-10:N2}", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be("SELECT 123.45, 123.45 "); - } - - [Fact] - public void BuildDbCommand_ShouldReturnCommandDisposer() - { - var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - commandDisposer - .Should().NotBeNull(); - } - - [Fact] - public void BuildDbCommand_ShouldStoreLiteral() - { - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection - ); - - command.CommandText - .Should().Be("SELECT 1"); - } - - [Fact] - public void BuildDbCommand_Transaction_ShouldUseTransaction() - { - using var transaction = this.MockDbConnection.BeginTransaction(); - - var (command, _) = DbCommandBuilder.BuildDbCommand( - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection, - transaction - ); - - command.Transaction - .Should().BeSameAs(transaction); - } - - [Fact] - public async Task BuildDbCommandAsync_CancellationToken_ShouldUseCancellationToken() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_CancellationToken_ShouldUseCancellationToken(Boolean useAsyncApi) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; await cancellationTokenSource.CancelAsync(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection, @@ -658,8 +30,10 @@ public async Task BuildDbCommandAsync_CancellationToken_ShouldUseCancellationTok command.Received().Cancel(); } - [Fact] - public async Task BuildDbCommandAsync_Code_Parameters_ShouldStoreCodeAndParameters() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_Code_Parameters_ShouldStoreCodeAndParameters(Boolean useAsyncApi) { var statement = new InterpolatedSqlStatement( "Code", @@ -668,7 +42,8 @@ public async Task BuildDbCommandAsync_Code_Parameters_ShouldStoreCodeAndParamete ("Parameter3", "Value3") ); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -699,12 +74,15 @@ public async Task BuildDbCommandAsync_Code_Parameters_ShouldStoreCodeAndParamete .Should().Be("Value3"); } - [Fact] - public async Task BuildDbCommandAsync_CommandTimeout_ShouldUseCommandTimeout() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_CommandTimeout_ShouldUseCommandTimeout(Boolean useAsyncApi) { var timeout = Generate.Single(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection, @@ -715,10 +93,13 @@ public async Task BuildDbCommandAsync_CommandTimeout_ShouldUseCommandTimeout() .Should().Be((Int32)timeout.TotalSeconds); } - [Fact] - public async Task BuildDbCommandAsync_CommandType_ShouldUseCommandType() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_CommandType_ShouldUseCommandType(Boolean useAsyncApi) { - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection, @@ -729,15 +110,20 @@ public async Task BuildDbCommandAsync_CommandType_ShouldUseCommandType() .Should().Be(CommandType.StoredProcedure); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {Parameter(enumValue)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -753,15 +139,20 @@ public async Task .Should().Be((Int32)enumValue); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {Parameter(enumValue)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -777,15 +168,18 @@ public async Task .Should().Be(enumValue.ToString()); } - [Fact] - public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldHandleNullAndNonNullValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedParameter_ShouldHandleNullAndNonNullValues(Boolean useAsyncApi) { Int64? id1 = Generate.Id(); Int64? id2 = null; Object value1 = Generate.Single(); Object? value2 = null; - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {Parameter(id1)}, {Parameter(id2)}, {Parameter(value1)}, {Parameter(value2)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -822,17 +216,24 @@ public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldHandleNullAndN .Should().Be(DBNull.Value); } - [Fact] - public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldInferNameFromValueExpressionIfPossible() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedParameter_ShouldInferNameFromValueExpressionIfPossible( + Boolean useAsyncApi + ) { var productId = Generate.Id(); static Int64 GetProductId() => Generate.Id(); #pragma warning disable RCS1163 // Unused parameter +#pragma warning disable IDE0060 // Remove unused parameter static Int64 GetProductIdByCategory(String category) => Generate.Id(); +#pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore RCS1163 // Unused parameter var productIds = Generate.Ids().ToArray(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $""" SELECT {Parameter(productId)}, {Parameter(GetProductId())}, @@ -879,12 +280,15 @@ public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldInferNameFromV .Should().Be("Parameter_6"); } - [Fact] - public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldStoreParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedParameter_ShouldStoreParameter(Boolean useAsyncApi) { var value = Generate.ScalarValue(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {Parameter(value)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -903,13 +307,16 @@ public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldStoreParameter .Should().Be(value); } - [Fact] - public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldSupportComplexExpressions() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpressions(Boolean useAsyncApi) { const Double baseDiscount = 0.1; var entityIds = Generate.Ids(20); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $""" SELECT {Parameter(baseDiscount * 5 / 3)}, {Parameter(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0])} @@ -942,15 +349,20 @@ public async Task BuildDbCommandAsync_InterpolatedParameter_ShouldSupportComplex .Should().Be(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0]); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_InterpolatedTemporaryTable_DatabaseAdapterDoesNotSupportTemporaryTables_ShouldThrow() + BuildDbCommand_InterpolatedTemporaryTable_DatabaseAdapterDoesNotSupportTemporaryTables_ShouldThrow( + Boolean useAsyncApi + ) { var entityIds = Generate.Ids(); this.MockDatabaseAdapter.SupportsTemporaryTables(Arg.Any()).Returns(false); - await Invoking(() => DbCommandBuilder.BuildDbCommandAsync( + await Invoking(() => CallApi( + useAsyncApi, $"SELECT Value FROM {TemporaryTable(entityIds)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -965,7 +377,8 @@ await Invoking(() => DbCommandBuilder.BuildDbCommandAsync( // No temporary table used - should not throw. - await Invoking(() => DbCommandBuilder.BuildDbCommandAsync( + await Invoking(() => CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection @@ -974,15 +387,21 @@ await Invoking(() => DbCommandBuilder.BuildDbCommandAsync( .Should().NotThrowAsync(); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_InterpolatedTemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible() + BuildDbCommand_InterpolatedTemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible( + Boolean useAsyncApi + ) { var entityIds = Generate.Ids(); static List Get() => Generate.Ids(); static List GetEntityIds() => Generate.Ids(); #pragma warning disable RCS1163 // Unused parameter +#pragma warning disable IDE0060 // Remove unused parameter static List GetEntityIdsByCategory(String category) => Generate.Ids(); +#pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore RCS1163 // Unused parameter InterpolatedSqlStatement statement = @@ -998,7 +417,8 @@ public async Task SELECT Value FROM {TemporaryTable(Get())} """; - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1040,8 +460,10 @@ SELECT Value FROM [#{temporaryTables[4].Name}] .Should().StartWith("Values_"); } - [Fact] - public async Task BuildDbCommandAsync_InterpolatedTemporaryTable_ShouldStoreTemporaryTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedTemporaryTable_ShouldStoreTemporaryTable(Boolean useAsyncApi) { var entities = Generate.Multiple(); var entityIds = Generate.Ids(); @@ -1053,7 +475,8 @@ SELECT Id WHERE Entities.Id IN (SELECT Value FROM {TemporaryTable(entityIds)}) """; - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1096,14 +519,17 @@ WHERE Entities.Id IN (SELECT Value FROM [#{table2.Name}]) ); } - [Fact] - public async Task BuildDbCommandAsync_MultipleInterpolatedParameters_ShouldStoreParameters() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParameters(Boolean useAsyncApi) { var value1 = Generate.ScalarValue(); var value2 = Generate.ScalarValue(); var value3 = Generate.ScalarValue(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {Parameter(value1)}, {Parameter(value2)}, {Parameter(value3)}", this.MockDatabaseAdapter, this.MockDbConnection @@ -1135,11 +561,15 @@ public async Task BuildDbCommandAsync_MultipleInterpolatedParameters_ShouldStore .Should().Be(value3); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); @@ -1148,7 +578,8 @@ public async Task ("Parameter1", enumValue) ); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1164,11 +595,15 @@ public async Task .Should().Be((Int32)enumValue); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildDbCommandAsync_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( + Boolean useAsyncApi + ) { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); @@ -1177,7 +612,8 @@ public async Task ("Parameter1", enumValue) ); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1193,10 +629,13 @@ public async Task .Should().Be(enumValue.ToString()); } - [Fact] - public async Task BuildDbCommandAsync_ShouldFormatAndStoreLiteral() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldFormatAndStoreLiteral(Boolean useAsyncApi) { - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, $"SELECT {123.45,10:N2}, {123.45,-10:N2}", this.MockDatabaseAdapter, this.MockDbConnection @@ -1206,10 +645,13 @@ public async Task BuildDbCommandAsync_ShouldFormatAndStoreLiteral() .Should().Be("SELECT 123.45, 123.45 "); } - [Fact] - public async Task BuildDbCommandAsync_ShouldReturnCommandDisposer() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldReturnCommandDisposer(Boolean useAsyncApi) { - var (_, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( + var (_, commandDisposer) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection @@ -1219,10 +661,13 @@ public async Task BuildDbCommandAsync_ShouldReturnCommandDisposer() .Should().NotBeNull(); } - [Fact] - public async Task BuildDbCommandAsync_ShouldStoreLiteral() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_ShouldStoreLiteral(Boolean useAsyncApi) { - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection @@ -1232,12 +677,15 @@ public async Task BuildDbCommandAsync_ShouldStoreLiteral() .Should().Be("SELECT 1"); } - [Fact] - public async Task BuildDbCommandAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection, @@ -1248,6 +696,50 @@ public async Task BuildDbCommandAsync_Transaction_ShouldUseTransaction() .Should().BeSameAs(transaction); } + private static Task<(DbCommand, DbCommandDisposer)> CallApi( + Boolean useAsyncApi, + InterpolatedSqlStatement statement, + IDatabaseAdapter databaseAdapter, + DbConnection connection, + DbTransaction? transaction = null, + TimeSpan? commandTimeout = null, + CommandType commandType = CommandType.Text, + CancellationToken cancellationToken = default + ) + { + if (useAsyncApi) + { + return DbCommandBuilder.BuildDbCommandAsync( + statement, + databaseAdapter, + connection, + transaction, + commandTimeout, + commandType, + cancellationToken + ); + } + + try + { + return Task.FromResult( + DbCommandBuilder.BuildDbCommand( + statement, + databaseAdapter, + connection, + transaction, + commandTimeout, + commandType, + cancellationToken + ) + ); + } + catch (Exception ex) + { + return Task.FromException<(DbCommand, DbCommandDisposer)>(ex); + } + } + private readonly List testEntityIds = Generate.Ids(); private readonly Int64 testProductId = Generate.Id(); } diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs index 6af24e0..9341184 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs @@ -1,4 +1,4 @@ -#pragma warning disable NS1001 +#pragma warning disable NS1001 using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.DbCommands; diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs index 6431927..9685496 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DbCommands; +using RentADeveloper.DbConnectionPlus.DbCommands; namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs index aca2417..9b19612 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs @@ -5,7 +5,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; public class DefaultDbCommandFactoryTests : UnitTestsBase { [Fact] - public void CreateSqlCommand_NoTimeout_ShouldUseDefaultTimeout() + public void CreateDbCommand_NoTimeout_ShouldUseDefaultTimeout() { var command = this.factory.CreateDbCommand(this.MockDbConnection, "SELECT 1"); @@ -14,7 +14,7 @@ public void CreateSqlCommand_NoTimeout_ShouldUseDefaultTimeout() } [Fact] - public void CreateSqlCommand_ShouldCreateSqlCommandWithSpecifiedSettings() + public void CreateDbCommand_ShouldCreateDbCommandWithSpecifiedSettings() { using var transaction = this.MockDbConnection.BeginTransaction(); diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs new file mode 100644 index 0000000..a70e978 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -0,0 +1,94 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests; + +public class DbConnectionExtensions_ConfigurationTests : UnitTestsBase +{ + [Fact] + public void Configure_ShouldConfigureDbConnectionPlus() + { + InterceptDbCommand interceptDbCommand = (_, _) => { }; + + Configure(config => + { + config.EnumSerializationMode = EnumSerializationMode.Integers; + config.InterceptDbCommand = interceptDbCommand; + + config.Entity() + .ToTable("MappingTestEntity"); + + config.Entity() + .Property(a => a.KeyColumn1_) + .HasColumnName("KeyColumn1") + .IsKey(); + + config.Entity() + .Property(a => a.KeyColumn2_) + .HasColumnName("KeyColumn2") + .IsKey(); + + config.Entity() + .Property(a => a.ComputedColumn_) + .HasColumnName("ComputedColumn") + .IsComputed(); + + config.Entity() + .Property(a => a.IdentityColumn_) + .HasColumnName("IdentityColumn") + .IsIdentity(); + + config.Entity() + .Property(a => a.NotMappedColumn) + .IsIgnored(); + } + ); + + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + .Should().Be(EnumSerializationMode.Integers); + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand + .Should().Be(interceptDbCommand); + + var entityTypeBuilders = DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders(); + + entityTypeBuilders + .Should().HaveCount(1); + + entityTypeBuilders + .Should().ContainKeys( + typeof(MappingTestEntityFluentApi) + ); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].TableName + .Should().Be("MappingTestEntity"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn1_"].ColumnName + .Should().Be("KeyColumn1"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn2_"].ColumnName + .Should().Be("KeyColumn2"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].ColumnName + .Should().Be("ComputedColumn"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].IsComputed + .Should().BeTrue(); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].IsIdentity + .Should().BeTrue(); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].ColumnName + .Should().Be("IdentityColumn"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMappedColumn"].IsIgnored + .Should().BeTrue(); + } + + [Fact] + public void Configure_ShouldFreezeConfiguration() + { + Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Integers); + + Invoking(() => Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Strings)) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + } +} diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs index 238976d..1fc2331 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs @@ -1,4 +1,5 @@ // ReSharper disable ReturnValueOfPureMethodIsNotUsed + #pragma warning disable NS1000, NS1004, CA1806 namespace RentADeveloper.DbConnectionPlus.UnitTests; diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs index 980578c..0bccc94 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs @@ -1,4 +1,5 @@ // ReSharper disable ReturnValueOfPureMethodIsNotUsed + #pragma warning disable NS1000, NS1004, CA1806 namespace RentADeveloper.DbConnectionPlus.UnitTests; diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.SettingsTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.SettingsTests.cs deleted file mode 100644 index b2ab96f..0000000 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.SettingsTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -using NSubstitute.DbConnection; -using RentADeveloper.DbConnectionPlus.SqlStatements; - -namespace RentADeveloper.DbConnectionPlus.UnitTests; - -public class DbConnectionExtensions_SettingsTests : UnitTestsBase -{ - [Fact] - public void InterceptDbCommand_ShouldInterceptDbCommands() - { - var interceptor = Substitute.For(); - - DbCommand? interceptedDbCommand = null; - IReadOnlyCollection? interceptedTemporaryTables = null; - - interceptor - .WhenForAnyArgs(interceptor2 => - interceptor2.Invoke( - Arg.Any(), - Arg.Any>() - ) - ) - .Do(info => - { - interceptedDbCommand = info.Arg(); - interceptedTemporaryTables = info.Arg>(); - } - ); - - DbConnectionExtensions.InterceptDbCommand = interceptor; - - this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); - - var entities = Generate.Multiple(); - var entityIds = Generate.Ids(); - var stringValue = entities[0].StringValue; - - InterpolatedSqlStatement statement = - $""" - SELECT Id, StringValue - FROM {TemporaryTable(entities)} TEntity - WHERE TEntity.Id IN ({TemporaryTable(entityIds)}) OR StringValue = {Parameter(stringValue)} - """; - - var temporaryTables = statement.TemporaryTables; - - var transaction = this.MockDbConnection.BeginTransaction(); - var timeout = Generate.Single(); - var cancellationToken = Generate.Single(); - - _ = this.MockDbConnection.Query( - statement, - transaction, - timeout, - CommandType.StoredProcedure, - cancellationToken - ).ToList(); - - interceptor.Received().Invoke( - Arg.Any(), - Arg.Any>() - ); - - interceptedDbCommand - .Should().NotBeNull(); - - interceptedDbCommand.CommandText - .Should().Be( - $""" - SELECT Id, StringValue - FROM [#{temporaryTables[0].Name}] TEntity - WHERE TEntity.Id IN ([#{temporaryTables[1].Name}]) OR StringValue = @StringValue - """ - ); - - interceptedDbCommand.Transaction - .Should().Be(transaction); - - interceptedDbCommand.CommandType - .Should().Be(CommandType.StoredProcedure); - - interceptedDbCommand.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); - - interceptedDbCommand.Parameters.Count - .Should().Be(1); - - interceptedDbCommand.Parameters[0].ParameterName - .Should().Be("StringValue"); - - interceptedDbCommand.Parameters[0].Value - .Should().Be(stringValue); - - interceptedTemporaryTables - .Should().NotBeNull(); - - interceptedTemporaryTables - .Should().BeEquivalentTo(temporaryTables); - } -} diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs index f65f191..f6b4691 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs @@ -1,4 +1,4 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests; +namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_UpdateEntitiesTests : UnitTestsBase { diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs index b068cf6..fdb004b 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs @@ -1,4 +1,4 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests; +namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_UpdateEntityTests : UnitTestsBase { diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index d7a1fe9..095c9e3 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -1,6 +1,4 @@ -#pragma warning disable IDE0200 - -using System.Reflection; +using System.Reflection; using AutoFixture; using AutoFixture.Kernel; using Bogus; @@ -153,12 +151,107 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe ); } + [Fact] + public void GetEntityTypeMetadata_FluentApiMapping_ShouldGetMetadataBasedOnFluentApiMapping() + { + var tableName = Generate.Single(); + var columnName = Generate.Single(); + + Configure(config => + { + config.Entity() + .ToTable(tableName); + + config.Entity() + .Property(a => a.Id).IsKey(); + + config.Entity() + .Property(a => a.BooleanValue).HasColumnName(columnName); + + config.Entity() + .Property(a => a.Int16Value).IsComputed(); + + config.Entity() + .Property(a => a.Int32Value).IsIdentity(); + + config.Entity() + .Property(a => a.Int64Value).IsIgnored(); + } + ); + + var metadata = EntityHelper.GetEntityTypeMetadata(typeof(Entity)); + + metadata + .Should().NotBeNull(); + + metadata.EntityType + .Should().Be(typeof(Entity)); + + metadata.TableName + .Should().Be(tableName); + + var idProperty = metadata.AllPropertiesByPropertyName["Id"]; + + idProperty.IsKey + .Should().BeTrue(); + + var booleanValueProperty = metadata.AllPropertiesByPropertyName["BooleanValue"]; + + booleanValueProperty.ColumnName + .Should().Be(columnName); + + var int16ValueProperty = metadata.AllPropertiesByPropertyName["Int16Value"]; + + int16ValueProperty.IsComputed + .Should().BeTrue(); + + var int32ValueProperty = metadata.AllPropertiesByPropertyName["Int32Value"]; + + int32ValueProperty.IsIdentity + .Should().BeTrue(); + + var int64ValueProperty = metadata.AllPropertiesByPropertyName["Int64Value"]; + + int64ValueProperty.IsIgnored + .Should().BeTrue(); + + metadata.MappedProperties + .Should() + .NotContain(int64ValueProperty); + + metadata.KeyProperties + .Should() + .Contain(idProperty); + + metadata.ComputedProperties + .Should().Contain(int16ValueProperty); + + metadata.IdentityProperty + .Should().Be(int32ValueProperty); + + metadata.InsertProperties + .Should().Contain([idProperty, booleanValueProperty]); + + metadata.UpdateProperties + .Should().Contain([booleanValueProperty]); + } + + [Fact] + public void GetEntityTypeMetadata_MoreThanOneIdentityProperty_ShouldThrow() => + Invoking(() => EntityHelper.GetEntityTypeMetadata(typeof(EntityWithMultipleIdentityProperties))) + .Should().Throw() + .WithMessage( + "There are multiple identity properties defined for the entity type " + + $"{typeof(EntityWithMultipleIdentityProperties)}. Only one property can be marked as an identity " + + "property per entity type." + ); + [Theory] - [InlineData(typeof(Entity))] - [InlineData(typeof(EntityWithTableAttribute))] - [InlineData(typeof(EntityWithIdentityAndComputedProperties))] - [InlineData(typeof(EntityWithColumnAttributes))] - public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType) + [InlineData(typeof(MappingTestEntity))] + [InlineData(typeof(MappingTestEntityAttributes))] + public void GetEntityTypeMetadata_NoFluentApiMapping_ShouldGetMetadataBasedOnDataAnnotationAttributes( + Type entityType + ) { var faker = new Faker(); @@ -194,16 +287,28 @@ public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType metadata.MappedProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsNotMapped: false })); + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false })); metadata.KeyProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsNotMapped: false, IsKeyProperty: true })); + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsKey: true })); + + metadata.ComputedProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsComputed: true })); + + metadata.IdentityProperty + .Should() + .Be(allPropertiesMetadata.FirstOrDefault(a => a is { IsIgnored: false, IsIdentity: true })); + + metadata.DatabaseGeneratedProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity))); metadata.InsertProperties .Should().BeEquivalentTo( allPropertiesMetadata.Where(a => a is - { IsNotMapped: false, DatabaseGeneratedOption: DatabaseGeneratedOption.None } + { IsIgnored: false, IsComputed: false, IsIdentity: false } ) ); @@ -211,19 +316,10 @@ public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType .Should().BeEquivalentTo( allPropertiesMetadata.Where(a => a is { - IsNotMapped: false, - IsKeyProperty: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.None - } - ) - ); - - metadata.DatabaseGeneratedProperties - .Should().BeEquivalentTo( - allPropertiesMetadata.Where(a => a is - { - IsNotMapped: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.Identity or DatabaseGeneratedOption.Computed + IsIgnored: false, + IsKey: false, + IsComputed: false, + IsIdentity: false } ) ); @@ -247,12 +343,24 @@ public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType propertyMetadata.PropertyInfo .Should().BeSameAs(property); - propertyMetadata.IsNotMapped + propertyMetadata.IsIgnored .Should().Be(property.GetCustomAttribute() is not null); - propertyMetadata.IsKeyProperty + propertyMetadata.IsKey .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.IsComputed + .Should().Be( + property.GetCustomAttribute()?.DatabaseGeneratedOption is + DatabaseGeneratedOption.Computed + ); + + propertyMetadata.IsIdentity + .Should().Be( + property.GetCustomAttribute()?.DatabaseGeneratedOption is + DatabaseGeneratedOption.Identity + ); + propertyMetadata.CanRead .Should().Be(property.CanRead); @@ -292,12 +400,6 @@ public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType propertyMetadata.PropertySetter .Should().BeNull(); } - - propertyMetadata.DatabaseGeneratedOption - .Should().Be( - property.GetCustomAttribute()?.DatabaseGeneratedOption ?? - DatabaseGeneratedOption.None - ); } } diff --git a/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs b/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs index 4e5cd4e..878017e 100644 --- a/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs @@ -8,6 +8,7 @@ global using Microsoft.Data.SqlClient; global using NSubstitute; global using RentADeveloper.ArgumentNullGuards; +global using RentADeveloper.DbConnectionPlus.Configuration; global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; global using static AwesomeAssertions.FluentActions; global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 25b4047..fc97129 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -303,6 +303,189 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsStringNotMatc ); } + [Fact] + public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() + { + var entity = Generate.Single(); + + var dataReader = Substitute.For(); + + dataReader.FieldCount.Returns(6); + + var ordinal = 0; + dataReader.GetName(ordinal).Returns("KeyColumn1"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1_); + + ordinal++; + dataReader.GetName(ordinal).Returns("KeyColumn2"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2_); + + ordinal++; + dataReader.GetName(ordinal).Returns("ValueColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.ValueColumn_); + + ordinal++; + dataReader.GetName(ordinal).Returns("ComputedColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.ComputedColumn_); + + ordinal++; + dataReader.GetName(ordinal).Returns("IdentityColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.IdentityColumn_); + + ordinal++; + var notMappedColumnOrdinal = ordinal; + dataReader.GetName(notMappedColumnOrdinal).Returns("NotMappedColumn"); + dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + var materializedEntity = materializer(dataReader); + + _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); + _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); + + materializedEntity.KeyColumn1_ + .Should().Be(entity.KeyColumn1_); + + materializedEntity.KeyColumn2_ + .Should().Be(entity.KeyColumn2_); + + materializedEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + materializedEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + materializedEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + materializedEntity.NotMappedColumn + .Should().BeNull(); + } + + [Fact] + public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() + { + MappingTestEntityFluentApi.Configure(); + + var entity = Generate.Single(); + + var dataReader = Substitute.For(); + + dataReader.FieldCount.Returns(6); + + var ordinal = 0; + dataReader.GetName(ordinal).Returns("KeyColumn1"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1_); + + ordinal++; + dataReader.GetName(ordinal).Returns("KeyColumn2"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2_); + + ordinal++; + dataReader.GetName(ordinal).Returns("ValueColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.ValueColumn_); + + ordinal++; + dataReader.GetName(ordinal).Returns("ComputedColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.ComputedColumn_); + + ordinal++; + dataReader.GetName(ordinal).Returns("IdentityColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.IdentityColumn_); + + ordinal++; + var notMappedColumnOrdinal = ordinal; + dataReader.GetName(notMappedColumnOrdinal).Returns("NotMappedColumn"); + dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + var materializedEntity = materializer(dataReader); + + _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); + _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); + + materializedEntity.KeyColumn1_ + .Should().Be(entity.KeyColumn1_); + + materializedEntity.KeyColumn2_ + .Should().Be(entity.KeyColumn2_); + + materializedEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + materializedEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + materializedEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + materializedEntity.NotMappedColumn + .Should().BeNull(); + } + + [Fact] + public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames() + { + var entity = Generate.Single(); + + var dataReader = Substitute.For(); + + dataReader.FieldCount.Returns(3); + + var ordinal = 0; + dataReader.GetName(ordinal).Returns("KeyColumn1"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1); + + ordinal++; + dataReader.GetName(ordinal).Returns("KeyColumn2"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2); + + ordinal++; + dataReader.GetName(ordinal).Returns("ValueColumn"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.ValueColumn); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + var materializedEntity = materializer(dataReader); + + materializedEntity.KeyColumn1 + .Should().Be(entity.KeyColumn1); + + materializedEntity.KeyColumn2 + .Should().Be(entity.KeyColumn2); + + materializedEntity.ValueColumn + .Should().Be(entity.ValueColumn); + } + [Fact] public void Materializer_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() { @@ -450,46 +633,6 @@ public void ); } - [Fact] - public void Materializer_NotMappedProperty_ShouldBeIgnored() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(3); - - var id = Generate.Id(); - var mappedValue = Generate.Single(); - - dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt64(0).Returns(id); - - dataReader.GetName(1).Returns("MappedValue"); - dataReader.GetFieldType(1).Returns(typeof(String)); - dataReader.IsDBNull(1).Returns(false); - dataReader.GetString(1).Returns(mappedValue); - - dataReader.GetName(2).Returns("NotMappedValue"); - dataReader.GetFieldType(2).Returns(typeof(String)); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - var entity = materializer(dataReader); - - entity.Id - .Should().Be(id); - - entity.MappedValue - .Should().Be(mappedValue); - - entity.NotMappedValue - .Should().BeNull(); - - _ = dataReader.DidNotReceive().IsDBNull(2); - _ = dataReader.DidNotReceive().GetString(2); - } - [Fact] public void Materializer_NullableCharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() @@ -598,24 +741,6 @@ public void Materializer_PropertiesWithDifferentCasing_ShouldMatchPropertiesCase .Should().BeEquivalentTo(entityWithDifferentCasingProperties); } - [Fact] - public void Materializer_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(1); - var entityWithColumnAttribute = Generate.MapTo(entities[0]); - - var dataReader = new EnumHandlingObjectReader(typeof(Entity), entities); - - dataReader.Read(); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - var materializedEntity = materializer(dataReader); - - materializedEntity - .Should().BeEquivalentTo(entityWithColumnAttribute); - } - [Fact] public void Materializer_ShouldMaterializeBinaryData() { diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs index 078f6d2..fdff508 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs @@ -187,21 +187,6 @@ public void DbDataReaderIsDBNullMethod_ShouldReferenceDbDataReaderIsDBNull() .Should().BeEquivalentTo([("ordinal", typeof(Int32))]); } - [Fact] - public void EnumConverterConvertValueToEnumMemberMethod_ShouldReferenceEnumConverterConvertValueToEnum() - { - var method = MaterializerFactoryHelper.EnumConverterConvertValueToEnumMemberMethod; - - method.DeclaringType - .Should().Be(typeof(EnumConverter)); - - method.Name - .Should().Be(nameof(EnumConverter.ConvertValueToEnumMember)); - - method.GetParameters().Select(p => (p.Name, p.ParameterType)) - .Should().BeEquivalentTo([("value", typeof(Object))]); - } - [Theory] [InlineData(typeof(Boolean), true)] [InlineData(typeof(Byte), true)] diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs index 8593bf3..3174296 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs @@ -1,4 +1,4 @@ -using System.Data.SqlTypes; +using System.Data.SqlTypes; using System.Numerics; using NSubstitute.ExceptionExtensions; using RentADeveloper.DbConnectionPlus.Materializers; diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 2e82db3..08c9ac6 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -1,4 +1,35 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/rent-a-developer/DbConnectionPlus.git")] +namespace RentADeveloper.DbConnectionPlus.Configuration +{ + public sealed class DbConnectionPlusConfiguration : RentADeveloper.DbConnectionPlus.Configuration.IFreezable + { + public RentADeveloper.DbConnectionPlus.EnumSerializationMode EnumSerializationMode { get; set; } + public RentADeveloper.DbConnectionPlus.Configuration.InterceptDbCommand? InterceptDbCommand { get; set; } + public static RentADeveloper.DbConnectionPlus.Configuration.DbConnectionPlusConfiguration Instance { get; } + public RentADeveloper.DbConnectionPlus.Configuration.EntityTypeBuilder Entity() { } + public void RegisterDatabaseAdapter(RentADeveloper.DbConnectionPlus.DatabaseAdapters.IDatabaseAdapter adapter) + where TConnection : System.Data.Common.DbConnection { } + } + public sealed class EntityPropertyBuilder : RentADeveloper.DbConnectionPlus.Configuration.IFreezable + { + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder HasColumnName(string columnName) { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsComputed() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsIdentity() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsIgnored() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsKey() { } + } + public sealed class EntityTypeBuilder : RentADeveloper.DbConnectionPlus.Configuration.IFreezable + { + public EntityTypeBuilder() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder Property(System.Linq.Expressions.Expression> propertyExpression) { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityTypeBuilder ToTable(string tableName) { } + } + public interface IFreezable + { + void Freeze(); + } + public delegate void InterceptDbCommand(System.Data.Common.DbCommand dbCommand, System.Collections.Generic.IReadOnlyList temporaryTables); +} namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters { public static class Constants @@ -6,11 +37,6 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters public const string Indent = " "; public const string SingleColumnTemporaryTableColumnName = "Value"; } - public static class DatabaseAdapterRegistry - { - public static void RegisterAdapter(RentADeveloper.DbConnectionPlus.DatabaseAdapters.IDatabaseAdapter adapter) - where TConnection : System.Data.Common.DbConnection { } - } public interface IDatabaseAdapter { RentADeveloper.DbConnectionPlus.DatabaseAdapters.IEntityManipulator EntityManipulator { get; } @@ -61,8 +87,7 @@ namespace RentADeveloper.DbConnectionPlus { public static class DbConnectionExtensions { - public static RentADeveloper.DbConnectionPlus.EnumSerializationMode EnumSerializationMode { get; set; } - public static RentADeveloper.DbConnectionPlus.InterceptDbCommand? InterceptDbCommand { get; set; } + public static void Configure(System.Action configureAction) { } public static int DeleteEntities(this System.Data.Common.DbConnection connection, System.Collections.Generic.IEnumerable entities, System.Data.Common.DbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default) where TEntity : class { } public static System.Threading.Tasks.Task DeleteEntitiesAsync(this System.Data.Common.DbConnection connection, System.Collections.Generic.IEnumerable entities, System.Data.Common.DbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default) @@ -123,9 +148,10 @@ namespace RentADeveloper.DbConnectionPlus Integers = 0, Strings = 1, } - public delegate void InterceptDbCommand(System.Data.Common.DbCommand dbCommand, System.Collections.Generic.IReadOnlyList temporaryTables); public static class ThrowHelper { + [System.Diagnostics.CodeAnalysis.DoesNotReturn] + public static void ThrowConfigurationIsFrozenException() { } [System.Diagnostics.CodeAnalysis.DoesNotReturn] public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(RentADeveloper.DbConnectionPlus.DatabaseAdapters.IDatabaseAdapter databaseAdapter) { } [System.Diagnostics.CodeAnalysis.DoesNotReturn] @@ -181,13 +207,14 @@ namespace RentADeveloper.DbConnectionPlus.Entities } public sealed record EntityPropertyMetadata : System.IEquatable { - public EntityPropertyMetadata(string ColumnName, string PropertyName, System.Type PropertyType, System.Reflection.PropertyInfo PropertyInfo, bool IsNotMapped, bool IsKeyProperty, bool CanRead, bool CanWrite, Fasterflect.MemberGetter? PropertyGetter, Fasterflect.MemberSetter? PropertySetter, System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption DatabaseGeneratedOption) { } + public EntityPropertyMetadata(string ColumnName, string PropertyName, System.Type PropertyType, System.Reflection.PropertyInfo PropertyInfo, bool IsIgnored, bool IsKey, bool IsComputed, bool IsIdentity, bool CanRead, bool CanWrite, Fasterflect.MemberGetter? PropertyGetter, Fasterflect.MemberSetter? PropertySetter) { } public bool CanRead { get; init; } public bool CanWrite { get; init; } public string ColumnName { get; init; } - public System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption DatabaseGeneratedOption { get; init; } - public bool IsKeyProperty { get; init; } - public bool IsNotMapped { get; init; } + public bool IsComputed { get; init; } + public bool IsIdentity { get; init; } + public bool IsIgnored { get; init; } + public bool IsKey { get; init; } public Fasterflect.MemberGetter? PropertyGetter { get; init; } public System.Reflection.PropertyInfo PropertyInfo { get; init; } public string PropertyName { get; init; } @@ -196,11 +223,13 @@ namespace RentADeveloper.DbConnectionPlus.Entities } public sealed record EntityTypeMetadata : System.IEquatable { - public EntityTypeMetadata(System.Type EntityType, string TableName, System.Collections.Generic.IReadOnlyList AllProperties, System.Collections.Generic.IReadOnlyDictionary AllPropertiesByPropertyName, System.Collections.Generic.IReadOnlyList MappedProperties, System.Collections.Generic.IReadOnlyList KeyProperties, System.Collections.Generic.IReadOnlyList InsertProperties, System.Collections.Generic.IReadOnlyList UpdateProperties, System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties) { } + public EntityTypeMetadata(System.Type EntityType, string TableName, System.Collections.Generic.IReadOnlyList AllProperties, System.Collections.Generic.IReadOnlyDictionary AllPropertiesByPropertyName, System.Collections.Generic.IReadOnlyList MappedProperties, System.Collections.Generic.IReadOnlyList KeyProperties, System.Collections.Generic.IReadOnlyList ComputedProperties, RentADeveloper.DbConnectionPlus.Entities.EntityPropertyMetadata? IdentityProperty, System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties, System.Collections.Generic.IReadOnlyList InsertProperties, System.Collections.Generic.IReadOnlyList UpdateProperties) { } public System.Collections.Generic.IReadOnlyList AllProperties { get; init; } public System.Collections.Generic.IReadOnlyDictionary AllPropertiesByPropertyName { get; init; } + public System.Collections.Generic.IReadOnlyList ComputedProperties { get; init; } public System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties { get; init; } public System.Type EntityType { get; init; } + public RentADeveloper.DbConnectionPlus.Entities.EntityPropertyMetadata? IdentityProperty { get; init; } public System.Collections.Generic.IReadOnlyList InsertProperties { get; init; } public System.Collections.Generic.IReadOnlyList KeyProperties { get; init; } public System.Collections.Generic.IReadOnlyList MappedProperties { get; init; } diff --git a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs index 8102dae..7e7d49e 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs @@ -2,7 +2,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Readers; -public class EnumHandlingObjectReaderTests +public class EnumHandlingObjectReaderTests : UnitTestsBase { [Fact] public void GetFieldType_CharProperty_ShouldReturnString() @@ -18,7 +18,7 @@ public void GetFieldType_CharProperty_ShouldReturnString() [Fact] public void GetFieldType_EnumValues_EnumSerializationModeIsIntegers_ShouldReturnInt32() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(1); @@ -31,7 +31,7 @@ public void GetFieldType_EnumValues_EnumSerializationModeIsIntegers_ShouldReturn [Fact] public void GetFieldType_EnumValues_EnumSerializationModeIsStrings_ShouldReturnString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(1); @@ -108,7 +108,7 @@ public void GetValues_CharProperty_ShouldConvertToString() [Fact] public void GetValues_EnumValues_EnumSerializationModeIsIntegers_ShouldSerializeEnumsAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -132,7 +132,7 @@ public void GetValues_EnumValues_EnumSerializationModeIsIntegers_ShouldSerialize [Fact] public void GetValues_EnumValues_EnumSerializationModeIsStrings_ShouldSerializeEnumsAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); diff --git a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs index 9aaf740..1477025 100644 --- a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs +++ b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.SqlStatements; +using RentADeveloper.DbConnectionPlus.SqlStatements; namespace RentADeveloper.DbConnectionPlus.UnitTests.SqlStatements; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 5ed98c7..3f06d4c 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -22,4 +22,4 @@ public record Entity public String StringValue { get; set; } = null!; public TimeOnly TimeOnlyValue { get; set; } public TimeSpan TimeSpanValue { get; set; } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs deleted file mode 100644 index 1488913..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -[Table("Entity")] -public record EntityWithColumnAttributes -{ - [Column("BooleanValue")] - public Boolean ValueBoolean { get; set; } - - [Column("ByteValue")] - public Byte ValueByte { get; set; } - - [Column("CharValue")] - public Char ValueChar { get; set; } - - [Column("DateOnlyValue")] - public DateOnly ValueDateOnly { get; set; } - - [Column("DateTimeValue")] - public DateTime ValueDateTime { get; set; } - - [Column("DecimalValue")] - public Decimal ValueDecimal { get; set; } - - [Column("DoubleValue")] - public Double ValueDouble { get; set; } - - [Column("EnumValue")] - public TestEnum ValueEnum { get; set; } - - [Column("GuidValue")] - public Guid ValueGuid { get; set; } - - [Key] - [Column("Id")] - public Int64 ValueId { get; set; } - - [Column("Int16Value")] - public Int16 ValueInt16 { get; set; } - - [Column("Int32Value")] - public Int32 ValueInt32 { get; set; } - - [Column("Int64Value")] - public Int64 ValueInt64 { get; set; } - - [Column("SingleValue")] - public Single ValueSingle { get; set; } - - [Column("StringValue")] - public String ValueString { get; set; } = null!; - - [Column("TimeOnlyValue")] - public TimeOnly ValueTimeOnly { get; set; } - - [Column("TimeSpanValue")] - public TimeSpan ValueTimeSpan { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCompositeKey.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCompositeKey.cs deleted file mode 100644 index 02ed24c..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCompositeKey.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithCompositeKey -{ - [Key] - public Int64 Key1 { get; set; } - - [Key] - public Int64 Key2 { get; set; } - - public String StringValue { get; set; } = ""; -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithIdentityAndComputedProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithIdentityAndComputedProperties.cs deleted file mode 100644 index a9de781..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithIdentityAndComputedProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithIdentityAndComputedProperties -{ - public Int64 BaseValue { get; set; } - - [DatabaseGenerated(DatabaseGeneratedOption.Computed)] - public Int64 ComputedValue { get; set; } - - [Key] - public Int64 Id { get; set; } - - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Int64 IdentityValue { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs new file mode 100644 index 0000000..5da85f8 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs @@ -0,0 +1,10 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +public class EntityWithMultipleIdentityProperties +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Int64 Identity1 { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Int64 Identity2 { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNotMappedProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNotMappedProperty.cs deleted file mode 100644 index 26f8f55..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNotMappedProperty.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithNotMappedProperty -{ - [Key] - public Int64 Id { get; set; } - - public String MappedValue { get; set; } = ""; - - [NotMapped] - public String? NotMappedValue { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index 801f4dc..f2ddfec 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -1,4 +1,6 @@ -#pragma warning disable IDE0290 +// ReSharper disable ConvertToPrimaryConstructor + +#pragma warning disable IDE0290 namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithTableAttribute.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithTableAttribute.cs deleted file mode 100644 index eb95674..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithTableAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -[Table("Entity")] -public record EntityWithTableAttribute : Entity; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 00d6d1e..d445102 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -1,5 +1,6 @@ -// ReSharper disable ConvertToLambdaExpression +// ReSharper disable ConvertToLambdaExpression // ReSharper disable RedundantTypeArgumentsOfMethod + #pragma warning disable IDE0053 using AutoFixture; @@ -106,30 +107,6 @@ static Generate() TypeAdapterConfig .NewConfig() .NameMatchingStrategy(NameMatchingStrategy.IgnoreCase); - - TypeAdapterConfig - .NewConfig() - .ConstructUsing(entity => new() - { - ValueId = entity.Id, - ValueBoolean = entity.BooleanValue, - ValueByte = entity.ByteValue, - ValueChar = entity.CharValue, - ValueDateOnly = entity.DateOnlyValue, - ValueDateTime = entity.DateTimeValue, - ValueDecimal = entity.DecimalValue, - ValueDouble = entity.DoubleValue, - ValueEnum = entity.EnumValue, - ValueGuid = entity.GuidValue, - ValueInt16 = entity.Int16Value, - ValueInt32 = entity.Int32Value, - ValueInt64 = entity.Int64Value, - ValueSingle = entity.SingleValue, - ValueString = entity.StringValue, - ValueTimeSpan = entity.TimeSpanValue, - ValueTimeOnly = entity.TimeOnlyValue - } - ); } /// @@ -164,7 +141,7 @@ public static List MapTo(IEnumerable objects) => /// /// Maps to an instance of containing the same data. /// - /// The type of object to map to. + /// The type of object to map to. /// The object to map. /// /// An instance of containing the same data as . @@ -302,7 +279,7 @@ public static T UpdateFor(T entity) /// A list with copies of where all properties except the key property / properties /// have new values. /// - public static List UpdatesFor(List entities) => + public static List UpdateFor(List entities) => [.. entities.Select(UpdateFor)]; /// diff --git a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs index 1ca6773..d38e148 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs @@ -1,4 +1,6 @@ -#pragma warning disable IDE0290 +// ReSharper disable ConvertToPrimaryConstructor + +#pragma warning disable IDE0290 namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs new file mode 100644 index 0000000..5288b27 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -0,0 +1,12 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +public record MappingTestEntity +{ + [Key] + public Int64 KeyColumn1 { get; set; } + + [Key] + public Int64 KeyColumn2 { get; set; } + + public Int32 ValueColumn { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs new file mode 100644 index 0000000..358d0d8 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -0,0 +1,29 @@ +// ReSharper disable InconsistentNaming + +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +[Table("MappingTestEntity")] +public record MappingTestEntityAttributes +{ + [Column("ComputedColumn")] + [DatabaseGenerated(DatabaseGeneratedOption.Computed)] + public Int32 ComputedColumn_ { get; set; } + + [Column("IdentityColumn")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Int32 IdentityColumn_ { get; set; } + + [Key] + [Column("KeyColumn1")] + public Int64 KeyColumn1_ { get; set; } + + [Key] + [Column("KeyColumn2")] + public Int64 KeyColumn2_ { get; set; } + + [NotMapped] + public String? NotMappedColumn { get; set; } + + [Column("ValueColumn")] + public Int32 ValueColumn_ { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs new file mode 100644 index 0000000..d4cb012 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -0,0 +1,52 @@ +// ReSharper disable InconsistentNaming + +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +public record MappingTestEntityFluentApi +{ + public Int32 ComputedColumn_ { get; set; } + public Int32 IdentityColumn_ { get; set; } + public Int64 KeyColumn1_ { get; set; } + public Int64 KeyColumn2_ { get; set; } + public String? NotMappedColumn { get; set; } + public Int32 ValueColumn_ { get; set; } + + /// + /// Configures the mapping for this entity using the Fluent API. + /// + public static void Configure() => + DbConnectionExtensions.Configure(config => + { + config.Entity() + .ToTable("MappingTestEntity"); + + config.Entity() + .Property(a => a.KeyColumn1_) + .HasColumnName("KeyColumn1") + .IsKey(); + + config.Entity() + .Property(a => a.KeyColumn2_) + .HasColumnName("KeyColumn2") + .IsKey(); + + config.Entity() + .Property(a => a.ValueColumn_) + .HasColumnName("ValueColumn"); + + config.Entity() + .Property(a => a.ComputedColumn_) + .HasColumnName("ComputedColumn") + .IsComputed(); + + config.Entity() + .Property(a => a.IdentityColumn_) + .HasColumnName("IdentityColumn") + .IsIdentity(); + + config.Entity() + .Property(a => a.NotMappedColumn) + .IsIgnored(); + } + ); +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs index cdb39dc..93e6ac0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs @@ -17,4 +17,4 @@ public class TemporaryTableTestItem public String String { get; set; } = null!; public TimeOnly TimeOnly { get; set; } public TimeSpan TimeSpan { get; set; } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index ee52b90..dfcd812 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -6,6 +6,7 @@ using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; using RentADeveloper.DbConnectionPlus.DbCommands; +using RentADeveloper.DbConnectionPlus.Entities; using RentADeveloper.DbConnectionPlus.Extensions; namespace RentADeveloper.DbConnectionPlus.UnitTests; @@ -26,8 +27,12 @@ public UnitTestsBase() // Reset all settings to defaults before each test. - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - DbConnectionExtensions.InterceptDbCommand = null; + DbConnectionPlusConfiguration.Instance = new() + { + EnumSerializationMode = EnumSerializationMode.Strings, + InterceptDbCommand = null + }; + EntityHelper.ResetEntityTypeMetadataCache(); OracleDatabaseAdapter.AllowTemporaryTables = false; this.MockDbConnection = Substitute.For().SetupCommands(); @@ -36,9 +41,9 @@ public UnitTestsBase() this.MockDatabaseAdapter = Substitute.For(); this.MockEntityManipulator = Substitute.For(); - typeof(DatabaseAdapterRegistry).GetMethod(nameof(DatabaseAdapterRegistry.RegisterAdapter))! + typeof(DbConnectionPlusConfiguration).GetMethod(nameof(DbConnectionPlusConfiguration.RegisterDatabaseAdapter))! .MakeGenericMethod(this.MockDbConnection.GetType()) - .Invoke(null, [this.MockDatabaseAdapter]); + .Invoke(DbConnectionPlusConfiguration.Instance, [this.MockDatabaseAdapter]); DbCommandFactory = this.MockCommandFactory; @@ -106,7 +111,7 @@ public UnitTestsBase() if (value is Enum enumValue) { - parameter.DbType = DbConnectionExtensions.EnumSerializationMode switch + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { EnumSerializationMode.Integers => DbType.Int32, @@ -117,12 +122,16 @@ public UnitTestsBase() _ => throw new NotSupportedException( $"The {nameof(EnumSerializationMode)} " + - $"{DbConnectionExtensions.EnumSerializationMode.ToDebugString()} is not supported." + $"{DbConnectionPlusConfiguration.Instance.EnumSerializationMode.ToDebugString()} " + + "is not supported." ) }; parameter.Value = - EnumSerializer.SerializeEnum(enumValue, DbConnectionExtensions.EnumSerializationMode); + EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); } else {