From 032f5fa07968405867625f041078da81c91dedbe Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 30 Jan 2026 09:29:11 +0100 Subject: [PATCH 01/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- CHANGELOG.md | 3 +- README.md | 2 + docs/DESIGN-DECISIONS.md | 53 ++- .../DbConnectionPlusConfiguration.cs | 112 +++++ .../Configuration/EntityPropertyBuilder.cs | 134 ++++++ .../Configuration/EntityTypeBuilder.cs | 96 +++++ .../Configuration/IEntityPropertyBuilder.cs | 37 ++ .../Configuration/IEntityTypeBuilder.cs | 22 + .../Configuration/IFreezable.cs | 12 + .../{ => Configuration}/InterceptDbCommand.cs | 2 +- .../Converters/EnumConverter.cs | 10 +- .../Converters/ValueConverter.cs | 17 +- .../DatabaseAdapterRegistry.cs | 1 + .../DatabaseAdapters/IDatabaseAdapter.cs | 2 +- .../MySql/MySqlDatabaseAdapter.cs | 11 +- .../MySql/MySqlEntityManipulator.cs | 110 ++--- .../MySql/MySqlTemporaryTableBuilder.cs | 19 +- .../Oracle/OracleDatabaseAdapter.cs | 11 +- .../Oracle/OracleEntityManipulator.cs | 6 +- .../Oracle/OracleTemporaryTableBuilder.cs | 10 +- .../PostgreSql/PostgreSqlDatabaseAdapter.cs | 11 +- .../PostgreSql/PostgreSqlEntityManipulator.cs | 118 +++--- .../PostgreSqlTemporaryTableBuilder.cs | 28 +- .../SqlServer/SqlServerDatabaseAdapter.cs | 11 +- .../SqlServer/SqlServerEntityManipulator.cs | 106 ++--- .../SqlServerTemporaryTableBuilder.cs | 22 +- .../Sqlite/SqliteDatabaseAdapter.cs | 11 +- .../Sqlite/SqliteEntityManipulator.cs | 2 +- .../Sqlite/SqliteTemporaryTableBuilder.cs | 32 +- .../DbCommands/DbCommandBuilder.cs | 6 +- .../DbConnectionExtensions.Configuration.cs | 50 +++ .../DbConnectionExtensions.ExecuteNonQuery.cs | 2 +- .../DbConnectionExtensions.ExecuteReader.cs | 2 +- .../DbConnectionExtensions.ExecuteScalar.cs | 2 +- .../DbConnectionExtensions.InsertEntities.cs | 7 +- .../DbConnectionExtensions.Parameter.cs | 2 +- .../DbConnectionExtensions.Query.cs | 2 +- .../DbConnectionExtensions.QueryFirst.cs | 2 +- .../DbConnectionExtensions.QueryFirstOfT.cs | 2 +- .../DbConnectionExtensions.Settings.cs | 80 ---- .../DbConnectionExtensions.TemporaryTable.cs | 2 +- src/DbConnectionPlus/Entities/EntityHelper.cs | 133 +++--- .../Entities/EntityPropertyMetadata.cs | 16 +- .../Entities/EntityTypeMetadata.cs | 18 +- src/DbConnectionPlus/GlobalUsings.cs | 1 + .../DisposeSignalingDataReaderDecorator.cs | 2 +- .../Readers/EnumHandlingObjectReader.cs | 11 +- .../SqlStatements/InterpolatedSqlStatement.cs | 4 +- src/DbConnectionPlus/ThrowHelper.cs | 12 + .../Assertions/EntityAssertions.cs | 2 +- .../EntityManipulator.DeleteEntitiesTests.cs | 385 +++++------------- .../EntityManipulator.DeleteEntityTests.cs | 4 +- .../EntityManipulator.InsertEntitiesTests.cs | 10 +- .../EntityManipulator.InsertEntityTests.cs | 10 +- .../EntityManipulator.UpdateEntitiesTests.cs | 14 +- .../EntityManipulator.UpdateEntityTests.cs | 14 +- .../Oracle/OracleDatabaseAdapterTests.cs | 2 +- .../PostgreSqlDatabaseAdapterTests.cs | 2 +- .../SqlServerDatabaseAdapterTests.cs | 2 +- .../TemporaryTableBuilderTests.cs | 30 +- .../DbConnectionExtensions.ParameterTests.cs | 4 +- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 5 +- ...onnectionExtensions.TemporaryTableTests.cs | 16 +- .../GlobalUsings.cs | 1 + .../IntegrationTestsBase.cs | 23 +- .../Assertions/DecoratorAssertions.cs | 2 +- .../DbConnectionPlusConfigurationTests.cs | 203 +++++++++ .../EntityPropertyBuilderTests.cs | 198 +++++++++ .../Configuration/EntityTypeBuilderTests.cs | 123 ++++++ .../MySql/MySqlDatabaseAdapterTests.cs | 4 +- .../MySql/MySqlTemporaryTableBuilderTests.cs | 2 +- .../Oracle/OracleDatabaseAdapterTests.cs | 4 +- .../OracleTemporaryTableBuilderTests.cs | 2 +- .../PostgreSqlDatabaseAdapterTests.cs | 4 +- .../PostgreSqlTemporaryTableBuilderTests.cs | 2 +- .../SqlServerDatabaseAdapterTests.cs | 4 +- .../SqlServerTemporaryTableBuilderTests.cs | 2 +- .../Sqlite/SqliteDatabaseAdapterTests.cs | 4 +- .../SqliteTemporaryTableBuilderTests.cs | 2 +- .../TemporaryTableDisposerTests.cs | 2 +- .../DbCommands/DbCommandBuilderTests.cs | 19 +- .../DbCommands/DbCommandDisposerTests.cs | 2 +- .../DbCommands/DbCommandHelperTests.cs | 2 +- ...ConnectionExtensions.ConfigurationTests.cs | 88 ++++ .../DbConnectionExtensions.QueryOfTTests.cs | 1 + .../DbConnectionExtensions.QueryTests.cs | 1 + .../DbConnectionExtensions.SettingsTests.cs | 100 ----- ...onnectionExtensions.UpdateEntitiesTests.cs | 2 +- ...bConnectionExtensions.UpdateEntityTests.cs | 2 +- .../Entities/EntityHelperTests.cs | 146 +++++-- .../GlobalUsings.cs | 1 + .../EntityMaterializerFactoryTests.cs | 37 +- .../ValueTupleMaterializerFactoryTests.cs | 2 +- ...piTest.PublicApiHasNotChanged.verified.txt | 47 ++- .../Readers/EnumHandlingObjectReaderTests.cs | 8 +- .../InterpolatedSqlStatementTests.cs | 2 +- .../TestData/Entity.cs | 2 +- .../TestData/EntityWithColumnAttributes.cs | 24 +- .../TestData/Generate.cs | 9 +- .../TestData/TemporaryTableTestItem.cs | 2 +- .../UnitTestsBase.cs | 14 +- 101 files changed, 2015 insertions(+), 945 deletions(-) create mode 100644 src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs create mode 100644 src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs create mode 100644 src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs create mode 100644 src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs create mode 100644 src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs create mode 100644 src/DbConnectionPlus/Configuration/IFreezable.cs rename src/DbConnectionPlus/{ => Configuration}/InterceptDbCommand.cs (91%) create mode 100644 src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs delete mode 100644 src/DbConnectionPlus/DbConnectionExtensions.Settings.cs create mode 100644 tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs create mode 100644 tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs create mode 100644 tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs create mode 100644 tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.SettingsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d7064..db8b357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ 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 global settings and entity mappings - 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 diff --git a/README.md b/README.md index 4fcf945..d49c5ea 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Other database systems and database connectors can be supported by implementing All examples in this document use SQL Server. +TODO: Update for Fluent API and update version + ## Table of contents - **[Quick start](#quick-start)** - [Examples](#examples) diff --git a/docs/DESIGN-DECISIONS.md b/docs/DESIGN-DECISIONS.md index 1e99fa8..8502b7a 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 properties + - 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 @@ -994,6 +1035,8 @@ public List Query_Entities_DbConnectionPlus() ## Configuration and Extensibility +TODO: Update this section. + ### Global Configuration **Decision:** Use static properties for global settings that rarely change. diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs new file mode 100644 index 0000000..b349c67 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -0,0 +1,112 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// The configuration for DbConnectionPlus. +/// +public sealed class DbConnectionPlusConfiguration : IFreezable +{ + /// + /// + /// 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 . + /// + 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. + /// + 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. + public EntityTypeBuilder Entity() + { + this.EnsureNotFrozen(); + + return (EntityTypeBuilder)this.entityTypeBuilders.GetOrAdd( + typeof(TEntity), + _ => new EntityTypeBuilder() + ); + } + + /// + 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(); + + /// + /// Gets the configured entity type builders. + /// + /// The configured entity type builders. + internal IReadOnlyDictionary GetEntityTypeBuilders() => this.entityTypeBuilders; + + private void EnsureNotFrozen() + { + if (this.isFrozen) + { + ThrowHelper.ThrowConfigurationIsFrozenException(); + } + } + + 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..3d414ce --- /dev/null +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -0,0 +1,134 @@ +namespace RentADeveloper.DbConnectionPlus.Configuration; + +/// +/// A builder for configuring an entity property. +/// +public sealed class EntityPropertyBuilder : IEntityPropertyBuilder +{ + 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. + 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. + 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. + 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. + public EntityPropertyBuilder IsIgnored() + { + this.EnsureNotFrozen(); + + this.isIgnored = true; + return this; + } + + /// + /// Marks the property as a key property. + /// + /// This builder instance for further configuration. + 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; + + 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..96907b6 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs @@ -0,0 +1,96 @@ +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 . + /// + public EntityPropertyBuilder Property(Expression> propertyExpression) + { + ArgumentNullException.ThrowIfNull(propertyExpression); + + 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. + public EntityTypeBuilder ToTable(String tableName) + { + this.EnsureNotFrozen(); + + this.tableName = tableName; + + return this; + } + + /// + void IFreezable.Freeze() + { + this.isFrozen = true; + + foreach (var propertyBuilder in this.propertyBuilders.Values) + { + propertyBuilder.Freeze(); + } + } + + /// + Type IEntityTypeBuilder.EntityType => typeof(TEntity); + + /// + IReadOnlyDictionary IEntityTypeBuilder.PropertyBuilders => + this.propertyBuilders; + + /// + String? IEntityTypeBuilder.TableName => this.tableName; + + private void EnsureNotFrozen() + { + if (this.isFrozen) + { + ThrowHelper.ThrowConfigurationIsFrozenException(); + } + } + + 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/IEntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs new file mode 100644 index 0000000..07fc5a2 --- /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 property being configured. + /// + internal String PropertyName { get; } + + /// + /// 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; } +} 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/Configuration/IFreezable.cs b/src/DbConnectionPlus/Configuration/IFreezable.cs new file mode 100644 index 0000000..613ee94 --- /dev/null +++ b/src/DbConnectionPlus/Configuration/IFreezable.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/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..b703730 100644 --- a/src/DbConnectionPlus/Converters/EnumConverter.cs +++ b/src/DbConnectionPlus/Converters/EnumConverter.cs @@ -132,10 +132,16 @@ 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. } } diff --git a/src/DbConnectionPlus/Converters/ValueConverter.cs b/src/DbConnectionPlus/Converters/ValueConverter.cs index bf795f1..1bdef09 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,13 +204,21 @@ 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 } } diff --git a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs b/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs index 06ce2ad..82ed2b5 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs @@ -13,6 +13,7 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters; +// TODO: Move to DbConnectionPlusConfiguration. /// /// A registry for database adapters that adopt DbConnectionPlus to specific database systems. /// 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/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..dcfd31c 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -899,6 +899,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 +1025,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,9 +1172,9 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => sqlBuilder.Append(Constants.Indent); - var identityProperty = entityTypeMetadata.DatabaseGeneratedProperties.FirstOrDefault(a => - a.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity - ); +#pragma warning disable CA1826 + var identityProperty = entityTypeMetadata.IdentityProperties.FirstOrDefault(); +#pragma warning restore CA1826 if (identityProperty is not null) { 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..b1ad057 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.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; @@ -640,7 +640,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 +691,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); diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs index dc2ae73..4716dbf 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 ); @@ -166,7 +166,7 @@ public async Task BuildTemporaryTableAsync( // ReSharper disable once PossibleMultipleEnumeration values, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); @@ -187,7 +187,7 @@ public async Task BuildTemporaryTableAsync( this.BuildCreateMultiColumnTemporaryTableSqlCode( quotedTableName, valuesType, - DbConnectionExtensions.EnumSerializationMode + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ), transaction ); 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..ca1ce6c 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.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; @@ -771,7 +771,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 +839,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 +921,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 +1047,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. /// diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs index d8f572a..e79d489 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..b32a5f0 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.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; @@ -890,6 +890,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 +1016,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. /// 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..0a01b99 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.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; 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.ExecuteNonQuery.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs index cd209f0..ef07844 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; diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs index 704f17d..99018eb 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; diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs index 969201b..de3d649 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; diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs index 5938987..99729f5 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs @@ -188,6 +188,11 @@ public static Task InsertEntitiesAsync( var databaseAdapter = DatabaseAdapterRegistry.GetAdapter(connection.GetType()); return databaseAdapter.EntityManipulator - .InsertEntitiesAsync(connection, entities, transaction, cancellationToken); + .InsertEntitiesAsync( + connection, + entities, + transaction, + cancellationToken + ); } } 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..6c9e772 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; diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs index d992b53..723d94e 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; diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs index b1951f3..f8767f2 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; 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/Entities/EntityHelper.cs b/src/DbConnectionPlus/Entities/EntityHelper.cs index afd9451..3e328ff 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; @@ -122,6 +122,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 . /// @@ -131,7 +137,27 @@ public static EntityTypeMetadata GetEntityTypeMetadata(Type entityType) /// private static EntityTypeMetadata CreateEntityTypeMetadata(Type entityType) { - var tableName = entityType.GetCustomAttribute()?.Name ?? entityType.Name; + // TODO: Throw exception when there is more than one identity property and change + // EntityTypeMetadata.IdentityProperties to a single property. + + 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,60 +165,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 + ); + } } return new( entityType, tableName, - AllProperties: propertiesMetadata, - AllPropertiesByPropertyName: propertiesMetadata + propertiesMetadata, + propertiesMetadata .ToDictionary(p => p.PropertyName), - MappedProperties: propertiesMetadata - .Where(p => !p.IsNotMapped) + propertiesMetadata + .Where(p => !p.IsIgnored) + .ToList(), + propertiesMetadata + .Where(p => p is { IsIgnored: false, IsKey: true }) + .ToList(), + propertiesMetadata + .Where(p => p is { IsIgnored: false, IsComputed: true }) .ToList(), - KeyProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - IsKeyProperty: true - }) + propertiesMetadata + .Where(p => p is { IsIgnored: false, IsIdentity: true }) .ToList(), - InsertProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.None - }) + propertiesMetadata + .Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity)) .ToList(), - UpdateProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - IsKeyProperty: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.None - }) + propertiesMetadata + .Where(p => p is { IsIgnored: false, IsComputed: false, IsIdentity: false }) .ToList(), - DatabaseGeneratedProperties: propertiesMetadata - .Where(p => p is - { - IsNotMapped: false, - DatabaseGeneratedOption: DatabaseGeneratedOption.Identity or DatabaseGeneratedOption.Computed - } - ) + 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..7c25033 100644 --- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs @@ -20,15 +20,21 @@ 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 properties of the entity type. +/// +/// +/// 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 +42,9 @@ public sealed record EntityTypeMetadata( IReadOnlyDictionary AllPropertiesByPropertyName, IReadOnlyList MappedProperties, IReadOnlyList KeyProperties, + IReadOnlyList ComputedProperties, + IReadOnlyList IdentityProperties, + 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/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..312b5c0 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 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..2b20595 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; @@ -22,6 +23,17 @@ public sealed class EntityManipulator_DeleteEntitiesTests_SqlServer : EntityManipulator_DeleteEntitiesTests; +// TODO: Implement integration test (CRUD, Query, Temporary Tables) for fluent API config as well as attribute based +// config. + +// TODO: Table mapping via Type Name +// TODO: Table Name mapping via Attribute +// TODO: Table Name mapping via Fluent API +// TODO: Column Name mapping via Attribute +// TODO: Column Name mapping via Fluent API +// TODO: Key Property mapping via Attribute +// TODO: Key Property mapping via Fluent API + public abstract class EntityManipulator_DeleteEntitiesTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() @@ -30,8 +42,12 @@ 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, ""); @@ -41,14 +57,15 @@ public void DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellation 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) @@ -59,31 +76,17 @@ public void DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellation } } - [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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entitiesToDelete = this.CreateEntitiesInDb(); - this.manipulator.DeleteEntities( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -97,98 +100,17 @@ public void DeleteEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeName } } - [Fact] - public void DeleteEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( + Boolean useAsyncApi + ) { 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_MoreThan10Entities_ShouldBatchDeleteIfPossible() - { - // 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 entitiesToDelete = this.CreateEntitiesInDb(20); - - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - 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) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void DeleteEntities_MoreThan10EntitiesWithCompositeKey_ShouldBatchDeleteIfPossible() - { - // 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 entitiesToDelete = this.CreateEntitiesInDb(20); - - this.manipulator.DeleteEntities( - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void DeleteEntities_ShouldHandleEntityWithCompositeKey() - { - var entities = this.CreateEntitiesInDb(); - - this.manipulator.DeleteEntities( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -202,113 +124,15 @@ public void DeleteEntities_ShouldHandleEntityWithCompositeKey() } } - [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) - { - 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 DeleteEntitiesAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.manipulator.DeleteEntitiesAsync( + return Invoking(() => this.CallApi( + useAsyncApi, this.Connection, [entityWithoutKeyProperty], null, @@ -322,53 +146,18 @@ public Task DeleteEntitiesAsync_EntitiesHaveNoKeyProperty_ShouldThrow() ); } - [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)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldBatchDeleteIfPossible(Boolean useAsyncApi) { // 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 entitiesToDelete = this.CreateEntitiesInDb(20); - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -382,8 +171,10 @@ await this.manipulator.DeleteEntitiesAsync( } } - [Fact] - public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need // to test that as well. @@ -391,7 +182,8 @@ public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldUseConfiguredColu var entities = this.CreateEntitiesInDb(20); var entitiesWithColumnAttributes = Generate.MapTo(entities); - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entitiesWithColumnAttributes, null, @@ -405,12 +197,15 @@ await this.manipulator.DeleteEntitiesAsync( } } - [Fact] - public async Task DeleteEntitiesAsync_ShouldHandleEntityWithCompositeKey() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -424,12 +219,15 @@ await this.manipulator.DeleteEntitiesAsync( } } - [Fact] - public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entitiesToDelete = this.CreateEntitiesInDb(); - (await this.manipulator.DeleteEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -437,7 +235,8 @@ public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() )) .Should().Be(entitiesToDelete.Count); - (await this.manipulator.DeleteEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entitiesToDelete, null, @@ -446,13 +245,16 @@ public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task DeleteEntitiesAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var entitiesWithColumnAttributes = Generate.MapTo(entities); - await this.manipulator.DeleteEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entitiesWithColumnAttributes, null, @@ -466,14 +268,17 @@ await this.manipulator.DeleteEntitiesAsync( } } - [Fact] - public async Task DeleteEntitiesAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntitiesAsync_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 +301,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..d791da1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -57,7 +57,7 @@ public void DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIs } [Fact] - public void DeleteEntity_EntityHasNoKeyProperty_ShouldThrow() + public void DeleteEntity_MissingKeyProperty_ShouldThrow() { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); @@ -214,7 +214,7 @@ await Invoking(() => this.manipulator.DeleteEntityAsync( } [Fact] - public Task DeleteEntityAsync_EntityHasNoKeyProperty_ShouldThrow() + public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow() { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 8b4f3d7..4938ec7 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -89,7 +89,7 @@ public void InsertEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttr [Fact] public void InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -105,7 +105,7 @@ public void InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValues [Fact] public void InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -327,7 +327,7 @@ await this.manipulator.InsertEntitiesAsync( [Fact] public async Task InsertEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -348,7 +348,7 @@ await this.manipulator.InsertEntitiesAsync( [Fact] public async Task InsertEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index a613aa8..3c5ffb9 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -80,7 +80,7 @@ public void InsertEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribut [Fact] public void InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); @@ -96,7 +96,7 @@ public void InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAs [Fact] public void InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); @@ -289,7 +289,7 @@ await this.manipulator.InsertEntityAsync( [Fact] public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); @@ -305,7 +305,7 @@ public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreE [Fact] public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 315fe08..09ae431 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -55,7 +55,7 @@ public void UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellation } [Fact] - public void UpdateEntities_EntitiesHaveNoKeyProperty_ShouldThrow() => + public void UpdateEntities_MissingKeyProperty_ShouldThrow() => Invoking(() => this.manipulator.UpdateEntities( this.Connection, @@ -108,7 +108,7 @@ public void UpdateEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttr [Fact] public void UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -141,7 +141,7 @@ public void UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValues [Fact] public void UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -365,7 +365,7 @@ await Invoking(() => } [Fact] - public Task UpdateEntitiesAsync_EntitiesHaveNoKeyProperty_ShouldThrow() => + public Task UpdateEntitiesAsync_MissingKeyProperty_ShouldThrow() => Invoking(() => this.manipulator.UpdateEntitiesAsync( this.Connection, @@ -423,7 +423,7 @@ await this.manipulator.UpdateEntitiesAsync( [Fact] public async Task UpdateEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -461,7 +461,7 @@ await this.manipulator.UpdateEntitiesAsync( [Fact] public async Task UpdateEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 002da97..fa29d60 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -55,7 +55,7 @@ public void UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIs } [Fact] - public void UpdateEntity_EntityHasNoKeyProperty_ShouldThrow() => + public void UpdateEntity_MissingKeyProperty_ShouldThrow() => Invoking(() => this.manipulator.UpdateEntity( this.Connection, @@ -108,7 +108,7 @@ public void UpdateEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribut [Fact] public void UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); @@ -141,7 +141,7 @@ public void UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAs [Fact] public void UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); @@ -360,7 +360,7 @@ await Invoking(() => } [Fact] - public Task UpdateEntityAsync_EntityHasNoKeyProperty_ShouldThrow() => + public Task UpdateEntityAsync_MissingKeyProperty_ShouldThrow() => Invoking(() => this.manipulator.UpdateEntityAsync( this.Connection, @@ -418,7 +418,7 @@ await this.manipulator.UpdateEntityAsync( [Fact] public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entity = Generate.Single(); @@ -451,7 +451,7 @@ await this.manipulator.UpdateEntityAsync( [Fact] public async Task UpdateEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entity = Generate.Single(); 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..f99f5af 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -1,4 +1,4 @@ -using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.Extensions; using RentADeveloper.DbConnectionPlus.UnitTests.Assertions; @@ -57,7 +57,7 @@ public void BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupp [Fact] public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -96,7 +96,7 @@ public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_S [Fact] public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -138,7 +138,7 @@ public void { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; using var tableDisposer = this.builder.BuildTemporaryTable( this.Connection, @@ -289,7 +289,7 @@ public void BuildTemporaryTable_ScalarValues_DateTimeOffsetValues_ShouldSupportD [Fact] public void BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var values = Generate.Multiple(); @@ -328,7 +328,7 @@ public void BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_Sho [Fact] public void BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.Multiple(); @@ -370,7 +370,7 @@ public void { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; using var tableDisposer = this.builder.BuildTemporaryTable( this.Connection, @@ -391,7 +391,7 @@ public void public void BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.MultipleNullable(); @@ -521,7 +521,7 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_DateTimeOffsetProperty public async Task BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); @@ -561,7 +561,7 @@ public async Task public async Task BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); @@ -603,7 +603,7 @@ public async Task { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( this.Connection, @@ -756,7 +756,7 @@ public async Task BuildTemporaryTableAsync_ScalarValues_DateTimeOffsetValues_Sho public async Task BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var values = Generate.Multiple(); @@ -796,7 +796,7 @@ public async Task public async Task BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.Multiple(); @@ -838,7 +838,7 @@ public async Task { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( this.Connection, @@ -859,7 +859,7 @@ public async Task public async Task BuildTemporaryTableAsync_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var values = Generate.MultipleNullable(); 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.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index a5ba3d7..de4165d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -626,7 +626,10 @@ public void QueryFirstOrDefault_Parameter_ShouldPassParameter() ("Id", entities[0].Id) ); - this.Connection.QueryFirstOrDefault(statement, cancellationToken: TestContext.Current.CancellationToken) + this.Connection.QueryFirstOrDefault( + statement, + cancellationToken: TestContext.Current.CancellationToken + ) .Should().Be(entities[0]); } 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..27f6f83 100644 --- a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs @@ -3,6 +3,7 @@ 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..28eb2d3 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,15 @@ 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(); + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = DbCommandLogger.LogDbCommand; } /// @@ -231,7 +234,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 +267,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.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..dc45292 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -0,0 +1,203 @@ +using NSubstitute.DbConnection; +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"); + + ((IFreezable)configuration).Freeze(); + + Invoking(() => configuration.EnumSerializationMode = EnumSerializationMode.Integers) + .Should().Throw() + .WithMessage("This configuration is frozen and can no longer be modified."); + + Invoking(() => configuration.InterceptDbCommand = null) + .Should().Throw() + .WithMessage("This configuration is frozen and can no longer be modified."); + + Invoking(() => configuration.Entity()) + .Should().Throw() + .WithMessage("This configuration is frozen and can no longer be modified."); + + Invoking(() => entityTypeBuilder.ToTable("Entities")) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + } + + [Fact] + public void GetEntityTypeBuilders_ShouldGetConfiguredBuilders() + { + var configuration = new DbConnectionPlusConfiguration(); + + var entityBuilder = configuration.Entity(); + var entityWithIdentityAndComputedPropertiesBuilder = + configuration.Entity(); + var entityWithNotMappedPropertyBuilder = configuration.Entity(); + + var entityTypeBuilders = configuration.GetEntityTypeBuilders(); + + entityTypeBuilders + .Should().ContainKeys( + typeof(Entity), + typeof(EntityWithIdentityAndComputedProperties), + typeof(EntityWithNotMappedProperty) + ); + + entityTypeBuilders[typeof(Entity)] + .Should().BeSameAs(entityBuilder); + + entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)] + .Should().BeSameAs(entityWithIdentityAndComputedPropertiesBuilder); + + entityTypeBuilders[typeof(EntityWithNotMappedProperty)] + .Should().BeSameAs(entityWithNotMappedPropertyBuilder); + } + + [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); + } +} diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs new file mode 100644 index 0000000..21bb7d0 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -0,0 +1,198 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; + +public class EntityPropertyBuilderTests +{ + [Fact] + public void Freeze_ShouldFreezeBuilder() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + ((IFreezable)builder).Freeze(); + + Invoking(() => builder.HasColumnName("Identifier")) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.IsComputed()) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.IsIdentity()) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.IsIgnored()) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.IsKey()) + .Should().Throw() + .WithMessage("This builder 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..ffcdaa0 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs @@ -0,0 +1,123 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; + +public class EntityTypeBuilderTests +{ + [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("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.Property(a => a.Id).HasColumnName("Identifier")) + .Should().Throw() + .WithMessage("This builder is frozen and can no longer be modified."); + + Invoking(() => builder.Property(a => a.StringValue).HasColumnName("String")) + .Should().Throw() + .WithMessage("This builder 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/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..1718bf9 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -1,4 +1,5 @@ -// ReSharper disable UnusedParameter.Local +// ReSharper disable UnusedParameter.Local + #pragma warning disable NS1001 using RentADeveloper.DbConnectionPlus.SqlStatements; @@ -146,7 +147,7 @@ public void BuildDbCommand_InterpolatedParameter_DuplicateInferredNameWithDiffer public void BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); @@ -170,7 +171,7 @@ public void public void BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); @@ -540,7 +541,7 @@ public void BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParameters( [Fact] public void BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); @@ -564,7 +565,7 @@ public void BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_S [Fact] public void BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); @@ -733,7 +734,7 @@ public async Task BuildDbCommandAsync_CommandType_ShouldUseCommandType() public async Task BuildDbCommandAsync_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); @@ -757,7 +758,7 @@ public async Task public async Task BuildDbCommandAsync_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); @@ -1139,7 +1140,7 @@ public async Task BuildDbCommandAsync_MultipleInterpolatedParameters_ShouldStore public async Task BuildDbCommandAsync_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); @@ -1168,7 +1169,7 @@ public async Task public async Task BuildDbCommandAsync_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); 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/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs new file mode 100644 index 0000000..175551c --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -0,0 +1,88 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests; + +public class DbConnectionExtensions_ConfigurationTests : UnitTestsBase +{ + [Fact] + public void Configure_ShouldConfigureDbConnectionPlus() + { + InterceptDbCommand interceptDbCommand = (_, _) => { }; + + Configure(configuration => + { + configuration.EnumSerializationMode = EnumSerializationMode.Integers; + configuration.InterceptDbCommand = interceptDbCommand; + + configuration.Entity() + .ToTable("Entities"); + + configuration.Entity() + .Property(a => a.Id) + .HasColumnName("Identifier") + .IsKey(); + + configuration.Entity() + .Property(a => a.ComputedValue) + .IsComputed(); + + configuration.Entity() + .Property(a => a.NotMappedValue) + .IsIgnored(); + } + ); + + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + .Should().Be(EnumSerializationMode.Integers); + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand + .Should().Be(interceptDbCommand); + + var entityTypeBuilders = DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders(); + + entityTypeBuilders + .Should().HaveCount(3); + + entityTypeBuilders + .Should().ContainKeys( + typeof(Entity), + typeof(EntityWithIdentityAndComputedProperties), + typeof(EntityWithNotMappedProperty) + ); + + entityTypeBuilders[typeof(Entity)].TableName + .Should().Be("Entities"); + + entityTypeBuilders[typeof(Entity)].PropertyBuilders["Id"].ColumnName + .Should().Be("Identifier"); + + entityTypeBuilders[typeof(Entity)].PropertyBuilders["Id"].IsKey + .Should().BeTrue(); + + entityTypeBuilders + .Should().ContainKey(typeof(EntityWithIdentityAndComputedProperties)); + + entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)].TableName + .Should().BeNull(); + + entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)].PropertyBuilders["ComputedValue"].IsComputed + .Should().BeTrue(); + + entityTypeBuilders + .Should().ContainKey(typeof(EntityWithNotMappedProperty)); + + entityTypeBuilders[typeof(EntityWithNotMappedProperty)].TableName + .Should().BeNull(); + + entityTypeBuilders[typeof(EntityWithNotMappedProperty)].PropertyBuilders["NotMappedValue"].IsIgnored + .Should().BeTrue(); + } + + [Fact] + public void Configure_ShouldFreezeConfiguration() + { + Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Integers); + + Invoking(() => Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Strings)) + .Should().Throw() + .WithMessage("This configuration 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..df80bcc 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -153,12 +153,99 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe ); } + [Fact] + public void GetEntityTypeMetadata_FluentAPIConfig_ShouldGetMetadataBasedOnFluentAPIConfig() + { + 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.IdentityProperties + .Should().Contain(int32ValueProperty); + + metadata.InsertProperties + .Should().Contain([idProperty, booleanValueProperty]); + + metadata.UpdateProperties + .Should().Contain([booleanValueProperty]); + } + [Theory] [InlineData(typeof(Entity))] [InlineData(typeof(EntityWithTableAttribute))] [InlineData(typeof(EntityWithIdentityAndComputedProperties))] [InlineData(typeof(EntityWithColumnAttributes))] - public void GetEntityTypeMetadata_ShouldGetMetadataForEntityType(Type entityType) + public void GetEntityTypeMetadata_NoFluentAPIConfig_ShouldGetMetadataBasedOnDataAnnotationAttributes( + Type entityType + ) { var faker = new Faker(); @@ -194,16 +281,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.IdentityProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(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 +310,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 +337,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 +394,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..855ea3b 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -598,24 +598,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() { @@ -667,6 +649,25 @@ public void Materializer_ShouldMaterializeDateTimeOffsetValue() .Should().BeEquivalentTo(entity); } + [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 ShouldGuardAgainstNullArguments() => ArgumentNullGuardVerifier.Verify(() => 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..0226ab8 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -1,4 +1,34 @@ [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 DbConnectionPlusConfiguration() { } + public RentADeveloper.DbConnectionPlus.EnumSerializationMode EnumSerializationMode { get; set; } + public RentADeveloper.DbConnectionPlus.Configuration.InterceptDbCommand? InterceptDbCommand { get; set; } + public RentADeveloper.DbConnectionPlus.Configuration.EntityTypeBuilder Entity() { } + } + public sealed class EntityPropertyBuilder : RentADeveloper.DbConnectionPlus.Configuration.IFreezable + { + public EntityPropertyBuilder() { } + 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 @@ -61,8 +91,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,7 +152,6 @@ 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] @@ -181,13 +209,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 +225,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, System.Collections.Generic.IReadOnlyList IdentityProperties, 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 System.Collections.Generic.IReadOnlyList IdentityProperties { 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..6cfb364 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs @@ -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 index 1488913..ce7fd61 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs @@ -8,25 +8,25 @@ public record EntityWithColumnAttributes [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; } @@ -39,19 +39,19 @@ public record EntityWithColumnAttributes [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/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 00d6d1e..4d1d108 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; @@ -128,8 +129,8 @@ static Generate() ValueString = entity.StringValue, ValueTimeSpan = entity.TimeSpanValue, ValueTimeOnly = entity.TimeOnlyValue - } - ); + } + ); } /// @@ -164,7 +165,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 . 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..cca010f 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,8 @@ public UnitTestsBase() // Reset all settings to defaults before each test. - DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings; - DbConnectionExtensions.InterceptDbCommand = null; + DbConnectionPlusConfiguration.Instance = new(); + EntityHelper.ResetEntityTypeMetadataCache(); OracleDatabaseAdapter.AllowTemporaryTables = false; this.MockDbConnection = Substitute.For().SetupCommands(); @@ -106,7 +107,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 +118,15 @@ 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 { From 98b4406daeb1bf93da78dfc43d7ad82622eb6e57 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 30 Jan 2026 10:02:45 +0100 Subject: [PATCH 02/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .editorconfig | 3 + .../MySql/MySqlEntityManipulator.cs | 8 +- .../Sqlite/SqliteEntityManipulator.cs | 8 +- src/DbConnectionPlus/Entities/EntityHelper.cs | 51 +- .../Entities/EntityTypeMetadata.cs | 7 +- .../EntityManipulator.DeleteEntitiesTests.cs | 20 +- .../EntityManipulator.DeleteEntityTests.cs | 267 +++---- .../EntityManipulator.InsertEntitiesTests.cs | 374 +++------- .../EntityManipulator.InsertEntityTests.cs | 338 +++------ .../EntityManipulator.UpdateEntitiesTests.cs | 499 ++++--------- .../EntityManipulator.UpdateEntityTests.cs | 492 ++++--------- .../TemporaryTableBuilderTests.cs | 683 +++++------------- .../Entities/EntityHelperTests.cs | 22 +- .../EntityWithMultipleIdentityProperties.cs | 10 + 14 files changed, 798 insertions(+), 1984 deletions(-) create mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs 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/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index dcfd31c..ebce3f1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -1172,14 +1172,10 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => sqlBuilder.Append(Constants.Indent); -#pragma warning disable CA1826 - var identityProperty = entityTypeMetadata.IdentityProperties.FirstOrDefault(); -#pragma warning restore CA1826 - - 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 diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 0a01b99..01da5a7 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -832,14 +832,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 diff --git a/src/DbConnectionPlus/Entities/EntityHelper.cs b/src/DbConnectionPlus/Entities/EntityHelper.cs index 3e328ff..7c5024e 100644 --- a/src/DbConnectionPlus/Entities/EntityHelper.cs +++ b/src/DbConnectionPlus/Entities/EntityHelper.cs @@ -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); @@ -135,6 +138,9 @@ internal static void ResetEntityTypeMetadataCache() => /// /// 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) { // TODO: Throw exception when there is more than one identity property and change @@ -208,33 +214,32 @@ entityTypeBuilder is not 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, propertiesMetadata, - propertiesMetadata - .ToDictionary(p => p.PropertyName), - propertiesMetadata - .Where(p => !p.IsIgnored) - .ToList(), - propertiesMetadata - .Where(p => p is { IsIgnored: false, IsKey: true }) - .ToList(), - propertiesMetadata - .Where(p => p is { IsIgnored: false, IsComputed: true }) - .ToList(), - propertiesMetadata - .Where(p => p is { IsIgnored: false, IsIdentity: true }) - .ToList(), - propertiesMetadata - .Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity)) - .ToList(), - propertiesMetadata - .Where(p => p is { IsIgnored: false, IsComputed: false, IsIdentity: false }) - .ToList(), - propertiesMetadata - .Where(p => p is { IsIgnored: false, IsKey: false, IsComputed: false, IsIdentity: false }) - .ToList() + 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 } + ) + ] ); } diff --git a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs index 7c25033..ddc4727 100644 --- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs @@ -23,8 +23,9 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// /// The metadata of the computed properties of the entity type. /// -/// -/// The metadata of the identity 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. @@ -43,7 +44,7 @@ public sealed record EntityTypeMetadata( IReadOnlyList MappedProperties, IReadOnlyList KeyProperties, IReadOnlyList ComputedProperties, - IReadOnlyList IdentityProperties, + EntityPropertyMetadata? IdentityProperty, IReadOnlyList DatabaseGeneratedProperties, IReadOnlyList InsertProperties, IReadOnlyList UpdateProperties diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 2b20595..7817f96 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -79,7 +79,7 @@ await Invoking(() => this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + public async Task DeleteEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( Boolean useAsyncApi ) { @@ -103,7 +103,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( + public async Task DeleteEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( Boolean useAsyncApi ) { @@ -127,7 +127,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public Task DeleteEntitiesAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task DeleteEntities_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); @@ -149,7 +149,7 @@ public Task DeleteEntitiesAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncA [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldBatchDeleteIfPossible(Boolean useAsyncApi) + public async Task DeleteEntities_MoreThan10Entities_ShouldBatchDeleteIfPossible(Boolean useAsyncApi) { // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need // to test that as well. @@ -174,7 +174,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_MoreThan10Entities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task DeleteEntities_MoreThan10Entities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need // to test that as well. @@ -200,7 +200,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) + public async Task DeleteEntities_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -222,7 +222,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entitiesToDelete = this.CreateEntitiesInDb(); @@ -248,7 +248,7 @@ public async Task DeleteEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean u [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task DeleteEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var entitiesWithColumnAttributes = Generate.MapTo(entities); @@ -271,7 +271,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntitiesAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task DeleteEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entitiesToDelete = this.CreateEntitiesInDb(); @@ -301,6 +301,8 @@ await this.CallApi( } } + // Refactor all tests to use this strategy. + private Task CallApi( Boolean useAsyncApi, DbConnection connection, diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index d791da1..60ec9ff 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,12 @@ 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 DeleteEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,14 +46,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 +62,17 @@ public void DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIs .Should().BeTrue(); } - [Fact] - public void DeleteEntity_MissingKeyProperty_ShouldThrow() - { - var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - - 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entityToDelete = this.CreateEntityInDb(); - this.manipulator.DeleteEntity( + await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -92,28 +83,15 @@ public void DeleteEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTa .Should().BeFalse(); } - [Fact] - public void DeleteEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { 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( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -124,101 +102,15 @@ public void DeleteEntity_ShouldHandleEntityWithCompositeKey() .Should().BeFalse(); } - [Fact] - public void DeleteEntity_ShouldReturnNumberOfAffectedRows() - { - 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); - } - - [Fact] - public void DeleteEntity_ShouldUseConfiguredColumnNames() - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - this.manipulator.DeleteEntity( - this.Connection, - entityWithColumnAttributes, - 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); - - // Since the operation was cancelled, the entity should still exist. - this.ExistsEntityInDb(entityToDelete) - .Should().BeTrue(); - } - - [Fact] - public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.manipulator.DeleteEntityAsync( + return Invoking(() => this.CallApi( + useAsyncApi, this.Connection, entityWithoutKeyProperty, null, @@ -233,44 +125,15 @@ public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow() ); } - [Fact] - public async Task DeleteEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entityToDelete = this.CreateEntityInDb(); - - await this.manipulator.DeleteEntityAsync( - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ); - - 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); - await this.manipulator.DeleteEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -281,12 +144,15 @@ await this.manipulator.DeleteEntityAsync( .Should().BeFalse(); } - [Fact] - public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entityToDelete = this.CreateEntityInDb(); - (await this.manipulator.DeleteEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -294,7 +160,8 @@ public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() )) .Should().Be(1); - (await this.manipulator.DeleteEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, entityToDelete, null, @@ -303,13 +170,16 @@ public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task DeleteEntityAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); - await this.manipulator.DeleteEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entityWithColumnAttributes, null, @@ -320,14 +190,17 @@ await this.manipulator.DeleteEntityAsync( .Should().BeFalse(); } - [Fact] - public async Task DeleteEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntityAsync_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 +217,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 4938ec7..53f5d6c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,239 +31,12 @@ public abstract class EntityManipulator_InsertEntitiesTests protected EntityManipulator_InsertEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entities = Generate.Multiple(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => this.manipulator.InsertEntities(this.Connection, entities, null, cancellationToken)) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entities should not have been inserted. - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public void InsertEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void InsertEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() - { - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities( - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); - } - } - - [Fact] - public void InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - this.manipulator.InsertEntities(this.Connection, entities, null, TestContext.Current.CancellationToken); - - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); - } - - [Fact] - public void InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - 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( - 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_Transaction_ShouldUseTransaction() - { - 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) - { - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); - } - - transaction.Rollback(); - } - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Fact] - public async Task InsertEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -273,7 +47,7 @@ public async Task InsertEntitiesAsync_CancellationToken_ShouldCancelOperationIfC this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.manipulator.InsertEntitiesAsync(this.Connection, entities, null, cancellationToken) + this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) ) .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); @@ -286,12 +60,17 @@ await Invoking(() => } } - [Fact] - public async Task InsertEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -305,12 +84,15 @@ await this.manipulator.InsertEntitiesAsync( } } - [Fact] - public async Task InsertEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -324,14 +106,19 @@ await this.manipulator.InsertEntitiesAsync( } } - [Fact] - public async Task InsertEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -345,14 +132,17 @@ await this.manipulator.InsertEntitiesAsync( .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); } - [Fact] - public async Task InsertEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -366,12 +156,15 @@ await this.manipulator.InsertEntitiesAsync( .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); } - [Fact] - public async Task InsertEntitiesAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -387,13 +180,16 @@ await this.Connection.QueryAsync( ); } - [Fact] - public async Task InsertEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entities = Generate.Multiple(); entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -412,12 +208,15 @@ await this.manipulator.InsertEntitiesAsync( } } - [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 +231,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 +247,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 +257,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,12 +281,15 @@ await this.manipulator.InsertEntitiesAsync( .Should().BeEquivalentTo(entities); } - [Fact] - public async Task InsertEntitiesAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = Generate.Multiple(); - await this.manipulator.InsertEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, entities, null, @@ -494,14 +303,17 @@ await this.manipulator.InsertEntitiesAsync( .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 +337,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 3c5ffb9..82249d3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,210 +31,12 @@ public abstract class EntityManipulator_InsertEntityTests protected EntityManipulator_InsertEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entity = Generate.Single(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - Invoking(() => this.manipulator.InsertEntity(this.Connection, entity, null, cancellationToken)) - .Should().Throw() - .Where(a => a.CancellationToken == cancellationToken); - - // Since the operation was cancelled, the entity should not have been inserted. - this.ExistsEntityInDb(entity) - .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() - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().Be((Int32)entity.Enum); - } - - [Fact] - public void InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - - var entity = Generate.Single(); - - this.manipulator.InsertEntity(this.Connection, entity, null, TestContext.Current.CancellationToken); - - this.Connection.QuerySingle( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entity.Enum.ToString()); - } - - [Fact] - public void InsertEntity_ShouldHandleIdentityAndComputedColumns() - { - var entity = Generate.Single(); - - this.manipulator.InsertEntity( - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - entity - .Should().BeEquivalentTo( - this.Connection.QuerySingle( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) - ); - } - - [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() - { - 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_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(); - } - - [Fact] - public async Task InsertEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -244,7 +47,7 @@ public async Task InsertEntityAsync_CancellationToken_ShouldCancelOperationIfCan this.DbCommandFactory.DelayNextDbCommand = true; await Invoking(() => - this.manipulator.InsertEntityAsync(this.Connection, entity, null, cancellationToken) + this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken) ) .Should().ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); @@ -254,12 +57,17 @@ await Invoking(() => .Should().BeFalse(); } - [Fact] - public async Task InsertEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entity = Generate.Single(); - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -270,12 +78,15 @@ await this.manipulator.InsertEntityAsync( .Should().BeTrue(); } - [Fact] - public async Task InsertEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entity = Generate.Single(); - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -286,14 +97,18 @@ await this.manipulator.InsertEntityAsync( .Should().BeTrue(); } - [Fact] - public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; 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 {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", @@ -302,14 +117,18 @@ public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreE .Should().Be((Int32)entity.Enum); } - [Fact] - public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; 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 {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", @@ -318,12 +137,15 @@ public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEn .Should().BeEquivalentTo(entity.Enum.ToString()); } - [Fact] - public async Task InsertEntityAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entity = Generate.Single(); - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -338,13 +160,16 @@ await this.Connection.QuerySingleAsync( ); } - [Fact] - public async Task InsertEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entity = Generate.Single(); entity.NotMappedValue = "ShouldNotBePersisted"; - await this.manipulator.InsertEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, entity, null, @@ -363,12 +188,14 @@ await this.manipulator.InsertEntityAsync( } } - [Fact] - public async Task InsertEntityAsync_ShouldInsertEntity() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_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 +205,27 @@ public async Task InsertEntityAsync_ShouldInsertEntity() .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_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 InsertEntityAsync_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,12 +234,14 @@ public async Task InsertEntityAsync_ShouldSupportDateTimeOffsetValues() .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames(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); (await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("Entity")}", @@ -417,14 +250,17 @@ public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames() .Should().BeEquivalentTo(entity); } - [Fact] - public async Task InsertEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntityAsync_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 +278,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 09ae431..a1d098a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,316 +31,12 @@ public abstract class EntityManipulator_UpdateEntitiesTests protected EntityManipulator_UpdateEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Fact] - public void UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested() - { - 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_MissingKeyProperty_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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -351,7 +48,7 @@ public async Task UpdateEntitiesAsync_CancellationToken_ShouldCancelOperationIfC 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,29 +61,18 @@ await Invoking(() => .Should().BeEquivalentTo(entities); } - [Fact] - public Task UpdateEntitiesAsync_MissingKeyProperty_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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -400,13 +86,18 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( + Boolean useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -420,8 +111,12 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -443,7 +138,8 @@ await this.manipulator.InsertEntitiesAsync( var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -458,8 +154,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 UpdateEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -481,7 +181,8 @@ await this.manipulator.InsertEntitiesAsync( var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -496,13 +197,35 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldHandleEntityWithCompositeKey() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntitiesAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => + this.CallApi( + useAsyncApi, + 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)}." + ); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -516,13 +239,16 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -537,15 +263,18 @@ await this.Connection.QueryAsync( ); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); updatedEntities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -564,13 +293,16 @@ await this.manipulator.UpdateEntitiesAsync( } } - [Fact] - public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -580,7 +312,8 @@ public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() var nonExistentEntities = Generate.Multiple(); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, nonExistentEntities, null, @@ -589,15 +322,18 @@ public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -611,13 +347,16 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldUpdateEntities() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldUpdateEntities(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -632,13 +371,16 @@ public async Task UpdateEntitiesAsync_ShouldUpdateEntities() .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); - await this.manipulator.UpdateEntitiesAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, null, @@ -652,8 +394,10 @@ await this.manipulator.UpdateEntitiesAsync( .Should().BeEquivalentTo(updatedEntities); } - [Fact] - public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -661,7 +405,8 @@ public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction() { var updatedEntities = Generate.UpdatesFor(entities); - (await this.manipulator.UpdateEntitiesAsync( + (await this.CallApi( + useAsyncApi, this.Connection, updatedEntities, transaction, @@ -681,5 +426,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 fa29d60..435cf0a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -30,311 +31,12 @@ 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_MissingKeyProperty_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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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 UpdateEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -346,7 +48,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,29 +61,18 @@ await Invoking(() => .Should().BeEquivalentTo(entity); } - [Fact] - public Task UpdateEntityAsync_MissingKeyProperty_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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + Boolean useAsyncApi + ) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -395,13 +86,16 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -415,8 +109,12 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -433,7 +131,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,8 +147,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 UpdateEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -466,7 +169,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,13 +185,35 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); } - [Fact] - public async Task UpdateEntityAsync_ShouldHandleEntityWithCompositeKey() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => + Invoking(() => + this.CallApi( + useAsyncApi, + 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)}." + ); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -501,13 +227,16 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldHandleIdentityAndComputedColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -522,15 +251,18 @@ await this.Connection.QuerySingleAsync( ); } - [Fact] - public async Task UpdateEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(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, @@ -549,13 +281,16 @@ await this.manipulator.UpdateEntityAsync( } } - [Fact] - public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_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 +300,8 @@ public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() var nonExistentEntity = Generate.Single(); - (await this.manipulator.UpdateEntityAsync( + (await this.CallApi( + useAsyncApi, this.Connection, nonExistentEntity, null, @@ -574,15 +310,18 @@ public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows() .Should().Be(0); } - [Fact] - public async Task UpdateEntityAsync_ShouldSupportDateTimeOffsetValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_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 +335,16 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldUpdateEntity() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_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,13 +359,16 @@ public async Task UpdateEntityAsync_ShouldUpdateEntity() .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.manipulator.UpdateEntityAsync( + await this.CallApi( + useAsyncApi, this.Connection, updatedEntity, null, @@ -637,8 +382,10 @@ await this.manipulator.UpdateEntityAsync( .Should().BeEquivalentTo(updatedEntity); } - [Fact] - public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -646,7 +393,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 +412,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/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index f99f5af..8e9ad6f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -1,3 +1,5 @@ +using System.Collections; +using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.Extensions; using RentADeveloper.DbConnectionPlus.UnitTests.Assertions; @@ -31,477 +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 BuildTemporaryTableAsync_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset( + Boolean useAsyncApi + ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var items = Generate.Multiple(); - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - items, - typeof(TemporaryTableTestItemWithDateTimeOffset), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(items); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - - var entities = Generate.Multiple(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - entities, - typeof(EntityWithEnumProperty), - TestContext.Current.CancellationToken - ); - - if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) - { - this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum") - .Should().Be(this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers)); - } - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Enum")} FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldType(0) - .Should().BeAnyOf(typeof(Int32), typeof(Int64)); - - foreach (var entity in entities) - { - reader.Read(); - - reader.GetInt32(0) - .Should().Be((Int32)entity.Enum); - } - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - - var entities = Generate.Multiple(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - 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")); - } - - using var reader = this.Connection.ExecuteReader( - $"SELECT {Q("Enum")} FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - reader.GetFieldType(0) - .Should().Be(typeof(String)); - - foreach (var entity in entities) - { - reader.Read(); - - reader.GetString(0) - .Should().Be(entity.Enum.ToString()); - } - } - - [Fact] - public void - BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() - { - Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); - - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - Generate.Multiple(), - typeof(EntityWithEnumProperty), - TestContext.Current.CancellationToken - ); - - var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "Enum"); - - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties() - { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - entities, - typeof(EntityWithNotMappedProperty), - TestContext.Current.CancellationToken - ); - - using var reader = this.Connection.ExecuteReader( - $"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(); - - using var tableDisposer = this.builder.BuildTemporaryTable( - this.Connection, - null, - "Objects", - items, - typeof(TemporaryTableTestItem), - TestContext.Current.CancellationToken - ); - - this.Connection.Query( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(items); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns() - { - 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); - } - - [Fact] - public void BuildTemporaryTable_ComplexObjects_ShouldUseConfiguredColumnNames() - { - var entities = Generate.Multiple(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - 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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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, ""); - - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -517,15 +61,20 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_DateTimeOffsetProperty .Should().BeEquivalentTo(items); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.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", @@ -557,15 +106,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var entities = Generate.Multiple(); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -597,15 +151,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() + BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + Boolean useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -620,14 +179,19 @@ public async Task .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties() + BuildTemporaryTableAsync_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties( + Boolean useAsyncApi + ) { var entities = Generate.Multiple(); entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -645,12 +209,15 @@ public async Task .Should().NotContain(nameof(EntityWithNotMappedProperty.NotMappedValue)); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColumnTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_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 +233,17 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColum .Should().BeEquivalentTo(items); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_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,13 +258,16 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseCollationOfDa .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredColumnNames() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = Generate.Multiple(); var entitiesWithColumnAttributes = Generate.MapTo(entities); - await using var tableDisposer = await this.builder.BuildTemporaryTableAsync( + await using var tableDisposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Objects", @@ -708,12 +283,15 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredCol .Should().BeEquivalentTo(entities); } - [Fact] - public async Task BuildTemporaryTableAsync_ComplexObjects_WithNullables_ShouldHandleNullValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_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 +307,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 BuildTemporaryTableAsync_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 +335,20 @@ public async Task BuildTemporaryTableAsync_ScalarValues_DateTimeOffsetValues_Sho .Should().BeEquivalentTo(values); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers() + BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { 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 +380,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings() + BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + Boolean useAsyncApi + ) { 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 +425,20 @@ public async Task } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns() + BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + Boolean useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); 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 +453,18 @@ public async Task .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls() + BuildTemporaryTableAsync_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls(Boolean useAsyncApi) { 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 +480,15 @@ public async Task .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumnTable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_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 +504,17 @@ public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumn .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_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 +529,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 BuildTemporaryTableAsync_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 +553,13 @@ public async Task BuildTemporaryTableAsync_ScalarValuesWithNullValues_ShouldHand .Should().BeEquivalentTo(values); } - [Fact] - public async Task BuildTemporaryTableAsync_ShouldReturnDisposerThatDropsTableAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTableAsync_ShouldReturnDisposerThatDropsTableAsync(Boolean useAsyncApi) { - var disposer = await this.builder.BuildTemporaryTableAsync( + var disposer = await this.CallApi( + useAsyncApi, this.Connection, null, "Values", @@ -962,5 +577,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.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index df80bcc..523e198 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,6 +151,16 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe ); } + [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." + ); + [Fact] public void GetEntityTypeMetadata_FluentAPIConfig_ShouldGetMetadataBasedOnFluentAPIConfig() { @@ -228,8 +236,8 @@ public void GetEntityTypeMetadata_FluentAPIConfig_ShouldGetMetadataBasedOnFluent metadata.ComputedProperties .Should().Contain(int16ValueProperty); - metadata.IdentityProperties - .Should().Contain(int32ValueProperty); + metadata.IdentityProperty + .Should().Be(int32ValueProperty); metadata.InsertProperties .Should().Contain([idProperty, booleanValueProperty]); @@ -291,9 +299,9 @@ Type entityType .Should() .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsComputed: true })); - metadata.IdentityProperties + metadata.IdentityProperty .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsIdentity: true })); + .Be(allPropertiesMetadata.FirstOrDefault(a => a is { IsIgnored: false, IsIdentity: true })); metadata.DatabaseGeneratedProperties .Should() 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; } +} From 4a70502fb95601144e26a76f2f3f8575f045dda3 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 30 Jan 2026 21:54:37 +0100 Subject: [PATCH 03/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../DbConnectionPlusConfiguration.cs | 15 + .../Configuration/EntityPropertyBuilder.cs | 25 + .../Configuration/EntityTypeBuilder.cs | 29 +- .../{IFreezable.cs => Freezable.cs} | 0 .../EntityManipulator.DeleteEntitiesTests.cs | 26 +- .../EntityManipulator.DeleteEntityTests.cs | 16 +- .../EntityManipulator.InsertEntityTests.cs | 24 +- .../EntityManipulator.UpdateEntitiesTests.cs | 28 +- .../EntityManipulator.UpdateEntityTests.cs | 28 +- .../TemporaryTableBuilderTests.cs | 36 +- .../DbCommands/DbCommandBuilderTests.cs | 320 +--- .../DefaultDbCommandFactoryTests.cs | 4 +- ...nnectionExtensions.ExecuteNonQueryTests.cs | 391 ++-- ...ConnectionExtensions.ExecuteReaderTests.cs | 449 ++--- ...ConnectionExtensions.ExecuteScalarTests.cs | 607 ++---- .../DbConnectionExtensions.ExistsTests.cs | 320 ++-- ...ConnectionExtensions.QueryFirstOfTTests.cs | 1601 +++++----------- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 1648 ++++++---------- ...tionExtensions.QueryFirstOrDefaultTests.cs | 357 ++-- .../DbConnectionExtensions.QueryFirstTests.cs | 361 ++-- .../DbConnectionExtensions.QueryOfTTests.cs | 1589 +++++----------- ...onnectionExtensions.QuerySingleOfTTests.cs | 1635 +++++----------- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 1678 ++++++----------- ...ionExtensions.QuerySingleOrDefaultTests.cs | 385 ++-- ...DbConnectionExtensions.QuerySingleTests.cs | 383 ++-- .../DbConnectionExtensions.QueryTests.cs | 352 ++-- .../IntegrationTestsBase.cs | 8 +- .../DbConnectionPlusConfigurationTests.cs | 20 +- .../EntityPropertyBuilderTests.cs | 10 +- .../Configuration/EntityTypeBuilderTests.cs | 6 +- .../DbCommands/DbCommandBuilderTests.cs | 891 ++------- .../DefaultDbCommandFactoryTests.cs | 4 +- ...ConnectionExtensions.ConfigurationTests.cs | 2 +- ...piTest.PublicApiHasNotChanged.verified.txt | 8 +- 34 files changed, 4284 insertions(+), 8972 deletions(-) rename src/DbConnectionPlus/Configuration/{IFreezable.cs => Freezable.cs} (100%) diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs index b349c67..be5c0c6 100644 --- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -38,6 +38,10 @@ public sealed class DbConnectionPlusConfiguration : IFreezable /// /// 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; @@ -52,6 +56,10 @@ public EnumSerializationMode EnumSerializationMode /// 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; @@ -67,6 +75,9 @@ public InterceptDbCommand? InterceptDbCommand /// /// 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(); @@ -99,6 +110,10 @@ void IFreezable.Freeze() /// 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) diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs index 3d414ce..8396c86 100644 --- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -5,6 +5,11 @@ /// 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; @@ -16,6 +21,10 @@ internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String prop /// /// 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(); @@ -30,6 +39,9 @@ public EntityPropertyBuilder HasColumnName(String columnName) /// 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(); @@ -47,6 +59,9 @@ public EntityPropertyBuilder IsComputed() /// 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(); @@ -73,6 +88,9 @@ public EntityPropertyBuilder IsIdentity() /// 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(); @@ -85,6 +103,9 @@ public EntityPropertyBuilder IsIgnored() /// 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(); @@ -114,6 +135,10 @@ public EntityPropertyBuilder IsKey() /// String IEntityPropertyBuilder.PropertyName => this.propertyName; + /// + /// Ensures this instance is not frozen. + /// + /// This object is already frozen. private void EnsureNotFrozen() { if (this.isFrozen) diff --git a/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs index 96907b6..d81b4cb 100644 --- a/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs @@ -18,15 +18,20 @@ public sealed class EntityTypeBuilder : IEntityTypeBuilder /// /// The builder for the specified property. /// - /// is not a valid property access expression. + /// 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( @@ -41,6 +46,10 @@ public EntityPropertyBuilder Property(Expression /// 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(); @@ -50,6 +59,9 @@ public EntityTypeBuilder ToTable(String tableName) return this; } + /// + Type IEntityTypeBuilder.EntityType => typeof(TEntity); + /// void IFreezable.Freeze() { @@ -61,9 +73,6 @@ void IFreezable.Freeze() } } - /// - Type IEntityTypeBuilder.EntityType => typeof(TEntity); - /// IReadOnlyDictionary IEntityTypeBuilder.PropertyBuilders => this.propertyBuilders; @@ -71,6 +80,10 @@ void IFreezable.Freeze() /// String? IEntityTypeBuilder.TableName => this.tableName; + /// + /// Ensures this instance is not frozen. + /// + /// This object is already frozen. private void EnsureNotFrozen() { if (this.isFrozen) @@ -79,6 +92,14 @@ private void EnsureNotFrozen() } } + /// + /// 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; diff --git a/src/DbConnectionPlus/Configuration/IFreezable.cs b/src/DbConnectionPlus/Configuration/Freezable.cs similarity index 100% rename from src/DbConnectionPlus/Configuration/IFreezable.cs rename to src/DbConnectionPlus/Configuration/Freezable.cs diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 7817f96..a1bb328 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -26,13 +26,25 @@ public sealed class // TODO: Implement integration test (CRUD, Query, Temporary Tables) for fluent API config as well as attribute based // config. -// TODO: Table mapping via Type Name -// TODO: Table Name mapping via Attribute -// TODO: Table Name mapping via Fluent API -// TODO: Column Name mapping via Attribute -// TODO: Column Name mapping via Fluent API -// TODO: Key Property mapping via Attribute -// TODO: Key Property mapping via Fluent API +// TODO: Table Name -> via Type Name +// TODO: Table Name -> via Attribute +// TODO: Table Name -> via Fluent API + +// TODO: Column Name -> via Property Name +// TODO: Column Name -> via Attribute +// TODO: Column Name -> via Fluent API + +// TODO: Key Property -> via Attribute +// TODO: Key Property -> via Fluent API + +// TODO: Computed Property -> via Attribute +// TODO: Computed Property -> via Fluent API + +// TODO: Identity Property -> via Attribute +// TODO: Identity Property -> via Fluent API + +// TODO: Ignore Property -> via Attribute +// TODO: Ignore Property -> via Fluent API public abstract class EntityManipulator_DeleteEntitiesTests : IntegrationTestsBase diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 60ec9ff..96dfff2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -34,7 +34,7 @@ protected EntityManipulator_DeleteEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( Boolean useAsyncApi ) { @@ -65,7 +65,7 @@ await Invoking(() => this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + public async Task DeleteEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( Boolean useAsyncApi ) { @@ -86,7 +86,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task DeleteEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -105,7 +105,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task DeleteEntity_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); @@ -128,7 +128,7 @@ public Task DeleteEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) + public async Task DeleteEntity_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -147,7 +147,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entityToDelete = this.CreateEntityInDb(); @@ -173,7 +173,7 @@ public async Task DeleteEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean use [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task DeleteEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); @@ -193,7 +193,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntityAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task DeleteEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entityToDelete = this.CreateEntityInDb(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index 82249d3..185a49f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -34,7 +34,7 @@ protected EntityManipulator_InsertEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( Boolean useAsyncApi ) { @@ -60,7 +60,7 @@ await Invoking(() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + public async Task InsertEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( Boolean useAsyncApi ) { @@ -81,7 +81,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task InsertEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -100,7 +100,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( Boolean useAsyncApi ) { @@ -120,7 +120,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( Boolean useAsyncApi ) { @@ -140,7 +140,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) + public async Task InsertEntity_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -163,7 +163,7 @@ await this.Connection.QuerySingleAsync( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) + public async Task InsertEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entity = Generate.Single(); entity.NotMappedValue = "ShouldNotBePersisted"; @@ -191,7 +191,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldInsertEntity(Boolean useAsyncApi) + public async Task InsertEntity_ShouldInsertEntity(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -208,7 +208,7 @@ public async Task InsertEntityAsync_ShouldInsertEntity(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task InsertEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -219,7 +219,7 @@ public async Task InsertEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean use [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -237,7 +237,7 @@ public async Task InsertEntityAsync_ShouldSupportDateTimeOffsetValues(Boolean us [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task InsertEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -253,7 +253,7 @@ public async Task InsertEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAs [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntityAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task InsertEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = Generate.Single(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index a1d098a..819fe30 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -34,7 +34,7 @@ protected EntityManipulator_UpdateEntitiesTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + public async Task UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( Boolean useAsyncApi ) { @@ -64,7 +64,7 @@ await Invoking(() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + public async Task UpdateEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( Boolean useAsyncApi ) { @@ -89,7 +89,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( + public async Task UpdateEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( Boolean useAsyncApi ) { @@ -114,7 +114,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + public async Task UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( Boolean useAsyncApi ) { @@ -157,7 +157,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( Boolean useAsyncApi ) { @@ -200,7 +200,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public Task UpdateEntitiesAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => + public Task UpdateEntities_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => Invoking(() => this.CallApi( useAsyncApi, @@ -219,7 +219,7 @@ [new EntityWithoutKeyProperty()], [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); @@ -242,7 +242,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); @@ -266,7 +266,7 @@ await this.Connection.QueryAsync( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -296,7 +296,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); @@ -325,7 +325,7 @@ public async Task UpdateEntitiesAsync_ShouldReturnNumberOfAffectedRows(Boolean u [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -350,7 +350,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldUpdateEntities(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldUpdateEntities(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); @@ -374,7 +374,7 @@ public async Task UpdateEntitiesAsync_ShouldUpdateEntities(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdatesFor(entities); @@ -397,7 +397,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntitiesAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task UpdateEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 435cf0a..2e065ac 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -34,7 +34,7 @@ protected EntityManipulator_UpdateEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( Boolean useAsyncApi ) { @@ -64,7 +64,7 @@ await Invoking(() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( + public async Task UpdateEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( Boolean useAsyncApi ) { @@ -89,7 +89,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task UpdateEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -112,7 +112,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( Boolean useAsyncApi ) { @@ -150,7 +150,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( Boolean useAsyncApi ) { @@ -188,7 +188,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public Task UpdateEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => + public Task UpdateEntity_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => Invoking(() => this.CallApi( useAsyncApi, @@ -207,7 +207,7 @@ public Task UpdateEntityAsync_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -230,7 +230,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -254,7 +254,7 @@ await this.Connection.QuerySingleAsync( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -284,7 +284,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -313,7 +313,7 @@ public async Task UpdateEntityAsync_ShouldReturnNumberOfAffectedRows(Boolean use [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -338,7 +338,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldUpdateEntity(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldUpdateEntity(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -362,7 +362,7 @@ public async Task UpdateEntityAsync_ShouldUpdateEntity(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -385,7 +385,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntityAsync_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task UpdateEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 8e9ad6f..2b94636 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -36,7 +36,7 @@ protected TemporaryTableBuilderTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset( + public async Task BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset( Boolean useAsyncApi ) { @@ -65,7 +65,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( Boolean useAsyncApi ) { @@ -110,7 +110,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( Boolean useAsyncApi ) { @@ -155,7 +155,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( Boolean useAsyncApi ) { @@ -183,7 +183,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties( + BuildTemporaryTable_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties( Boolean useAsyncApi ) { @@ -212,7 +212,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColumnTable(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTable(Boolean useAsyncApi) { var items = Generate.Multiple(); @@ -236,7 +236,7 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldCreateMultiColum [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns( + public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns( Boolean useAsyncApi ) { @@ -261,7 +261,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = Generate.Multiple(); var entitiesWithColumnAttributes = Generate.MapTo(entities); @@ -286,7 +286,7 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_ShouldUseConfiguredCol [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ComplexObjects_WithNullables_ShouldHandleNullValues(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_WithNullables_ShouldHandleNullValues(Boolean useAsyncApi) { var itemsWithNulls = new List { new() }; @@ -310,7 +310,7 @@ public async Task BuildTemporaryTableAsync_ComplexObjects_WithNullables_ShouldHa [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset( + public async Task BuildTemporaryTable_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset( Boolean useAsyncApi ) { @@ -339,7 +339,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( Boolean useAsyncApi ) { @@ -384,7 +384,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( Boolean useAsyncApi ) { @@ -429,7 +429,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( Boolean useAsyncApi ) { @@ -457,7 +457,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - BuildTemporaryTableAsync_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls(Boolean useAsyncApi) + BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls(Boolean useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -483,7 +483,7 @@ public async Task [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumnTable(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable(Boolean useAsyncApi) { var values = Generate.Multiple(); @@ -507,7 +507,7 @@ public async Task BuildTemporaryTableAsync_ScalarValues_ShouldCreateSingleColumn [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns( + public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns( Boolean useAsyncApi ) { @@ -532,7 +532,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ScalarValuesWithNullValues_ShouldHandleNullValues(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ScalarValuesWithNullValues_ShouldHandleNullValues(Boolean useAsyncApi) { var values = Generate.MultipleNullable(); @@ -556,7 +556,7 @@ public async Task BuildTemporaryTableAsync_ScalarValuesWithNullValues_ShouldHand [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTableAsync_ShouldReturnDisposerThatDropsTableAsync(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ShouldReturnDisposerThatDropsTableAsync(Boolean useAsyncApi) { var disposer = await this.CallApi( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs index f0512c0..e6187af 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,12 @@ 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 +92,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 +114,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 +134,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 +153,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 +211,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 +230,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 +256,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..0334acb 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,13 @@ 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 +120,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 +139,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 +177,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 +198,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 +210,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 +224,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 +239,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 +258,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 +287,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 +317,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 +354,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..8c7376b 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,51 @@ 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 +378,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 +397,32 @@ 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 +433,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 +466,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..902862c 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,12 @@ 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 +80,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 +92,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 +117,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 +145,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 +168,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 +180,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 +245,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.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 77ab4d6..8eb35b7 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); +using System.Data.Common; - 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)); - } - - [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,16 @@ 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 +134,16 @@ 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 +155,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 +205,44 @@ 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 +251,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 +262,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 +281,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 +295,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 +307,43 @@ 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 +362,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1210,23 +382,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 +422,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 +440,9 @@ public async Task QueryFirstAsync_EntityType_ColumnHasNoName_ShouldThrow() }; await Invoking(() => - this.Connection.QueryFirstAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1266,35 +454,53 @@ 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 +511,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 +555,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 +580,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 +597,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 +614,14 @@ public async Task QueryFirstAsync_EntityType_EnumEntityProperty_ShouldConvertStr .Should().Be(enumValue); } - [Fact] - public Task QueryFirstAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [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 +632,57 @@ 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 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 +694,90 @@ 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); - (await this.Connection.QueryFirstAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"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 +788,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 +816,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 +841,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 +856,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 +868,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 +894,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 +915,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 +925,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 +959,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1669,23 +979,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 +1019,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 +1045,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 +1071,53 @@ 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 +1129,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 +1170,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 +1226,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 de4165d..8bc3a74 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -1,967 +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)}"; +using System.Data.Common; - 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)); - } - - [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 ) @@ -979,7 +60,9 @@ public async Task } (await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) @@ -996,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 ) @@ -1023,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 ) @@ -1037,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 ) @@ -1052,34 +159,50 @@ 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 ) @@ -1090,30 +213,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, ""); @@ -1122,7 +259,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 ) @@ -1131,14 +270,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 @@ -1146,9 +289,13 @@ 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), ""); @@ -1158,7 +305,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1168,31 +317,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 ) @@ -1211,7 +372,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1229,23 +392,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 ) @@ -1257,8 +432,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 { @@ -1273,7 +450,9 @@ public async Task QueryFirstOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThro }; await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1285,36 +464,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 ) @@ -1325,24 +522,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 ) @@ -1359,10 +568,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 ) @@ -1379,12 +594,18 @@ 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 ))! @@ -1392,12 +613,18 @@ 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 ))! @@ -1405,10 +632,14 @@ public async Task QueryFirstOrDefaultAsync_EntityType_EnumEntityProperty_ShouldC .Should().Be(enumValue); } - [Fact] - public Task QueryFirstOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [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( @@ -1419,41 +650,59 @@ 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 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 ) @@ -1465,68 +714,90 @@ 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"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 ) @@ -1537,20 +808,28 @@ 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); @@ -1559,38 +838,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), ""); @@ -1601,7 +892,9 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await this.Connection.QueryFirstOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken )) @@ -1611,16 +904,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")} @@ -1631,14 +930,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 @@ -1648,23 +951,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 ) @@ -1683,7 +994,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QueryFirstOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1701,24 +1014,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 ) @@ -1730,11 +1055,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 ) @@ -1751,10 +1082,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 ) @@ -1771,39 +1108,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 ) @@ -1815,26 +1170,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 ) @@ -1846,41 +1213,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 ) @@ -1890,4 +1269,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..5fee049 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,18 @@ 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 +142,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 +154,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 +164,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 +192,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 +209,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 +232,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 +249,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 +268,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..a476487 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,12 @@ 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 +41,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 +73,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 +88,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 +101,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 +123,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 +140,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 +152,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 +162,29 @@ 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 +194,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 +211,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 +234,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 +251,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 +270,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..9165333 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), ""); +using System.Data.Common; - 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)]); - } - - [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,41 @@ 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 +368,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 +388,35 @@ 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 +428,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 +446,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 +460,51 @@ 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 +515,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 +559,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 +584,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 +601,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 +618,14 @@ public async Task QueryAsync_EntityType_EnumEntityProperty_ShouldConvertStringTo .Should().Be(enumValue); } - [Fact] - public Task QueryAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [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 +637,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 +699,90 @@ 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var entitiesWithColumnAttributes = 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(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 +793,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 +821,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 +845,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 +873,22 @@ 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 +899,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 +920,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 +962,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 +982,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 +1022,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 +1048,16 @@ 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 +1074,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 +1130,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 +1171,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 +1227,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..140e2e5 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])}"; +using System.Data.Common; - 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)" - ); - - 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,35 @@ 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 +116,16 @@ 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 +136,16 @@ 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 +157,48 @@ 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 +209,44 @@ 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 +255,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 +266,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 +285,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 +299,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 +311,43 @@ 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 +366,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1226,23 +386,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 +426,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 +444,9 @@ public async Task QuerySingleAsync_EntityType_ColumnHasNoName_ShouldThrow() }; await Invoking(() => - this.Connection.QuerySingleAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1282,35 +458,53 @@ 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 +515,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 +560,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 +585,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 +602,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 +619,14 @@ public async Task QuerySingleAsync_EntityType_EnumEntityProperty_ShouldConvertSt .Should().Be(enumValue); } - [Fact] - public Task QuerySingleAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [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 +637,59 @@ 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 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 +701,90 @@ 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); - (await this.Connection.QuerySingleAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"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 +795,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 +823,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 +852,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 +868,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 +883,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 +895,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 +921,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 +942,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 +952,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 +986,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1702,23 +1006,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 +1046,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 +1072,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 +1098,55 @@ 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 +1158,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 +1199,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 +1255,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..16735de 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); - - 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)); - } +using System.Data.Common; - [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,52 @@ 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 +215,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 +261,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 +272,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 +291,13 @@ 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 +307,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 +319,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 +374,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Char")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1245,23 +394,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 +434,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 +452,9 @@ public async Task QuerySingleOrDefaultAsync_EntityType_ColumnHasNoName_ShouldThr }; await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync( + CallApi( + useAsyncApi, + this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken ) @@ -1301,36 +466,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 +524,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 +570,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 +596,18 @@ 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 +615,18 @@ 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 +634,16 @@ public async Task QuerySingleOrDefaultAsync_EntityType_EnumEntityProperty_Should .Should().Be(enumValue); } - [Fact] - public Task QuerySingleOrDefaultAsync_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow() => + [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 +656,59 @@ 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 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 +720,90 @@ 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() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var entityWithColumnAttributes = Generate.MapTo(entity); - (await this.Connection.QuerySingleOrDefaultAsync( + (await CallApi( + useAsyncApi, + this.Connection, $"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 +814,28 @@ 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 +844,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 +873,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 +918,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 +930,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 +956,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 +977,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 +1020,9 @@ await Invoking(() => } await Invoking(() => - this.Connection.QuerySingleOrDefaultAsync>( + CallApi>( + useAsyncApi, + this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) @@ -1735,24 +1040,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 +1081,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 +1108,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 +1135,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 +1197,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 +1240,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 +1296,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..12461fe 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,18 @@ 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 +143,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 +155,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 +165,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 +186,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 +213,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 +230,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 +253,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 +270,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 +292,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..2311ee1 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,12 @@ 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 +42,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 +53,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 +73,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 +88,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 +101,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 +123,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 +140,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 +152,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 +162,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 +183,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 +199,12 @@ 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 +214,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 +231,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 +254,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 +271,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 +293,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/IntegrationTestsBase.cs b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs index 28eb2d3..f3a4c5d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs +++ b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs @@ -45,9 +45,11 @@ protected IntegrationTestsBase() OracleDatabaseAdapter.AllowTemporaryTables = true; // Reset all settings to defaults before each test. - DbConnectionPlusConfiguration.Instance = new(); - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - DbConnectionPlusConfiguration.Instance.InterceptDbCommand = DbCommandLogger.LogDbCommand; + DbConnectionPlusConfiguration.Instance = new() + { + EnumSerializationMode = EnumSerializationMode.Strings, + InterceptDbCommand = DbCommandLogger.LogDbCommand + }; } /// diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index dc45292..bc6fd0e 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -60,23 +60,35 @@ public void Freeze_ShouldFreezeConfigurationAndEntityTypeBuilders() entityTypeBuilder.ToTable("Entities"); + var entityPropertyBuilder = entityTypeBuilder.Property(a => a.Id); + + entityPropertyBuilder.IsKey(); + ((IFreezable)configuration).Freeze(); Invoking(() => configuration.EnumSerializationMode = EnumSerializationMode.Integers) .Should().Throw() - .WithMessage("This configuration is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => configuration.InterceptDbCommand = null) .Should().Throw() - .WithMessage("This configuration is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => configuration.Entity()) .Should().Throw() - .WithMessage("This configuration is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => entityTypeBuilder.ToTable("Entities")) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .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] diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs index 21bb7d0..d09be69 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -10,23 +10,23 @@ public void Freeze_ShouldFreezeBuilder() Invoking(() => builder.HasColumnName("Identifier")) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsComputed()) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsIdentity()) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsIgnored()) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsKey()) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs index ffcdaa0..c931541 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs @@ -15,15 +15,15 @@ public void Freeze_ShouldFreezeBuilderAndAllPropertyBuilders() Invoking(() => builder.ToTable("Entities2")) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.Property(a => a.Id).HasColumnName("Identifier")) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.Property(a => a.StringValue).HasColumnName("String")) .Should().Throw() - .WithMessage("This builder is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs index 1718bf9..9b84e94 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -1,655 +1,26 @@ // ReSharper disable UnusedParameter.Local -#pragma warning disable NS1001 - -using RentADeveloper.DbConnectionPlus.SqlStatements; -using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder; - -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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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() - { - DbConnectionPlusConfiguration.Instance.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 - ); +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DbCommands; +using RentADeveloper.DbConnectionPlus.SqlStatements; +using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder; - command.Transaction - .Should().BeSameAs(transaction); - } +namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; - [Fact] - public async Task BuildDbCommandAsync_CancellationToken_ShouldUseCancellationToken() +public class DbCommandBuilderTests : UnitTestsBase +{ + [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, @@ -659,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", @@ -669,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 @@ -700,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, @@ -716,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, @@ -730,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 + ) { 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 @@ -754,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 + ) { 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 @@ -778,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 @@ -823,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())}, @@ -880,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 @@ -904,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])} @@ -943,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 @@ -966,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 @@ -975,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 = @@ -999,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 @@ -1041,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(); @@ -1054,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 @@ -1097,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 @@ -1136,9 +561,13 @@ 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 + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -1149,7 +578,8 @@ public async Task ("Parameter1", enumValue) ); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1165,9 +595,13 @@ 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 + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -1178,7 +612,8 @@ public async Task ("Parameter1", enumValue) ); - var (command, _) = await DbCommandBuilder.BuildDbCommandAsync( + var (command, _) = await CallApi( + useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection @@ -1194,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 @@ -1207,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 @@ -1220,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 @@ -1233,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, @@ -1249,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/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 index 175551c..ae6cc07 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -83,6 +83,6 @@ public void Configure_ShouldFreezeConfiguration() Invoking(() => Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Strings)) .Should().Throw() - .WithMessage("This configuration is frozen and can no longer be modified."); + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } } diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 0226ab8..3242099 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -6,11 +6,11 @@ namespace RentADeveloper.DbConnectionPlus.Configuration public DbConnectionPlusConfiguration() { } 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 sealed class EntityPropertyBuilder : RentADeveloper.DbConnectionPlus.Configuration.IFreezable { - public EntityPropertyBuilder() { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder HasColumnName(string columnName) { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsComputed() { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsIdentity() { } @@ -154,6 +154,8 @@ namespace RentADeveloper.DbConnectionPlus } 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] @@ -225,13 +227,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 ComputedProperties, System.Collections.Generic.IReadOnlyList IdentityProperties, System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties, System.Collections.Generic.IReadOnlyList InsertProperties, System.Collections.Generic.IReadOnlyList UpdateProperties) { } + 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 System.Collections.Generic.IReadOnlyList IdentityProperties { 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; } From f1473e982a43b8ea2cc616c45275defd33c88470 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 30 Jan 2026 22:10:02 +0100 Subject: [PATCH 04/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../DbConnectionPlusConfiguration.cs | 74 +++++++++++- .../DatabaseAdapterRegistry.cs | 84 -------------- .../DbConnectionExtensions.DeleteEntities.cs | 4 +- .../DbConnectionExtensions.DeleteEntity.cs | 4 +- .../DbConnectionExtensions.ExecuteNonQuery.cs | 4 +- .../DbConnectionExtensions.ExecuteReader.cs | 4 +- .../DbConnectionExtensions.ExecuteScalar.cs | 4 +- .../DbConnectionExtensions.Exists.cs | 4 +- .../DbConnectionExtensions.InsertEntities.cs | 4 +- .../DbConnectionExtensions.InsertEntity.cs | 4 +- .../DbConnectionExtensions.Query.cs | 4 +- .../DbConnectionExtensions.QueryFirst.cs | 4 +- .../DbConnectionExtensions.QueryFirstOfT.cs | 4 +- ...onnectionExtensions.QueryFirstOrDefault.cs | 4 +- ...ectionExtensions.QueryFirstOrDefaultOfT.cs | 4 +- .../DbConnectionExtensions.QueryOfT.cs | 4 +- .../DbConnectionExtensions.QuerySingle.cs | 4 +- .../DbConnectionExtensions.QuerySingleOfT.cs | 4 +- ...nnectionExtensions.QuerySingleOrDefault.cs | 4 +- ...ctionExtensions.QuerySingleOrDefaultOfT.cs | 4 +- .../DbConnectionExtensions.UpdateEntities.cs | 4 +- .../DbConnectionExtensions.UpdateEntity.cs | 4 +- src/DbConnectionPlus/Entities/EntityHelper.cs | 3 - .../DbConnectionPlusConfigurationTests.cs | 107 ++++++++++++++++- .../DatabaseAdapterRegistryTests.cs | 109 ------------------ .../UnitTestsBase.cs | 4 +- 26 files changed, 221 insertions(+), 240 deletions(-) delete mode 100644 src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/DatabaseAdapters/DatabaseAdapterRegistryTests.cs diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs index be5c0c6..ad2de3e 100644 --- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -1,10 +1,32 @@ -namespace RentADeveloper.DbConnectionPlus.Configuration; +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 @@ -88,6 +110,26 @@ public EntityTypeBuilder Entity() ); } + /// + /// 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() { @@ -104,6 +146,35 @@ void IFreezable.Freeze() /// 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. /// @@ -122,6 +193,7 @@ private void EnsureNotFrozen() } } + private readonly ConcurrentDictionary databaseAdapters = []; private readonly ConcurrentDictionary entityTypeBuilders = new(); private Boolean isFrozen; } diff --git a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs b/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs deleted file mode 100644 index 82ed2b5..0000000 --- a/src/DbConnectionPlus/DatabaseAdapters/DatabaseAdapterRegistry.cs +++ /dev/null @@ -1,84 +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; - -// TODO: Move to DbConnectionPlusConfiguration. -/// -/// 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/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index fa415a9..dc04c58 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -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, @@ -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..edcafae 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs @@ -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, @@ -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 ef07844..9b99813 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs @@ -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 99018eb..4e8cc70 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs @@ -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 de3d649..8739339 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs @@ -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 99729f5..76d9802 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs @@ -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, @@ -185,7 +185,7 @@ 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( diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs index e1424d2..b6495dd 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs @@ -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, @@ -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.Query.cs b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs index 6c9e772..12079ce 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Query.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs @@ -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 723d94e..0e349a8 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs @@ -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 f8767f2..0f62d40 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs @@ -187,7 +187,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, @@ -433,7 +433,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..1c0b63c 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs @@ -187,7 +187,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, @@ -437,7 +437,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..3b421bc 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs @@ -180,7 +180,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, @@ -441,7 +441,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..1304256 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs @@ -200,7 +200,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, @@ -469,7 +469,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..8115658 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs @@ -188,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, @@ -446,7 +446,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.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs index 4992be9..ada8617 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs @@ -109,7 +109,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, @@ -222,7 +222,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..e123398 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs @@ -100,7 +100,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, @@ -203,7 +203,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 7c5024e..bc364d7 100644 --- a/src/DbConnectionPlus/Entities/EntityHelper.cs +++ b/src/DbConnectionPlus/Entities/EntityHelper.cs @@ -143,9 +143,6 @@ internal static void ResetEntityTypeMetadataCache() => /// private static EntityTypeMetadata CreateEntityTypeMetadata(Type entityType) { - // TODO: Throw exception when there is more than one identity property and change - // EntityTypeMetadata.IdentityProperties to a single property. - String tableName; DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders() diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index bc6fd0e..86b7c63 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -1,4 +1,14 @@ -using NSubstitute.DbConnection; +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; @@ -91,6 +101,51 @@ public void Freeze_ShouldFreezeConfigurationAndEntityTypeBuilders() .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(DbConnectionExtensions.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() { @@ -212,4 +267,54 @@ WHERE TEntity.Id IN ([#{temporaryTables[1].Name}]) OR StringValue = @StringValu 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/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/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index cca010f..e41a3a5 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -37,9 +37,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; From f18d64d82c5f73e66e3cd7702a06d80d62e99b93 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 31 Jan 2026 19:30:20 +0100 Subject: [PATCH 05/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 4 +- .../DbConnectionExtensions.InsertEntities.cs | 2 - .../DbConnectionExtensions.InsertEntity.cs | 2 - .../EntityManipulator.DeleteEntitiesTests.cs | 234 ++++++------ .../EntityManipulator.DeleteEntityTests.cs | 153 ++++---- .../EntityManipulator.InsertEntitiesTests.cs | 251 ++++++++----- .../EntityManipulator.InsertEntityTests.cs | 227 +++++++----- .../EntityManipulator.UpdateEntitiesTests.cs | 349 ++++++++++-------- .../EntityManipulator.UpdateEntityTests.cs | 312 ++++++++-------- .../IntegrationTestsBase.cs | 1 + .../TestDatabase/MySqlTestDatabaseProvider.cs | 17 +- .../OracleTestDatabaseProvider.cs | 20 +- .../PostgreSqlTestDatabaseProvider.cs | 13 + .../SQLiteTestDatabaseProvider.cs | 10 + .../SqlServerTestDatabaseProvider.cs | 15 + .../DbConnectionPlusConfigurationTests.cs | 14 +- ...ConnectionExtensions.ConfigurationTests.cs | 66 ++-- .../Entities/EntityHelperTests.cs | 10 +- ...piTest.PublicApiHasNotChanged.verified.txt | 8 +- .../TestData/Entity.cs | 2 +- .../TestData/EntityWithPublicConstructor.cs | 3 +- .../TestData/EntityWithTableAttribute.cs | 4 - .../TestData/Generate.cs | 2 +- .../TestData/ItemWithConstructor.cs | 3 +- .../TestData/MappingTestEntity.cs | 12 + .../TestData/MappingTestEntityAttributes.cs | 27 ++ .../TestData/MappingTestEntityFluentApi.cs | 11 + .../UnitTestsBase.cs | 6 +- 28 files changed, 1022 insertions(+), 756 deletions(-) delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithTableAttribute.cs create mode 100644 tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs create mode 100644 tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs create mode 100644 tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.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/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs index 76d9802..7866c04 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs @@ -67,7 +67,6 @@ public static partial class DbConnectionExtensions /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -161,7 +160,6 @@ public static Int32 InsertEntities( /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs index b6495dd..547a7e4 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs @@ -67,7 +67,6 @@ public static partial class DbConnectionExtensions /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } @@ -161,7 +160,6 @@ public static Int32 InsertEntity( /// /// class Product /// { - /// [Key] /// public Int64 Id { get; set; } /// public Int64 SupplierId { get; set; } /// public String Name { get; set; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index a1bb328..c6d300a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -55,47 +55,86 @@ protected EntityManipulator_DeleteEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + [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) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entitiesToDelete = this.CreateEntitiesInDb(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities/2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); - await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entitiesToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + await this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + TestContext.Current.CancellationToken + ); foreach (var entity in entitiesToDelete) { - // Since the operation was cancelled, the entities should still exist. + this.ExistsEntityInDb(entity) + .Should().BeFalse(); + } + + foreach (var entity in entitiesToKeep) + { this.ExistsEntityInDb(entity) .Should().BeTrue(); } } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + [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) { - var entitiesToDelete = this.CreateEntitiesInDb(); + 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(); + } + ); + + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); await this.CallApi( useAsyncApi, @@ -110,36 +149,52 @@ await this.CallApi( this.ExistsEntityInDb(entity) .Should().BeFalse(); } + + foreach (var entity in entitiesToKeep) + { + this.ExistsEntityInDb(entity) + .Should().BeTrue(); + } } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( - Boolean useAsyncApi - ) + [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_ShouldUseDefaults(Boolean useAsyncApi, Int32 numberOfEntities) { - var entities = this.CreateEntitiesInDb(); + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); await this.CallApi( useAsyncApi, this.Connection, - entities, + entitiesToDelete, null, TestContext.Current.CancellationToken ); - foreach (var entity in entities) + foreach (var entity in entitiesToDelete) { this.ExistsEntityInDb(entity) .Should().BeFalse(); } + + foreach (var entity in entitiesToKeep) + { + this.ExistsEntityInDb(entity) + .Should().BeTrue(); + } } [Theory] [InlineData(false)] [InlineData(true)] - public Task DeleteEntities_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); @@ -161,73 +216,35 @@ public Task DeleteEntities_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntities_MoreThan10Entities_ShouldBatchDeleteIfPossible(Boolean useAsyncApi) - { - // 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 entitiesToDelete = this.CreateEntitiesInDb(20); - - await this.CallApi( - useAsyncApi, - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_MoreThan10Entities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { - // 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); + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - await this.CallApi( - useAsyncApi, - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); + this.DbCommandFactory.DelayNextDbCommand = true; - await this.CallApi( - useAsyncApi, - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); + await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + cancellationToken + ) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); foreach (var entity in entities) { + // Since the operation was cancelled, all entities should still exist. this.ExistsEntityInDb(entity) - .Should().BeFalse(); + .Should().BeTrue(); } } @@ -257,29 +274,6 @@ public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsy .Should().Be(0); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - await this.CallApi( - useAsyncApi, - this.Connection, - entitiesWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -313,8 +307,6 @@ await this.CallApi( } } - // Refactor all tests to use this strategy. - private Task CallApi( Boolean useAsyncApi, DbConnection connection, diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 96dfff2..95dd3ae 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -34,42 +34,70 @@ protected EntityManipulator_DeleteEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entityToDelete = this.CreateEntityInDb(); + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + await this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken + ); - // Since the operation was cancelled, the entity should still exist. this.ExistsEntityInDb(entityToDelete) + .Should().BeFalse(); + + this.ExistsEntityInDb(entityToKeep) .Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entityToDelete = this.CreateEntityInDb(); + 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(); + } + ); + + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; await this.CallApi( useAsyncApi, @@ -81,31 +109,39 @@ await this.CallApi( this.ExistsEntityInDb(entityToDelete) .Should().BeFalse(); + + this.ExistsEntityInDb(entityToKeep) + .Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task DeleteEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; await this.CallApi( useAsyncApi, this.Connection, - entity, + entityToDelete, null, TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) + this.ExistsEntityInDb(entityToDelete) .Should().BeFalse(); + + this.ExistsEntityInDb(entityToKeep) + .Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public Task DeleteEntity_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); @@ -120,28 +156,39 @@ public Task DeleteEntity_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) .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)}." + $"Make sure that at least one instance property of that type is denoted with a {typeof(KeyAttribute)}." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) + public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { - var entity = this.CreateEntityInDb(); + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - await this.CallApi( - useAsyncApi, - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); + var entityToDelete = this.CreateEntityInDb(); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + cancellationToken + ) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); + + // Since the operation was cancelled, the entity should still exist. + this.ExistsEntityInDb(entityToDelete) + .Should().BeTrue(); } [Theory] @@ -170,26 +217,6 @@ public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsync .Should().Be(0); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); - - await this.CallApi( - useAsyncApi, - this.Connection, - entityWithColumnAttributes, - null, - TestContext.Current.CancellationToken - ); - - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 53f5d6c..06ffd45 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -34,40 +34,101 @@ protected EntityManipulator_InsertEntitiesTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + var entities = Generate.Multiple(); + entities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); - var entities = Generate.Multiple(); + await this.CallApi( + useAsyncApi, + this.Connection, + entities, + null, + TestContext.Current.CancellationToken + ); - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + foreach (var entity in entities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); - this.DbCommandFactory.DelayNextDbCommand = true; + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); - // Since the operation was cancelled, the entities should not have been inserted. - foreach (var entityToInsert in entities) - { - this.ExistsEntityInDb(entityToInsert) - .Should().BeFalse(); + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entities = Generate.Multiple(); + 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(); + } + ); + + var entities = Generate.Multiple(); + entities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); await this.CallApi( useAsyncApi, @@ -79,17 +140,38 @@ await this.CallApi( foreach (var entity in entities) { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task InsertEntities_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) { - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); await this.CallApi( useAsyncApi, @@ -101,8 +183,49 @@ await this.CallApi( foreach (var entity in entities) { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) + { + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + + var entities = Generate.Multiple(); + + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + 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 entityToInsert in entities) + { + this.ExistsEntityInDb(entityToInsert) + .Should().BeFalse(); } } @@ -156,58 +279,6 @@ await this.CallApi( .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntities_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) - { - var entities = Generate.Multiple(); - - await this.CallApi( - useAsyncApi, - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); - - entities - .Should().BeEquivalentTo( - await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken) - ); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) - { - var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - - 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(); - } - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -281,28 +352,6 @@ await this.CallApi( .Should().BeEquivalentTo(entities); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entities = Generate.Multiple(); - - await this.CallApi( - useAsyncApi, - 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); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index 185a49f..e97d3f9 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -34,37 +34,90 @@ protected EntityManipulator_InsertEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task InsertEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + var entity = Generate.Single(); + entity.ComputedColumn_ = 0; + entity.IdentityColumn_ = 0; + entity.NotMappedColumn = "ShouldNotBePersisted"; - var entity = Generate.Single(); + await this.CallApi( + useAsyncApi, + this.Connection, + entity, + null, + TestContext.Current.CancellationToken + ); - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2_)} + """ + ); - this.DbCommandFactory.DelayNextDbCommand = true; + readBackEntity + .Should().NotBeNull(); - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); - // Since the operation was cancelled, the entity should not have been inserted. - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entity = Generate.Single(); + 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(); + } + ); + + var entity = Generate.Single(); + entity.ComputedColumn_ = 0; + entity.IdentityColumn_ = 0; + entity.NotMappedColumn = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -74,16 +127,37 @@ await this.CallApi( TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task InsertEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) { - var entity = Generate.Single(); + var entity = Generate.Single(); await this.CallApi( useAsyncApi, @@ -93,8 +167,46 @@ await this.CallApi( TestContext.Current.CancellationToken ); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(entity.KeyColumn1)} AND + KeyColumn2 = {Parameter(entity.KeyColumn2)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) + { + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + + var entity = Generate.Single(); + + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + 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. this.ExistsEntityInDb(entity) - .Should().BeTrue(); + .Should().BeFalse(); } [Theory] @@ -137,57 +249,6 @@ Boolean useAsyncApi .Should().BeEquivalentTo(entity.Enum.ToString()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) - { - var entity = Generate.Single(); - - await this.CallApi( - useAsyncApi, - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); - - entity - .Should().BeEquivalentTo( - await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) - ); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) - { - var entity = Generate.Single(); - entity.NotMappedValue = "ShouldNotBePersisted"; - - 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(); - } - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -234,22 +295,6 @@ public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyn .Should().BeEquivalentTo(entity); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entity = Generate.Single(); - - await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 819fe30..8dff659 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -34,42 +34,105 @@ protected EntityManipulator_UpdateEntitiesTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + var entities = this.CreateEntitiesInDb(); + + var updatedEntities = Generate.UpdateFor(entities); + updatedEntities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ); - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + foreach (var updatedEntity in updatedEntities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + """ + ); - this.DbCommandFactory.DelayNextDbCommand = true; + readBackEntity + .Should().NotBeNull(); - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + readBackEntity.ValueColumn_ + .Should().Be(updatedEntity.ValueColumn_); - // Since the operation was cancelled, the entities should not have been updated. - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + readBackEntity.ComputedColumn_ + .Should().Be(updatedEntity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(updatedEntity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_EntitiesWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + 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(); + } + ); + + var entities = this.CreateEntitiesInDb(); + + var updatedEntities = Generate.UpdateFor(entities); + updatedEntities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); await this.CallApi( useAsyncApi, @@ -79,22 +142,41 @@ await this.CallApi( TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + foreach (var updatedEntity in updatedEntities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(updatedEntity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(updatedEntity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(updatedEntity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_EntitiesWithTableAttribute_ShouldUseTableNameFromAttribute( - Boolean useAsyncApi - ) + public async Task UpdateEntities_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var entities = this.CreateEntitiesInDb(); + var updatedEntities = Generate.UpdateFor(entities); await this.CallApi( useAsyncApi, @@ -104,11 +186,75 @@ await this.CallApi( TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( + foreach (var updatedEntity in updatedEntities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(updatedEntity.ValueColumn); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + { + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + [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)}." + ); + } + + [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.UpdateFor(entities); + + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + await Invoking(() => + this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); + + // Since the operation was cancelled, the entities should not have been updated. + (await this.Connection.QueryAsync( $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + .Should().BeEquivalentTo(entities); } [Theory] @@ -136,7 +282,7 @@ 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.CallApi( useAsyncApi, @@ -179,7 +325,7 @@ 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.CallApi( useAsyncApi, @@ -197,109 +343,13 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public Task UpdateEntities_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => - this.CallApi( - useAsyncApi, - 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)}." - ); - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - 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); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - updatedEntities - .Should().BeEquivalentTo( - await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ).ToListAsync(TestContext.Current.CancellationToken) - ); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - - var updatedEntities = Generate.UpdatesFor(entities); - updatedEntities.ForEach(a => a.NotMappedValue = "ShouldNotBePersisted"); - - 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(); - } - } - [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.CallApi( useAsyncApi, @@ -330,7 +380,7 @@ public async Task UpdateEntities_ShouldSupportDateTimeOffsetValues(Boolean useAs Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); await this.CallApi( useAsyncApi, @@ -353,7 +403,7 @@ await this.CallApi( public async Task UpdateEntities_ShouldUpdateEntities(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); (await this.CallApi( useAsyncApi, @@ -371,29 +421,6 @@ public async Task UpdateEntities_ShouldUpdateEntities(Boolean useAsyncApi) .Should().BeEquivalentTo(updatedEntities); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - var updatedEntities = Generate.UpdatesFor(entities); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync()) - .Should().BeEquivalentTo(updatedEntities); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -403,7 +430,7 @@ public async Task UpdateEntities_Transaction_ShouldUseTransaction(Boolean useAsy await using (var transaction = await this.Connection.BeginTransactionAsync()) { - var updatedEntities = Generate.UpdatesFor(entities); + var updatedEntities = Generate.UpdateFor(entities); (await this.CallApi( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 2e065ac..ed2b4e1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -34,42 +34,94 @@ protected EntityManipulator_UpdateEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + var entity = this.CreateEntityInDb(); - var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ); - this.DbCommandFactory.DelayNextDbCommand = true; + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + """ + ); - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + readBackEntity + .Should().NotBeNull(); - // Since the operation was cancelled, the entity should not have been updated. - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + readBackEntity.ValueColumn_ + .Should().Be(updatedEntity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(updatedEntity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(updatedEntity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EntityWithoutTableAttribute_ShouldUseEntityTypeNameAsTableName( - Boolean useAsyncApi - ) + public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + 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(); + } + ); + + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -79,19 +131,37 @@ await this.CallApi( TestContext.Current.CancellationToken ); - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(updatedEntity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(updatedEntity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(updatedEntity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EntityWithTableAttribute_ShouldUseTableNameFromAttribute(Boolean useAsyncApi) + public async Task UpdateEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); + var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); await this.CallApi( @@ -102,11 +172,72 @@ await this.CallApi( TestContext.Current.CancellationToken ); - (await this.Connection.QuerySingleAsync( + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1)} AND + KeyColumn2 = {Parameter(updatedEntity.KeyColumn2)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(updatedEntity.ValueColumn); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + { + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + 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)}." + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) + { + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + await Invoking(() => + this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); + + // Since the operation was cancelled, the entity should not have been updated. + (await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(updatedEntity); + .Should().BeEquivalentTo(entity); } [Theory] @@ -185,102 +316,6 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public Task UpdateEntity_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => - this.CallApi( - useAsyncApi, - 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)}." - ); - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_ShouldHandleEntityWithCompositeKey(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - 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); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_ShouldHandleIdentityAndComputedColumns(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - updatedEntity - .Should().BeEquivalentTo( - await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("EntityWithIdentityAndComputedProperties")}" - ) - ); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_ShouldIgnorePropertiesDenotedWithNotMappedAttribute(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.NotMappedValue = "ShouldNotBePersisted"; - - 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(); - } - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -359,29 +394,6 @@ public async Task UpdateEntity_ShouldUpdateEntity(Boolean useAsyncApi) .Should().BeEquivalentTo(updatedEntity); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - (await this.Connection.QuerySingleAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs index f3a4c5d..7931e13 100644 --- a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs +++ b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs @@ -50,6 +50,7 @@ protected IntegrationTestsBase() EnumSerializationMode = EnumSerializationMode.Strings, InterceptDbCommand = DbCommandLogger.LogDbCommand }; + EntityHelper.ResetEntityTypeMetadataCache(); } /// diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 8f02976..0645876 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -234,7 +234,19 @@ CREATE TABLE `EntityWithNotMappedProperty` `NotMappedValue` VARCHAR(200) NULL ); GO - + + CREATE TABLE `MappingTestEntity` + ( + `KeyColumn1` BIGINT NOT NULL, + `KeyColumn2` BIGINT NOT NULL, + `ValueColumn` INT NOT NULL, + `ComputedColumn` AS (`ValueColumn`+999), + `IdentityColumn` INT AUTO_INCREMENT NOT NULL, + `NotMappedColumn` TEXT NULL, + PRIMARY KEY (`KeyColumn1`, `KeyColumn2`) + ); + GO + CREATE PROCEDURE `GetEntities` () BEGIN SELECT * FROM `Entity`; @@ -299,6 +311,9 @@ CREATE TABLE `EntityWithNotMappedProperty` TRUNCATE TABLE `EntityWithNotMappedProperty`; GO + + TRUNCATE TABLE `MappingTestEntity`; + GO """; private static readonly String connectionString; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 8cffba0..500f360 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -221,7 +221,19 @@ CREATE TABLE "EntityWithNotMappedProperty" "NotMappedValue" NVARCHAR2(200) NULL ); GO - + + CREATE TABLE "MappingTestEntity" + ( + "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 + CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS BEGIN DELETE FROM "Entity"; @@ -259,6 +271,9 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS DROP TABLE IF EXISTS "EntityWithNotMappedProperty" PURGE; GO + DROP TABLE IF EXISTS "MappingTestEntity" PURGE; + GO + DROP PROCEDURE IF EXISTS "DeleteAllEntities"; GO """; @@ -291,6 +306,9 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS TRUNCATE TABLE "EntityWithNotMappedProperty"; GO + + TRUNCATE TABLE "MappingTestEntity"; + GO """; private static readonly String connectionString; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index d452114..a93fcd0 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -206,6 +206,18 @@ CREATE TABLE "EntityWithNotMappedProperty" "NotMappedValue" text NULL ); + + CREATE TABLE "MappingTestEntity" + ( + "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" () LANGUAGE SQL AS $$ @@ -255,6 +267,7 @@ DELETE FROM "Entity" 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..a1d276d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -198,5 +198,15 @@ CREATE TABLE EntityWithNotMappedProperty MappedValue TEXT NOT NULL, NotMappedValue TEXT NULL ); + + CREATE TABLE MappingTestEntity + ( + 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..d93412d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -246,6 +246,18 @@ NotMappedValue NVARCHAR(200) NULL ); GO + CREATE TABLE MappingTestEntity + ( + 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 + CREATE PROCEDURE GetEntities AS BEGIN @@ -319,6 +331,9 @@ DELETE FROM Entity TRUNCATE TABLE EntityWithNotMappedProperty; GO + + TRUNCATE TABLE MappingTestEntity; + GO """; private static readonly String connectionString; diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index 86b7c63..b8c6c6d 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -152,27 +152,21 @@ public void GetEntityTypeBuilders_ShouldGetConfiguredBuilders() var configuration = new DbConnectionPlusConfiguration(); var entityBuilder = configuration.Entity(); - var entityWithIdentityAndComputedPropertiesBuilder = - configuration.Entity(); - var entityWithNotMappedPropertyBuilder = configuration.Entity(); + var mappingTestEntityFluentApiBuilder = configuration.Entity(); var entityTypeBuilders = configuration.GetEntityTypeBuilders(); entityTypeBuilders .Should().ContainKeys( typeof(Entity), - typeof(EntityWithIdentityAndComputedProperties), - typeof(EntityWithNotMappedProperty) + typeof(MappingTestEntityFluentApi) ); entityTypeBuilders[typeof(Entity)] .Should().BeSameAs(entityBuilder); - entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)] - .Should().BeSameAs(entityWithIdentityAndComputedPropertiesBuilder); - - entityTypeBuilders[typeof(EntityWithNotMappedProperty)] - .Should().BeSameAs(entityWithNotMappedPropertyBuilder); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .Should().BeSameAs(mappingTestEntityFluentApiBuilder); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs index ae6cc07..aff18e9 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -12,20 +12,31 @@ public void Configure_ShouldConfigureDbConnectionPlus() configuration.EnumSerializationMode = EnumSerializationMode.Integers; configuration.InterceptDbCommand = interceptDbCommand; - configuration.Entity() - .ToTable("Entities"); + configuration.Entity() + .ToTable("MappingTestEntity"); - configuration.Entity() - .Property(a => a.Id) - .HasColumnName("Identifier") + configuration.Entity() + .Property(a => a.KeyColumn1_) + .HasColumnName("KeyColumn1") .IsKey(); - configuration.Entity() - .Property(a => a.ComputedValue) + configuration.Entity() + .Property(a => a.KeyColumn2_) + .HasColumnName("KeyColumn2") + .IsKey(); + + configuration.Entity() + .Property(a => a.ComputedColumn_) + .HasColumnName("ComputedColumn") .IsComputed(); - configuration.Entity() - .Property(a => a.NotMappedValue) + configuration.Entity() + .Property(a => a.IdentityColumn_) + .HasColumnName("IdentityColumn") + .IsIdentity(); + + configuration.Entity() + .Property(a => a.NotMappedColumn) .IsIgnored(); } ); @@ -39,40 +50,35 @@ public void Configure_ShouldConfigureDbConnectionPlus() var entityTypeBuilders = DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders(); entityTypeBuilders - .Should().HaveCount(3); + .Should().HaveCount(1); entityTypeBuilders .Should().ContainKeys( - typeof(Entity), - typeof(EntityWithIdentityAndComputedProperties), - typeof(EntityWithNotMappedProperty) + typeof(MappingTestEntityFluentApi) ); - entityTypeBuilders[typeof(Entity)].TableName - .Should().Be("Entities"); - - entityTypeBuilders[typeof(Entity)].PropertyBuilders["Id"].ColumnName - .Should().Be("Identifier"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].TableName + .Should().Be("MappingTestEntity"); - entityTypeBuilders[typeof(Entity)].PropertyBuilders["Id"].IsKey - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn1_"].ColumnName + .Should().Be("KeyColumn1"); - entityTypeBuilders - .Should().ContainKey(typeof(EntityWithIdentityAndComputedProperties)); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn2_"].ColumnName + .Should().Be("KeyColumn2"); - entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)].TableName - .Should().BeNull(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].ColumnName + .Should().Be("ComputedColumn"); - entityTypeBuilders[typeof(EntityWithIdentityAndComputedProperties)].PropertyBuilders["ComputedValue"].IsComputed + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].IsComputed .Should().BeTrue(); - entityTypeBuilders - .Should().ContainKey(typeof(EntityWithNotMappedProperty)); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].IsIdentity + .Should().BeTrue(); - entityTypeBuilders[typeof(EntityWithNotMappedProperty)].TableName - .Should().BeNull(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].ColumnName + .Should().Be("IdentityColumn"); - entityTypeBuilders[typeof(EntityWithNotMappedProperty)].PropertyBuilders["NotMappedValue"].IsIgnored + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMappedColumn"].IsIgnored .Should().BeTrue(); } diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 523e198..2851289 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -162,7 +162,7 @@ public void GetEntityTypeMetadata_MoreThanOneIdentityProperty_ShouldThrow() => ); [Fact] - public void GetEntityTypeMetadata_FluentAPIConfig_ShouldGetMetadataBasedOnFluentAPIConfig() + public void GetEntityTypeMetadata_FluentApiMapping_ShouldGetMetadataBasedOnFluentApiMapping() { var tableName = Generate.Single(); var columnName = Generate.Single(); @@ -247,11 +247,9 @@ public void GetEntityTypeMetadata_FluentAPIConfig_ShouldGetMetadataBasedOnFluent } [Theory] - [InlineData(typeof(Entity))] - [InlineData(typeof(EntityWithTableAttribute))] - [InlineData(typeof(EntityWithIdentityAndComputedProperties))] - [InlineData(typeof(EntityWithColumnAttributes))] - public void GetEntityTypeMetadata_NoFluentAPIConfig_ShouldGetMetadataBasedOnDataAnnotationAttributes( + [InlineData(typeof(MappingTestEntity))] + [InlineData(typeof(MappingTestEntityAttributes))] + public void GetEntityTypeMetadata_NoFluentApiMapping_ShouldGetMetadataBasedOnDataAnnotationAttributes( Type entityType ) { diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 3242099..08c9ac6 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -3,11 +3,12 @@ namespace RentADeveloper.DbConnectionPlus.Configuration { public sealed class DbConnectionPlusConfiguration : RentADeveloper.DbConnectionPlus.Configuration.IFreezable { - public DbConnectionPlusConfiguration() { } 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 { @@ -36,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; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 3f06d4c..5ed98c7 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/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index 801f4dc..5bb57eb 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -1,4 +1,5 @@ -#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 4d1d108..1feaa82 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -303,7 +303,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..23bffbb 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs @@ -1,4 +1,5 @@ -#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..1c761ba --- /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..6e4746e --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -0,0 +1,27 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +[Table("MappingTestEntity")] +public record MappingTestEntityAttributes +{ + [Key] + [Column("KeyColumn1")] + public Int64 KeyColumn1_ { get; set; } + + [Key] + [Column("KeyColumn2")] + public Int64 KeyColumn2_ { get; set; } + + [Column("ValueColumn")] + public Int32 ValueColumn_ { get; set; } + + [Column("ComputedColumn")] + [DatabaseGenerated(DatabaseGeneratedOption.Computed)] + public Int32 ComputedColumn_ { get; set; } + + [Column("IdentityColumn")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Int32 IdentityColumn_ { get; set; } + + [NotMapped] + public String? NotMappedColumn { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs new file mode 100644 index 0000000..83863c3 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -0,0 +1,11 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +public record MappingTestEntityFluentApi +{ + public Int64 KeyColumn1_ { get; set; } + public Int64 KeyColumn2_ { get; set; } + public Int32 ValueColumn_ { get; set; } + public Int32 ComputedColumn_ { get; set; } + public Int32 IdentityColumn_ { get; set; } + public String? NotMappedColumn { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index e41a3a5..d0c9511 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -27,7 +27,11 @@ public UnitTestsBase() // Reset all settings to defaults before each test. - DbConnectionPlusConfiguration.Instance = new(); + DbConnectionPlusConfiguration.Instance = new() + { + EnumSerializationMode = EnumSerializationMode.Strings, + InterceptDbCommand = null + }; EntityHelper.ResetEntityTypeMetadataCache(); OracleDatabaseAdapter.AllowTemporaryTables = false; From a1365ec8356f7ded5d8a74f9cf9edfa2a4005366 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 31 Jan 2026 22:09:24 +0100 Subject: [PATCH 06/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../Converters/EnumConverter.cs | 136 +++++++ .../Converters/ValueConverter.cs | 156 ++++++++ .../MaterializerFactoryHelper.cs | 12 +- .../Converters/EnumConverterTests.cs | 130 ++++++- .../Converters/ValueConverterTests.cs | 343 ++++++++++++++++-- .../MaterializerFactoryHelperTests.cs | 15 - 6 files changed, 737 insertions(+), 55 deletions(-) diff --git a/src/DbConnectionPlus/Converters/EnumConverter.cs b/src/DbConnectionPlus/Converters/EnumConverter.cs index b703730..7833d80 100644 --- a/src/DbConnectionPlus/Converters/EnumConverter.cs +++ b/src/DbConnectionPlus/Converters/EnumConverter.cs @@ -146,6 +146,142 @@ internal static class EnumConverter } } + /// + /// 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 1bdef09..f13ac7f 100644 --- a/src/DbConnectionPlus/Converters/ValueConverter.cs +++ b/src/DbConnectionPlus/Converters/ValueConverter.cs @@ -224,6 +224,162 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove } } + /// + /// 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/Materializers/MaterializerFactoryHelper.cs b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs index 2e7c22f..1fd7573 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,11 @@ 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.Name == nameof(ValueConverter.ConvertValueToType) && + m.IsGenericMethod + )!; /// /// Creates an that gets the value of a field of the specified field type from a 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/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)] From c35563752947c5b65d70f79751a03156ca2b40c8 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 31 Jan 2026 23:11:35 +0100 Subject: [PATCH 07/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../MySql/MySqlEntityManipulator.cs | 5 + .../Oracle/OracleEntityManipulator.cs | 4 + .../PostgreSql/PostgreSqlEntityManipulator.cs | 5 + .../SqlServer/SqlServerEntityManipulator.cs | 5 + .../Sqlite/SqliteEntityManipulator.cs | 5 + .../EntityManipulator.DeleteEntitiesTests.cs | 25 +- .../EntityManipulator.DeleteEntityTests.cs | 2 +- .../EntityManipulator.InsertEntitiesTests.cs | 14 +- .../EntityManipulator.InsertEntityTests.cs | 14 +- .../EntityManipulator.UpdateEntitiesTests.cs | 14 +- .../EntityManipulator.UpdateEntityTests.cs | 14 +- .../TemporaryTableBuilderTests.cs | 195 ++++++++++--- ...ConnectionExtensions.QueryFirstOfTTests.cs | 119 +++++++- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 119 +++++++- .../DbConnectionExtensions.QueryOfTTests.cs | 182 +++++++++--- ...onnectionExtensions.QuerySingleOfTTests.cs | 119 +++++++- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 119 +++++++- .../TestDatabase/MySqlTestDatabaseProvider.cs | 43 +-- .../OracleTestDatabaseProvider.cs | 44 --- .../PostgreSqlTestDatabaseProvider.cs | 27 -- .../SQLiteTestDatabaseProvider.cs | 23 -- .../SqlServerTestDatabaseProvider.cs | 35 --- .../EntityMaterializerFactoryTests.cs | 275 ++++++++++++++---- .../TestData/EntityWithColumnAttributes.cs | 57 ---- .../TestData/EntityWithCompositeKey.cs | 12 - ...EntityWithIdentityAndComputedProperties.cs | 15 - .../TestData/EntityWithNotMappedProperty.cs | 12 - .../TestData/Generate.cs | 24 -- 28 files changed, 1015 insertions(+), 512 deletions(-) delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithCompositeKey.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithIdentityAndComputedProperties.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithNotMappedProperty.cs diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index ebce3f1..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; @@ -1389,6 +1390,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1426,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/Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs index b1ad057..a1bf7d1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs @@ -2,6 +2,7 @@ // 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; @@ -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/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index ca1ce6c..9567f39 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -3,6 +3,7 @@ using LinkDotNet.StringBuilder; using Npgsql; +using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -1336,6 +1337,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1373,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/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index b32a5f0..cbd2924 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -2,6 +2,7 @@ // 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; @@ -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/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 01da5a7..1519eda 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -2,6 +2,7 @@ // 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; @@ -1052,6 +1053,8 @@ CancellationToken cancellationToken var value = reader.GetValue(i); + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + property.PropertySetter!(entity, value); } } @@ -1089,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/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index c6d300a..c14cf59 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -23,29 +23,6 @@ public sealed class EntityManipulator_DeleteEntitiesTests_SqlServer : EntityManipulator_DeleteEntitiesTests; -// TODO: Implement integration test (CRUD, Query, Temporary Tables) for fluent API config as well as attribute based -// config. - -// TODO: Table Name -> via Type Name -// TODO: Table Name -> via Attribute -// TODO: Table Name -> via Fluent API - -// TODO: Column Name -> via Property Name -// TODO: Column Name -> via Attribute -// TODO: Column Name -> via Fluent API - -// TODO: Key Property -> via Attribute -// TODO: Key Property -> via Fluent API - -// TODO: Computed Property -> via Attribute -// TODO: Computed Property -> via Fluent API - -// TODO: Identity Property -> via Attribute -// TODO: Identity Property -> via Fluent API - -// TODO: Ignore Property -> via Attribute -// TODO: Ignore Property -> via Fluent API - public abstract class EntityManipulator_DeleteEntitiesTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() @@ -164,7 +141,7 @@ await this.CallApi( // to test that as well. [InlineData(false, 30)] [InlineData(true, 30)] - public async Task DeleteEntities_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi, Int32 numberOfEntities) + public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi, Int32 numberOfEntities) { var entities = this.CreateEntitiesInDb(numberOfEntities); var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 95dd3ae..61591ab 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -117,7 +117,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) + public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); var entityToDelete = entities[0]; diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 06ffd45..12f3e21 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -59,8 +59,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} """ ); @@ -144,8 +144,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} """ ); @@ -169,7 +169,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) + public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { var entities = Generate.Multiple(); @@ -187,8 +187,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} """ ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index e97d3f9..6a32358 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -53,8 +53,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} """ ); @@ -131,8 +131,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} """ ); @@ -155,7 +155,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) + public async Task InsertEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { var entity = Generate.Single(); @@ -171,8 +171,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(entity.KeyColumn1)} AND - KeyColumn2 = {Parameter(entity.KeyColumn2)} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} """ ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 8dff659..5c6d382 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -61,8 +61,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} """ ); @@ -148,8 +148,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} """ ); @@ -173,7 +173,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) + public async Task UpdateEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); @@ -192,8 +192,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2)} """ ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index ed2b4e1..3108f9e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -55,8 +55,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} """ ); @@ -135,8 +135,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1_)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2_)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} """ ); @@ -159,7 +159,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_Mapping_NoMapping_ShouldUseDefaults(Boolean useAsyncApi) + public async Task UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); @@ -176,8 +176,8 @@ await this.CallApi( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE KeyColumn1 = {Parameter(updatedEntity.KeyColumn1)} AND - KeyColumn2 = {Parameter(updatedEntity.KeyColumn2)} + WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1)} AND + {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2)} """ ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 2b94636..a1f8ab9 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -182,13 +182,10 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ComplexObjects_NotMappedProperties_ShouldNotCreateColumnsForNotMappedProperties( - Boolean useAsyncApi - ) + 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"); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -196,7 +193,7 @@ Boolean useAsyncApi null, "Objects", entities, - typeof(EntityWithNotMappedProperty), + typeof(MappingTestEntityAttributes), TestContext.Current.CancellationToken ); @@ -206,7 +203,164 @@ Boolean useAsyncApi ); reader.GetFieldNames() - .Should().NotContain(nameof(EntityWithNotMappedProperty.NotMappedValue)); + .Should().NotContain(nameof(MappingTestEntityAttributes.NotMappedColumn)); + + foreach (var entity in entities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {QT("Objects")} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entities = Generate.Multiple(); + entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); + + await using var tableDisposer = await this.CallApi( + useAsyncApi, + this.Connection, + null, + "Objects", + entities, + typeof(MappingTestEntityFluentApi), + TestContext.Current.CancellationToken + ); + + await using var reader = await this.Connection.ExecuteReaderAsync( + $"SELECT * FROM {QT("Objects")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + reader.GetFieldNames() + .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMappedColumn)); + + foreach (var entity in entities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {QT("Objects")} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entities = Generate.Multiple(); + + await using var tableDisposer = await this.CallApi( + useAsyncApi, + this.Connection, + null, + "Objects", + entities, + typeof(MappingTestEntity), + TestContext.Current.CancellationToken + ); + + await using var reader = await this.Connection.ExecuteReaderAsync( + $"SELECT * FROM {QT("Objects")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + foreach (var entity in entities) + { + var readBackEntity = this.Connection.QueryFirstOrDefault( + $""" + SELECT * + FROM {QT("Objects")} + WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND + {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} + """ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); + } } [Theory] @@ -258,31 +412,6 @@ Boolean useAsyncApi .Should().Be(this.TestDatabaseProvider.DatabaseCollation); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entities = Generate.Multiple(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - await using var tableDisposer = await this.CallApi( - useAsyncApi, - 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); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 8eb35b7..4ad334e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -751,18 +751,117 @@ public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(Boolea [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); + var entity = this.CreateEntityInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index 8bc3a74..1c9013c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -771,18 +771,117 @@ public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValu [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); + var entity = this.CreateEntityInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QueryFirstOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 9165333..37c9af2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -319,9 +319,7 @@ public async Task [InlineData(false)] [InlineData(true)] public async Task - Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -410,9 +408,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -463,9 +459,7 @@ await Invoking(() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( - Boolean useAsyncApi - ) + public async Task Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -621,9 +615,144 @@ public async Task Query_EntityType_EnumEntityProperty_ShouldConvertStringToEnum( [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( - Boolean useAsyncApi - ) => + public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + + var readBackEntities = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken); + + foreach (var entity in entities) + { + var readBackEntity = readBackEntities.FirstOrDefault(a => + a.KeyColumn1_ == entity.KeyColumn1_ && a.KeyColumn2_ == entity.KeyColumn2_ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entities = this.CreateEntitiesInDb(); + + var readBackEntities = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken); + + foreach (var entity in entities) + { + var readBackEntity = readBackEntities.FirstOrDefault(a => + a.KeyColumn1_ == entity.KeyColumn1_ && a.KeyColumn2_ == entity.KeyColumn2_ + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Query_EntityType_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + + var readBackEntities = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken); + + foreach (var entity in entities) + { + var readBackEntity = readBackEntities.FirstOrDefault(a => + a.KeyColumn1 == entity.KeyColumn1 && a.KeyColumn2 == entity.KeyColumn2 + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") .ToListAsync(TestContext.Current.CancellationToken).AsTask() @@ -702,9 +831,7 @@ public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_Should [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) + 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)" @@ -753,23 +880,6 @@ public async Task Query_EntityType_ShouldSupportDateTimeOffsetValues(Boolean use .Should().BeEquivalentTo(entities); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task Query_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - var entitiesWithColumnAttributes = Generate.MapTo(entities); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entitiesWithColumnAttributes); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -877,9 +987,7 @@ public async Task [InlineData(false)] [InlineData(true)] public async Task - Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -1051,9 +1159,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => + public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi>( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 140e2e5..4d4036a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -758,18 +758,117 @@ public async Task QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues(Boole [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); + var entity = this.CreateEntityInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 16735de..e1b023c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -777,18 +777,117 @@ public async Task QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetVal [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_ShouldUseConfiguredColumnNames(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entity = this.CreateEntityInDb(); - var entityWithColumnAttributes = Generate.MapTo(entity); + var entity = this.CreateEntityInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithColumnAttributes); + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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(); + } + ); + + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn_ + .Should().Be(entity.ValueColumn_); + + readBackEntity.ComputedColumn_ + .Should().Be(entity.ComputedColumn_); + + readBackEntity.IdentityColumn_ + .Should().Be(entity.IdentityColumn_); + + readBackEntity.NotMappedColumn + .Should().BeNull(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task QuerySingleOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + var readBackEntity = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ); + + readBackEntity + .Should().NotBeNull(); + + readBackEntity.ValueColumn + .Should().Be(entity.ValueColumn); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 0645876..0b4ec38 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -207,43 +207,15 @@ CREATE TABLE `EntityWithNullableProperty` `Value` BIGINT NULL ); GO - - CREATE TABLE `EntityWithIdentityAndComputedProperties` - ( - `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 - ); - GO CREATE TABLE `MappingTestEntity` ( `KeyColumn1` BIGINT NOT NULL, `KeyColumn2` BIGINT NOT NULL, `ValueColumn` INT NOT NULL, - `ComputedColumn` AS (`ValueColumn`+999), - `IdentityColumn` INT AUTO_INCREMENT NOT NULL, - `NotMappedColumn` TEXT NULL, - PRIMARY KEY (`KeyColumn1`, `KeyColumn2`) + `ComputedColumn` INT AS (`ValueColumn`+999), + `IdentityColumn` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `NotMappedColumn` TEXT NULL ); GO @@ -303,15 +275,6 @@ PRIMARY KEY (`KeyColumn1`, `KeyColumn2`) TRUNCATE TABLE `EntityWithNullableProperty`; GO - TRUNCATE TABLE `EntityWithIdentityAndComputedProperties`; - GO - - TRUNCATE TABLE `EntityWithCompositeKey`; - GO - - TRUNCATE TABLE `EntityWithNotMappedProperty`; - GO - TRUNCATE TABLE `MappingTestEntity`; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 500f360..ad9087e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -195,32 +195,6 @@ CREATE TABLE "EntityWithNullableProperty" "Value" NUMBER(19) NULL ); GO - - CREATE TABLE "EntityWithIdentityAndComputedProperties" - ( - "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 - ); - GO CREATE TABLE "MappingTestEntity" ( @@ -262,15 +236,6 @@ 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; - GO - DROP TABLE IF EXISTS "MappingTestEntity" PURGE; GO @@ -298,15 +263,6 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS TRUNCATE TABLE "EntityWithNullableProperty"; GO - TRUNCATE TABLE "EntityWithIdentityAndComputedProperties"; - GO - - TRUNCATE TABLE "EntityWithCompositeKey"; - GO - - TRUNCATE TABLE "EntityWithNotMappedProperty"; - GO - TRUNCATE TABLE "MappingTestEntity"; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index a93fcd0..44c4283 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -182,30 +182,6 @@ CREATE TABLE "EntityWithNullableProperty" "Id" bigint NOT NULL PRIMARY KEY, "Value" bigint NULL ); - - CREATE TABLE "EntityWithIdentityAndComputedProperties" - ( - "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 - ); - CREATE TABLE "MappingTestEntity" ( @@ -264,9 +240,6 @@ DELETE FROM "Entity" TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; TRUNCATE TABLE "EntityWithNonNullableProperty"; TRUNCATE TABLE "EntityWithNullableProperty"; - TRUNCATE TABLE "EntityWithIdentityAndComputedProperties"; - TRUNCATE TABLE "EntityWithCompositeKey"; - TRUNCATE TABLE "EntityWithNotMappedProperty"; TRUNCATE TABLE "MappingTestEntity"; """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index a1d276d..0d89489 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -175,29 +175,6 @@ CREATE TABLE EntityWithNullableProperty Id INTEGER NOT NULL, Value INTEGER NULL ); - - CREATE TABLE EntityWithIdentityAndComputedProperties - ( - 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 - ); CREATE TABLE MappingTestEntity ( diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index d93412d..f793914 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -220,32 +220,6 @@ Value BIGINT NULL ); GO - CREATE TABLE EntityWithIdentityAndComputedProperties - ( - 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 - ); - GO - CREATE TABLE MappingTestEntity ( KeyColumn1 BIGINT NOT NULL, @@ -323,15 +297,6 @@ DELETE FROM Entity TRUNCATE TABLE EntityWithNullableProperty; GO - TRUNCATE TABLE EntityWithIdentityAndComputedProperties; - GO - - TRUNCATE TABLE EntityWithCompositeKey; - GO - - TRUNCATE TABLE EntityWithNotMappedProperty; - GO - TRUNCATE TABLE MappingTestEntity; GO """; diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 855ea3b..2b7cac8 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -303,6 +303,222 @@ 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() + { + 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(); + } + ); + + 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 +666,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() @@ -649,25 +825,6 @@ public void Materializer_ShouldMaterializeDateTimeOffsetValue() .Should().BeEquivalentTo(entity); } - [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 ShouldGuardAgainstNullArguments() => ArgumentNullGuardVerifier.Verify(() => diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithColumnAttributes.cs deleted file mode 100644 index ce7fd61..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/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/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 1feaa82..d445102 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -107,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 - } - ); } /// From 2b11f5e39749fe4f2dcdff719ed89f61a7b1aeea Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 31 Jan 2026 23:57:51 +0100 Subject: [PATCH 08/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../Configuration/EntityPropertyBuilder.cs | 2 +- .../Configuration/IEntityPropertyBuilder.cs | 10 +- .../Oracle/OracleTemporaryTableBuilder.cs | 12 +- .../PostgreSqlTemporaryTableBuilder.cs | 4 +- .../DbConnectionExtensions.DeleteEntities.cs | 1 + .../Entities/EntityTypeMetadata.cs | 2 +- .../MaterializerFactoryHelper.cs | 5 +- .../ValueTupleMaterializerFactory.cs | 4 +- .../EntityManipulator.DeleteEntitiesTests.cs | 146 ++++----- .../EntityManipulator.DeleteEntityTests.cs | 113 +++---- .../EntityManipulator.InsertEntitiesTests.cs | 244 +++++--------- .../EntityManipulator.InsertEntityTests.cs | 235 +++++-------- .../EntityManipulator.UpdateEntitiesTests.cs | 308 +++++++----------- .../EntityManipulator.UpdateEntityTests.cs | 291 ++++++----------- .../TemporaryTableBuilderTests.cs | 139 ++------ .../DbCommands/DbCommandBuilderTests.cs | 4 +- ...ConnectionExtensions.ExecuteReaderTests.cs | 4 +- ...ConnectionExtensions.ExecuteScalarTests.cs | 8 +- .../DbConnectionExtensions.ExistsTests.cs | 4 +- ...ConnectionExtensions.QueryFirstOfTTests.cs | 198 ++++------- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 196 ++++------- ...tionExtensions.QueryFirstOrDefaultTests.cs | 4 +- .../DbConnectionExtensions.QueryFirstTests.cs | 8 +- .../DbConnectionExtensions.QueryOfTTests.cs | 135 ++------ ...onnectionExtensions.QuerySingleOfTTests.cs | 210 ++++-------- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 200 ++++-------- ...ionExtensions.QuerySingleOrDefaultTests.cs | 4 +- ...DbConnectionExtensions.QuerySingleTests.cs | 8 +- .../TestDatabase/MySqlTestDatabaseProvider.cs | 4 +- .../OracleTestDatabaseProvider.cs | 4 +- .../PostgreSqlTestDatabaseProvider.cs | 4 +- .../SQLiteTestDatabaseProvider.cs | 2 +- .../DbConnectionPlusConfigurationTests.cs | 2 +- ...ConnectionExtensions.ConfigurationTests.cs | 18 +- .../Entities/EntityHelperTests.cs | 20 +- .../EntityMaterializerFactoryTests.cs | 35 +- .../TestData/Entity.cs | 2 +- .../TestData/EntityWithPublicConstructor.cs | 1 + .../TestData/ItemWithConstructor.cs | 1 + .../TestData/MappingTestEntity.cs | 2 +- .../TestData/MappingTestEntityAttributes.cs | 22 +- .../TestData/MappingTestEntityFluentApi.cs | 45 ++- 42 files changed, 899 insertions(+), 1762 deletions(-) diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs index 8396c86..f2e18a6 100644 --- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -6,7 +6,7 @@ public sealed class EntityPropertyBuilder : IEntityPropertyBuilder { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The entity type builder this property builder belongs to. /// The name of the property being configured. diff --git a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs index 07fc5a2..c0c6b79 100644 --- a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs @@ -5,11 +5,6 @@ /// internal interface IEntityPropertyBuilder : IFreezable { - /// - /// The name of the property being configured. - /// - internal String PropertyName { get; } - /// /// The name of the column the property is mapped to. /// @@ -34,4 +29,9 @@ internal interface IEntityPropertyBuilder : IFreezable /// 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/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs index 4716dbf..a7c35dd 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs @@ -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), @@ -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/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs index e79d489..aa78493 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs @@ -288,7 +288,7 @@ CancellationToken cancellationToken var npgsqlDbTypes = dataReader .GetFieldTypes() - .Select(t => + .Select(t => this.databaseAdapter.GetDbType(t, DbConnectionPlusConfiguration.Instance.EnumSerializationMode) ) .ToArray(); @@ -349,7 +349,7 @@ CancellationToken cancellationToken var npgsqlDbTypes = dataReader .GetFieldTypes() - .Select(a => + .Select(a => this.databaseAdapter.GetDbType(a, DbConnectionPlusConfiguration.Instance.EnumSerializationMode) ) .ToArray(); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index dc04c58..a5d00cf 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -8,6 +8,7 @@ namespace RentADeveloper.DbConnectionPlus; /// public static partial class DbConnectionExtensions { + // TODO: Update ALL documentation regarding attributes and Fluent API. /// /// Deletes the specified entities, identified by their key property/properties, from the database. /// diff --git a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs index ddc4727..3b55e8a 100644 --- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs @@ -25,7 +25,7 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// /// /// The metadata of the identity property of the entity type. -/// This is if the entity type does not have an identity property. +/// This is if the entity type does not have an identity property. /// /// /// The metadata of the database-generated properties of the entity type. diff --git a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs index 1fd7573..14c6fe0 100644 --- a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs +++ b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs @@ -49,10 +49,7 @@ internal static class MaterializerFactoryHelper /// internal static MethodInfo ValueConverterConvertValueToTypeMethod { get; } = typeof(ValueConverter) .GetMethods(BindingFlags.Static | BindingFlags.NonPublic) - .First(m => - m.Name == nameof(ValueConverter.ConvertValueToType) && - m.IsGenericMethod - )!; + .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/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index c14cf59..73dde28 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -32,34 +32,35 @@ protected EntityManipulator_DeleteEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; [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) + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { - var entities = this.CreateEntitiesInDb(numberOfEntities); - var entitiesToDelete = entities.Take(numberOfEntities/2).ToList(); - var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - await this.CallApi( - useAsyncApi, - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); - foreach (var entity in entitiesToDelete) - { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - foreach (var entity in entitiesToKeep) + this.DbCommandFactory.DelayNextDbCommand = true; + + await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + cancellationToken + ) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); + + foreach (var entity in entities) { + // Since the operation was cancelled, all entities should still exist. this.ExistsEntityInDb(entity) .Should().BeTrue(); } @@ -72,44 +73,12 @@ await this.CallApi( // to test that as well. [InlineData(false, 30)] [InlineData(true, 30)] - public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi, Int32 numberOfEntities) + public async Task DeleteEntities_Mapping_Attributes_ShouldUseAttributesMapping( + Boolean useAsyncApi, + Int32 numberOfEntities + ) { - 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(); - } - ); - - var entities = this.CreateEntitiesInDb(numberOfEntities); + var entities = this.CreateEntitiesInDb(numberOfEntities); var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); @@ -141,9 +110,14 @@ await this.CallApi( // to test that as well. [InlineData(false, 30)] [InlineData(true, 30)] - public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi, Int32 numberOfEntities) + public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping( + Boolean useAsyncApi, + Int32 numberOfEntities + ) { - var entities = this.CreateEntitiesInDb(numberOfEntities); + MappingTestEntityFluentApi.Configure(); + + var entities = this.CreateEntitiesInDb(numberOfEntities); var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); @@ -191,35 +165,37 @@ public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsy } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi + [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 ) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entities = this.CreateEntitiesInDb(10); - var entitiesToDelete = entities.Take(5).ToList(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + var entities = this.CreateEntitiesInDb(numberOfEntities); + var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); + var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); - this.DbCommandFactory.DelayNextDbCommand = true; + await this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + TestContext.Current.CancellationToken + ); - await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entitiesToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + foreach (var entity in entitiesToDelete) + { + this.ExistsEntityInDb(entity) + .Should().BeFalse(); + } - foreach (var entity in entities) + foreach (var entity in entitiesToKeep) { - // Since the operation was cancelled, all entities should still exist. this.ExistsEntityInDb(entity) .Should().BeTrue(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 61591ab..9e92f2e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -34,68 +34,38 @@ protected EntityManipulator_DeleteEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(2); - var entityToDelete = entities[0]; - var entityToKeep = entities[1]; + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - await this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ); + var entityToDelete = this.CreateEntityInDb(); - this.ExistsEntityInDb(entityToDelete) - .Should().BeFalse(); + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.ExistsEntityInDb(entityToKeep) + this.DbCommandFactory.DelayNextDbCommand = true; + + await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + cancellationToken + ) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); + + // Since the operation was cancelled, the entity should still exist. + this.ExistsEntityInDb(entityToDelete) .Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - 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(); - } - ); - - var entities = this.CreateEntitiesInDb(2); + var entities = this.CreateEntitiesInDb(2); var entityToDelete = entities[0]; var entityToKeep = entities[1]; @@ -117,9 +87,11 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(2); + MappingTestEntityFluentApi.Configure(); + + var entities = this.CreateEntitiesInDb(2); var entityToDelete = entities[0]; var entityToKeep = entities[1]; @@ -163,31 +135,24 @@ public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsync [Theory] [InlineData(false)] [InlineData(true)] - public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entityToDelete = this.CreateEntityInDb(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; + var entities = this.CreateEntitiesInDb(2); + var entityToDelete = entities[0]; + var entityToKeep = entities[1]; - await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - cancellationToken - ) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + await this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken + ); - // Since the operation was cancelled, the entity should still exist. this.ExistsEntityInDb(entityToDelete) + .Should().BeFalse(); + + this.ExistsEntityInDb(entityToKeep) .Should().BeTrue(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 12f3e21..85b00e1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -34,101 +34,42 @@ protected EntityManipulator_InsertEntitiesTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( + Boolean useAsyncApi + ) { - var entities = Generate.Multiple(); - entities.ForEach(a => - { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; - } - ); - - await this.CallApi( - useAsyncApi, - this.Connection, - entities, - null, - TestContext.Current.CancellationToken - ); + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); + var entities = Generate.Multiple(); - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); + this.DbCommandFactory.DelayNextDbCommand = true; - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); + await Invoking(() => + this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) + ) + .Should().ThrowAsync() + .Where(a => a.CancellationToken == cancellationToken); - readBackEntity.NotMappedColumn - .Should().BeNull(); + // Since the operation was cancelled, the entities should not have been inserted. + foreach (var entityToInsert in entities) + { + this.ExistsEntityInDb(entityToInsert) + .Should().BeFalse(); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + Boolean useAsyncApi + ) { - 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(); - } - ); + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - var entities = Generate.Multiple(); - entities.ForEach(a => - { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; - } - ); + var entities = Generate.Multiple(); await this.CallApi( useAsyncApi, @@ -138,40 +79,21 @@ await this.CallApi( TestContext.Current.CancellationToken ); - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } + (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)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { - var entities = Generate.Multiple(); + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; + + var entities = Generate.Multiple(); await this.CallApi( useAsyncApi, @@ -181,64 +103,58 @@ await this.CallApi( TestContext.Current.CancellationToken ); - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } + (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())); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entities = Generate.Multiple(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; + var entities = Generate.Multiple(); + entities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) - ) - .Should().ThrowAsync() - .Where(a => a.CancellationToken == cancellationToken); + await this.CallApi( + useAsyncApi, + this.Connection, + entities, + null, + TestContext.Current.CancellationToken + ); - // Since the operation was cancelled, the entities should not have been inserted. - foreach (var entityToInsert in entities) - { - this.ExistsEntityInDb(entityToInsert) - .Should().BeFalse(); - } + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .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 InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) + public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; + MappingTestEntityFluentApi.Configure(); - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); + entities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); await this.CallApi( useAsyncApi, @@ -248,21 +164,20 @@ await this.CallApi( 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")) + ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) + public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); await this.CallApi( useAsyncApi, @@ -272,11 +187,8 @@ await this.CallApi( TestContext.Current.CancellationToken ); - (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())); + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(entities); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index 6a32358..b44664a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -31,6 +31,66 @@ public abstract class EntityManipulator_InsertEntityTests protected EntityManipulator_InsertEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + { + Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); + + var entity = Generate.Single(); + + var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); + + this.DbCommandFactory.DelayNextDbCommand = true; + + 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. + this.ExistsEntityInDb(entity) + .Should().BeFalse(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) + { + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; + + var entity = Generate.Single(); + + await this.CallApi(useAsyncApi, 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); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) + { + DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; + + var entity = Generate.Single(); + + await this.CallApi(useAsyncApi, 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()); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -49,29 +109,12 @@ await this.CallApi( TestContext.Current.CancellationToken ); - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } [Theory] @@ -79,40 +122,7 @@ await this.CallApi( [InlineData(true)] public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - 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(); - } - ); + MappingTestEntityFluentApi.Configure(); var entity = Generate.Single(); entity.ComputedColumn_ = 0; @@ -127,29 +137,12 @@ await this.CallApi( TestContext.Current.CancellationToken ); - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + entity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); } [Theory] @@ -167,86 +160,8 @@ await this.CallApi( TestContext.Current.CancellationToken ); - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) - { - Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); - - var entity = Generate.Single(); - - var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - - this.DbCommandFactory.DelayNextDbCommand = true; - - 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. - this.ExistsEntityInDb(entity) - .Should().BeFalse(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - - var entity = Generate.Single(); - - await this.CallApi(useAsyncApi, 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); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( - Boolean useAsyncApi - ) - { - DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - - var entity = Generate.Single(); - - await this.CallApi(useAsyncApi, 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()); + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(entity); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 5c6d382..08616df 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -31,202 +31,6 @@ public abstract class EntityManipulator_UpdateEntitiesTests protected EntityManipulator_UpdateEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entities = this.CreateEntitiesInDb(); - - var updatedEntities = Generate.UpdateFor(entities); - updatedEntities.ForEach(a => - { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; - } - ); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var updatedEntity in updatedEntities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(updatedEntity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(updatedEntity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(updatedEntity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entities = this.CreateEntitiesInDb(); - - var updatedEntities = Generate.UpdateFor(entities); - updatedEntities.ForEach(a => - { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; - } - ); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var updatedEntity in updatedEntities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(updatedEntity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(updatedEntity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(updatedEntity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - } - - [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.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); - - foreach (var updatedEntity in updatedEntities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(updatedEntity.ValueColumn); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) - { - var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - - return Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - [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)}." - ); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -303,9 +107,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( - Boolean useAsyncApi - ) + public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -343,6 +145,114 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + + var updatedEntities = Generate.UpdateFor(entities); + updatedEntities.ForEach(a => + { + a.ComputedColumn_ = 0; + a.IdentityColumn_ = 0; + a.NotMappedColumn = "ShouldNotBePersisted"; + } + ); + + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + 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.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + { + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + [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)}." + ); + } + + [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.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(updatedEntities); + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 3108f9e..9ed0b70 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -34,188 +34,7 @@ protected EntityManipulator_UpdateEntityTests() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.ComputedColumn_ = 0; - updatedEntity.IdentityColumn_ = 0; - updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(updatedEntity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(updatedEntity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(updatedEntity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entity = this.CreateEntityInDb(); - - var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.ComputedColumn_ = 0; - updatedEntity.IdentityColumn_ = 0; - updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2_)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(updatedEntity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(updatedEntity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(updatedEntity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - var updatedEntity = Generate.UpdateFor(entity); - - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); - - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("KeyColumn1")} = {Parameter(updatedEntity.KeyColumn1)} AND - {Q("KeyColumn2")} = {Parameter(updatedEntity.KeyColumn2)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(updatedEntity.ValueColumn); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) - { - var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - - return Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - 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)}." - ); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -243,9 +62,7 @@ await Invoking(() => [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) + public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -281,9 +98,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( - Boolean useAsyncApi - ) + public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -316,6 +131,106 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + + var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; + + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + { + MappingTestEntityFluentApi.Configure(); + + var entity = this.CreateEntityInDb(); + + var updatedEntity = Generate.UpdateFor(entity); + updatedEntity.ComputedColumn_ = 0; + updatedEntity.IdentityColumn_ = 0; + updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; + + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo( + updatedEntity, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + { + var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); + + return Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + 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)}." + ); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + + await this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ); + + this.Connection.QueryFirst($"SELECT * FROM {Q("MappingTestEntity")}") + .Should().BeEquivalentTo(updatedEntity); + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index a1f8ab9..e1b395c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -182,7 +182,9 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUseAttributesMapping( + Boolean useAsyncApi + ) { var entities = Generate.Multiple(); entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); @@ -205,73 +207,22 @@ public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUs reader.GetFieldNames() .Should().NotContain(nameof(MappingTestEntityAttributes.NotMappedColumn)); - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {QT("Objects")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping( + Boolean useAsyncApi + ) { - 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(); - } - ); + MappingTestEntityFluentApi.Configure(); var entities = Generate.Multiple(); entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); @@ -294,38 +245,20 @@ public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUse reader.GetFieldNames() .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMappedColumn)); - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {QT("Objects")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1_)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2_)} - """ + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo( + entities, + options => options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMappedColumn")) ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( + Boolean useAsyncApi + ) { var entities = Generate.Multiple(); @@ -339,28 +272,8 @@ public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTy TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - foreach (var entity in entities) - { - var readBackEntity = this.Connection.QueryFirstOrDefault( - $""" - SELECT * - FROM {QT("Objects")} - WHERE {Q("KeyColumn1")} = {Parameter(entity.KeyColumn1)} AND - {Q("KeyColumn2")} = {Parameter(entity.KeyColumn2)} - """ - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } + this.Connection.Query($"SELECT * FROM {QT("Objects")}") + .Should().BeEquivalentTo(entities); } [Theory] @@ -390,9 +303,7 @@ public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTabl [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); @@ -636,9 +547,7 @@ public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs index e6187af..243ad8d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs @@ -76,9 +76,7 @@ SELECT Value [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs index 0334acb..3879b34 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs @@ -106,9 +106,7 @@ public async Task ExecuteReader_CommandType_ShouldUseCommandType(Boolean useAsyn [InlineData(false)] [InlineData(true)] public async Task - ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal( - Boolean useAsyncApi - ) + ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs index 8c7376b..3839540 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs @@ -327,9 +327,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter(Boolean useAsyncApi) { var character = Generate.Single(); @@ -400,9 +398,7 @@ public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThro [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs index 902862c..dcb1211 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs @@ -64,9 +64,7 @@ public async Task Exists_CommandType_ShouldUseCommandType(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 4ad334e..ac1fc12 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -117,9 +117,7 @@ public Task QueryFirst_BuiltInType_ColumnValueCannotBeConvertedToTargetType_Shou [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -137,9 +135,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -240,9 +236,7 @@ public async Task QueryFirst_BuiltInType_ShouldSupportDateTimeOffsetValues(Boole [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -332,9 +326,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -475,9 +467,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -614,6 +604,48 @@ public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertStringTo .Should().Be(enumValue); } + [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)] @@ -670,6 +702,22 @@ Boolean useAsyncApi .Should().Be(entities[0]); } + [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)] @@ -748,122 +796,6 @@ public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(Boolea .Should().Be(entities[0]); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -1205,9 +1137,7 @@ public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStr [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi - ) + public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index 1c9013c..a2b7538 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -162,9 +162,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -293,9 +291,7 @@ public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean u [InlineData(false)] [InlineData(true)] public async Task - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -597,9 +593,7 @@ await Invoking(() => CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -616,9 +610,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -632,6 +624,48 @@ Boolean useAsyncApi .Should().Be(enumValue); } + [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)] @@ -688,6 +722,24 @@ Boolean useAsyncApi .Should().Be(entities[0]); } + [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)] @@ -768,122 +820,6 @@ public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValu .Should().Be(entities[0]); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -910,9 +846,7 @@ public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Bool [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs index 5fee049..c4e4101 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs @@ -126,9 +126,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entities = this.CreateEntitiesInDb(2); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs index a476487..17a2a2c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs @@ -31,9 +31,7 @@ public abstract class [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -182,9 +180,7 @@ public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 37c9af2..e6191c2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -619,34 +619,17 @@ public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping { var entities = this.CreateEntitiesInDb(); - var readBackEntities = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); - - foreach (var entity in entities) - { - var readBackEntity = readBackEntities.FirstOrDefault(a => - a.KeyColumn1_ == entity.KeyColumn1_ && a.KeyColumn2_ == entity.KeyColumn2_ + (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")) ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } } [Theory] @@ -654,71 +637,21 @@ public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping [InlineData(true)] public async Task Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { - 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(); - } - ); + MappingTestEntityFluentApi.Configure(); var entities = this.CreateEntitiesInDb(); - var readBackEntities = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); - - foreach (var entity in entities) - { - var readBackEntity = readBackEntities.FirstOrDefault(a => - a.KeyColumn1_ == entity.KeyColumn1_ && a.KeyColumn2_ == entity.KeyColumn2_ + (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")) ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } } [Theory] @@ -728,25 +661,13 @@ public async Task Query_EntityType_Mapping_NoMapping_ShouldUseEntityTypeNameAndP { var entities = this.CreateEntitiesInDb(); - var readBackEntities = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); - - foreach (var entity in entities) - { - var readBackEntity = readBackEntities.FirstOrDefault(a => - a.KeyColumn1 == entity.KeyColumn1 && a.KeyColumn2 == entity.KeyColumn2 - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } + (await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ).ToListAsync(TestContext.Current.CancellationToken)) + .Should().BeEquivalentTo(entities); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 4d4036a..e776306 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -99,9 +99,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -119,9 +117,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -139,9 +135,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -192,9 +186,7 @@ public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertStringToEn [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -244,9 +236,7 @@ public async Task QuerySingle_BuiltInType_ShouldSupportDateTimeOffsetValues(Bool [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -336,9 +326,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task - QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -479,9 +467,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -619,6 +605,48 @@ public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertStringT .Should().Be(enumValue); } + [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)] @@ -678,9 +706,23 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi - ) + 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)" @@ -755,122 +797,6 @@ public async Task QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues(Boole .Should().BeEquivalentTo(entity); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -1200,9 +1126,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index e1b023c..15953ad 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -162,9 +162,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -180,9 +178,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -295,9 +291,7 @@ public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean [InlineData(false)] [InlineData(true)] public async Task - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -599,9 +593,7 @@ await Invoking(() => CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -618,9 +610,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) { var enumValue = Generate.Single(); @@ -634,6 +624,48 @@ Boolean useAsyncApi .Should().Be(enumValue); } + [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)] @@ -694,6 +726,24 @@ Boolean useAsyncApi .Should().BeEquivalentTo(entity); } + [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)] @@ -774,122 +824,6 @@ public async Task QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetVal .Should().BeEquivalentTo(entity); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) - { - 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(); - } - ); - - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); - - readBackEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); - - readBackEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); - - readBackEntity.NotMappedColumn - .Should().BeNull(); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) - { - var entity = this.CreateEntityInDb(); - - var readBackEntity = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ); - - readBackEntity - .Should().NotBeNull(); - - readBackEntity.ValueColumn - .Should().Be(entity.ValueColumn); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -916,9 +850,7 @@ public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boo [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs index 12461fe..5616799 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs @@ -127,9 +127,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) { var entity = this.CreateEntityInDb(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs index 2311ee1..92e873b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs @@ -31,9 +31,7 @@ public abstract class [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -202,9 +200,7 @@ public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 0b4ec38..1401b6f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -207,7 +207,7 @@ CREATE TABLE `EntityWithNullableProperty` `Value` BIGINT NULL ); GO - + CREATE TABLE `MappingTestEntity` ( `KeyColumn1` BIGINT NOT NULL, @@ -218,7 +218,7 @@ CREATE TABLE `MappingTestEntity` `NotMappedColumn` TEXT NULL ); GO - + CREATE PROCEDURE `GetEntities` () BEGIN SELECT * FROM `Entity`; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index ad9087e..aaa0d65 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -195,7 +195,7 @@ CREATE TABLE "EntityWithNullableProperty" "Value" NUMBER(19) NULL ); GO - + CREATE TABLE "MappingTestEntity" ( "KeyColumn1" NUMBER(19) NOT NULL, @@ -207,7 +207,7 @@ CREATE TABLE "MappingTestEntity" PRIMARY KEY ("KeyColumn1", "KeyColumn2") ); GO - + CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS BEGIN DELETE FROM "Entity"; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index 44c4283..66bb79b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -182,7 +182,7 @@ CREATE TABLE "EntityWithNullableProperty" "Id" bigint NOT NULL PRIMARY KEY, "Value" bigint NULL ); - + CREATE TABLE "MappingTestEntity" ( "KeyColumn1" bigint NOT NULL, @@ -193,7 +193,7 @@ CREATE TABLE "MappingTestEntity" "NotMappedColumn" text NULL, PRIMARY KEY ("KeyColumn1", "KeyColumn2") ); - + CREATE PROCEDURE "GetEntities" () LANGUAGE SQL AS $$ diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index 0d89489..b1f519c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -175,7 +175,7 @@ CREATE TABLE EntityWithNullableProperty Id INTEGER NOT NULL, Value INTEGER NULL ); - + CREATE TABLE MappingTestEntity ( KeyColumn1 INTEGER NOT NULL, diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index b8c6c6d..6c2952c 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -108,7 +108,7 @@ public void GetDatabaseAdapter_NoAdapterRegisteredForConnectionType_ShouldThrow( .WithMessage( "No database adapter is registered for the database connection of the type " + $"{typeof(FakeConnectionC)}. Please call {nameof(DbConnectionExtensions)}." + - $"{nameof(DbConnectionExtensions.Configure)} to register an adapter for that connection type." + $"{nameof(Configure)} to register an adapter for that connection type." ); [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs index aff18e9..a70e978 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -7,35 +7,35 @@ public void Configure_ShouldConfigureDbConnectionPlus() { InterceptDbCommand interceptDbCommand = (_, _) => { }; - Configure(configuration => + Configure(config => { - configuration.EnumSerializationMode = EnumSerializationMode.Integers; - configuration.InterceptDbCommand = interceptDbCommand; + config.EnumSerializationMode = EnumSerializationMode.Integers; + config.InterceptDbCommand = interceptDbCommand; - configuration.Entity() + config.Entity() .ToTable("MappingTestEntity"); - configuration.Entity() + config.Entity() .Property(a => a.KeyColumn1_) .HasColumnName("KeyColumn1") .IsKey(); - configuration.Entity() + config.Entity() .Property(a => a.KeyColumn2_) .HasColumnName("KeyColumn2") .IsKey(); - configuration.Entity() + config.Entity() .Property(a => a.ComputedColumn_) .HasColumnName("ComputedColumn") .IsComputed(); - configuration.Entity() + config.Entity() .Property(a => a.IdentityColumn_) .HasColumnName("IdentityColumn") .IsIdentity(); - configuration.Entity() + config.Entity() .Property(a => a.NotMappedColumn) .IsIgnored(); } diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 2851289..095c9e3 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -151,16 +151,6 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe ); } - [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." - ); - [Fact] public void GetEntityTypeMetadata_FluentApiMapping_ShouldGetMetadataBasedOnFluentApiMapping() { @@ -246,6 +236,16 @@ public void GetEntityTypeMetadata_FluentApiMapping_ShouldGetMetadataBasedOnFluen .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(MappingTestEntity))] [InlineData(typeof(MappingTestEntityAttributes))] diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 2b7cac8..fc97129 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -376,40 +376,7 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() [Fact] public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() { - 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(); - } - ); + MappingTestEntityFluentApi.Configure(); var entity = Generate.Single(); 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/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index 5bb57eb..f2ddfec 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -1,4 +1,5 @@ // ReSharper disable ConvertToPrimaryConstructor + #pragma warning disable IDE0290 namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs index 23bffbb..d38e148 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs @@ -1,4 +1,5 @@ // 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 index 1c761ba..5288b27 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -4,7 +4,7 @@ public record MappingTestEntity { [Key] public Int64 KeyColumn1 { get; set; } - + [Key] public Int64 KeyColumn2 { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index 6e4746e..fdc7edc 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -3,17 +3,6 @@ [Table("MappingTestEntity")] public record MappingTestEntityAttributes { - [Key] - [Column("KeyColumn1")] - public Int64 KeyColumn1_ { get; set; } - - [Key] - [Column("KeyColumn2")] - public Int64 KeyColumn2_ { get; set; } - - [Column("ValueColumn")] - public Int32 ValueColumn_ { get; set; } - [Column("ComputedColumn")] [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public Int32 ComputedColumn_ { get; set; } @@ -22,6 +11,17 @@ public record MappingTestEntityAttributes [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 index 83863c3..0e7bc72 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -2,10 +2,49 @@ public record MappingTestEntityFluentApi { - public Int64 KeyColumn1_ { get; set; } - public Int64 KeyColumn2_ { get; set; } - public Int32 ValueColumn_ { get; set; } 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(); + } + ); } From 89ee46ebaed17507d196bf4ffae45fe576086b4b Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sun, 1 Feb 2026 00:37:59 +0100 Subject: [PATCH 09/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- .../DatabaseAdapters/TemporaryTableBuilderTests.cs | 8 ++++++-- .../Configuration/EntityPropertyBuilderTests.cs | 2 +- .../Configuration/EntityTypeBuilderTests.cs | 2 +- .../Readers/EnumHandlingObjectReaderTests.cs | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index e1b395c..07dbbaa 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -199,7 +199,7 @@ Boolean useAsyncApi TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( + var reader = await this.Connection.ExecuteReaderAsync( $"SELECT * FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -207,6 +207,8 @@ Boolean useAsyncApi reader.GetFieldNames() .Should().NotContain(nameof(MappingTestEntityAttributes.NotMappedColumn)); + await reader.DisposeAsync(); + this.Connection.Query($"SELECT * FROM {QT("Objects")}") .Should().BeEquivalentTo( entities, @@ -237,7 +239,7 @@ Boolean useAsyncApi TestContext.Current.CancellationToken ); - await using var reader = await this.Connection.ExecuteReaderAsync( + var reader = await this.Connection.ExecuteReaderAsync( $"SELECT * FROM {QT("Objects")}", cancellationToken: TestContext.Current.CancellationToken ); @@ -245,6 +247,8 @@ Boolean useAsyncApi reader.GetFieldNames() .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMappedColumn)); + await reader.DisposeAsync(); + this.Connection.Query($"SELECT * FROM {QT("Objects")}") .Should().BeEquivalentTo( entities, diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs index d09be69..31eea9e 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -1,6 +1,6 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; -public class EntityPropertyBuilderTests +public class EntityPropertyBuilderTests : UnitTestsBase { [Fact] public void Freeze_ShouldFreezeBuilder() diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs index c931541..b2918b2 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs @@ -1,6 +1,6 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Configuration; -public class EntityTypeBuilderTests +public class EntityTypeBuilderTests : UnitTestsBase { [Fact] public void Freeze_ShouldFreezeBuilderAndAllPropertyBuilders() diff --git a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs index 6cfb364..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() From 6154339bcb58b6f9682e0bf39d7a86a31c1ce834 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sun, 1 Feb 2026 22:41:29 +0100 Subject: [PATCH 10/11] WIP: Implement feature Add Fluent API for Configuration and Entity Type Mappings --- CHANGELOG.md | 3 +- README.md | 119 ++++++--- docs/DESIGN-DECISIONS.md | 52 +++- .../DatabaseAdapters/IEntityManipulator.cs | 239 ++++++++++-------- .../DbConnectionExtensions.DeleteEntities.cs | 25 +- .../DbConnectionExtensions.DeleteEntity.cs | 24 +- .../DbConnectionExtensions.InsertEntities.cs | 46 ++-- .../DbConnectionExtensions.InsertEntity.cs | 46 ++-- .../DbConnectionExtensions.QueryFirstOfT.cs | 6 +- ...ectionExtensions.QueryFirstOrDefaultOfT.cs | 6 +- .../DbConnectionExtensions.QueryOfT.cs | 6 +- .../DbConnectionExtensions.QuerySingleOfT.cs | 6 +- ...ctionExtensions.QuerySingleOrDefaultOfT.cs | 6 +- .../DbConnectionExtensions.UpdateEntities.cs | 58 +++-- .../DbConnectionExtensions.UpdateEntity.cs | 58 +++-- src/DbConnectionPlus/ThrowHelper.cs | 4 +- .../EntityManipulator.DeleteEntitiesTests.cs | 4 +- .../EntityManipulator.DeleteEntityTests.cs | 4 +- .../EntityManipulator.UpdateEntitiesTests.cs | 4 +- .../EntityManipulator.UpdateEntityTests.cs | 4 +- 20 files changed, 419 insertions(+), 301 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db8b357..e5d9a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ this project adheres to [Semantic Versioning](https://semver.org/). ## [1.1.0] - TODO: Add date of release ### Added -- Fluent configuration API for global settings and entity mappings +- 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 d49c5ea..4c9bbc1 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,6 @@ Other database systems and database connectors can be supported by implementing All examples in this document use SQL Server. -TODO: Update for Fluent API and update version - ## Table of contents - **[Quick start](#quick-start)** - [Examples](#examples) @@ -40,6 +38,7 @@ TODO: Update for Fluent API and update version - [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) @@ -328,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 @@ -376,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; @@ -406,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) @@ -443,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: @@ -1083,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/docs/DESIGN-DECISIONS.md b/docs/DESIGN-DECISIONS.md index 8502b7a..28eef03 100644 --- a/docs/DESIGN-DECISIONS.md +++ b/docs/DESIGN-DECISIONS.md @@ -675,7 +675,7 @@ public static class EntityHelper - Mapped properties (excluding ignored properties) - Key properties - Computed properties - - Identity 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) @@ -1035,26 +1035,47 @@ public List Query_Entities_DbConnectionPlus() ## Configuration and Extensibility -TODO: Update this section. - ### 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(); +}); ``` --- @@ -1079,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/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/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index a5d00cf..751fc3e 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -8,7 +8,6 @@ namespace RentADeveloper.DbConnectionPlus; /// public static partial class DbConnectionExtensions { - // TODO: Update ALL documentation regarding attributes and Fluent API. /// /// Deletes the specified entities, identified by their key property/properties, from the database. /// @@ -33,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. /// /// /// @@ -111,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. /// /// /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs index edcafae..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. /// /// /// @@ -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. /// /// /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs index 7866c04..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. /// @@ -131,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. /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs index 547a7e4..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. /// @@ -131,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. /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs index 0f62d40..22118ef 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.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 . @@ -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 . diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs index 1c0b63c..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 . @@ -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 . diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs index 3b421bc..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 . @@ -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 . diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs index 1304256..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 . @@ -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 . diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs index 8115658..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 . @@ -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 . diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs index ada8617..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. /// @@ -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. /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs index e123398..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. /// @@ -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. /// diff --git a/src/DbConnectionPlus/ThrowHelper.cs b/src/DbConnectionPlus/ThrowHelper.cs index 312b5c0..a25c5ac 100644 --- a/src/DbConnectionPlus/ThrowHelper.cs +++ b/src/DbConnectionPlus/ThrowHelper.cs @@ -50,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/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 73dde28..8ba0bea 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -159,8 +159,8 @@ public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsy ) .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." ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 9e92f2e..974d820 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -127,8 +127,8 @@ public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsync ) .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." ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 08616df..558f574 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -228,8 +228,8 @@ public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsy ) .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." ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 9ed0b70..5d7c4c8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -206,8 +206,8 @@ public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsync ) .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." ); } From 2c23477e4eada8a771889cd624fe92d34cb2f08c Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sun, 1 Feb 2026 23:19:32 +0100 Subject: [PATCH 11/11] Implement feature Add Fluent API for Configuration and Entity Type Mappings Fixes #3 --- tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs | 1 - .../TestData/MappingTestEntityAttributes.cs | 4 +++- .../TestData/MappingTestEntityFluentApi.cs | 4 +++- tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs index 27f6f83..d350e53 100644 --- a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs @@ -1,4 +1,3 @@ -global using System.ComponentModel.DataAnnotations; global using System.Data; global using Xunit; global using AwesomeAssertions; diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index fdc7edc..358d0d8 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -1,4 +1,6 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; +// ReSharper disable InconsistentNaming + +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; [Table("MappingTestEntity")] public record MappingTestEntityAttributes diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs index 0e7bc72..d4cb012 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -1,4 +1,6 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; +// ReSharper disable InconsistentNaming + +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record MappingTestEntityFluentApi { diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index d0c9511..dfcd812 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -122,7 +122,8 @@ public UnitTestsBase() _ => throw new NotSupportedException( $"The {nameof(EnumSerializationMode)} " + - $"{DbConnectionPlusConfiguration.Instance.EnumSerializationMode.ToDebugString()} is not supported." + $"{DbConnectionPlusConfiguration.Instance.EnumSerializationMode.ToDebugString()} " + + "is not supported." ) };