From 234f9c2b4d204aa7a7579ce2abe09e71b1b5abe4 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Mon, 2 Feb 2026 12:48:43 +0100 Subject: [PATCH 01/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../Configuration/EntityPropertyBuilder.cs | 43 ++++ .../Configuration/IEntityPropertyBuilder.cs | 10 + src/DbConnectionPlus/Entities/EntityHelper.cs | 93 +++++--- .../Entities/EntityPropertyMetadata.cs | 34 +-- .../Entities/EntityTypeMetadata.cs | 32 ++- .../EntityManipulator.InsertEntitiesTests.cs | 16 +- .../EntityManipulator.InsertEntityTests.cs | 16 +- .../EntityManipulator.UpdateEntitiesTests.cs | 16 +- .../EntityManipulator.UpdateEntityTests.cs | 16 +- .../TemporaryTableBuilderTests.cs | 12 +- ...ConnectionExtensions.QueryFirstOfTTests.cs | 4 +- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 4 +- .../DbConnectionExtensions.QueryOfTTests.cs | 4 +- ...onnectionExtensions.QuerySingleOfTTests.cs | 4 +- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 4 +- .../TestDatabase/MySqlTestDatabaseProvider.cs | 12 +- .../OracleTestDatabaseProvider.cs | 14 +- .../PostgreSqlTestDatabaseProvider.cs | 14 +- .../SQLiteTestDatabaseProvider.cs | 12 +- .../SqlServerTestDatabaseProvider.cs | 14 +- ...ConnectionExtensions.ConfigurationTests.cs | 81 +++++-- .../Entities/EntityHelperTests.cs | 211 ++++++++++-------- .../EntityMaterializerFactoryTests.cs | 170 ++++++++------ ...piTest.PublicApiHasNotChanged.verified.txt | 10 +- .../TestData/Entity.cs | 2 +- .../TestData/MappingTestEntity.cs | 6 +- .../TestData/MappingTestEntityAttributes.cs | 30 ++- .../TestData/MappingTestEntityFluentApi.cs | 54 +++-- 28 files changed, 589 insertions(+), 349 deletions(-) diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs index f2e18a6..cc353ec 100644 --- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -50,6 +50,23 @@ public EntityPropertyBuilder IsComputed() return this; } + /// + /// Marks the property as participating in optimistic concurrency checks. + /// Such properties will be checked during delete and update operations. + /// When their values in the database do not match the original values, the delete or update will fail. + /// + /// This builder instance for further configuration. + /// + /// The configuration of DbConnectionPlus is already frozen and can no longer be modified. + /// + public EntityPropertyBuilder IsConcurrencyToken() + { + this.EnsureNotFrozen(); + + this.isConcurrencyToken = true; + return this; + } + /// /// Marks the property as mapped to an identity database column. /// Such properties will be ignored during insert and update operations. @@ -114,6 +131,24 @@ public EntityPropertyBuilder IsKey() return this; } + /// + /// Marks the property as mapped to a row version database column. + /// Such properties will be checked during delete and update operations. + /// When their values in the database do not match the original values, the delete or update will fail. + /// After an insert or update, their values will be read back from the database 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 IsRowVersion() + { + this.EnsureNotFrozen(); + + this.isRowVersion = true; + return this; + } + /// String? IEntityPropertyBuilder.ColumnName => this.columnName; @@ -123,6 +158,9 @@ public EntityPropertyBuilder IsKey() /// Boolean IEntityPropertyBuilder.IsComputed => this.isComputed; + /// + Boolean IEntityPropertyBuilder.IsConcurrencyToken => this.isConcurrencyToken; + /// Boolean IEntityPropertyBuilder.IsIdentity => this.isIdentity; @@ -132,6 +170,9 @@ public EntityPropertyBuilder IsKey() /// Boolean IEntityPropertyBuilder.IsKey => this.isKey; + /// + Boolean IEntityPropertyBuilder.IsRowVersion => this.isRowVersion; + /// String IEntityPropertyBuilder.PropertyName => this.propertyName; @@ -152,8 +193,10 @@ private void EnsureNotFrozen() private String? columnName; private Boolean isComputed; + private Boolean isConcurrencyToken; private Boolean isFrozen; private Boolean isIdentity; private Boolean isIgnored; private Boolean isKey; + private Boolean isRowVersion; } diff --git a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs index c0c6b79..50efe91 100644 --- a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs @@ -15,6 +15,11 @@ internal interface IEntityPropertyBuilder : IFreezable /// internal Boolean IsComputed { get; } + /// + /// Determines whether the property participates in optimistic concurrency checks. + /// + internal Boolean IsConcurrencyToken { get; } + /// /// Determines whether the property is mapped to an identity database column. /// @@ -30,6 +35,11 @@ internal interface IEntityPropertyBuilder : IFreezable /// internal Boolean IsKey { get; } + /// + /// Determines whether the property is a row version used for concurrency control. + /// + internal Boolean IsRowVersion { get; } + /// /// The name of the property being configured. /// diff --git a/src/DbConnectionPlus/Entities/EntityHelper.cs b/src/DbConnectionPlus/Entities/EntityHelper.cs index bc364d7..b99ce95 100644 --- a/src/DbConnectionPlus/Entities/EntityHelper.cs +++ b/src/DbConnectionPlus/Entities/EntityHelper.cs @@ -174,39 +174,43 @@ entityTypeBuilder is not null && ) { propertiesMetadata[i] = new( + property.CanRead, + property.CanWrite, !String.IsNullOrWhiteSpace(propertyBuilder.ColumnName) ? propertyBuilder.ColumnName : property.Name, - property.Name, - property.PropertyType, - property, - propertyBuilder.IsIgnored, - propertyBuilder.IsKey, propertyBuilder.IsComputed, + propertyBuilder.IsConcurrencyToken, propertyBuilder.IsIdentity, - property.CanRead, - property.CanWrite, + propertyBuilder.IsIgnored, + propertyBuilder.IsKey, + propertyBuilder.IsRowVersion, property.CanRead ? Reflect.PropertyGetter(property) : null, - property.CanWrite ? Reflect.PropertySetter(property) : null + property, + property.Name, + property.CanWrite ? Reflect.PropertySetter(property) : null, + property.PropertyType ); } else { propertiesMetadata[i] = new( + property.CanRead, + property.CanWrite, 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() is not null, property.GetCustomAttribute()?.DatabaseGeneratedOption is DatabaseGeneratedOption.Identity, - property.CanRead, - property.CanWrite, + property.GetCustomAttribute() is not null, + property.GetCustomAttribute() is not null, + property.GetCustomAttribute() is not null, property.CanRead ? Reflect.PropertyGetter(property) : null, - property.CanWrite ? Reflect.PropertySetter(property) : null + property, + property.Name, + property.CanWrite ? Reflect.PropertySetter(property) : null, + property.PropertyType ); } } @@ -221,22 +225,59 @@ entityTypeBuilder is not null && ); } + IReadOnlyList computedProperties = + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: true })]; + + IReadOnlyList concurrencyTokenProperties = + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsConcurrencyToken: true })]; + + IReadOnlyList databaseGeneratedProperties = + [.. propertiesMetadata.Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity || p.IsRowVersion))]; + + IReadOnlyList insertProperties = + [ + .. propertiesMetadata.Where(p => p is + { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false } + ) + ]; + + IReadOnlyList keyProperties = + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsKey: true })]; + + IReadOnlyList mappedProperties = + [.. propertiesMetadata.Where(p => !p.IsIgnored)]; + + IReadOnlyList rowVersionProperties = + [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsRowVersion: true })]; + + IReadOnlyList updateProperties = + [ + .. propertiesMetadata.Where(p => p is + { + IsComputed: false, + IsConcurrencyToken: false, + IsIgnored: false, + IsIdentity: false, + IsKey: false, + IsRowVersion: false + } + ) + ]; + return new( entityType, tableName, propertiesMetadata, propertiesMetadata.ToDictionary(p => p.PropertyName), - [.. propertiesMetadata.Where(p => !p.IsIgnored)], - [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsKey: true })], - [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: true })], + computedProperties, + concurrencyTokenProperties, + databaseGeneratedProperties, 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 } - ) - ] + insertProperties, + keyProperties, + mappedProperties, + rowVersionProperties, + updateProperties ); } diff --git a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs index adfc45d..2e8d1f6 100644 --- a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs @@ -9,35 +9,39 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// /// Metadata of an entity property. /// +/// Determines whether the property can be read. +/// Determines whether the property can be written to. /// The name of the column to which the property is mapped. -/// The name of the property. -/// The property type of the property. -/// The property info of the 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 participates in optimistic concurrency checks. /// Determines whether the property is an identity property. -/// Determines whether the property can be read. -/// Determines whether the property can be written to. +/// 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 row version used for concurrency control. /// /// The getter function for the property. /// This is if the property has no getter. /// +/// The property info of the property. +/// The name of the property. /// /// The setter function for the property. /// This is if the property has no setter. /// +/// The property type of the property. public sealed record EntityPropertyMetadata( + Boolean CanRead, + Boolean CanWrite, String ColumnName, - String PropertyName, - Type PropertyType, - PropertyInfo PropertyInfo, - Boolean IsIgnored, - Boolean IsKey, Boolean IsComputed, + Boolean IsConcurrencyToken, Boolean IsIdentity, - Boolean CanRead, - Boolean CanWrite, + Boolean IsIgnored, + Boolean IsKey, + Boolean IsRowVersion, MemberGetter? PropertyGetter, - MemberSetter? PropertySetter + PropertyInfo PropertyInfo, + String PropertyName, + MemberSetter? PropertySetter, + Type PropertyType ); diff --git a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs index 3b55e8a..e7da079 100644 --- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs @@ -14,25 +14,31 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// The keys of the dictionary are the property names. /// The values of the dictionary are the corresponding property metadata. /// -/// -/// The metadata of the mapped properties of the entity type. -/// -/// -/// The metadata of the key properties of the entity type. -/// /// /// The metadata of the computed properties of the entity type. /// -/// -/// The metadata of the identity property of the entity type. -/// This is if the entity type does not have an identity property. +/// +/// The metadata of the concurrency token properties of the entity type. /// /// /// The metadata of the database-generated 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 properties needed to insert an entity of the entity type into the database. /// +/// +/// The metadata of the key properties of the entity type. +/// +/// +/// The metadata of the mapped properties of the entity type. +/// +/// +/// The metadata of the row version properties of the entity type. +/// /// /// The metadata of the properties needed to update an entity of the entity type in the database. /// @@ -41,11 +47,13 @@ public sealed record EntityTypeMetadata( String TableName, IReadOnlyList AllProperties, IReadOnlyDictionary AllPropertiesByPropertyName, - IReadOnlyList MappedProperties, - IReadOnlyList KeyProperties, IReadOnlyList ComputedProperties, - EntityPropertyMetadata? IdentityProperty, + IReadOnlyList ConcurrencyTokenProperties, IReadOnlyList DatabaseGeneratedProperties, + EntityPropertyMetadata? IdentityProperty, IReadOnlyList InsertProperties, + IReadOnlyList KeyProperties, + IReadOnlyList MappedProperties, + IReadOnlyList RowVersionProperties, IReadOnlyList UpdateProperties ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 85b00e1..4ff17a3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -118,9 +118,9 @@ public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(B var entities = Generate.Multiple(); entities.ForEach(a => { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; + a.Computed_ = 0; + a.Identity_ = 0; + a.NotMapped = "ShouldNotBePersisted"; } ); @@ -136,7 +136,7 @@ await this.CallApi( .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -150,9 +150,9 @@ public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boo var entities = Generate.Multiple(); entities.ForEach(a => { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; + a.Computed_ = 0; + a.Identity_ = 0; + a.NotMapped = "ShouldNotBePersisted"; } ); @@ -168,7 +168,7 @@ await this.CallApi( .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index b44664a..95e41f0 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -97,9 +97,9 @@ public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumVal public async Task InsertEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { var entity = Generate.Single(); - entity.ComputedColumn_ = 0; - entity.IdentityColumn_ = 0; - entity.NotMappedColumn = "ShouldNotBePersisted"; + entity.Computed_ = 0; + entity.Identity_ = 0; + entity.NotMapped = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -113,7 +113,7 @@ await this.CallApi( .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -125,9 +125,9 @@ public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boole MappingTestEntityFluentApi.Configure(); var entity = Generate.Single(); - entity.ComputedColumn_ = 0; - entity.IdentityColumn_ = 0; - entity.NotMappedColumn = "ShouldNotBePersisted"; + entity.Computed_ = 0; + entity.Identity_ = 0; + entity.NotMapped = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -141,7 +141,7 @@ await this.CallApi( .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 558f574..b5369e5 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -155,9 +155,9 @@ public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(B var updatedEntities = Generate.UpdateFor(entities); updatedEntities.ForEach(a => { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; + a.Computed_ = 0; + a.Identity_ = 0; + a.NotMapped = "ShouldNotBePersisted"; } ); @@ -173,7 +173,7 @@ await this.CallApi( .Should().BeEquivalentTo( updatedEntities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -189,9 +189,9 @@ public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boo var updatedEntities = Generate.UpdateFor(entities); updatedEntities.ForEach(a => { - a.ComputedColumn_ = 0; - a.IdentityColumn_ = 0; - a.NotMappedColumn = "ShouldNotBePersisted"; + a.Computed_ = 0; + a.Identity_ = 0; + a.NotMapped = "ShouldNotBePersisted"; } ); @@ -207,7 +207,7 @@ await this.CallApi( .Should().BeEquivalentTo( updatedEntities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 5d7c4c8..892bd1c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -139,9 +139,9 @@ public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boo var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.ComputedColumn_ = 0; - updatedEntity.IdentityColumn_ = 0; - updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; + updatedEntity.Computed_ = 0; + updatedEntity.Identity_ = 0; + updatedEntity.NotMapped = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -155,7 +155,7 @@ await this.CallApi( .Should().BeEquivalentTo( updatedEntity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -169,9 +169,9 @@ public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boole var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.ComputedColumn_ = 0; - updatedEntity.IdentityColumn_ = 0; - updatedEntity.NotMappedColumn = "ShouldNotBePersisted"; + updatedEntity.Computed_ = 0; + updatedEntity.Identity_ = 0; + updatedEntity.NotMapped = "ShouldNotBePersisted"; await this.CallApi( useAsyncApi, @@ -185,7 +185,7 @@ await this.CallApi( .Should().BeEquivalentTo( updatedEntity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 07dbbaa..69d4bfe 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -187,7 +187,7 @@ Boolean useAsyncApi ) { var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); + entities.ForEach(a => a.NotMapped = "ShouldNotBePersisted"); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -205,7 +205,7 @@ Boolean useAsyncApi ); reader.GetFieldNames() - .Should().NotContain(nameof(MappingTestEntityAttributes.NotMappedColumn)); + .Should().NotContain(nameof(MappingTestEntityAttributes.NotMapped)); await reader.DisposeAsync(); @@ -213,7 +213,7 @@ Boolean useAsyncApi .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -227,7 +227,7 @@ Boolean useAsyncApi MappingTestEntityFluentApi.Configure(); var entities = Generate.Multiple(); - entities.ForEach(a => a.NotMappedColumn = "ShouldNotBePersisted"); + entities.ForEach(a => a.NotMapped = "ShouldNotBePersisted"); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -245,7 +245,7 @@ Boolean useAsyncApi ); reader.GetFieldNames() - .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMappedColumn)); + .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMapped)); await reader.DisposeAsync(); @@ -253,7 +253,7 @@ Boolean useAsyncApi .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index ac1fc12..b5e439b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -620,7 +620,7 @@ public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMa .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -642,7 +642,7 @@ public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapp .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index a2b7538..00d6db6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -640,7 +640,7 @@ public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAtt .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -662,7 +662,7 @@ public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFlue .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index e6191c2..6ddf64b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -628,7 +628,7 @@ public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -650,7 +650,7 @@ public async Task Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(B .Should().BeEquivalentTo( entities, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index e776306..3c97a8e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -621,7 +621,7 @@ public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesM .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -643,7 +643,7 @@ public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMap .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 15953ad..6b92809 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -640,7 +640,7 @@ public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAt .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -662,7 +662,7 @@ public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFlu .Should().BeEquivalentTo( entity, options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMappedColumn")) + .When(info => info.Path.EndsWith("NotMapped")) ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 1401b6f..c31846e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -210,12 +210,12 @@ CREATE TABLE `EntityWithNullableProperty` CREATE TABLE `MappingTestEntity` ( - `KeyColumn1` BIGINT NOT NULL, - `KeyColumn2` BIGINT NOT NULL, - `ValueColumn` INT NOT NULL, - `ComputedColumn` INT AS (`ValueColumn`+999), - `IdentityColumn` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, - `NotMappedColumn` TEXT NULL + `Key1` BIGINT NOT NULL, + `Key2` BIGINT NOT NULL, + `Name` TEXT NOT NULL, + `Computed` INT AS (`Name`+999), + `Identity` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `NotMapped` TEXT NULL ); GO diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index aaa0d65..1315d89 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -198,13 +198,13 @@ CREATE TABLE "EntityWithNullableProperty" 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") + "Key1" NUMBER(19) NOT NULL, + "Key2" NUMBER(19) NOT NULL, + "Name" NVARCHAR2(2000) NOT NULL, + "Computed" GENERATED ALWAYS AS (("Name"+999)), + "Identity" NUMBER(10) GENERATED ALWAYS AS IDENTITY(START with 1 INCREMENT by 1), + "NotMapped" CLOB NULL, + PRIMARY KEY ("Key1", "Key2") ); GO diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index 66bb79b..f487568 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -185,13 +185,13 @@ CREATE TABLE "EntityWithNullableProperty" 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") + "Key1" bigint NOT NULL, + "Key2" bigint NOT NULL, + "Name" text NOT NULL, + "Computed" integer GENERATED ALWAYS AS ("Name"+(999)), + "Identity" integer GENERATED ALWAYS AS IDENTITY NOT NULL, + "NotMapped" text NULL, + PRIMARY KEY ("Key1", "Key2") ); CREATE PROCEDURE "GetEntities" () diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index b1f519c..b3c9602 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -178,12 +178,12 @@ Value INTEGER 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 + Key1 INTEGER NOT NULL, + Key2 INTEGER NOT NULL, + Name TEXT NOT NULL, + Computed INTEGER GENERATED ALWAYS AS (Name+999) VIRTUAL, + Identity INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + NotMapped TEXT NULL ); """; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index f793914..b3113d7 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -222,13 +222,13 @@ Value BIGINT NULL 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) + Key1 BIGINT NOT NULL, + Key2 BIGINT NOT NULL, + Name NVARCHAR(MAX) NOT NULL, + Computed AS ([Name]+(999)), + Identity INT IDENTITY(1,1) NOT NULL, + NotMapped VARCHAR(200) NULL, + PRIMARY KEY (Key1, Key2) ); GO diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs index a70e978..415f115 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -16,28 +16,42 @@ public void Configure_ShouldConfigureDbConnectionPlus() .ToTable("MappingTestEntity"); config.Entity() - .Property(a => a.KeyColumn1_) - .HasColumnName("KeyColumn1") - .IsKey(); + .Property(a => a.Computed_) + .HasColumnName("Computed") + .IsComputed(); + + config.Entity() + .Property(a => a.ConcurrencyToken_) + .HasColumnName("ConcurrencyToken") + .IsConcurrencyToken(); + + config.Entity() + .Property(a => a.Identity_) + .HasColumnName("Identity") + .IsIdentity(); config.Entity() - .Property(a => a.KeyColumn2_) - .HasColumnName("KeyColumn2") + .Property(a => a.Key1_) + .HasColumnName("Key1") .IsKey(); config.Entity() - .Property(a => a.ComputedColumn_) - .HasColumnName("ComputedColumn") - .IsComputed(); + .Property(a => a.Key2_) + .HasColumnName("Key2") + .IsKey(); config.Entity() - .Property(a => a.IdentityColumn_) - .HasColumnName("IdentityColumn") - .IsIdentity(); + .Property(a => a.Name_) + .HasColumnName("Name"); config.Entity() - .Property(a => a.NotMappedColumn) + .Property(a => a.NotMapped) .IsIgnored(); + + config.Entity() + .Property(a => a.RowVersion_) + .HasColumnName("RowVersion") + .IsRowVersion(); } ); @@ -60,25 +74,46 @@ public void Configure_ShouldConfigureDbConnectionPlus() entityTypeBuilders[typeof(MappingTestEntityFluentApi)].TableName .Should().Be("MappingTestEntity"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn1_"].ColumnName - .Should().Be("KeyColumn1"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Computed_"].ColumnName + .Should().Be("Computed"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["KeyColumn2_"].ColumnName - .Should().Be("KeyColumn2"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Computed_"].IsComputed + .Should().BeTrue(); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ConcurrencyToken_"].ColumnName + .Should().Be("ConcurrencyToken"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ConcurrencyToken_"].IsConcurrencyToken + .Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].ColumnName - .Should().Be("ComputedColumn"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Identity_"].ColumnName + .Should().Be("Identity"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ComputedColumn_"].IsComputed + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Identity_"].IsIdentity .Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].IsIdentity + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].ColumnName + .Should().Be("Key1"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].IsKey + .Should().BeTrue(); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].ColumnName + .Should().Be("Key2"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].IsKey + .Should().BeTrue(); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Name_"].ColumnName + .Should().Be("Name"); + + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMapped"].IsIgnored .Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["IdentityColumn_"].ColumnName - .Should().Be("IdentityColumn"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["RowVersion_"].ColumnName + .Should().Be("RowVersion"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMappedColumn"].IsIgnored + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["RowVersion_"].IsRowVersion .Should().BeTrue(); } diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 095c9e3..2d4087f 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -152,88 +152,112 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe } [Fact] - public void GetEntityTypeMetadata_FluentApiMapping_ShouldGetMetadataBasedOnFluentApiMapping() + public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFluentApiMapping() { - var tableName = Generate.Single(); - var columnName = Generate.Single(); + MappingTestEntityFluentApi.Configure(); - Configure(config => - { - config.Entity() - .ToTable(tableName); + var metadata = EntityHelper.GetEntityTypeMetadata(typeof(MappingTestEntityFluentApi)); - config.Entity() - .Property(a => a.Id).IsKey(); + metadata + .Should().NotBeNull(); - config.Entity() - .Property(a => a.BooleanValue).HasColumnName(columnName); + metadata.EntityType + .Should().Be(typeof(MappingTestEntityFluentApi)); - config.Entity() - .Property(a => a.Int16Value).IsComputed(); + metadata.TableName + .Should().Be("MappingTestEntity"); - config.Entity() - .Property(a => a.Int32Value).IsIdentity(); + metadata.AllProperties + .Should().HaveCount(8); - config.Entity() - .Property(a => a.Int64Value).IsIgnored(); - } - ); + metadata.AllPropertiesByPropertyName + .Should().BeEquivalentTo(metadata.AllProperties.ToDictionary(a => a.PropertyName)); - var metadata = EntityHelper.GetEntityTypeMetadata(typeof(Entity)); + var computedProperty = metadata.AllPropertiesByPropertyName["Computed_"]; - metadata - .Should().NotBeNull(); + computedProperty.ColumnName + .Should().Be("Computed"); - metadata.EntityType - .Should().Be(typeof(Entity)); + computedProperty.IsComputed + .Should().BeTrue(); - metadata.TableName - .Should().Be(tableName); + var concurrencyTokenProperty = metadata.AllPropertiesByPropertyName["ConcurrencyToken_"]; - var idProperty = metadata.AllPropertiesByPropertyName["Id"]; + concurrencyTokenProperty.ColumnName + .Should().Be("ConcurrencyToken"); - idProperty.IsKey + concurrencyTokenProperty.IsConcurrencyToken .Should().BeTrue(); - var booleanValueProperty = metadata.AllPropertiesByPropertyName["BooleanValue"]; + var identityProperty = metadata.AllPropertiesByPropertyName["Identity_"]; + + identityProperty.ColumnName + .Should().Be("Identity"); - booleanValueProperty.ColumnName - .Should().Be(columnName); + identityProperty.IsIdentity + .Should().BeTrue(); - var int16ValueProperty = metadata.AllPropertiesByPropertyName["Int16Value"]; + var key1Property = metadata.AllPropertiesByPropertyName["Key1_"]; + + key1Property.ColumnName + .Should().Be("Key1"); - int16ValueProperty.IsComputed + key1Property.IsKey .Should().BeTrue(); - var int32ValueProperty = metadata.AllPropertiesByPropertyName["Int32Value"]; + var key2Property = metadata.AllPropertiesByPropertyName["Key2_"]; + + key2Property.ColumnName + .Should().Be("Key2"); - int32ValueProperty.IsIdentity + key2Property.IsKey .Should().BeTrue(); - var int64ValueProperty = metadata.AllPropertiesByPropertyName["Int64Value"]; + var nameProperty = metadata.AllPropertiesByPropertyName["Name_"]; + + nameProperty.ColumnName + .Should().Be("Name"); - int64ValueProperty.IsIgnored + var notMappedProperty = metadata.AllPropertiesByPropertyName["NotMapped"]; + + notMappedProperty.IsIgnored .Should().BeTrue(); - metadata.MappedProperties - .Should() - .NotContain(int64ValueProperty); + var rowVersionProperty = metadata.AllPropertiesByPropertyName["RowVersion_"]; - metadata.KeyProperties - .Should() - .Contain(idProperty); + rowVersionProperty.IsRowVersion + .Should().BeTrue(); metadata.ComputedProperties - .Should().Contain(int16ValueProperty); + .Should().BeEquivalentTo([computedProperty]); + + metadata.ConcurrencyTokenProperties + .Should().BeEquivalentTo([concurrencyTokenProperty]); + + metadata.DatabaseGeneratedProperties + .Should().BeEquivalentTo([computedProperty, identityProperty, rowVersionProperty]); metadata.IdentityProperty - .Should().Be(int32ValueProperty); + .Should().Be(identityProperty); metadata.InsertProperties - .Should().Contain([idProperty, booleanValueProperty]); + .Should().BeEquivalentTo([concurrencyTokenProperty, key1Property, key2Property, nameProperty]); + + metadata.KeyProperties + .Should().BeEquivalentTo([key1Property, key2Property]); + + metadata.MappedProperties + .Should().BeEquivalentTo( + [computedProperty, concurrencyTokenProperty, identityProperty, key1Property, key2Property, nameProperty, rowVersionProperty] + ); + + metadata.RowVersionProperties + .Should().BeEquivalentTo( + [rowVersionProperty] + ); metadata.UpdateProperties - .Should().Contain([booleanValueProperty]); + .Should().BeEquivalentTo([nameProperty]); } [Fact] @@ -246,12 +270,8 @@ public void GetEntityTypeMetadata_MoreThanOneIdentityProperty_ShouldThrow() => "property per entity type." ); - [Theory] - [InlineData(typeof(MappingTestEntity))] - [InlineData(typeof(MappingTestEntityAttributes))] - public void GetEntityTypeMetadata_NoFluentApiMapping_ShouldGetMetadataBasedOnDataAnnotationAttributes( - Type entityType - ) + [Fact] + public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAttributes() { var faker = new Faker(); @@ -259,6 +279,8 @@ Type entityType fixture.Register(() => faker.Date.PastDateOnly()); fixture.Register(() => faker.Date.RecentTimeOnly()); + var entityType = typeof(MappingTestEntityAttributes); + var entity = specimenFactoryCreateMethod .MakeGenericMethod(entityType) .Invoke(null, [fixture]); @@ -280,46 +302,55 @@ Type entityType allPropertiesMetadata .Should().HaveSameCount(entityProperties); - - + metadata.AllPropertiesByPropertyName .Should().BeEquivalentTo(allPropertiesMetadata.ToDictionary(a => a.PropertyName)); - metadata.MappedProperties + metadata.ComputedProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false })); + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsComputed: true })); - metadata.KeyProperties + metadata.ConcurrencyTokenProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsKey: true })); + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsConcurrencyToken: true })); - metadata.ComputedProperties + metadata.DatabaseGeneratedProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsComputed: true })); + .BeEquivalentTo(allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity || a.IsRowVersion))); metadata.IdentityProperty .Should() .Be(allPropertiesMetadata.FirstOrDefault(a => a is { IsIgnored: false, IsIdentity: true })); - metadata.DatabaseGeneratedProperties - .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity))); - metadata.InsertProperties .Should().BeEquivalentTo( allPropertiesMetadata.Where(a => a is - { IsIgnored: false, IsComputed: false, IsIdentity: false } + { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion:false } ) ); + metadata.KeyProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsKey: true })); + + metadata.MappedProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false })); + + metadata.RowVersionProperties + .Should() + .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsRowVersion: true })); + metadata.UpdateProperties .Should().BeEquivalentTo( allPropertiesMetadata.Where(a => a is { + IsComputed: false, + IsConcurrencyToken: false, IsIgnored: false, + IsIdentity: false, IsKey: false, - IsComputed: false, - IsIdentity: false + IsRowVersion: false } ) ); @@ -331,23 +362,14 @@ Type entityType propertyMetadata .Should().NotBeNull(); - propertyMetadata.ColumnName - .Should().Be(property.GetCustomAttribute()?.Name ?? property.Name); - - propertyMetadata.PropertyName - .Should().Be(property.Name); - - propertyMetadata.PropertyType - .Should().Be(property.PropertyType); - - propertyMetadata.PropertyInfo - .Should().BeSameAs(property); + propertyMetadata.CanRead + .Should().Be(property.CanRead); - propertyMetadata.IsIgnored - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.CanWrite + .Should().Be(property.CanWrite); - propertyMetadata.IsKey - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.ColumnName + .Should().Be(property.GetCustomAttribute()?.Name ?? property.Name); propertyMetadata.IsComputed .Should().Be( @@ -355,17 +377,23 @@ Type entityType DatabaseGeneratedOption.Computed ); + propertyMetadata.IsConcurrencyToken + .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.IsIdentity .Should().Be( property.GetCustomAttribute()?.DatabaseGeneratedOption is DatabaseGeneratedOption.Identity ); - propertyMetadata.CanRead - .Should().Be(property.CanRead); + propertyMetadata.IsIgnored + .Should().Be(property.GetCustomAttribute() is not null); - propertyMetadata.CanWrite - .Should().Be(property.CanWrite); + propertyMetadata.IsKey + .Should().Be(property.GetCustomAttribute() is not null); + + propertyMetadata.IsRowVersion + .Should().Be(property.GetCustomAttribute() is not null); if (propertyMetadata.CanRead) { @@ -381,6 +409,12 @@ Type entityType .Should().BeNull(); } + propertyMetadata.PropertyInfo + .Should().BeSameAs(property); + + propertyMetadata.PropertyName + .Should().Be(property.Name); + if (propertyMetadata.CanWrite) { propertyMetadata.PropertySetter @@ -400,6 +434,9 @@ Type entityType propertyMetadata.PropertySetter .Should().BeNull(); } + + propertyMetadata.PropertyType + .Should().Be(property.PropertyType); } } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index fc97129..3a5a1bf 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -310,43 +310,55 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(6); + dataReader.FieldCount.Returns(8); var ordinal = 0; - dataReader.GetName(ordinal).Returns("KeyColumn1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetName(ordinal).Returns("Computed"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1_); + dataReader.GetInt32(ordinal).Returns(entity.Computed_); ordinal++; - dataReader.GetName(ordinal).Returns("KeyColumn2"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetName(ordinal).Returns("ConcurrencyToken"); + dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2_); + dataReader.GetValue(ordinal).Returns(entity.ConcurrencyToken_); ordinal++; - dataReader.GetName(ordinal).Returns("ValueColumn"); + dataReader.GetName(ordinal).Returns("Identity"); dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.ValueColumn_); + dataReader.GetInt32(ordinal).Returns(entity.Identity_); ordinal++; - dataReader.GetName(ordinal).Returns("ComputedColumn"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetName(ordinal).Returns("Key1"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.ComputedColumn_); + dataReader.GetInt64(ordinal).Returns(entity.Key1_); ordinal++; - dataReader.GetName(ordinal).Returns("IdentityColumn"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetName(ordinal).Returns("Key2"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.Key2_); + + ordinal++; + dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetFieldType(ordinal).Returns(typeof(String)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.IdentityColumn_); + dataReader.GetString(ordinal).Returns(entity.Name_); ordinal++; var notMappedColumnOrdinal = ordinal; - dataReader.GetName(notMappedColumnOrdinal).Returns("NotMappedColumn"); + dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + ordinal++; + dataReader.GetName(ordinal).Returns("RowVersion"); + dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetValue(ordinal).Returns(entity.RowVersion_); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var materializedEntity = materializer(dataReader); @@ -354,23 +366,29 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); - materializedEntity.KeyColumn1_ - .Should().Be(entity.KeyColumn1_); + materializedEntity.Computed_ + .Should().Be(entity.Computed_); + + materializedEntity.ConcurrencyToken_ + .Should().BeEquivalentTo(entity.ConcurrencyToken_); - materializedEntity.KeyColumn2_ - .Should().Be(entity.KeyColumn2_); + materializedEntity.Identity_ + .Should().Be(entity.Identity_); - materializedEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); + materializedEntity.Key1_ + .Should().Be(entity.Key1_); - materializedEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); + materializedEntity.Key2_ + .Should().Be(entity.Key2_); - materializedEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); + materializedEntity.Name_ + .Should().Be(entity.Name_); - materializedEntity.NotMappedColumn + materializedEntity.NotMapped .Should().BeNull(); + + materializedEntity.RowVersion_ + .Should().BeEquivalentTo(entity.RowVersion_); } [Fact] @@ -382,43 +400,55 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(6); + dataReader.FieldCount.Returns(8); var ordinal = 0; - dataReader.GetName(ordinal).Returns("KeyColumn1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetName(ordinal).Returns("Computed"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1_); + dataReader.GetInt32(ordinal).Returns(entity.Computed_); ordinal++; - dataReader.GetName(ordinal).Returns("KeyColumn2"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetName(ordinal).Returns("ConcurrencyToken"); + dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2_); + dataReader.GetValue(ordinal).Returns(entity.ConcurrencyToken_); ordinal++; - dataReader.GetName(ordinal).Returns("ValueColumn"); + dataReader.GetName(ordinal).Returns("Identity"); dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.ValueColumn_); + dataReader.GetInt32(ordinal).Returns(entity.Identity_); ordinal++; - dataReader.GetName(ordinal).Returns("ComputedColumn"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetName(ordinal).Returns("Key1"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.ComputedColumn_); + dataReader.GetInt64(ordinal).Returns(entity.Key1_); ordinal++; - dataReader.GetName(ordinal).Returns("IdentityColumn"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetName(ordinal).Returns("Key2"); + dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.IdentityColumn_); + dataReader.GetInt64(ordinal).Returns(entity.Key2_); + + ordinal++; + dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetString(ordinal).Returns(entity.Name_); ordinal++; var notMappedColumnOrdinal = ordinal; - dataReader.GetName(notMappedColumnOrdinal).Returns("NotMappedColumn"); + dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + ordinal++; + dataReader.GetName(ordinal).Returns("RowVersion"); + dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetValue(ordinal).Returns(entity.RowVersion_); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var materializedEntity = materializer(dataReader); @@ -426,23 +456,29 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); - materializedEntity.KeyColumn1_ - .Should().Be(entity.KeyColumn1_); + materializedEntity.Computed_ + .Should().Be(entity.Computed_); - materializedEntity.KeyColumn2_ - .Should().Be(entity.KeyColumn2_); + materializedEntity.ConcurrencyToken_ + .Should().BeEquivalentTo(entity.ConcurrencyToken_); - materializedEntity.ValueColumn_ - .Should().Be(entity.ValueColumn_); + materializedEntity.Identity_ + .Should().Be(entity.Identity_); - materializedEntity.ComputedColumn_ - .Should().Be(entity.ComputedColumn_); + materializedEntity.Key1_ + .Should().Be(entity.Key1_); - materializedEntity.IdentityColumn_ - .Should().Be(entity.IdentityColumn_); + materializedEntity.Key2_ + .Should().Be(entity.Key2_); - materializedEntity.NotMappedColumn + materializedEntity.Name_ + .Should().Be(entity.Name_); + + materializedEntity.NotMapped .Should().BeNull(); + + materializedEntity.RowVersion_ + .Should().BeEquivalentTo(entity.RowVersion_); } [Fact] @@ -455,35 +491,35 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam dataReader.FieldCount.Returns(3); var ordinal = 0; - dataReader.GetName(ordinal).Returns("KeyColumn1"); + dataReader.GetName(ordinal).Returns("Key1"); dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn1); + dataReader.GetInt64(ordinal).Returns(entity.Key1); ordinal++; - dataReader.GetName(ordinal).Returns("KeyColumn2"); + dataReader.GetName(ordinal).Returns("Key2"); dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.KeyColumn2); + dataReader.GetInt64(ordinal).Returns(entity.Key2); ordinal++; - dataReader.GetName(ordinal).Returns("ValueColumn"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetFieldType(ordinal).Returns(typeof(String)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.ValueColumn); + dataReader.GetString(ordinal).Returns(entity.Name); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var materializedEntity = materializer(dataReader); - materializedEntity.KeyColumn1 - .Should().Be(entity.KeyColumn1); + materializedEntity.Key1 + .Should().Be(entity.Key1); - materializedEntity.KeyColumn2 - .Should().Be(entity.KeyColumn2); + materializedEntity.Key2 + .Should().Be(entity.Key2); - materializedEntity.ValueColumn - .Should().Be(entity.ValueColumn); + materializedEntity.Name + .Should().Be(entity.Name); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 08c9ac6..7dc5a90 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -14,9 +14,11 @@ namespace RentADeveloper.DbConnectionPlus.Configuration { public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder HasColumnName(string columnName) { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsComputed() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsConcurrencyToken() { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsIdentity() { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsIgnored() { } public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsKey() { } + public RentADeveloper.DbConnectionPlus.Configuration.EntityPropertyBuilder IsRowVersion() { } } public sealed class EntityTypeBuilder : RentADeveloper.DbConnectionPlus.Configuration.IFreezable { @@ -207,14 +209,16 @@ namespace RentADeveloper.DbConnectionPlus.Entities } public sealed record EntityPropertyMetadata : System.IEquatable { - 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 EntityPropertyMetadata(bool CanRead, bool CanWrite, string ColumnName, bool IsComputed, bool IsConcurrencyToken, bool IsIdentity, bool IsIgnored, bool IsKey, bool IsRowVersion, Fasterflect.MemberGetter? PropertyGetter, System.Reflection.PropertyInfo PropertyInfo, string PropertyName, Fasterflect.MemberSetter? PropertySetter, System.Type PropertyType) { } public bool CanRead { get; init; } public bool CanWrite { get; init; } public string ColumnName { get; init; } public bool IsComputed { get; init; } + public bool IsConcurrencyToken { get; init; } public bool IsIdentity { get; init; } public bool IsIgnored { get; init; } public bool IsKey { get; init; } + public bool IsRowVersion { get; init; } public Fasterflect.MemberGetter? PropertyGetter { get; init; } public System.Reflection.PropertyInfo PropertyInfo { get; init; } public string PropertyName { get; init; } @@ -223,16 +227,18 @@ 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, RentADeveloper.DbConnectionPlus.Entities.EntityPropertyMetadata? IdentityProperty, 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 ComputedProperties, System.Collections.Generic.IReadOnlyList ConcurrencyTokenProperties, System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties, RentADeveloper.DbConnectionPlus.Entities.EntityPropertyMetadata? IdentityProperty, System.Collections.Generic.IReadOnlyList InsertProperties, System.Collections.Generic.IReadOnlyList KeyProperties, System.Collections.Generic.IReadOnlyList MappedProperties, System.Collections.Generic.IReadOnlyList RowVersionProperties, 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 ConcurrencyTokenProperties { get; init; } public System.Collections.Generic.IReadOnlyList DatabaseGeneratedProperties { get; init; } public System.Type EntityType { get; init; } public RentADeveloper.DbConnectionPlus.Entities.EntityPropertyMetadata? IdentityProperty { get; init; } public System.Collections.Generic.IReadOnlyList InsertProperties { get; init; } public System.Collections.Generic.IReadOnlyList KeyProperties { get; init; } public System.Collections.Generic.IReadOnlyList MappedProperties { get; init; } + public System.Collections.Generic.IReadOnlyList RowVersionProperties { get; init; } public string TableName { get; init; } public System.Collections.Generic.IReadOnlyList UpdateProperties { get; init; } } 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/MappingTestEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs index 5288b27..5ea22d3 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -3,10 +3,10 @@ public record MappingTestEntity { [Key] - public Int64 KeyColumn1 { get; set; } + public Int64 Key1 { get; set; } [Key] - public Int64 KeyColumn2 { get; set; } + public Int64 Key2 { get; set; } - public Int32 ValueColumn { get; set; } + public String Name { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index 358d0d8..d38a562 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -5,25 +5,33 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; [Table("MappingTestEntity")] public record MappingTestEntityAttributes { - [Column("ComputedColumn")] + [Column("Computed")] [DatabaseGenerated(DatabaseGeneratedOption.Computed)] - public Int32 ComputedColumn_ { get; set; } + public Int32 Computed_ { get; set; } - [Column("IdentityColumn")] + [Column("ConcurrencyToken")] + [ConcurrencyCheck] + public Byte[]? ConcurrencyToken_ { get; set; } + + [Column("Identity")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Int32 IdentityColumn_ { get; set; } + public Int32 Identity_ { get; set; } [Key] - [Column("KeyColumn1")] - public Int64 KeyColumn1_ { get; set; } + [Column("Key1")] + public Int64 Key1_ { get; set; } [Key] - [Column("KeyColumn2")] - public Int64 KeyColumn2_ { get; set; } + [Column("Key2")] + public Int64 Key2_ { get; set; } + + [Column("Name")] + public String Name_ { get; set; } [NotMapped] - public String? NotMappedColumn { get; set; } + public String? NotMapped { get; set; } - [Column("ValueColumn")] - public Int32 ValueColumn_ { get; set; } + [Column("RowVersion")] + [Timestamp] + public Byte[]? RowVersion_ { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs index d4cb012..c70f136 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -4,12 +4,14 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record MappingTestEntityFluentApi { - public Int32 ComputedColumn_ { get; set; } - public Int32 IdentityColumn_ { get; set; } - public Int64 KeyColumn1_ { get; set; } - public Int64 KeyColumn2_ { get; set; } - public String? NotMappedColumn { get; set; } - public Int32 ValueColumn_ { get; set; } + public Int32 Computed_ { get; set; } + public Byte[]? ConcurrencyToken_ { get; set; } + public Int32 Identity_ { get; set; } + public Int64 Key1_ { get; set; } + public Int64 Key2_ { get; set; } + public String Name_ { get; set; } + public String? NotMapped { get; set; } + public Byte[]? RowVersion_ { get; set; } /// /// Configures the mapping for this entity using the Fluent API. @@ -21,32 +23,42 @@ public static void Configure() => .ToTable("MappingTestEntity"); config.Entity() - .Property(a => a.KeyColumn1_) - .HasColumnName("KeyColumn1") - .IsKey(); + .Property(a => a.Computed_) + .HasColumnName("Computed") + .IsComputed(); config.Entity() - .Property(a => a.KeyColumn2_) - .HasColumnName("KeyColumn2") - .IsKey(); + .Property(a => a.ConcurrencyToken_) + .HasColumnName("ConcurrencyToken") + .IsConcurrencyToken(); config.Entity() - .Property(a => a.ValueColumn_) - .HasColumnName("ValueColumn"); + .Property(a => a.Identity_) + .HasColumnName("Identity") + .IsIdentity(); config.Entity() - .Property(a => a.ComputedColumn_) - .HasColumnName("ComputedColumn") - .IsComputed(); + .Property(a => a.Key1_) + .HasColumnName("Key1") + .IsKey(); config.Entity() - .Property(a => a.IdentityColumn_) - .HasColumnName("IdentityColumn") - .IsIdentity(); + .Property(a => a.Key2_) + .HasColumnName("Key2") + .IsKey(); + + config.Entity() + .Property(a => a.Name_) + .HasColumnName("Name"); config.Entity() - .Property(a => a.NotMappedColumn) + .Property(a => a.NotMapped) .IsIgnored(); + + config.Entity() + .Property(a => a.RowVersion_) + .HasColumnName("RowVersion") + .IsRowVersion(); } ); } From 67bb3b339b4ec7864ac7e49c24a1ee82d9720cc2 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Tue, 3 Feb 2026 11:13:41 +0100 Subject: [PATCH 02/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../DatabaseAdapters/IEntityManipulator.cs | 72 +-- .../MySql/MySqlEntityManipulator.cs | 369 +------------- .../PostgreSql/PostgreSqlEntityManipulator.cs | 390 +-------------- .../SqlServer/SqlServerEntityManipulator.cs | 459 +++++------------- .../DbUpdateConcurrencyException.cs | 46 ++ src/DbConnectionPlus/ThrowHelper.cs | 26 + .../EntityManipulator.DeleteEntitiesTests.cs | 134 +++-- .../EntityManipulator.DeleteEntityTests.cs | 68 ++- .../EntityManipulator.UpdateEntitiesTests.cs | 114 ++++- .../EntityManipulator.UpdateEntityTests.cs | 88 +++- .../SqlServerTestDatabaseProvider.cs | 8 +- ...ConnectionExtensions.ConfigurationTests.cs | 8 +- .../Entities/EntityHelperTests.cs | 2 +- .../EntityMaterializerFactoryTests.cs | 24 +- .../TestData/Generate.cs | 76 ++- .../TestData/MappingTestEntity.cs | 4 +- .../TestData/MappingTestEntityAttributes.cs | 4 +- .../TestData/MappingTestEntityFluentApi.cs | 6 +- 18 files changed, 673 insertions(+), 1225 deletions(-) create mode 100644 src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs diff --git a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs index a7f800a..192f76c 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs @@ -42,12 +42,12 @@ public interface IEntityManipulator /// /// /// The table from which the entities will be deleted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// public Int32 DeleteEntities( @@ -93,12 +93,12 @@ CancellationToken cancellationToken /// /// /// The table from which the entities will be deleted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// public Task DeleteEntitiesAsync( @@ -140,12 +140,12 @@ CancellationToken cancellationToken /// /// /// The table from which the entity will be deleted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// public Int32 DeleteEntity( @@ -191,12 +191,12 @@ CancellationToken cancellationToken /// /// /// The table from which the entity will be deleted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// public Task DeleteEntityAsync( @@ -235,14 +235,14 @@ CancellationToken cancellationToken /// /// /// The table into which the entities will be inserted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used /// as the table 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. @@ -254,7 +254,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -299,13 +299,13 @@ CancellationToken cancellationToken /// /// /// The table into which the entities will be inserted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table 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. @@ -317,7 +317,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -358,13 +358,13 @@ CancellationToken cancellationToken /// /// /// The table into which the entity will be inserted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table 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. @@ -376,7 +376,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -421,13 +421,13 @@ CancellationToken cancellationToken /// /// /// The table into which the entity will be inserted can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table 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. @@ -439,7 +439,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -483,17 +483,17 @@ CancellationToken cancellationToken /// /// /// The table in which the entities will be updated can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -505,7 +505,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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. /// @@ -554,17 +554,17 @@ CancellationToken cancellationToken /// /// /// The table in which the entities will be updated can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -576,7 +576,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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. /// @@ -620,17 +620,17 @@ CancellationToken cancellationToken /// /// /// The table in which the entity will be updated can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -642,7 +642,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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. /// @@ -690,17 +690,17 @@ CancellationToken cancellationToken /// /// /// The table in which the entity will be updated can be configured via or - /// . Per default, the singular name of the type + /// . Per default, the singular name of the type /// is used as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -712,7 +712,7 @@ CancellationToken cancellationToken /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) are also not updated. /// Once an entity is updated, the values for these properties are retrieved from the database and the entity /// properties are updated accordingly. /// diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index cba7f92..787aae4 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; -using MySqlConnector; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -24,21 +23,6 @@ public MySqlEntityManipulator(MySqlDatabaseAdapter databaseAdapter) => #pragma warning restore IDE0290 // Use primary constructor /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public Int32 DeleteEntities( DbConnection connection, IEnumerable entities, @@ -49,106 +33,22 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var totalNumberOfAffectedRows = 0; - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) + foreach (var entity in entities) { - var totalNumberOfAffectedRows = 0; - - foreach (var entity in entitiesList) + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + continue; } - return totalNumberOfAffectedRows; + totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); } - if (connection is not MySqlConnection mySqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var mySqlTransaction = transaction as MySqlTransaction; - - if (transaction is not null && mySqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); - } - - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var onClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties - .Select(p => $"TKeys.`{p.PropertyName}` = `{entityTypeMetadata.TableName}`.`{p.ColumnName}`") - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - this.BuildEntityKeysTemporaryTable( - mySqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - mySqlTransaction, - cancellationToken - ); - - var numberOfAffectedRows = connection.ExecuteNonQuery( - $""" - DELETE - {Constants.Indent}`{entityTypeMetadata.TableName}` - FROM - {Constants.Indent}`{entityTypeMetadata.TableName}` - INNER JOIN - {Constants.Indent}`{keysTableName}` AS TKeys - ON - {Constants.Indent}{onClause} - """, - transaction, - cancellationToken: cancellationToken - ); - -#pragma warning disable CA2016 - connection.ExecuteNonQuery($"DROP TEMPORARY TABLE `{keysTableName}`", transaction); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public async Task DeleteEntitiesAsync( DbConnection connection, IEnumerable entities, @@ -161,89 +61,20 @@ CancellationToken cancellationToken var entitiesList = entities.ToList(); - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) - { - var totalNumberOfAffectedRows = 0; + var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + foreach (var entity in entitiesList) + { + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + continue; } - return totalNumberOfAffectedRows; + totalNumberOfAffectedRows += await this + .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); } - if (connection is not MySqlConnection mySqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var mySqlTransaction = transaction as MySqlTransaction; - - if (transaction is not null && mySqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); - } - - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var onClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties - .Select(p => $"TKeys.`{p.PropertyName}` = `{entityTypeMetadata.TableName}`.`{p.ColumnName}`") - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - await this.BuildEntityKeysTemporaryTableAsync( - mySqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - mySqlTransaction, - cancellationToken - ).ConfigureAwait(false); - - var numberOfAffectedRows = await connection.ExecuteNonQueryAsync( - $""" - DELETE - {Constants.Indent}`{entityTypeMetadata.TableName}` - FROM - {Constants.Indent}`{entityTypeMetadata.TableName}` - INNER JOIN - {Constants.Indent}`{keysTableName}` AS TKeys - ON - {Constants.Indent}{onClause} - """, - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - -#pragma warning disable CA2016 - // ReSharper disable once MethodSupportsCancellation - await connection.ExecuteNonQueryAsync($"DROP TEMPORARY TABLE `{keysTableName}`", transaction) - .ConfigureAwait(false); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// @@ -744,125 +575,6 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, } } - /// - /// Builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - private void BuildEntityKeysTemporaryTable( - MySqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - MySqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - connection.ExecuteNonQuery( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ); - - using var keysTable = new DataTable(); - - foreach (var property in entityTypeMetadata.KeyProperties) - { - var columnType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - keysTable.Columns.Add(property.PropertyName, columnType); - } - - foreach (var entity in entities) - { - if (entity is null) - { - continue; - } - - var keysRow = keysTable.NewRow(); - - foreach (var keyProperty in entityTypeMetadata.KeyProperties) - { - keysRow[keyProperty.PropertyName] = keyProperty.PropertyGetter!(entity); - } - - keysTable.Rows.Add(keysRow); - } - - var mySqlBulkCopy = new MySqlBulkCopy(connection, transaction) - { - BulkCopyTimeout = 0, - DestinationTableName = $"`{keysTableName}`" - }; - - mySqlBulkCopy.WriteToServer(keysTable); - } - - /// - /// Asynchronously builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. - private async Task BuildEntityKeysTemporaryTableAsync( - MySqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - MySqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - await connection.ExecuteNonQueryAsync( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - - using var keysTable = new DataTable(); - - foreach (var property in entityTypeMetadata.KeyProperties) - { - var columnType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - keysTable.Columns.Add(property.PropertyName, columnType); - } - - foreach (var entity in entities) - { - if (entity is null) - { - continue; - } - - var keysRow = keysTable.NewRow(); - - foreach (var keyProperty in entityTypeMetadata.KeyProperties) - { - keysRow[keyProperty.PropertyName] = keyProperty.PropertyGetter!(entity); - } - - keysTable.Rows.Add(keysRow); - } - - var mySqlBulkCopy = new MySqlBulkCopy(connection, transaction) - { - BulkCopyTimeout = 0, - DestinationTableName = $"`{keysTableName}`" - }; - - await mySqlBulkCopy.WriteToServerAsync(keysTable, cancellationToken).ConfigureAwait(false); - } - /// /// Creates a command to delete an entity. /// @@ -900,58 +612,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, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Creates a command to insert an entity. /// @@ -1440,5 +1100,4 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new(); - private const Int32 BulkDeleteThreshold = 10; } diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index 9567f39..8c27b73 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using LinkDotNet.StringBuilder; -using Npgsql; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; @@ -22,21 +21,6 @@ public PostgreSqlEntityManipulator(PostgreSqlDatabaseAdapter databaseAdapter) => this.databaseAdapter = databaseAdapter; /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public Int32 DeleteEntities( DbConnection connection, IEnumerable entities, @@ -49,103 +33,22 @@ CancellationToken cancellationToken var entitiesList = entities.ToList(); - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) - { - var totalNumberOfAffectedRows = 0; + var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + foreach (var entity in entitiesList) + { + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + continue; } - return totalNumberOfAffectedRows; + totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); } - if (connection is not NpgsqlConnection npgsqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var npgsqlTransaction = transaction as NpgsqlTransaction; - - if (transaction is not null && npgsqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); - } - - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var whereClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties.Select(p => - $"TKeys.\"{p.PropertyName}\" = \"{entityTypeMetadata.TableName}\".\"{p.ColumnName}\"" - ) - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - this.BuildEntityKeysTemporaryTable( - npgsqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - npgsqlTransaction, - cancellationToken - ); - - var numberOfAffectedRows = connection.ExecuteNonQuery( - $""" - DELETE FROM - {Constants.Indent}"{entityTypeMetadata.TableName}" - USING - {Constants.Indent}"{keysTableName}" AS TKeys - WHERE - {Constants.Indent}{whereClause} - """, - transaction, - cancellationToken: cancellationToken - ); - -#pragma warning disable CA2016 - connection.ExecuteNonQuery($"DROP TABLE \"{keysTableName}\"", transaction); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public async Task DeleteEntitiesAsync( DbConnection connection, IEnumerable entities, @@ -158,90 +61,20 @@ CancellationToken cancellationToken var entitiesList = entities.ToList(); - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) - { - var totalNumberOfAffectedRows = 0; + var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + foreach (var entity in entitiesList) + { + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + continue; } - return totalNumberOfAffectedRows; + totalNumberOfAffectedRows += await this + .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); } - if (connection is not NpgsqlConnection npgsqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var npgsqlTransaction = transaction as NpgsqlTransaction; - - if (transaction is not null && npgsqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); - } - - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var whereClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties.Select(p => - $"TKeys.\"{p.PropertyName}\" = \"{entityTypeMetadata.TableName}\".\"{p.ColumnName}\"" - ) - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - await this.BuildEntityKeysTemporaryTableAsync( - npgsqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - npgsqlTransaction, - cancellationToken - ).ConfigureAwait(false); - - var numberOfAffectedRows = await connection.ExecuteNonQueryAsync( - $""" - DELETE FROM - {Constants.Indent}"{entityTypeMetadata.TableName}" - USING - {Constants.Indent}"{keysTableName}" AS TKeys - WHERE - {Constants.Indent}{whereClause} - """, - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - -#pragma warning disable CA2016 - await connection.ExecuteNonQueryAsync( - $"DROP TABLE \"{keysTableName}\"", - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// @@ -745,146 +578,6 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, } } - /// - /// Builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - private void BuildEntityKeysTemporaryTable( - NpgsqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - NpgsqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - connection.ExecuteNonQuery( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ); - - var npgsqlDbTypes = entityTypeMetadata - .KeyProperties - .Select(p => this.databaseAdapter.GetDbType( - p.PropertyType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ) - ) - .ToArray(); - - using var importer = connection.BeginBinaryImport($"COPY \"{keysTableName}\" FROM STDIN (FORMAT BINARY)"); - - foreach (var entity in entities) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (entity is null) - { - continue; - } - - importer.StartRow(); - - for (var i = 0; i < entityTypeMetadata.KeyProperties.Count; i++) - { - var keyProperty = entityTypeMetadata.KeyProperties[i]; - var keyValue = keyProperty.PropertyGetter!(entity); - - if (keyValue is null) - { - importer.WriteNull(); - } - else - { - importer.Write(keyValue, npgsqlDbTypes[i]); - } - } - } - - importer.Complete(); - importer.Close(); - } - - /// - /// Asynchronously builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. - private async Task BuildEntityKeysTemporaryTableAsync( - NpgsqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - NpgsqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - await connection.ExecuteNonQueryAsync( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - - var npgsqlDbTypes = entityTypeMetadata - .KeyProperties - .Select(p => this.databaseAdapter.GetDbType( - p.PropertyType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ) - ) - .ToArray(); - -#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await using var importer = await connection.BeginBinaryImportAsync( - $"COPY \"{keysTableName}\" FROM STDIN (FORMAT BINARY)", - cancellationToken - ).ConfigureAwait(false); -#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - - foreach (var entity in entities) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (entity is null) - { - continue; - } - - await importer.StartRowAsync(cancellationToken).ConfigureAwait(false); - - for (var i = 0; i < entityTypeMetadata.KeyProperties.Count; i++) - { - var keyProperty = entityTypeMetadata.KeyProperties[i]; - var keyValue = keyProperty.PropertyGetter!(entity); - - if (keyValue is null) - { - await importer.WriteNullAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await importer.WriteAsync(keyValue, npgsqlDbTypes[i], cancellationToken).ConfigureAwait(false); - } - } - } - - await importer.CompleteAsync(cancellationToken).ConfigureAwait(false); - await importer.CloseAsync(cancellationToken).ConfigureAwait(false); - } - /// /// Creates a command to delete an entity. /// @@ -922,58 +615,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, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Creates a command to insert an entity. /// @@ -1387,5 +1028,4 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new(); - private const Int32 BulkDeleteThreshold = 10; } diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index cbd2924..ba955e8 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -21,21 +21,6 @@ public SqlServerEntityManipulator(SqlServerDatabaseAdapter databaseAdapter) => this.databaseAdapter = databaseAdapter; /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public Int32 DeleteEntities( DbConnection connection, IEnumerable entities, @@ -48,103 +33,22 @@ CancellationToken cancellationToken var entitiesList = entities.ToList(); - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) - { - var totalNumberOfAffectedRows = 0; + var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + foreach (var entity in entitiesList) + { + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + continue; } - return totalNumberOfAffectedRows; + totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); } - if (connection is not SqlConnection sqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var sqlTransaction = transaction as SqlTransaction; - - if (transaction is not null && sqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); - } - - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var onClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties.Select(p => $"TKeys.[{p.PropertyName}] = TEntities.[{p.ColumnName}]") - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - this.BuildEntityKeysTemporaryTable( - sqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - sqlTransaction, - cancellationToken - ); - - var numberOfAffectedRows = connection.ExecuteNonQuery( - $""" - DELETE - {Constants.Indent}[{entityTypeMetadata.TableName}] - FROM - {Constants.Indent}[{entityTypeMetadata.TableName}] AS TEntities - INNER JOIN - {Constants.Indent}[#{keysTableName}] AS TKeys - ON - {Constants.Indent}{onClause} - """, - transaction, - cancellationToken: cancellationToken - ); - -#pragma warning disable CA2016 - connection.ExecuteNonQuery($"DROP TABLE [#{keysTableName}]", transaction); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// - /// - /// - /// - /// - /// is not a . - /// - /// - /// - /// - /// is not and not a - /// . - /// - /// - /// - /// public async Task DeleteEntitiesAsync( DbConnection connection, IEnumerable entities, @@ -157,87 +61,20 @@ CancellationToken cancellationToken var entitiesList = entities.ToList(); - // For a small number of entities deleting them one by one is more efficient than creating a temp table. - if (entitiesList.Count < BulkDeleteThreshold) - { - var totalNumberOfAffectedRows = 0; + var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + foreach (var entity in entitiesList) + { + if (entity is null) { - if (entity is null) - { - continue; - } - - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + continue; } - return totalNumberOfAffectedRows; - } - - if (connection is not SqlConnection sqlConnection) - { - return ThrowHelper.ThrowWrongConnectionTypeException(); - } - - var sqlTransaction = transaction as SqlTransaction; - - if (transaction is not null && sqlTransaction is null) - { - return ThrowHelper.ThrowWrongTransactionTypeException(); + totalNumberOfAffectedRows += await this + .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); } - var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); - - var onClause = String.Join( - " AND ", - entityTypeMetadata.KeyProperties.Select(p => $"TKeys.[{p.PropertyName}] = TEntities.[{p.ColumnName}]") - ); - - try - { - var keysTableName = "Keys_" + Guid.NewGuid().ToString("N"); - - await this.BuildEntityKeysTemporaryTableAsync( - sqlConnection, - keysTableName, - entitiesList, - entityTypeMetadata, - sqlTransaction, - cancellationToken - ).ConfigureAwait(false); - - var numberOfAffectedRows = await connection.ExecuteNonQueryAsync( - $""" - DELETE - {Constants.Indent}[{entityTypeMetadata.TableName}] - FROM - {Constants.Indent}[{entityTypeMetadata.TableName}] AS TEntities - INNER JOIN - {Constants.Indent}[#{keysTableName}] AS TKeys - ON - {Constants.Indent}{onClause} - """, - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - -#pragma warning disable CA2016 - // ReSharper disable once MethodSupportsCancellation - await connection.ExecuteNonQueryAsync($"DROP TABLE [#{keysTableName}]", transaction).ConfigureAwait(false); -#pragma warning restore CA2016 - - return numberOfAffectedRows; - } - catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( - exception, - cancellationToken - ) - ) - { - throw new OperationCanceledException(cancellationToken); - } + return totalNumberOfAffectedRows; } /// @@ -265,7 +102,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return command.ExecuteNonQuery(); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -303,7 +151,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -570,6 +429,19 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 + // when we selected database generated properties via the OUTPUT clause. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -634,6 +506,19 @@ await UpdateDatabaseGeneratedPropertiesAsync( cancellationToken ).ConfigureAwait(false); + // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 + // when we selected database generated properties via the OUTPUT clause. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -681,6 +566,19 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 + // when we selected database generated properties via the OUTPUT clause. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -730,6 +628,19 @@ CancellationToken cancellationToken await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken) .ConfigureAwait(false); + // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 + // when we selected database generated properties via the OUTPUT clause. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -741,119 +652,6 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, } } - /// - /// Builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - private void BuildEntityKeysTemporaryTable( - SqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - SqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - connection.ExecuteNonQuery( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ); - - using var keysTable = new DataTable(); - - foreach (var property in entityTypeMetadata.KeyProperties) - { - var columnType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - keysTable.Columns.Add(property.PropertyName, columnType); - } - - foreach (var entity in entities) - { - if (entity is null) - { - continue; - } - - var keysRow = keysTable.NewRow(); - - foreach (var keyProperty in entityTypeMetadata.KeyProperties) - { - keysRow[keyProperty.PropertyName] = keyProperty.PropertyGetter!(entity); - } - - keysTable.Rows.Add(keysRow); - } - - using var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction); - sqlBulkCopy.BatchSize = 0; - sqlBulkCopy.DestinationTableName = "#" + keysTableName; - sqlBulkCopy.WriteToServer(keysTable); - } - - /// - /// Asynchronously builds a temporary table containing the keys of the provided entities. - /// - /// The type of entities for which the table is built. - /// The database connection to use to build the table. - /// The name of the table to build. - /// The entities whose keys should be stored in the table. - /// The metadata for the entity type. - /// The database transaction within to perform the operation. - /// A token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. - private async Task BuildEntityKeysTemporaryTableAsync( - SqlConnection connection, - String keysTableName, - List entities, - EntityTypeMetadata entityTypeMetadata, - SqlTransaction? transaction, - CancellationToken cancellationToken - ) - { - await connection.ExecuteNonQueryAsync( - this.CreateEntityKeysTemporaryTableSqlCode(keysTableName, entityTypeMetadata), - transaction, - cancellationToken: cancellationToken - ).ConfigureAwait(false); - - using var keysTable = new DataTable(); - - foreach (var property in entityTypeMetadata.KeyProperties) - { - var columnType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - keysTable.Columns.Add(property.PropertyName, columnType); - } - - foreach (var entity in entities) - { - if (entity is null) - { - continue; - } - - var keysRow = keysTable.NewRow(); - - foreach (var keyProperty in entityTypeMetadata.KeyProperties) - { - keysRow[keyProperty.PropertyName] = keyProperty.PropertyGetter!(entity); - } - - keysTable.Rows.Add(keysRow); - } - - using var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction); - sqlBulkCopy.BatchSize = 0; - sqlBulkCopy.DestinationTableName = "#" + keysTableName; - await sqlBulkCopy.WriteToServerAsync(keysTable, cancellationToken).ConfigureAwait(false); - } - /// /// Creates a command to delete an entity. /// @@ -880,7 +678,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -891,58 +693,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, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ) - ); - - prependSeparator = true; - } - - createKeysTableSqlBuilder.AppendLine(")"); - - return createKeysTableSqlBuilder.ToString(); - } - /// /// Creates a command to insert an entity. /// @@ -1047,7 +797,11 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => var prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { if (prependSeparator) { @@ -1055,9 +809,9 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => } sqlBuilder.Append('['); - sqlBuilder.Append(keyProperty.ColumnName); + sqlBuilder.Append(property.ColumnName); sqlBuilder.Append("] = @"); - sqlBuilder.Append(keyProperty.PropertyName); + sqlBuilder.Append(property.PropertyName); prependSeparator = true; } @@ -1232,7 +986,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { if (prependSeparator) { @@ -1356,5 +1114,4 @@ await reader.ReadAsync(cancellationToken).ConfigureAwait(false) private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new(); private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new(); - private const Int32 BulkDeleteThreshold = 10; } diff --git a/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs b/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs new file mode 100644 index 0000000..10b172e --- /dev/null +++ b/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs @@ -0,0 +1,46 @@ +namespace RentADeveloper.DbConnectionPlus.Exceptions; + +/// +/// An exception that is thrown when a concurrency violation is encountered while deleting or updating an entity in a +/// database. A concurrency violation occurs when an unexpected number of rows are affected by a delete or update +/// operation. This is usually because the data in the database has been modified since the entity has been loaded. +/// +public class DbUpdateConcurrencyException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The entity that was involved in the concurrency violation. + public DbUpdateConcurrencyException(String message, Object entity) : base(message) => + this.Entity = entity; + + /// + /// Initializes a new instance of the class. + /// + public DbUpdateConcurrencyException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public DbUpdateConcurrencyException(String message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The inner exception. + public DbUpdateConcurrencyException(String message, Exception innerException) : base(message, innerException) + { + } + + /// + /// The entity that was involved in the concurrency violation. + /// + public Object? Entity { get; set; } +} diff --git a/src/DbConnectionPlus/ThrowHelper.cs b/src/DbConnectionPlus/ThrowHelper.cs index a25c5ac..c0bf03b 100644 --- a/src/DbConnectionPlus/ThrowHelper.cs +++ b/src/DbConnectionPlus/ThrowHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using System.Diagnostics.CodeAnalysis; +using RentADeveloper.DbConnectionPlus.Exceptions; using RentADeveloper.DbConnectionPlus.Extensions; namespace RentADeveloper.DbConnectionPlus; @@ -41,6 +42,31 @@ public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(ID #pragma warning restore CA1062 ); + /// + /// Throws an indicating that a concurrency violation was encountered + /// while deleting or updating an entity in a database. A concurrency violation occurs when an unexpected number of + /// rows are affected by a delete or update operation. This is usually because the data in the database has been + /// modified since the entity has been loaded. + /// + /// The expected number of affected rows. + /// The actual number of affected rows. + /// The entity that was involved in the operation. + /// Always thrown. + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + public static void ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + Int32 expectedNumberOfAffectedRows, + Int32 actualNumberOfAffectedRows, + Object entity + ) => + throw new DbUpdateConcurrencyException( + $"The database operation was expected to affect {expectedNumberOfAffectedRows} row(s), but actually " + + $"affected {actualNumberOfAffectedRows} row(s). Data in the database may have been modified or deleted " + + $"since entities were loaded. See {nameof(DbUpdateConcurrencyException)}." + + $"{nameof(DbUpdateConcurrencyException.Entity)} for the entity that was involved in the operation.", + entity + ); + /// /// Throws an indicating that the specified entity type has no key property. /// diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 8ba0bea..110c2e8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -1,5 +1,6 @@ using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -67,20 +68,50 @@ await Invoking(() => this.CallApi( } [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_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entitiesToDelete = this.CreateEntitiesInDb(5); + + var failingEntity = entitiesToDelete[^1]; + failingEntity.ConcurrencyToken_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(failingEntity); + + foreach (var entity in entitiesToDelete.Except([failingEntity])) + { + this.ExistsEntityInDb(entity) + .Should().BeFalse(); + } + + this.ExistsEntityInDb(failingEntity) + .Should().BeTrue(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(numberOfEntities); - var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); - var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); + var entitiesToKeep = entities.Skip(5).ToList(); await this.CallApi( useAsyncApi, @@ -104,22 +135,15 @@ await this.CallApi( } [Theory] - [InlineData(false, 10)] - [InlineData(true, 10)] - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. - [InlineData(false, 30)] - [InlineData(true, 30)] - public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping( - Boolean useAsyncApi, - Int32 numberOfEntities - ) + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) { MappingTestEntityFluentApi.Configure(); - var entities = this.CreateEntitiesInDb(numberOfEntities); - var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); - var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); + var entitiesToKeep = entities.Skip(5).ToList(); await this.CallApi( useAsyncApi, @@ -165,20 +189,13 @@ public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsy } [Theory] - [InlineData(false, 10)] - [InlineData(true, 10)] - // Some database adapters (like the SQL Server one) use batch deletion for more than 10 entities, so we need - // to test that as well. - [InlineData(false, 30)] - [InlineData(true, 30)] - public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( - Boolean useAsyncApi, - Int32 numberOfEntities - ) + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) { - var entities = this.CreateEntitiesInDb(numberOfEntities); - var entitiesToDelete = entities.Take(numberOfEntities / 2).ToList(); - var entitiesToKeep = entities.Skip(numberOfEntities / 2).ToList(); + var entities = this.CreateEntitiesInDb(10); + var entitiesToDelete = entities.Take(5).ToList(); + var entitiesToKeep = entities.Skip(5).ToList(); await this.CallApi( useAsyncApi, @@ -201,6 +218,43 @@ await this.CallApi( } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntities_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entitiesToDelete = this.CreateEntitiesInDb(5); + + var failingEntity = entitiesToDelete[^1]; + failingEntity.RowVersion_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(failingEntity); + + foreach (var entity in entitiesToDelete.Except([failingEntity])) + { + this.ExistsEntityInDb(entity) + .Should().BeFalse(); + } + + this.ExistsEntityInDb(failingEntity) + .Should().BeTrue(); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -220,7 +274,7 @@ public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsy (await this.CallApi( useAsyncApi, this.Connection, - entitiesToDelete, + Array.Empty(), null, TestContext.Current.CancellationToken )) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 974d820..1e3945d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -1,5 +1,6 @@ using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -60,6 +61,35 @@ await Invoking(() => this.CallApi( .Should().BeTrue(); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entityToDelete = this.CreateEntityInDb(); + entityToDelete.ConcurrencyToken_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(entityToDelete); + + this.ExistsEntityInDb(entityToDelete) + .Should().BeTrue(); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -156,6 +186,35 @@ await this.CallApi( .Should().BeTrue(); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DeleteEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entityToDelete = this.CreateEntityInDb(); + entityToDelete.RowVersion_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(entityToDelete); + + this.ExistsEntityInDb(entityToDelete) + .Should().BeTrue(); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -171,15 +230,6 @@ public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsync TestContext.Current.CancellationToken )) .Should().Be(1); - - (await this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(0); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index b5369e5..c55df34 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -1,5 +1,6 @@ using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -61,6 +62,58 @@ await Invoking(() => .Should().BeEquivalentTo(entities); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + var updatedEntities = Generate.UpdateFor(entities); + + var failingEntity = updatedEntities[^1]; + failingEntity.ConcurrencyToken_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(failingEntity); + + foreach (var entity in updatedEntities.Except([failingEntity])) + { + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(entity.Key1_)} AND Key2 = {Parameter(entity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(failingEntity.Key1_)} AND Key2 = {Parameter(failingEntity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entities[^1]); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -172,8 +225,9 @@ await this.CallApi( this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") .Should().BeEquivalentTo( updatedEntities, - options => options.Using(context => context.Subject.Should().BeNull()) - .When(info => info.Path.EndsWith("NotMapped")) + options => + options.Using(context => context.Subject.Should().BeNull()) + .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -253,6 +307,58 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntities); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entities = this.CreateEntitiesInDb(); + var updatedEntities = Generate.UpdateFor(entities); + + var failingEntity = updatedEntities[^1]; + failingEntity.RowVersion_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(failingEntity); + + foreach (var entity in updatedEntities.Except([failingEntity])) + { + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(entity.Key1_)} AND Key2 = {Parameter(entity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(failingEntity.Key1_)} AND Key2 = {Parameter(failingEntity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entities[^1]); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -270,12 +376,10 @@ public async Task UpdateEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsy )) .Should().Be(entities.Count); - var nonExistentEntities = Generate.Multiple(); - (await this.CallApi( useAsyncApi, this.Connection, - nonExistentEntities, + Array.Empty(), null, TestContext.Current.CancellationToken )) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 892bd1c..df256f3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -1,5 +1,6 @@ using System.Data.Common; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; @@ -59,6 +60,44 @@ await Invoking(() => .Should().BeEquivalentTo(entity); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + + updatedEntity.ConcurrencyToken_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(updatedEntity); + + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(updatedEntity.Key1_)} AND Key2 = {Parameter(updatedEntity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -231,6 +270,44 @@ await this.CallApi( .Should().BeEquivalentTo(updatedEntity); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UpdateEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + { + var entity = this.CreateEntityInDb(); + var updatedEntity = Generate.UpdateFor(entity); + + updatedEntity.RowVersion_ = Generate.Single(); + + (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken + ) + ) + .Should().ThrowAsync() + .WithMessage( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + )) + .And.Entity.Should().Be(updatedEntity); + + (await this.Connection.QueryFirstAsync( + $""" + SELECT * + FROM {Q("MappingTestEntity")} + WHERE Key1 = {Parameter(updatedEntity.Key1_)} AND Key2 = {Parameter(updatedEntity.Key2_)} + """, + cancellationToken: TestContext.Current.CancellationToken + )) + .Should().BeEquivalentTo(entity); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -247,17 +324,6 @@ public async Task UpdateEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsync TestContext.Current.CancellationToken )) .Should().Be(1); - - var nonExistentEntity = Generate.Single(); - - (await this.CallApi( - useAsyncApi, - this.Connection, - nonExistentEntity, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(0); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index b3113d7..da00c8a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -222,12 +222,14 @@ Value BIGINT NULL CREATE TABLE MappingTestEntity ( + Computed AS ([Value]+(999)), + ConcurrencyToken VARBINARY(max), + [Identity] INT IDENTITY(1,1) NOT NULL, Key1 BIGINT NOT NULL, Key2 BIGINT NOT NULL, - Name NVARCHAR(MAX) NOT NULL, - Computed AS ([Name]+(999)), - Identity INT IDENTITY(1,1) NOT NULL, + Value INT NOT NULL, NotMapped VARCHAR(200) NULL, + RowVersion ROWVERSION, PRIMARY KEY (Key1, Key2) ); GO diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs index 415f115..3dce9a7 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -41,8 +41,8 @@ public void Configure_ShouldConfigureDbConnectionPlus() .IsKey(); config.Entity() - .Property(a => a.Name_) - .HasColumnName("Name"); + .Property(a => a.Value_) + .HasColumnName("Value"); config.Entity() .Property(a => a.NotMapped) @@ -104,8 +104,8 @@ public void Configure_ShouldConfigureDbConnectionPlus() entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].IsKey .Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Name_"].ColumnName - .Should().Be("Name"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Value_"].ColumnName + .Should().Be("Value"); entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMapped"].IsIgnored .Should().BeTrue(); diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 2d4087f..15a003d 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -216,7 +216,7 @@ public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFlue var nameProperty = metadata.AllPropertiesByPropertyName["Name_"]; nameProperty.ColumnName - .Should().Be("Name"); + .Should().Be("Value"); var notMappedProperty = metadata.AllPropertiesByPropertyName["NotMapped"]; diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 3a5a1bf..6cf47fa 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -343,10 +343,10 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() dataReader.GetInt64(ordinal).Returns(entity.Key2_); ordinal++; - dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetName(ordinal).Returns("Value"); dataReader.GetFieldType(ordinal).Returns(typeof(String)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetString(ordinal).Returns(entity.Name_); + dataReader.GetInt32(ordinal).Returns(entity.Value_); ordinal++; var notMappedColumnOrdinal = ordinal; @@ -381,8 +381,8 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() materializedEntity.Key2_ .Should().Be(entity.Key2_); - materializedEntity.Name_ - .Should().Be(entity.Name_); + materializedEntity.Value_ + .Should().Be(entity.Value_); materializedEntity.NotMapped .Should().BeNull(); @@ -433,10 +433,10 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() dataReader.GetInt64(ordinal).Returns(entity.Key2_); ordinal++; - dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetName(ordinal).Returns("Value"); dataReader.GetFieldType(ordinal).Returns(typeof(String)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetString(ordinal).Returns(entity.Name_); + dataReader.GetInt32(ordinal).Returns(entity.Value_); ordinal++; var notMappedColumnOrdinal = ordinal; @@ -471,8 +471,8 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() materializedEntity.Key2_ .Should().Be(entity.Key2_); - materializedEntity.Name_ - .Should().Be(entity.Name_); + materializedEntity.Value_ + .Should().Be(entity.Value_); materializedEntity.NotMapped .Should().BeNull(); @@ -503,10 +503,10 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam dataReader.GetInt64(ordinal).Returns(entity.Key2); ordinal++; - dataReader.GetName(ordinal).Returns("Name"); + dataReader.GetName(ordinal).Returns("Value"); dataReader.GetFieldType(ordinal).Returns(typeof(String)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetString(ordinal).Returns(entity.Name); + dataReader.GetInt32(ordinal).Returns(entity.Value); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); @@ -518,8 +518,8 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam materializedEntity.Key2 .Should().Be(entity.Key2); - materializedEntity.Name - .Should().Be(entity.Name); + materializedEntity.Value + .Should().Be(entity.Value); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index d445102..627d9b0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -3,7 +3,9 @@ #pragma warning disable IDE0053 +using System.Reflection; using AutoFixture; +using AutoFixture.Kernel; using Bogus; using Mapster; using RentADeveloper.DbConnectionPlus.Entities; @@ -23,6 +25,8 @@ static Generate() faker = new(); fixture = new(); + fixture.Customize(new OmitIgnoredPropertiesCustomization()); + fixture.Register(() => faker.Random.Bool()); fixture.Register(() => faker.Random.Byte()); fixture.Register(() => faker.Random.Bytes(SmallNumber())); @@ -245,56 +249,63 @@ public static Int32 SmallNumber() => faker.Random.Int(5, 15); /// - /// Creates a copy of where all properties except the key property / properties have new - /// values. + /// Creates a copy of where all properties except the key and concurrency token + /// properties have new values. /// /// The type of entity to create an updated copy of. /// The entity for which to create an updated copy. /// - /// A copy of where all properties except the key property / properties have new values. + /// A copy of where all properties except key and concurrency token properties have new + /// values. /// public static T UpdateFor(T entity) { var updatedEntity = Single(); - CopyKeys(entity, updatedEntity); + CopyKeysAndConcurrencyTokens(entity, updatedEntity); // For the rare case that all generated values are the same as in the original entity, // regenerate until at least one value is different. while (entity!.Equals(updatedEntity)) { updatedEntity = Single(); - CopyKeys(entity, updatedEntity); + CopyKeysAndConcurrencyTokens(entity, updatedEntity); } return updatedEntity; } /// - /// Creates a list with copies of where all properties except the key property / - /// properties have new values. + /// Creates a list with copies of where all properties except key and concurrency + /// token properties have new values. /// /// The type of entities to create updated copies of. /// The entities for which to create updated copies. /// - /// A list with copies of where all properties except the key property / properties - /// have new values. + /// A list with copies of where all properties except key and concurrency token + /// properties have new values. /// public static List UpdateFor(List entities) => [.. entities.Select(UpdateFor)]; /// - /// Copies the values of all key properties (properties denoted with a ) from + /// Copies the values of all key and concurrency token properties from /// to . /// - /// The type of the entities to copy keys from and to. - /// The source entity to copy keys from. - /// The target entity to copy keys to. - private static void CopyKeys(T sourceEntity, T targetEntity) + /// The type of the entities to copy keys and concurrency tokens from and to. + /// The source entity to copy keys and concurrency tokens from. + /// The target entity to copy keys and concurrency tokens to. + private static void CopyKeysAndConcurrencyTokens(T sourceEntity, T targetEntity) { - foreach (var keyProperty in EntityHelper.GetEntityTypeMetadata(typeof(T)).KeyProperties) + var metadata = EntityHelper.GetEntityTypeMetadata(typeof(T)); + + var propertiesToCopy = + metadata.KeyProperties + .Concat(metadata.ConcurrencyTokenProperties) + .Concat(metadata.RowVersionProperties); + + foreach (var property in propertiesToCopy) { - var keyPropertyValue = keyProperty.PropertyGetter!(sourceEntity); - keyProperty.PropertySetter!(targetEntity, keyPropertyValue); + property.PropertySetter!(targetEntity, property.PropertyGetter!(sourceEntity)); } } @@ -308,4 +319,35 @@ private static void CopyKeys(T sourceEntity, T targetEntity) private static readonly Faker faker; private static readonly Fixture fixture; private static Int64 entityId = 1; + + /// + /// An AutoFixture customization that excludes properties that are ignored in the entity model from being populated + /// with test data. + /// + public class OmitIgnoredPropertiesCustomization : ICustomization + { + public void Customize(IFixture fixture) => + fixture.Customizations.Add(new OmitNotMappedPropertySpecimenBuilder()); + + private class OmitNotMappedPropertySpecimenBuilder : ISpecimenBuilder + { + public Object Create(Object request, ISpecimenContext context) + { + if (request is PropertyInfo propertyInfo) + { + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(propertyInfo.DeclaringType!); + + var propertyMetadata = + entityTypeMetadata.AllPropertiesByPropertyName[propertyInfo.Name]; + + if (propertyMetadata.IsIgnored) + { + return new OmitSpecimen(); + } + } + + return new NoSpecimen(); + } + } + } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs index 5ea22d3..3edb8a0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -8,5 +8,7 @@ public record MappingTestEntity [Key] public Int64 Key2 { get; set; } - public String Name { get; set; } + public Int32 Value { get; set; } + + public Byte[]? ConcurrencyToken { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index d38a562..37f4d79 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -25,8 +25,8 @@ public record MappingTestEntityAttributes [Column("Key2")] public Int64 Key2_ { get; set; } - [Column("Name")] - public String Name_ { get; set; } + [Column("Value")] + public Int32 Value_ { get; set; } [NotMapped] public String? NotMapped { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs index c70f136..c3c98c7 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -9,7 +9,7 @@ public record MappingTestEntityFluentApi public Int32 Identity_ { get; set; } public Int64 Key1_ { get; set; } public Int64 Key2_ { get; set; } - public String Name_ { get; set; } + public Int32 Value_ { get; set; } public String? NotMapped { get; set; } public Byte[]? RowVersion_ { get; set; } @@ -48,8 +48,8 @@ public static void Configure() => .IsKey(); config.Entity() - .Property(a => a.Name_) - .HasColumnName("Name"); + .Property(a => a.Value_) + .HasColumnName("Value"); config.Entity() .Property(a => a.NotMapped) From dd5c5b0e10414cddd9255e56e13f657d4bac5a32 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Tue, 3 Feb 2026 13:26:06 +0100 Subject: [PATCH 03/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- CHANGELOG.md | 4 +- README.md | 2 + docs/DESIGN-DECISIONS.md | 2 + .../DatabaseAdapters/IEntityManipulator.cs | 2 + .../MySql/MySqlEntityManipulator.cs | 108 +++++++- .../Oracle/OracleEntityManipulator.cs | 112 +++++++- .../PostgreSql/PostgreSqlEntityManipulator.cs | 96 ++++++- .../SqlServer/SqlServerEntityManipulator.cs | 18 +- .../Sqlite/SqliteEntityManipulator.cs | 107 +++++++- .../DbConnectionExtensions.DeleteEntities.cs | 10 +- .../DbConnectionExtensions.DeleteEntity.cs | 10 +- .../DbConnectionExtensions.InsertEntities.cs | 12 +- .../DbConnectionExtensions.InsertEntity.cs | 12 +- .../DbConnectionExtensions.UpdateEntities.cs | 18 +- .../DbConnectionExtensions.UpdateEntity.cs | 18 +- .../EntityManipulator.DeleteEntitiesTests.cs | 57 ++-- .../EntityManipulator.DeleteEntityTests.cs | 68 ++--- .../EntityManipulator.UpdateEntitiesTests.cs | 76 +++--- .../EntityManipulator.UpdateEntityTests.cs | 72 ++--- .../TestDatabase/MySqlTestDatabaseProvider.cs | 26 +- .../OracleTestDatabaseProvider.cs | 16 +- .../PostgreSqlTestDatabaseProvider.cs | 23 +- .../SQLiteTestDatabaseProvider.cs | 15 +- .../Converters/ValueConverterTests.cs | 52 ++-- .../Entities/EntityHelperTests.cs | 249 +++++++++--------- .../TestData/Entity.cs | 2 +- .../TestData/MappingTestEntity.cs | 4 +- .../TestData/MappingTestEntityAttributes.cs | 6 +- .../TestData/MappingTestEntityFluentApi.cs | 2 +- 29 files changed, 841 insertions(+), 358 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fea0f5a..4212afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ 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/). - + +TODO: Update for concurrency token handling. + ## [1.1.0] - 2026-02-01 ### Added diff --git a/README.md b/README.md index 5a7df58..ebd060b 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 concurrency token handling (attributes and fluent API). + ## Table of contents - **[Quick start](#quick-start)** - [Examples](#examples) diff --git a/docs/DESIGN-DECISIONS.md b/docs/DESIGN-DECISIONS.md index e678f2c..9dedff6 100644 --- a/docs/DESIGN-DECISIONS.md +++ b/docs/DESIGN-DECISIONS.md @@ -1,5 +1,7 @@ # DbConnectionPlus - Design Decisions Document +TODO: Update version before next release. + **Version:** 1.1.0 **Last Updated:** February 2026 **Author:** David Liebeherr diff --git a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs index 192f76c..ae2c2c8 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs @@ -5,6 +5,8 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters; +// TODO: Update documentation regarding concurrency handling. + /// /// Provides CRUD database operations for entities. /// diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index 787aae4..1ef23d1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -101,7 +101,19 @@ CancellationToken cancellationToken try { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return command.ExecuteNonQuery(); + + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -138,7 +150,19 @@ CancellationToken cancellationToken try { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -404,6 +428,20 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -468,6 +506,20 @@ await UpdateDatabaseGeneratedPropertiesAsync( cancellationToken ).ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -515,6 +567,20 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -564,6 +630,20 @@ CancellationToken cancellationToken await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken) .ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -601,7 +681,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -715,7 +799,11 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => var prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { @@ -920,7 +1008,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { if (prependSeparator) { @@ -975,7 +1067,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .ToList(); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs index a1bf7d1..8c9a5ef 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs @@ -102,7 +102,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return command.ExecuteNonQuery(); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -140,7 +151,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -396,7 +418,18 @@ CancellationToken cancellationToken DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += command.ExecuteNonQuery(); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; var outputParameters = parameters.Where(a => a.Direction == ParameterDirection.Output).ToArray(); @@ -452,9 +485,20 @@ CancellationToken cancellationToken DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + var outputParameters = parameters.Where(a => a.Direction == ParameterDirection.Output).ToArray(); UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); @@ -502,6 +546,15 @@ CancellationToken cancellationToken var numberOfAffectedRows = command.ExecuteNonQuery(); + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + var outputParameters = parameters.Where(a => a.Direction == ParameterDirection.Output).ToArray(); UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); @@ -548,6 +601,15 @@ CancellationToken cancellationToken var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + var outputParameters = parameters.Where(a => a.Direction == ParameterDirection.Output).ToArray(); UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); @@ -589,7 +651,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -639,11 +705,20 @@ EntityTypeMetadata entityTypeMetadata { var parameter = command.CreateParameter(); parameter.ParameterName = "return_" + property.ColumnName; + parameter.DbType = this.databaseAdapter.GetDbType( property.PropertyType, DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + parameter.Direction = ParameterDirection.Output; + + if (property.PropertyType == typeof(Byte[])) + { + // Use max size for byte arrays to actually retrieve the full value: + parameter.Size = 32767; + } + parameters.Add(parameter); command.Parameters.Add(parameter); } @@ -677,7 +752,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.UpdateProperties.Concat(entityTypeMetadata.KeyProperties)) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in entityTypeMetadata.UpdateProperties.Concat(whereProperties)) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -690,11 +769,20 @@ EntityTypeMetadata entityTypeMetadata { var parameter = command.CreateParameter(); parameter.ParameterName = "return_" + property.ColumnName; + parameter.DbType = this.databaseAdapter.GetDbType( property.PropertyType, DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + parameter.Direction = ParameterDirection.Output; + + if (property.PropertyType == typeof(Byte[])) + { + // Use max size for byte arrays to actually retrieve the full value: + parameter.Size = 32767; + } + parameters.Add(parameter); command.Parameters.Add(parameter); } @@ -732,7 +820,11 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => var prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { @@ -912,7 +1004,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { if (prependSeparator) { diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index 8c27b73..e07c873 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -102,7 +102,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return command.ExecuteNonQuery(); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -140,7 +151,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -407,6 +429,19 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the RETURNING clause. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -471,6 +506,19 @@ await UpdateDatabaseGeneratedPropertiesAsync( cancellationToken ).ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the RETURNING clause. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -518,6 +566,19 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the RETURNING clause. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -567,6 +628,19 @@ CancellationToken cancellationToken await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken) .ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the RETURNING clause. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -604,7 +678,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -719,7 +797,11 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => var prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { @@ -880,7 +962,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { if (prependSeparator) { diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index ba955e8..aa7a05e 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -42,6 +42,8 @@ CancellationToken cancellationToken continue; } + // TODO: Reuse DELETE command instead of calling DeleteEntity for each entity. + // TODO: Do this for all entity manipulators. totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); } @@ -429,8 +431,8 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); - // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 - // when we selected database generated properties via the OUTPUT clause. + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the OUTPUT clause. reader.Close(); if (reader.RecordsAffected != 1) @@ -506,8 +508,8 @@ await UpdateDatabaseGeneratedPropertiesAsync( cancellationToken ).ConfigureAwait(false); - // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 - // when we selected database generated properties via the OUTPUT clause. + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the OUTPUT clause. await reader.CloseAsync().ConfigureAwait(false); if (reader.RecordsAffected != 1) @@ -566,8 +568,8 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); - // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 - // when we selected database generated properties via the OUTPUT clause. + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the OUTPUT clause. reader.Close(); if (reader.RecordsAffected != 1) @@ -628,8 +630,8 @@ CancellationToken cancellationToken await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken) .ConfigureAwait(false); - // We must close the reader before we can access RecordsAffected, because otherwise it returns -1 - // when we selected database generated properties via the OUTPUT clause. + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the OUTPUT clause. await reader.CloseAsync().ConfigureAwait(false); if (reader.RecordsAffected != 1) diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 1519eda..55dd7d5 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -98,7 +98,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return command.ExecuteNonQuery(); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -136,7 +147,18 @@ CancellationToken cancellationToken { DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + return numberOfAffectedRows; } catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( exception, @@ -403,6 +425,20 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -467,6 +503,20 @@ await UpdateDatabaseGeneratedPropertiesAsync( cancellationToken ).ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + totalNumberOfAffectedRows += reader.RecordsAffected; } } @@ -514,6 +564,20 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, reader, entity, cancellationToken); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + reader.Close(); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -563,6 +627,20 @@ CancellationToken cancellationToken await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken) .ConfigureAwait(false); + // We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it + // returns -1 when we select database generated properties via the SELECT statement after the + // UPDATE statement. + await reader.CloseAsync().ConfigureAwait(false); + + if (reader.RecordsAffected != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + reader.RecordsAffected, + entity + ); + } + return reader.RecordsAffected; } catch (Exception exception) when ( @@ -600,7 +678,11 @@ EntityTypeMetadata entityTypeMetadata var parameters = new List(); - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var property in whereProperties) { var parameter = command.CreateParameter(); parameter.ParameterName = property.PropertyName; @@ -715,7 +797,11 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => var prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { @@ -921,7 +1007,12 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var property in entityTypeMetadata.KeyProperties) + var whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .Concat(entityTypeMetadata.RowVersionProperties) + .ToList(); + + foreach (var property in whereProperties) { if (prependSeparator) { @@ -978,7 +1069,11 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => prependSeparator = false; - foreach (var keyProperty in entityTypeMetadata.KeyProperties) + whereProperties = entityTypeMetadata.KeyProperties + .Concat(entityTypeMetadata.ConcurrencyTokenProperties) + .ToList(); + + foreach (var keyProperty in whereProperties) { if (prependSeparator) { diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index 751fc3e..70a130d 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -3,6 +3,8 @@ namespace RentADeveloper.DbConnectionPlus; +// TODO: Update documentation regarding concurrency handling. + /// /// Provides extension members for the type . /// @@ -40,12 +42,12 @@ public static partial class DbConnectionExtensions /// /// /// The table from which the entities will be deleted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// @@ -118,12 +120,12 @@ public static Int32 DeleteEntities( /// /// /// The table from which the entities will be deleted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs index 289d038..d644ee6 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs @@ -3,6 +3,8 @@ namespace RentADeveloper.DbConnectionPlus; +// TODO: Update documentation regarding concurrency handling. + /// /// Provides extension members for the type . /// @@ -40,12 +42,12 @@ public static partial class DbConnectionExtensions /// /// /// The table from which the entity will be deleted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// @@ -121,12 +123,12 @@ public static Int32 DeleteEntity( /// /// /// The table from which the entity will be deleted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs index a22f16d..895a29f 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs @@ -39,13 +39,13 @@ public static partial class DbConnectionExtensions /// /// /// The table into which the entities will be inserted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table 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. @@ -57,7 +57,7 @@ public static partial class DbConnectionExtensions /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -133,13 +133,13 @@ public static Int32 InsertEntities( /// /// /// The table into which the entities will be inserted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table 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. @@ -151,7 +151,7 @@ public static Int32 InsertEntities( /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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 b73962f..bb39e85 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs @@ -39,13 +39,13 @@ public static partial class DbConnectionExtensions /// /// /// The table into which the entity will be inserted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table 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. @@ -57,7 +57,7 @@ public static partial class DbConnectionExtensions /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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. /// @@ -133,13 +133,13 @@ public static Int32 InsertEntity( /// /// /// The table into which the entity will be inserted can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table 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. @@ -151,7 +151,7 @@ public static Int32 InsertEntity( /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not inserted. + /// ) 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.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs index f736f63..f95efe5 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs @@ -5,6 +5,8 @@ namespace RentADeveloper.DbConnectionPlus; +// TODO: Update documentation regarding concurrency handling. + /// /// Provides extension members for the type . /// @@ -42,17 +44,17 @@ public static partial class DbConnectionExtensions /// /// /// The table in which the entities will be updated can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -64,7 +66,7 @@ public static partial class DbConnectionExtensions /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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. /// @@ -156,17 +158,17 @@ public static Int32 UpdateEntities( /// /// /// The table in which the entities will be updated can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -178,7 +180,7 @@ public static Int32 UpdateEntities( /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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 8114990..6b2f04f 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs @@ -5,6 +5,8 @@ namespace RentADeveloper.DbConnectionPlus; +// TODO: Update documentation regarding concurrency handling. + /// /// Provides extension members for the type . /// @@ -42,17 +44,17 @@ public static partial class DbConnectionExtensions /// /// /// The table in which the entity will be updated can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -64,7 +66,7 @@ public static partial class DbConnectionExtensions /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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. /// @@ -146,17 +148,17 @@ public static Int32 UpdateEntity( /// /// /// The table in which the entity will be updated can be configured via or - /// . Per default, the singular name of the type is used + /// . Per default, the singular name of the type is used /// as the table name. /// /// /// The type must have at least one instance property configured as key property. - /// Use or to configure key properties. + /// Use or to configure key properties. /// /// /// 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. @@ -168,7 +170,7 @@ public static Int32 UpdateEntity( /// /// /// Properties configured as identity or computed properties (via or - /// ) are also not updated. + /// ) 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/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 110c2e8..0912120 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -77,7 +77,7 @@ public async Task DeleteEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us var failingEntity = entitiesToDelete[^1]; failingEntity.ConcurrencyToken_ = Generate.Single(); - (await Invoking(() => this.CallApi( + var exception = (await Invoking(() => this.CallApi( useAsyncApi, this.Connection, entitiesToDelete, @@ -85,14 +85,19 @@ public async Task DeleteEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(failingEntity); + .Should().ThrowAsync()) + .Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(failingEntity); foreach (var entity in entitiesToDelete.Except([failingEntity])) { @@ -228,22 +233,26 @@ public async Task DeleteEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync var failingEntity = entitiesToDelete[^1]; failingEntity.RowVersion_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entitiesToDelete, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entitiesToDelete, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(failingEntity); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(failingEntity); foreach (var entity in entitiesToDelete.Except([failingEntity])) { diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 1e3945d..7941cfa 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -69,22 +69,26 @@ public async Task DeleteEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useA var entityToDelete = this.CreateEntityInDb(); entityToDelete.ConcurrencyToken_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(entityToDelete); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(entityToDelete); this.ExistsEntityInDb(entityToDelete) .Should().BeTrue(); @@ -194,22 +198,26 @@ public async Task DeleteEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncAp var entityToDelete = this.CreateEntityInDb(); entityToDelete.RowVersion_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - entityToDelete, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + entityToDelete, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(entityToDelete); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(entityToDelete); this.ExistsEntityInDb(entityToDelete) .Should().BeTrue(); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index c55df34..ed4ccbf 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -73,22 +73,26 @@ public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us var failingEntity = updatedEntities[^1]; failingEntity.ConcurrencyToken_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(failingEntity); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(failingEntity); foreach (var entity in updatedEntities.Except([failingEntity])) { @@ -96,7 +100,7 @@ public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(entity.Key1_)} AND Key2 = {Parameter(entity.Key2_)} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND {Q("Key2")} = {Parameter(entity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -107,7 +111,7 @@ public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(failingEntity.Key1_)} AND Key2 = {Parameter(failingEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND {Q("Key2")} = {Parameter(failingEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -318,22 +322,26 @@ public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync var failingEntity = updatedEntities[^1]; failingEntity.RowVersion_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(failingEntity); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(failingEntity); foreach (var entity in updatedEntities.Except([failingEntity])) { @@ -341,7 +349,7 @@ public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(entity.Key1_)} AND Key2 = {Parameter(entity.Key2_)} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND {Q("Key2")} = {Parameter(entity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -352,7 +360,7 @@ public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(failingEntity.Key1_)} AND Key2 = {Parameter(failingEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND {Q("Key2")} = {Parameter(failingEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index df256f3..a8ccde8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -70,28 +70,32 @@ public async Task UpdateEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useA updatedEntity.ConcurrencyToken_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(updatedEntity); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(updatedEntity); (await this.Connection.QueryFirstAsync( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(updatedEntity.Key1_)} AND Key2 = {Parameter(updatedEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND {Q("Key2")} = {Parameter(updatedEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -280,28 +284,32 @@ public async Task UpdateEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncAp updatedEntity.RowVersion_ = Generate.Single(); - (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ) + var exception = (await Invoking(() => this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + null, + TestContext.Current.CancellationToken ) - .Should().ThrowAsync() - .WithMessage( - "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + - "Data in the database may have been modified or deleted since entities were loaded. See " + - $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + - "the entity that was involved in the operation." - )) - .And.Entity.Should().Be(updatedEntity); + ) + .Should().ThrowAsync()).Subject.First(); + + exception.Message + .Should().Be( + "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " + + "Data in the database may have been modified or deleted since entities were loaded. See " + + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " + + "the entity that was involved in the operation." + ); + + exception.Entity + .Should().Be(updatedEntity); (await this.Connection.QueryFirstAsync( $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE Key1 = {Parameter(updatedEntity.Key1_)} AND Key2 = {Parameter(updatedEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND {Q("Key2")} = {Parameter(updatedEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index c31846e..4a8cfbc 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -210,15 +210,33 @@ CREATE TABLE `EntityWithNullableProperty` CREATE TABLE `MappingTestEntity` ( + `Computed` INT AS (`Value`+999), + `ConcurrencyToken` BLOB, + `Identity` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, `Key1` BIGINT NOT NULL, `Key2` BIGINT NOT NULL, - `Name` TEXT NOT NULL, - `Computed` INT AS (`Name`+999), - `Identity` INT AUTO_INCREMENT PRIMARY KEY NOT NULL, - `NotMapped` TEXT NULL + `Value` INT NOT NULL, + `NotMapped` TEXT NULL, + `RowVersion` BLOB ); GO + CREATE TRIGGER Trigger_BeforeInsert_MappingTestEntity + BEFORE INSERT ON MappingTestEntity + FOR EACH ROW + BEGIN + SET NEW.RowVersion = UNHEX(REPLACE(UUID(), '-', '')); + END; + GO + + CREATE TRIGGER Trigger_BeforeUpdate_MappingTestEntity + BEFORE UPDATE ON MappingTestEntity + FOR EACH ROW + BEGIN + SET NEW.RowVersion = UNHEX(REPLACE(UUID(), '-', '')); + END; + 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 1315d89..432aa57 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -198,16 +198,26 @@ CREATE TABLE "EntityWithNullableProperty" CREATE TABLE "MappingTestEntity" ( + "Computed" GENERATED ALWAYS AS (("Value"+999)), + "ConcurrencyToken" RAW(2000), + "Identity" NUMBER(10) GENERATED ALWAYS AS IDENTITY(START with 1 INCREMENT by 1), "Key1" NUMBER(19) NOT NULL, "Key2" NUMBER(19) NOT NULL, - "Name" NVARCHAR2(2000) NOT NULL, - "Computed" GENERATED ALWAYS AS (("Name"+999)), - "Identity" NUMBER(10) GENERATED ALWAYS AS IDENTITY(START with 1 INCREMENT by 1), + "Value" NUMBER(10) NOT NULL, "NotMapped" CLOB NULL, + "RowVersion" RAW(16), PRIMARY KEY ("Key1", "Key2") ); GO + CREATE OR REPLACE TRIGGER "TriggerMappingTestEntity" + BEFORE INSERT OR UPDATE ON "MappingTestEntity" + FOR EACH ROW + BEGIN + :NEW."RowVersion" := SYS_GUID(); + END; + 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 f487568..9f70f7b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -138,6 +138,8 @@ public void ResetDatabase() private const String CreateDatabaseObjectsSql = """ + CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Needed for gen_random_bytes() + CREATE TABLE "Entity" ( "Id" bigint NOT NULL PRIMARY KEY, @@ -185,15 +187,30 @@ CREATE TABLE "EntityWithNullableProperty" CREATE TABLE "MappingTestEntity" ( + "Computed" integer GENERATED ALWAYS AS ("Value"+(999)), + "ConcurrencyToken" bytea, + "Identity" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "Key1" bigint NOT NULL, "Key2" bigint NOT NULL, - "Name" text NOT NULL, - "Computed" integer GENERATED ALWAYS AS ("Name"+(999)), - "Identity" integer GENERATED ALWAYS AS IDENTITY NOT NULL, + "Value" integer NOT NULL, "NotMapped" text NULL, + "RowVersion" bytea DEFAULT gen_random_bytes(8), PRIMARY KEY ("Key1", "Key2") ); + CREATE OR REPLACE FUNCTION "UpdateMappingTestEntityRowVersion"() + RETURNS TRIGGER AS $$ + BEGIN + NEW."RowVersion" = gen_random_bytes(8); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER "TriggerMappingTestEntityRowVersion" + BEFORE UPDATE ON "MappingTestEntity" + FOR EACH ROW + EXECUTE FUNCTION "UpdateMappingTestEntityRowVersion"(); + CREATE PROCEDURE "GetEntities" () LANGUAGE SQL AS $$ diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index b3c9602..ef616d6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -178,12 +178,21 @@ Value INTEGER NULL CREATE TABLE MappingTestEntity ( + Computed INTEGER GENERATED ALWAYS AS (Value+999) VIRTUAL, + ConcurrencyToken BLOB, + Identity INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Key1 INTEGER NOT NULL, Key2 INTEGER NOT NULL, - Name TEXT NOT NULL, - Computed INTEGER GENERATED ALWAYS AS (Name+999) VIRTUAL, - Identity INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + Value INTEGER NOT NULL, + RowVersion BLOB DEFAULT (randomblob(8)), NotMapped TEXT NULL ); + + CREATE TRIGGER TriggerMappingTestEntity + BEFORE UPDATE ON MappingTestEntity + FOR EACH ROW + BEGIN + UPDATE MappingTestEntity SET RowVersion = randomblob(8) WHERE Key1 = OLD.Key1 AND Key2 = OLD.Key2; + END; """; } diff --git a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs index b6991ff..da6f8de 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs @@ -604,7 +604,7 @@ Object ExpectedTargetValue (typeof(Boolean), typeof(Decimal), true, true, (Decimal)1), (typeof(Boolean), typeof(Double), true, true, (Double)1), (typeof(Boolean), typeof(Int16), true, true, (Int16)1), - (typeof(Boolean), typeof(Int32), true, true, (Int32)1), + (typeof(Boolean), typeof(Int32), true, true, 1), (typeof(Boolean), typeof(Int64), true, true, (Int64)1), (typeof(Boolean), typeof(Object), true, true, true), (typeof(Boolean), typeof(SByte), true, true, (SByte)1), @@ -621,7 +621,7 @@ Object ExpectedTargetValue (typeof(Byte), typeof(Int16), true, byteValue, (Int16)byteValue), (typeof(Byte), typeof(Int32), true, byteValue, (Int32)byteValue), (typeof(Byte), typeof(Int64), true, byteValue, (Int64)byteValue), - (typeof(Byte), typeof(Object), true, byteValue, (Object)byteValue), + (typeof(Byte), typeof(Object), true, byteValue, byteValue), (typeof(Byte), typeof(SByte), true, byteValue, (SByte)byteValue), (typeof(Byte), typeof(Single), true, byteValue, (Single)byteValue), (typeof(Byte), typeof(String), true, byteValue, byteValue.ToString(CultureInfo.InvariantCulture)), @@ -635,21 +635,21 @@ Object ExpectedTargetValue (typeof(Char), typeof(Int16), true, charValue, (Int16)charValue), (typeof(Char), typeof(Int32), true, charValue, (Int32)charValue), (typeof(Char), typeof(Int64), true, charValue, (Int64)charValue), - (typeof(Char), typeof(Object), true, charValue, (Object)charValue), + (typeof(Char), typeof(Object), true, charValue, charValue), (typeof(Char), typeof(SByte), true, charValue, (SByte)charValue), (typeof(Char), typeof(String), true, charValue, charValue.ToString(CultureInfo.InvariantCulture)), (typeof(Char), typeof(UInt16), true, charValue, (UInt16)charValue), (typeof(Char), typeof(UInt32), true, charValue, (UInt32)charValue), (typeof(Char), typeof(UInt64), true, charValue, (UInt64)charValue), (typeof(DateOnly), typeof(DateOnly), true, dateOnlyValue, dateOnlyValue), - (typeof(DateOnly), typeof(Object), true, dateOnlyValue, (Object)dateOnlyValue), + (typeof(DateOnly), typeof(Object), true, dateOnlyValue, dateOnlyValue), (typeof(DateOnly), typeof(String), true, dateOnlyValue, dateOnlyValue.ToString("O", CultureInfo.InvariantCulture)), (typeof(DateTime), typeof(DateOnly), true, dateOnlyValue.ToDateTime(TimeOnly.MinValue), dateOnlyValue), (typeof(DateTime), typeof(DateTime), true, dateTimeValue, dateTimeValue), - (typeof(DateTime), typeof(Object), true, dateTimeValue, (Object)dateTimeValue), + (typeof(DateTime), typeof(Object), true, dateTimeValue, dateTimeValue), (typeof(DateTime), typeof(String), true, dateTimeValue, dateTimeValue.ToString("O", CultureInfo.InvariantCulture)), (typeof(DateTimeOffset), typeof(DateTimeOffset), true, dateTimeOffsetValue, dateTimeOffsetValue), - (typeof(DateTimeOffset), typeof(Object), true, dateTimeOffsetValue, (Object)dateTimeOffsetValue), + (typeof(DateTimeOffset), typeof(Object), true, dateTimeOffsetValue, dateTimeOffsetValue), (typeof(DateTimeOffset), typeof(String), true, dateTimeOffsetValue, dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture)), (typeof(Decimal), typeof(Boolean), true, 1M, true), (typeof(Decimal), typeof(Byte), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Byte), CultureInfo.InvariantCulture)), @@ -658,7 +658,7 @@ Object ExpectedTargetValue (typeof(Decimal), typeof(Int16), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int16), CultureInfo.InvariantCulture)), (typeof(Decimal), typeof(Int32), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int32), CultureInfo.InvariantCulture)), (typeof(Decimal), typeof(Int64), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Object), true, decimalValue, (Object)decimalValue), + (typeof(Decimal), typeof(Object), true, decimalValue, decimalValue), (typeof(Decimal), typeof(SByte), true, decimalValue, Convert.ChangeType(decimalValue, typeof(SByte), CultureInfo.InvariantCulture)), (typeof(Decimal), typeof(Single), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Single), CultureInfo.InvariantCulture)), (typeof(Decimal), typeof(String), true, decimalValue, decimalValue.ToString(CultureInfo.InvariantCulture)), @@ -673,7 +673,7 @@ Object ExpectedTargetValue (typeof(Double), typeof(Int16), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int16), CultureInfo.InvariantCulture)), (typeof(Double), typeof(Int32), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int32), CultureInfo.InvariantCulture)), (typeof(Double), typeof(Int64), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Object), true, doubleValue, (Object)doubleValue), + (typeof(Double), typeof(Object), true, doubleValue, doubleValue), (typeof(Double), typeof(SByte), true, doubleValue, Convert.ChangeType(doubleValue, typeof(SByte), CultureInfo.InvariantCulture)), (typeof(Double), typeof(Single), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Single), CultureInfo.InvariantCulture)), (typeof(Double), typeof(String), true, doubleValue, doubleValue.ToString(CultureInfo.InvariantCulture)), @@ -683,7 +683,7 @@ Object ExpectedTargetValue (typeof(Double), typeof(UInt64), true, doubleValue, Convert.ChangeType(doubleValue, typeof(UInt64), CultureInfo.InvariantCulture)), (typeof(Guid), typeof(Byte[]), true, guidValue, guidValue.ToByteArray()), (typeof(Guid), typeof(Guid), true, guidValue, guidValue), - (typeof(Guid), typeof(Object), true, guidValue, (Object)guidValue), + (typeof(Guid), typeof(Object), true, guidValue, guidValue), (typeof(Guid), typeof(String), true, guidValue, guidValue.ToString("D")), (typeof(Int16), typeof(Boolean), true, (Int16)1, true), (typeof(Int16), typeof(Byte), true, int16Value, (Byte) int16Value), @@ -693,7 +693,7 @@ Object ExpectedTargetValue (typeof(Int16), typeof(Int16), true, int16Value, int16Value), (typeof(Int16), typeof(Int32), true, int16Value, (Int32)int16Value), (typeof(Int16), typeof(Int64), true, int16Value, (Int64)int16Value), - (typeof(Int16), typeof(Object), true, int16Value, (Object)int16Value), + (typeof(Int16), typeof(Object), true, int16Value, int16Value), (typeof(Int16), typeof(SByte), true, int16Value, (SByte)int16Value), (typeof(Int16), typeof(Single), true, int16Value, (Single)int16Value), (typeof(Int16), typeof(String), true, int16Value, int16Value.ToString(CultureInfo.InvariantCulture)), @@ -701,7 +701,7 @@ Object ExpectedTargetValue (typeof(Int16), typeof(UInt16), true, int16Value, (UInt16)int16Value), (typeof(Int16), typeof(UInt32), true, int16Value, (UInt32)int16Value), (typeof(Int16), typeof(UInt64), true, int16Value, (UInt64)int16Value), - (typeof(Int32), typeof(Boolean), true, (Int32)1, true), + (typeof(Int32), typeof(Boolean), true, 1, true), (typeof(Int32), typeof(Byte), true, int32Value, (Byte)int32Value), (typeof(Int32), typeof(Char), true, int32Value, (Char) int32Value), (typeof(Int32), typeof(Decimal), true, int32Value, (Decimal)int32Value), @@ -709,7 +709,7 @@ Object ExpectedTargetValue (typeof(Int32), typeof(Int16), true, int32Value, (Int16)int32Value), (typeof(Int32), typeof(Int32), true, int32Value, int32Value), (typeof(Int32), typeof(Int64), true, int32Value, (Int64)int32Value), - (typeof(Int32), typeof(Object), true, int32Value, (Object)int32Value), + (typeof(Int32), typeof(Object), true, int32Value, int32Value), (typeof(Int32), typeof(SByte), true, int32Value, (SByte)int32Value), (typeof(Int32), typeof(Single), true, int32Value, (Single)int32Value), (typeof(Int32), typeof(String), true, int32Value, int32Value.ToString(CultureInfo.InvariantCulture)), @@ -725,7 +725,7 @@ Object ExpectedTargetValue (typeof(Int64), typeof(Int16), true, int64Value, (Int16)int64Value), (typeof(Int64), typeof(Int32), true, int64Value, (Int32)int64Value), (typeof(Int64), typeof(Int64), true, int64Value, int64Value), - (typeof(Int64), typeof(Object), true, int64Value, (Object)int64Value), + (typeof(Int64), typeof(Object), true, int64Value, int64Value), (typeof(Int64), typeof(SByte), true, int64Value, (SByte)int64Value), (typeof(Int64), typeof(Single), true, int64Value, (Single)int64Value), (typeof(Int64), typeof(String), true, int64Value, int64Value.ToString(CultureInfo.InvariantCulture)), @@ -734,7 +734,7 @@ Object ExpectedTargetValue (typeof(Int64), typeof(UInt32), true, int64Value, (UInt32)int64Value), (typeof(Int64), typeof(UInt64), true, int64Value, (UInt64)int64Value), (typeof(IntPtr), typeof(IntPtr), true, intPtrValue, intPtrValue), - (typeof(IntPtr), typeof(Object), true, intPtrValue, (Object)intPtrValue), + (typeof(IntPtr), typeof(Object), true, intPtrValue, intPtrValue), (typeof(SByte), typeof(Boolean), true, (SByte)1, true), (typeof(SByte), typeof(Byte), true, sbyteValue, (Byte)sbyteValue), (typeof(SByte), typeof(Char), true, sbyteValue, (Char) sbyteValue), @@ -743,7 +743,7 @@ Object ExpectedTargetValue (typeof(SByte), typeof(Int16), true, sbyteValue, (Int16)sbyteValue), (typeof(SByte), typeof(Int32), true, sbyteValue, (Int32)sbyteValue), (typeof(SByte), typeof(Int64), true, sbyteValue, (Int64)sbyteValue), - (typeof(SByte), typeof(Object), true, sbyteValue, (Object)sbyteValue), + (typeof(SByte), typeof(Object), true, sbyteValue, sbyteValue), (typeof(SByte), typeof(SByte), true, sbyteValue, sbyteValue), (typeof(SByte), typeof(Single), true, sbyteValue, (Single)sbyteValue), (typeof(SByte), typeof(String), true, sbyteValue, sbyteValue.ToString(CultureInfo.InvariantCulture)), @@ -758,7 +758,7 @@ Object ExpectedTargetValue (typeof(Single), typeof(Int16), true, singleValue, Convert.ChangeType(singleValue, typeof(Int16), CultureInfo.InvariantCulture)), (typeof(Single), typeof(Int32), true, singleValue, Convert.ChangeType(singleValue, typeof(Int32), CultureInfo.InvariantCulture)), (typeof(Single), typeof(Int64), true, singleValue, Convert.ChangeType(singleValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Object), true, singleValue, (Object)singleValue), + (typeof(Single), typeof(Object), true, singleValue, singleValue), (typeof(Single), typeof(SByte), true, singleValue, Convert.ChangeType(singleValue, typeof(SByte), CultureInfo.InvariantCulture)), (typeof(Single), typeof(Single), true, singleValue, Convert.ChangeType(singleValue, typeof(Single), CultureInfo.InvariantCulture)), (typeof(Single), typeof(String), true, singleValue, singleValue.ToString(CultureInfo.InvariantCulture)), @@ -778,7 +778,7 @@ Object ExpectedTargetValue (typeof(String), typeof(Int16), true, int16Value.ToString(CultureInfo.InvariantCulture), int16Value), (typeof(String), typeof(Int32), true, int32Value.ToString(CultureInfo.InvariantCulture), int32Value), (typeof(String), typeof(Int64), true, int64Value.ToString(CultureInfo.InvariantCulture), int64Value), - (typeof(String), typeof(Object), true, stringValue, (Object)stringValue), + (typeof(String), typeof(Object), true, stringValue, stringValue), (typeof(String), typeof(SByte), true, sbyteValue.ToString(CultureInfo.InvariantCulture), sbyteValue), (typeof(String), typeof(Single), true, singleValue.ToString(CultureInfo.InvariantCulture), singleValue), (typeof(String), typeof(String), true, stringValue, stringValue), @@ -801,10 +801,10 @@ Object ExpectedTargetValue (typeof(TestEnum), typeof(UInt16), true, enumValue, (UInt16)enumValue), (typeof(TestEnum), typeof(UInt32), true, enumValue, (UInt32)enumValue), (typeof(TestEnum), typeof(UInt64), true, enumValue, (UInt64)enumValue), - (typeof(TimeOnly), typeof(Object), true, timeOnlyValue, (Object)timeOnlyValue), + (typeof(TimeOnly), typeof(Object), true, timeOnlyValue, timeOnlyValue), (typeof(TimeOnly), typeof(String), true, timeOnlyValue, timeOnlyValue.ToString("O", CultureInfo.InvariantCulture)), (typeof(TimeOnly), typeof(TimeOnly), true, timeOnlyValue, timeOnlyValue), - (typeof(TimeSpan), typeof(Object), true, timeSpanValue, (Object)timeSpanValue), + (typeof(TimeSpan), typeof(Object), true, timeSpanValue, timeSpanValue), (typeof(TimeSpan), typeof(String), true, timeSpanValue, timeSpanValue.ToString("g", CultureInfo.InvariantCulture)), (typeof(TimeSpan), typeof(TimeOnly), true, timeSpanValue, TimeOnly.FromTimeSpan(timeSpanValue)), (typeof(TimeSpan), typeof(TimeSpan), true, timeSpanValue, timeSpanValue), @@ -816,12 +816,12 @@ Object ExpectedTargetValue (typeof(UInt16), typeof(Int16), true, uint16Value, (Int16)uint16Value), (typeof(UInt16), typeof(Int32), true, uint16Value, (Int32)uint16Value), (typeof(UInt16), typeof(Int64), true, uint16Value, (Int64)uint16Value), - (typeof(UInt16), typeof(Object), true, uint16Value, (Object)uint16Value), + (typeof(UInt16), typeof(Object), true, uint16Value, uint16Value), (typeof(UInt16), typeof(SByte), true, uint16Value, (SByte)uint16Value), (typeof(UInt16), typeof(Single), true, uint16Value, (Single)uint16Value), (typeof(UInt16), typeof(String), true, uint16Value, uint16Value.ToString(CultureInfo.InvariantCulture)), (typeof(UInt16), typeof(TestEnum), true, (UInt16)enumValue, enumValue), - (typeof(UInt16), typeof(UInt16), true, uint16Value, (UInt16)uint16Value), + (typeof(UInt16), typeof(UInt16), true, uint16Value, uint16Value), (typeof(UInt16), typeof(UInt32), true, uint16Value, (UInt32)uint16Value), (typeof(UInt16), typeof(UInt64), true, uint16Value, (UInt64)uint16Value), (typeof(UInt32), typeof(Boolean), true, (UInt32)1, true), @@ -832,13 +832,13 @@ Object ExpectedTargetValue (typeof(UInt32), typeof(Int32), true, uint32Value, (Int32)uint32Value), (typeof(UInt32), typeof(Int32), true, uint32Value, (Int32)uint32Value), (typeof(UInt32), typeof(Int64), true, uint32Value, (Int64)uint32Value), - (typeof(UInt32), typeof(Object), true, uint32Value, (Object)uint32Value), + (typeof(UInt32), typeof(Object), true, uint32Value, uint32Value), (typeof(UInt32), typeof(SByte), true, uint32Value, (SByte)uint32Value), (typeof(UInt32), typeof(Single), true, uint32Value, (Single)uint32Value), (typeof(UInt32), typeof(String), true, uint32Value, uint32Value.ToString(CultureInfo.InvariantCulture)), (typeof(UInt32), typeof(TestEnum), true, (UInt32)enumValue, enumValue), (typeof(UInt32), typeof(UInt16), true, uint32Value, (UInt16)uint32Value), - (typeof(UInt32), typeof(UInt32), true, uint32Value, (UInt32)uint32Value), + (typeof(UInt32), typeof(UInt32), true, uint32Value, uint32Value), (typeof(UInt32), typeof(UInt64), true, uint32Value, (UInt64)uint32Value), (typeof(UInt64), typeof(Boolean), true, (UInt64)1, true), (typeof(UInt64), typeof(Byte), true, uint64Value, (Byte) uint64Value), @@ -848,15 +848,15 @@ Object ExpectedTargetValue (typeof(UInt64), typeof(Int16), true, uint64Value, (Int16)uint64Value), (typeof(UInt64), typeof(Int32), true, uint64Value, (Int32)uint64Value), (typeof(UInt64), typeof(Int64), true, uint64Value, (Int64)uint64Value), - (typeof(UInt64), typeof(Object), true, uint64Value, (Object)uint64Value), + (typeof(UInt64), typeof(Object), true, uint64Value, uint64Value), (typeof(UInt64), typeof(SByte), true, uint64Value, (SByte)uint64Value), (typeof(UInt64), typeof(Single), true, uint64Value, (Single)uint64Value), (typeof(UInt64), typeof(String), true, uint64Value, uint64Value.ToString(CultureInfo.InvariantCulture)), (typeof(UInt64), typeof(TestEnum), true, (UInt64)enumValue, enumValue), (typeof(UInt64), typeof(UInt16), true, uint64Value, (UInt16)uint64Value), (typeof(UInt64), typeof(UInt32), true, uint64Value, (UInt32)uint64Value), - (typeof(UInt64), typeof(UInt64), true, uint64Value, (UInt64)uint64Value), - (typeof(UIntPtr), typeof(Object), true, uintPtrValue, (Object)uintPtrValue), + (typeof(UInt64), typeof(UInt64), true, uint64Value, uint64Value), + (typeof(UIntPtr), typeof(Object), true, uintPtrValue, uintPtrValue), (typeof(UIntPtr), typeof(UIntPtr), true, uintPtrValue, uintPtrValue), (typeof(Char), typeof(Guid), false, charValue, null), (typeof(Int32), typeof(Guid), false, int32Value, null), diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 15a003d..eb45f91 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -151,125 +151,6 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe ); } - [Fact] - public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFluentApiMapping() - { - MappingTestEntityFluentApi.Configure(); - - var metadata = EntityHelper.GetEntityTypeMetadata(typeof(MappingTestEntityFluentApi)); - - metadata - .Should().NotBeNull(); - - metadata.EntityType - .Should().Be(typeof(MappingTestEntityFluentApi)); - - metadata.TableName - .Should().Be("MappingTestEntity"); - - metadata.AllProperties - .Should().HaveCount(8); - - metadata.AllPropertiesByPropertyName - .Should().BeEquivalentTo(metadata.AllProperties.ToDictionary(a => a.PropertyName)); - - var computedProperty = metadata.AllPropertiesByPropertyName["Computed_"]; - - computedProperty.ColumnName - .Should().Be("Computed"); - - computedProperty.IsComputed - .Should().BeTrue(); - - var concurrencyTokenProperty = metadata.AllPropertiesByPropertyName["ConcurrencyToken_"]; - - concurrencyTokenProperty.ColumnName - .Should().Be("ConcurrencyToken"); - - concurrencyTokenProperty.IsConcurrencyToken - .Should().BeTrue(); - - var identityProperty = metadata.AllPropertiesByPropertyName["Identity_"]; - - identityProperty.ColumnName - .Should().Be("Identity"); - - identityProperty.IsIdentity - .Should().BeTrue(); - - var key1Property = metadata.AllPropertiesByPropertyName["Key1_"]; - - key1Property.ColumnName - .Should().Be("Key1"); - - key1Property.IsKey - .Should().BeTrue(); - - var key2Property = metadata.AllPropertiesByPropertyName["Key2_"]; - - key2Property.ColumnName - .Should().Be("Key2"); - - key2Property.IsKey - .Should().BeTrue(); - - var nameProperty = metadata.AllPropertiesByPropertyName["Name_"]; - - nameProperty.ColumnName - .Should().Be("Value"); - - var notMappedProperty = metadata.AllPropertiesByPropertyName["NotMapped"]; - - notMappedProperty.IsIgnored - .Should().BeTrue(); - - var rowVersionProperty = metadata.AllPropertiesByPropertyName["RowVersion_"]; - - rowVersionProperty.IsRowVersion - .Should().BeTrue(); - - metadata.ComputedProperties - .Should().BeEquivalentTo([computedProperty]); - - metadata.ConcurrencyTokenProperties - .Should().BeEquivalentTo([concurrencyTokenProperty]); - - metadata.DatabaseGeneratedProperties - .Should().BeEquivalentTo([computedProperty, identityProperty, rowVersionProperty]); - - metadata.IdentityProperty - .Should().Be(identityProperty); - - metadata.InsertProperties - .Should().BeEquivalentTo([concurrencyTokenProperty, key1Property, key2Property, nameProperty]); - - metadata.KeyProperties - .Should().BeEquivalentTo([key1Property, key2Property]); - - metadata.MappedProperties - .Should().BeEquivalentTo( - [computedProperty, concurrencyTokenProperty, identityProperty, key1Property, key2Property, nameProperty, rowVersionProperty] - ); - - metadata.RowVersionProperties - .Should().BeEquivalentTo( - [rowVersionProperty] - ); - - metadata.UpdateProperties - .Should().BeEquivalentTo([nameProperty]); - } - - [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_Mapping_Attributes_ShouldGetMetadataBasedOnAttributes() { @@ -302,7 +183,7 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt allPropertiesMetadata .Should().HaveSameCount(entityProperties); - + metadata.AllPropertiesByPropertyName .Should().BeEquivalentTo(allPropertiesMetadata.ToDictionary(a => a.PropertyName)); @@ -316,7 +197,9 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt metadata.DatabaseGeneratedProperties .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity || a.IsRowVersion))); + .BeEquivalentTo( + allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity || a.IsRowVersion)) + ); metadata.IdentityProperty .Should() @@ -325,7 +208,7 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt metadata.InsertProperties .Should().BeEquivalentTo( allPropertiesMetadata.Where(a => a is - { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion:false } + { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false } ) ); @@ -440,6 +323,128 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt } } + [Fact] + public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFluentApiMapping() + { + MappingTestEntityFluentApi.Configure(); + + var metadata = EntityHelper.GetEntityTypeMetadata(typeof(MappingTestEntityFluentApi)); + + metadata + .Should().NotBeNull(); + + metadata.EntityType + .Should().Be(typeof(MappingTestEntityFluentApi)); + + metadata.TableName + .Should().Be("MappingTestEntity"); + + metadata.AllProperties + .Should().HaveCount(8); + + metadata.AllPropertiesByPropertyName + .Should().BeEquivalentTo(metadata.AllProperties.ToDictionary(a => a.PropertyName)); + + var computedProperty = metadata.AllPropertiesByPropertyName["Computed_"]; + + computedProperty.ColumnName + .Should().Be("Computed"); + + computedProperty.IsComputed + .Should().BeTrue(); + + var concurrencyTokenProperty = metadata.AllPropertiesByPropertyName["ConcurrencyToken_"]; + + concurrencyTokenProperty.ColumnName + .Should().Be("ConcurrencyToken"); + + concurrencyTokenProperty.IsConcurrencyToken + .Should().BeTrue(); + + var identityProperty = metadata.AllPropertiesByPropertyName["Identity_"]; + + identityProperty.ColumnName + .Should().Be("Identity"); + + identityProperty.IsIdentity + .Should().BeTrue(); + + var key1Property = metadata.AllPropertiesByPropertyName["Key1_"]; + + key1Property.ColumnName + .Should().Be("Key1"); + + key1Property.IsKey + .Should().BeTrue(); + + var key2Property = metadata.AllPropertiesByPropertyName["Key2_"]; + + key2Property.ColumnName + .Should().Be("Key2"); + + key2Property.IsKey + .Should().BeTrue(); + + var nameProperty = metadata.AllPropertiesByPropertyName["Name_"]; + + nameProperty.ColumnName + .Should().Be("Value"); + + var notMappedProperty = metadata.AllPropertiesByPropertyName["NotMapped"]; + + notMappedProperty.IsIgnored + .Should().BeTrue(); + + var rowVersionProperty = metadata.AllPropertiesByPropertyName["RowVersion_"]; + + rowVersionProperty.IsRowVersion + .Should().BeTrue(); + + metadata.ComputedProperties + .Should().BeEquivalentTo([computedProperty]); + + metadata.ConcurrencyTokenProperties + .Should().BeEquivalentTo([concurrencyTokenProperty]); + + metadata.DatabaseGeneratedProperties + .Should().BeEquivalentTo([computedProperty, identityProperty, rowVersionProperty]); + + metadata.IdentityProperty + .Should().Be(identityProperty); + + metadata.InsertProperties + .Should().BeEquivalentTo([concurrencyTokenProperty, key1Property, key2Property, nameProperty]); + + metadata.KeyProperties + .Should().BeEquivalentTo([key1Property, key2Property]); + + metadata.MappedProperties + .Should().BeEquivalentTo( + [ + computedProperty, concurrencyTokenProperty, identityProperty, key1Property, key2Property, + nameProperty, rowVersionProperty + ] + ); + + metadata.RowVersionProperties + .Should().BeEquivalentTo( + [rowVersionProperty] + ); + + metadata.UpdateProperties + .Should().BeEquivalentTo([nameProperty]); + } + + [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 ShouldGuardAgainstNullArguments() { 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/MappingTestEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs index 3edb8a0..85d4bc7 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -2,6 +2,8 @@ public record MappingTestEntity { + public Byte[]? ConcurrencyToken { get; set; } + [Key] public Int64 Key1 { get; set; } @@ -9,6 +11,4 @@ public record MappingTestEntity public Int64 Key2 { get; set; } public Int32 Value { get; set; } - - public Byte[]? ConcurrencyToken { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index 37f4d79..8f4279a 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -25,13 +25,13 @@ public record MappingTestEntityAttributes [Column("Key2")] public Int64 Key2_ { get; set; } - [Column("Value")] - public Int32 Value_ { get; set; } - [NotMapped] public String? NotMapped { get; set; } [Column("RowVersion")] [Timestamp] public Byte[]? RowVersion_ { get; set; } + + [Column("Value")] + public Int32 Value_ { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs index c3c98c7..a887844 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -9,9 +9,9 @@ public record MappingTestEntityFluentApi public Int32 Identity_ { get; set; } public Int64 Key1_ { get; set; } public Int64 Key2_ { get; set; } - public Int32 Value_ { get; set; } public String? NotMapped { get; set; } public Byte[]? RowVersion_ { get; set; } + public Int32 Value_ { get; set; } /// /// Configures the mapping for this entity using the Fluent API. From 5c0d55bb554dee08a30068e0f8833aa551b0a8f5 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Tue, 3 Feb 2026 13:50:11 +0100 Subject: [PATCH 04/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../MySql/MySqlEntityManipulator.cs | 92 +++++++++++++++--- .../Oracle/OracleEntityManipulator.cs | 92 +++++++++++++++--- .../PostgreSql/PostgreSqlEntityManipulator.cs | 92 +++++++++++++++--- .../SqlServer/SqlServerEntityManipulator.cs | 94 ++++++++++++++++--- .../Sqlite/SqliteEntityManipulator.cs | 92 +++++++++++++++--- .../Entities/EntityPropertyMetadata.cs | 4 +- .../EntityManipulator.UpdateEntitiesTests.cs | 12 ++- .../EntityManipulator.UpdateEntityTests.cs | 6 +- 8 files changed, 413 insertions(+), 71 deletions(-) diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index 1ef23d1..5f4f772 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -33,16 +33,51 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); + var totalNumberOfAffectedRows = 0; - foreach (var entity in entities) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; @@ -59,19 +94,52 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs index 8c9a5ef..98bdaea 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs @@ -31,18 +31,51 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); + + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; @@ -59,19 +92,52 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); + + var numberOfAffectedRows = + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index e07c873..f639ed4 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -31,18 +31,51 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; @@ -59,19 +92,52 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index aa7a05e..74e7650 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -31,20 +31,51 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - // TODO: Reuse DELETE command instead of calling DeleteEntity for each entity. - // TODO: Do this for all entity manipulators. - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; @@ -61,19 +92,52 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); - var entitiesList = entities.ToList(); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var totalNumberOfAffectedRows = 0; - foreach (var entity in entitiesList) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 55dd7d5..4e7f4a8 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -31,16 +31,51 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); + var totalNumberOfAffectedRows = 0; - foreach (var entity in entities) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += this.DeleteEntity(connection, entity, transaction, cancellationToken); + var numberOfAffectedRows = command.ExecuteNonQuery(); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; @@ -57,17 +92,52 @@ CancellationToken cancellationToken ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entities); + var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity)); + + var (command, parameters) = this.CreateDeleteEntityCommand(connection, transaction, entityTypeMetadata); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); + var totalNumberOfAffectedRows = 0; - foreach (var entity in entities) + using (command) + using (cancellationTokenRegistration) { - if (entity is null) + try { - continue; - } + foreach (var entity in entities) + { + if (entity is null) + { + continue; + } + + this.PopulateParametersFromEntityProperties(entityTypeMetadata, parameters, entity); + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += await this - .DeleteEntityAsync(connection, entity, transaction, cancellationToken).ConfigureAwait(false); + var numberOfAffectedRows = + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + if (numberOfAffectedRows != 1) + { + ThrowHelper.ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException( + 1, + numberOfAffectedRows, + entity + ); + } + + totalNumberOfAffectedRows += numberOfAffectedRows; + } + } + catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken( + exception, + cancellationToken + ) + ) + { + throw new OperationCanceledException(cancellationToken); + } } return totalNumberOfAffectedRows; diff --git a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs index 2e8d1f6..8975c22 100644 --- a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs +++ b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs @@ -13,7 +13,9 @@ namespace RentADeveloper.DbConnectionPlus.Entities; /// Determines whether the property can be written to. /// The name of the column to which the property is mapped. /// Determines whether the property is a computed property. -/// Determines whether the property participates in optimistic concurrency checks. +/// +/// Determines whether the property participates in optimistic concurrency checks. +/// /// Determines whether the property is an identity property. /// Determines whether the property is ignored and not mapped to a database column. /// Determines whether the property is a key property. diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index ed4ccbf..3f9358e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -100,7 +100,8 @@ public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND {Q("Key2")} = {Parameter(entity.Key2_)} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND + {Q("Key2")} = {Parameter(entity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -111,7 +112,8 @@ public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND {Q("Key2")} = {Parameter(failingEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND + {Q("Key2")} = {Parameter(failingEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -349,7 +351,8 @@ public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND {Q("Key2")} = {Parameter(entity.Key2_)} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND + {Q("Key2")} = {Parameter(entity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -360,7 +363,8 @@ public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsync $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND {Q("Key2")} = {Parameter(failingEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND + {Q("Key2")} = {Parameter(failingEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index a8ccde8..9db1ef5 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -95,7 +95,8 @@ public async Task UpdateEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useA $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND {Q("Key2")} = {Parameter(updatedEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND + {Q("Key2")} = {Parameter(updatedEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) @@ -309,7 +310,8 @@ public async Task UpdateEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncAp $""" SELECT * FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND {Q("Key2")} = {Parameter(updatedEntity.Key2_)} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND + {Q("Key2")} = {Parameter(updatedEntity.Key2_)} """, cancellationToken: TestContext.Current.CancellationToken )) From 22c9b31abc8b6f5a9bf12890cfc8aa743eae9f5d Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 4 Feb 2026 13:10:53 +0100 Subject: [PATCH 05/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- CHANGELOG.md | 6 +- README.md | 67 ++++++++++++++++++- docs/DESIGN-DECISIONS.md | 4 +- .../DatabaseAdapters/IEntityManipulator.cs | 43 +++++++++++- .../DbConnectionExtensions.DeleteEntities.cs | 14 +++- .../DbConnectionExtensions.DeleteEntity.cs | 14 +++- .../DbConnectionExtensions.UpdateEntities.cs | 13 +++- .../DbConnectionExtensions.UpdateEntity.cs | 13 +++- src/DbConnectionPlus/DbConnectionPlus.csproj | 2 +- .../Entities/EntityHelperTests.cs | 2 +- .../EntityMaterializerFactoryTests.cs | 6 +- ...piTest.PublicApiHasNotChanged.verified.txt | 13 ++++ .../TestData/Generate.cs | 8 +-- 13 files changed, 179 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4212afe..d1d860b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ 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/). -TODO: Update for concurrency token handling. +TODO: Update date +## [1.2.0] - 2026-02-XX + +### Added +- Support for Optimistic Concurrency Support via Concurrency Tokens (Fixes [issue #5](https://github.com/rent-a-developer/DbConnectionPlus/issues/5)) ## [1.1.0] - 2026-02-01 diff --git a/README.md b/README.md index ebd060b..cc1ee23 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![NuGet Version](https://img.shields.io/nuget/v/RentADeveloper.DbConnectionPlus)](https://www.nuget.org/packages/RentADeveloper.DbConnectionPlus/) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rent-a-developer_DbConnectionPlus&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=rent-a-developer_DbConnectionPlus) [![license](https://img.shields.io/badge/License-MIT-purple.svg)](LICENSE.md) -![semver](https://img.shields.io/badge/semver-1.1.0-blue) +![semver](https://img.shields.io/badge/semver-1.2.0-blue) # ![image icon](https://raw.githubusercontent.com/rent-a-developer/DbConnectionPlus/main/icon.png) DbConnectionPlus A lightweight .NET ORM and extension library for the type @@ -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 concurrency token handling (attributes and fluent API). - ## Table of contents - **[Quick start](#quick-start)** - [Examples](#examples) @@ -490,9 +488,47 @@ DbConnectionExtensions.Configure(config => config.Entity() .Property(a => a.IsOnSale) .IsIgnored(); + + config.Entity() + .Property(a => a.Version) + .IsRowVersion(); + + config.Entity() + .Property(a => a.ConcurrencyToken) + .IsConcurrencyToken(); }); ``` +###### `Entity()` +Use this method to start configuring the mapping for the entity type `TEntity`. + +###### `ToTable(tableName)` +Use this method to specify the name of the table where entities of the entity type are stored in the database. + +###### `Property(propertyExpression)` +Use this method to start configuring the mapping for a property of the entity type. + +###### `HasColumnName(columnName)` +Use this method to specify the name of the column where the property is stored in the database. + +###### `IsKey()` +Use this method to specify that the property is part of the key by which entities of the entity type are identified. + +###### `IsIdentity()` +Use this method to specify that the property is generated by the database on insert. + +###### `IsComputed()` +Use this method to specify that the property is generated by the database on insert and update. + +###### `IsRowVersion()` +Use this method to specify that the property is a native database-generated concurrency token. + +###### `IsConcurrencyToken()` +Use this method to specify that the property is an application-managed concurrency token. + +###### `IsIgnored()` +Use this method to specify that the property should be ignored and not mapped to a column. + ##### Data annotation attributes You can use the following attributes to configure how entity types are mapped to database tables and columns: @@ -540,6 +576,31 @@ class Product Properties marked with this attribute are ignored (unless `DatabaseGeneratedOption.None` is used) when inserting new entities into the database or updating existing entities. When an entity is inserted or updated, the value of the property is read back from the database and set on the entity. + +###### `System.ComponentModel.DataAnnotations.TimestampAttribute` +Use this attribute to specify that a property of an entity type is a native database-generated concurrency token: +```csharp +class Product +{ + [Timestamp] + public Byte[] Version { get; set; } +} +``` +Properties marked with this attribute will be checked during delete and update operations. +When their values in the database do not match the original values, the delete or update will fail. +When an entity is inserted or updated, the value of the property is read back from the database and set on the entity. + +###### `System.ComponentModel.DataAnnotations.ConcurrencyCheckAttribute` +Use this attribute to specify that a property of an entity type is a application-managed concurrency token: +```csharp +class Product +{ + [ConcurrencyCheck] + public Byte[] ConcurrencyToken { get; set; } +} +``` +Properties marked with this attribute will be checked during delete and update operations. +When their values in the database do not match the original values, the delete or update will fail. ###### `System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute` Use this attribute to specify that a property of an entity type should be ignored and not mapped to a column: diff --git a/docs/DESIGN-DECISIONS.md b/docs/DESIGN-DECISIONS.md index 9dedff6..9ced144 100644 --- a/docs/DESIGN-DECISIONS.md +++ b/docs/DESIGN-DECISIONS.md @@ -1,8 +1,6 @@ # DbConnectionPlus - Design Decisions Document -TODO: Update version before next release. - -**Version:** 1.1.0 +**Version:** 1.2.0 **Last Updated:** February 2026 **Author:** David Liebeherr diff --git a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs index ae2c2c8..1927661 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs @@ -2,11 +2,10 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters; -// TODO: Update documentation regarding concurrency handling. - /// /// Provides CRUD database operations for entities. /// @@ -38,6 +37,11 @@ public interface IEntityManipulator /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -89,6 +93,11 @@ CancellationToken cancellationToken /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -136,6 +145,11 @@ CancellationToken cancellationToken /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -187,6 +201,11 @@ CancellationToken cancellationToken /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -479,6 +498,11 @@ CancellationToken cancellationToken /// /// /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -550,6 +574,11 @@ CancellationToken cancellationToken /// /// /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -616,6 +645,11 @@ CancellationToken cancellationToken /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -686,6 +720,11 @@ CancellationToken cancellationToken /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs index 70a130d..4531190 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs @@ -1,9 +1,9 @@ // Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. -namespace RentADeveloper.DbConnectionPlus; +using RentADeveloper.DbConnectionPlus.Exceptions; -// TODO: Update documentation regarding concurrency handling. +namespace RentADeveloper.DbConnectionPlus; /// /// Provides extension members for the type . @@ -36,6 +36,11 @@ public static partial class DbConnectionExtensions /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -114,6 +119,11 @@ public static Int32 DeleteEntities( /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs index d644ee6..ff484d0 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs @@ -1,9 +1,9 @@ // Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. -namespace RentADeveloper.DbConnectionPlus; +using RentADeveloper.DbConnectionPlus.Exceptions; -// TODO: Update documentation regarding concurrency handling. +namespace RentADeveloper.DbConnectionPlus; /// /// Provides extension members for the type . @@ -36,6 +36,11 @@ public static partial class DbConnectionExtensions /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -117,6 +122,11 @@ public static Int32 DeleteEntity( /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while deleting an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by a delete operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs index f95efe5..1b0b090 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs @@ -2,11 +2,10 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus; -// TODO: Update documentation regarding concurrency handling. - /// /// Provides extension members for the type . /// @@ -38,6 +37,11 @@ public static partial class DbConnectionExtensions /// /// /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -152,6 +156,11 @@ public static Int32 UpdateEntities( /// /// /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs index 6b2f04f..bddad01 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs @@ -2,11 +2,10 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using RentADeveloper.DbConnectionPlus.Converters; +using RentADeveloper.DbConnectionPlus.Exceptions; namespace RentADeveloper.DbConnectionPlus; -// TODO: Update documentation regarding concurrency handling. - /// /// Provides extension members for the type . /// @@ -38,6 +37,11 @@ public static partial class DbConnectionExtensions /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// @@ -142,6 +146,11 @@ public static Int32 UpdateEntity( /// /// No instance property of the type is configured as a key property. /// + /// + /// A concurrency violation was encountered while updating an entity. A concurrency violation occurs when an + /// unexpected number of rows are affected by an update operation. This is usually because the data in the database + /// has been modified since the entity has been loaded. + /// /// /// The operation was cancelled via . /// diff --git a/src/DbConnectionPlus/DbConnectionPlus.csproj b/src/DbConnectionPlus/DbConnectionPlus.csproj index ba8ce80..a2203ca 100644 --- a/src/DbConnectionPlus/DbConnectionPlus.csproj +++ b/src/DbConnectionPlus/DbConnectionPlus.csproj @@ -21,7 +21,7 @@ true true snupkg - 1.1.0 + 1.2.0 See CHANGELOG.md. true README.md diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index eb45f91..f565c88 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -385,7 +385,7 @@ public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFlue key2Property.IsKey .Should().BeTrue(); - var nameProperty = metadata.AllPropertiesByPropertyName["Name_"]; + var nameProperty = metadata.AllPropertiesByPropertyName["Value_"]; nameProperty.ColumnName .Should().Be("Value"); diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 6cf47fa..f1670a8 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -344,7 +344,7 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value_); @@ -434,7 +434,7 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value_); @@ -504,7 +504,7 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value); diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 7dc5a90..ca134b3 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -157,6 +157,8 @@ namespace RentADeveloper.DbConnectionPlus [System.Diagnostics.CodeAnalysis.DoesNotReturn] public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(RentADeveloper.DbConnectionPlus.DatabaseAdapters.IDatabaseAdapter databaseAdapter) { } [System.Diagnostics.CodeAnalysis.DoesNotReturn] + public static void ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException(int expectedNumberOfAffectedRows, int actualNumberOfAffectedRows, object entity) { } + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public static void ThrowEntityTypeHasNoKeyPropertyException(System.Type entityType) { } [System.Diagnostics.CodeAnalysis.DoesNotReturn] public static T ThrowInvalidEnumSerializationModeException(RentADeveloper.DbConnectionPlus.EnumSerializationMode enumSerializationMode) { } @@ -243,6 +245,17 @@ namespace RentADeveloper.DbConnectionPlus.Entities public System.Collections.Generic.IReadOnlyList UpdateProperties { get; init; } } } +namespace RentADeveloper.DbConnectionPlus.Exceptions +{ + public class DbUpdateConcurrencyException : System.Exception + { + public DbUpdateConcurrencyException() { } + public DbUpdateConcurrencyException(string message) { } + public DbUpdateConcurrencyException(string message, System.Exception innerException) { } + public DbUpdateConcurrencyException(string message, object entity) { } + public object? Entity { get; set; } + } +} namespace RentADeveloper.DbConnectionPlus.SqlStatements { public readonly struct InterpolatedParameter : System.IEquatable diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 627d9b0..0326a9d 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -337,10 +337,10 @@ public Object Create(Object request, ISpecimenContext context) { var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(propertyInfo.DeclaringType!); - var propertyMetadata = - entityTypeMetadata.AllPropertiesByPropertyName[propertyInfo.Name]; - - if (propertyMetadata.IsIgnored) + if ( + entityTypeMetadata.AllPropertiesByPropertyName.TryGetValue(propertyInfo.Name, out var propertyMetadata) && + propertyMetadata.IsIgnored + ) { return new OmitSpecimen(); } From 0872576dcc5df88ba1084de536d30d12682e5f16 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 4 Feb 2026 14:26:35 +0100 Subject: [PATCH 06/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../Configuration/EntityPropertyBuilder.cs | 18 ++ .../TemporaryTableBuilderTests.cs | 6 +- ...ConnectionExtensions.QueryFirstOfTTests.cs | 82 ++++---- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 82 ++++---- .../DbConnectionExtensions.QueryOfTTests.cs | 66 +++---- ...onnectionExtensions.QuerySingleOfTTests.cs | 66 +++---- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 66 +++---- .../TestDatabase/MySqlTestDatabaseProvider.cs | 11 +- .../OracleTestDatabaseProvider.cs | 14 +- .../PostgreSqlTestDatabaseProvider.cs | 8 +- .../SQLiteTestDatabaseProvider.cs | 7 +- .../SqlServerTestDatabaseProvider.cs | 11 +- .../EntityPropertyBuilderTests.cs | 185 +++++++++++++----- .../Extensions/ObjectExtensionsTests.cs | 4 +- .../EntityMaterializerFactoryTests.cs | 139 +++---------- .../Readers/EnumHandlingObjectReaderTests.cs | 24 +-- .../TestData/Entity.cs | 1 + .../TestData/EntityWithBinaryProperty.cs | 6 - .../TestData/EntityWithCharProperty.cs | 6 - .../EntityWithDifferentCasingProperties.cs | 1 + .../TestData/EntityWithNonNullableProperty.cs | 9 - .../EntityWithNullableCharProperty.cs | 6 - .../TestData/EntityWithPrivateConstructor.cs | 3 + ...tityWithPrivateParameterlessConstructor.cs | 1 + .../TestData/EntityWithPublicConstructor.cs | 3 + .../TestData/EntityWithStringProperty.cs | 6 - .../TestData/TestEnum.cs | 6 +- 27 files changed, 355 insertions(+), 482 deletions(-) delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithBinaryProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithCharProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithNonNullableProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableCharProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithStringProperty.cs diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs index cc353ec..cc4abb5 100644 --- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -10,8 +10,26 @@ public sealed class EntityPropertyBuilder : IEntityPropertyBuilder /// /// The entity type builder this property builder belongs to. /// The name of the property being configured. + /// + /// + /// + /// + /// is . + /// + /// + /// + /// + /// is . + /// + /// + /// + /// + /// is whitespace. internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String propertyName) { + ArgumentNullException.ThrowIfNull(entityTypeBuilder); + ArgumentException.ThrowIfNullOrWhiteSpace(propertyName); + this.entityTypeBuilder = entityTypeBuilder; this.propertyName = propertyName; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 69d4bfe..45b6001 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -316,12 +316,12 @@ public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabas this.Connection, null, "Objects", - Generate.Multiple(), - typeof(EntityWithStringProperty), + Generate.Multiple(), + typeof(Entity), TestContext.Current.CancellationToken ); - var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "String"); + var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "StringValue"); columnCollation .Should().Be(this.TestDatabaseProvider.DatabaseCollation); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index b5e439b..324b048 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -272,7 +272,7 @@ public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncAp commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -319,7 +319,7 @@ Boolean useAsyncApi $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -333,18 +333,18 @@ public async Task // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT '' AS {Q("Char")}", + $"SELECT '' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -354,18 +354,18 @@ await Invoking(() => } await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 'ab' AS {Q("Char")}", + $"SELECT 'ab' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + + "The column 'CharValue' 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.*" + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -384,13 +384,13 @@ Boolean useAsyncApi { var character = Generate.Single(); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT '{character}' AS {Q("Char")}", + $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); + .Should().BeEquivalentTo(new Entity { CharValue = character }); } [Theory] @@ -488,17 +488,17 @@ Boolean useAsyncApi ) { var entity = (await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().NotThrowAsync()).Subject; entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); + .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] @@ -699,7 +699,7 @@ Boolean useAsyncApi $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -724,21 +724,21 @@ public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndProp public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -762,22 +762,6 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirst_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) - { - var bytes = Generate.Single(); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -793,7 +777,7 @@ public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(Boolea $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -832,7 +816,7 @@ public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParamet $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -853,7 +837,7 @@ public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) statement, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -922,7 +906,7 @@ Boolean useAsyncApi """, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -941,7 +925,7 @@ public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncAp transaction, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); await transaction.RollbackAsync(); } @@ -1140,21 +1124,21 @@ public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStr public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index 00d6db6..e12afec 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -284,7 +284,7 @@ public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean u commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -331,7 +331,7 @@ Boolean useAsyncApi $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -347,18 +347,18 @@ Boolean useAsyncApi // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT '' AS {Q("Char")}", + $"SELECT '' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -368,18 +368,18 @@ await Invoking(() => } await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 'ab' AS {Q("Char")}", + $"SELECT 'ab' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + + "The column 'CharValue' 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.*" + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -398,13 +398,13 @@ Boolean useAsyncApi { var character = Generate.Single(); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT '{character}' AS {Q("Char")}", + $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); + .Should().BeEquivalentTo(new Entity { CharValue = character }); } [Theory] @@ -505,17 +505,17 @@ Boolean useAsyncApi ) { var entity = (await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().NotThrowAsync()).Subject; entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); + .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] @@ -719,7 +719,7 @@ Boolean useAsyncApi $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -748,21 +748,21 @@ Boolean useAsyncApi ) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -786,22 +786,6 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) - { - var bytes = Generate.Single(); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -817,7 +801,7 @@ public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValu $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -856,7 +840,7 @@ public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolat $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -877,7 +861,7 @@ public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useA statement, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -960,7 +944,7 @@ Boolean useAsyncApi """, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); } [Theory] @@ -979,7 +963,7 @@ public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean u transaction, cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entities[0]); + .Should().BeEquivalentTo(entities[0]); await transaction.RollbackAsync(); } @@ -1185,21 +1169,21 @@ Boolean useAsyncApi ) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 6ddf64b..638a571 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -345,18 +345,18 @@ public async Task // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT '' AS {Q("Char")}", + $"SELECT '' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().ThrowAsync() .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -366,18 +366,18 @@ await Invoking(() => } await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 'ab' AS {Q("Char")}", + $"SELECT 'ab' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().ThrowAsync() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + + "The column 'CharValue' 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.*" + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -396,13 +396,13 @@ Boolean useAsyncApi { var character = Generate.Single(); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT '{character}' AS {Q("Char")}", + $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new EntityWithCharProperty { Char = character }]); + .Should().BeEquivalentTo([new Entity { CharValue = character }]); } [Theory] @@ -496,17 +496,17 @@ Boolean useAsyncApi ) { var entities = (await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().NotThrowAsync()).Subject; entities - .Should().BeEquivalentTo([new EntityWithNonNullableProperty { Id = 1, Value = 2 }]); + .Should().BeEquivalentTo([new Entity { Id = 1, Int32Value = 2 }]); } [Theory] @@ -731,21 +731,21 @@ Boolean useAsyncApi public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -767,22 +767,6 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeEquivalentTo([new EntityWithNullableProperty { Id = 1, Value = null }]); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task Query_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) - { - var bytes = Generate.Single(); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new EntityWithBinaryProperty { BinaryData = bytes }]); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -1139,21 +1123,21 @@ public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertStringTo public Task Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken).AsTask() ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 3c97a8e..f045cc4 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -333,18 +333,18 @@ public async Task // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT '' AS {Q("Char")}", + $"SELECT '' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -354,18 +354,18 @@ await Invoking(() => } await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 'ab' AS {Q("Char")}", + $"SELECT 'ab' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + + "The column 'CharValue' 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.*" + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -384,13 +384,13 @@ Boolean useAsyncApi { var character = Generate.Single(); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT '{character}' AS {Q("Char")}", + $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); + .Should().BeEquivalentTo(new Entity { CharValue = character }); } [Theory] @@ -488,17 +488,17 @@ Boolean useAsyncApi ) { var entity = (await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().NotThrowAsync()).Subject; entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); + .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] @@ -725,21 +725,21 @@ public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPro public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -763,22 +763,6 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingle_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) - { - var bytes = Generate.Single(); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -1163,21 +1147,21 @@ Boolean useAsyncApi ) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 6b92809..06b50f0 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -347,18 +347,18 @@ Boolean useAsyncApi // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT '' AS {Q("Char")}", + $"SELECT '' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -368,18 +368,18 @@ await Invoking(() => } await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 'ab' AS {Q("Char")}", + $"SELECT 'ab' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted " + + "The column 'CharValue' 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.*" + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( @@ -398,13 +398,13 @@ Boolean useAsyncApi { var character = Generate.Single(); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT '{character}' AS {Q("Char")}", + $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithCharProperty { Char = character }); + .Should().BeEquivalentTo(new Entity { CharValue = character }); } [Theory] @@ -505,17 +505,17 @@ Boolean useAsyncApi ) { var entity = (await Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Value")}, 3 AS {Q("NonExistent")}", + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().NotThrowAsync()).Subject; entity - .Should().BeEquivalentTo(new EntityWithNonNullableProperty { Id = 1, Value = 2 }); + .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] @@ -752,21 +752,21 @@ Boolean useAsyncApi ) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -790,22 +790,6 @@ await this.Connection.ExecuteNonQueryAsync( .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_ShouldMaterializeBinaryData(Boolean useAsyncApi) - { - var bytes = Generate.Single(); - - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new EntityWithBinaryProperty { BinaryData = bytes }); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -1210,21 +1194,21 @@ Boolean useAsyncApi ) { this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("EntityWithNonNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" ); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNonNullableProperty")}", + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) .Should().ThrowAsync() .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.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + + $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index 4a8cfbc..def0381 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -161,6 +161,7 @@ private static void ExecuteScript(MySqlConnection connection, String script) CREATE TABLE `Entity` ( `Id` BIGINT, + `BytesValue` BLOB, `BooleanValue` TINYINT(1), `ByteValue` TINYINT UNSIGNED, `CharValue` CHAR(1), @@ -194,13 +195,6 @@ CREATE TABLE `EntityWithEnumStoredAsInteger` ); GO - CREATE TABLE `EntityWithNonNullableProperty` - ( - `Id` BIGINT NOT NULL PRIMARY KEY, - `Value` BIGINT NULL - ); - GO - CREATE TABLE `EntityWithNullableProperty` ( `Id` BIGINT NOT NULL PRIMARY KEY, @@ -287,9 +281,6 @@ FOR EACH ROW TRUNCATE TABLE `EntityWithEnumStoredAsInteger`; GO - TRUNCATE TABLE `EntityWithNonNullableProperty`; - GO - TRUNCATE TABLE `EntityWithNullableProperty`; GO diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 432aa57..09e4b89 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -142,6 +142,7 @@ private static void ExecuteScript(OracleConnection connection, String script) CREATE TABLE "Entity" ( "Id" NUMBER(19) NOT NULL PRIMARY KEY, + "BytesValue" RAW(2000), "BooleanValue" NUMBER(1), "ByteValue" NUMBER(3), "CharValue" CHAR(1), @@ -182,13 +183,6 @@ CREATE TABLE "EntityWithEnumStoredAsInteger" ); GO - CREATE TABLE "EntityWithNonNullableProperty" - ( - "Id" NUMBER(19) NOT NULL PRIMARY KEY, - "Value" NUMBER(19) NULL - ); - GO - CREATE TABLE "EntityWithNullableProperty" ( "Id" NUMBER(19) NOT NULL PRIMARY KEY, @@ -240,9 +234,6 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS DROP TABLE IF EXISTS "EntityWithEnumStoredAsInteger" PURGE; GO - DROP TABLE IF EXISTS "EntityWithNonNullableProperty" PURGE; - GO - DROP TABLE IF EXISTS "EntityWithNullableProperty" PURGE; GO @@ -267,9 +258,6 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; GO - TRUNCATE TABLE "EntityWithNonNullableProperty"; - GO - TRUNCATE TABLE "EntityWithNullableProperty"; GO diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index 9f70f7b..117f167 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -143,6 +143,7 @@ public void ResetDatabase() CREATE TABLE "Entity" ( "Id" bigint NOT NULL PRIMARY KEY, + "BytesValue" bytea, "BooleanValue" boolean, "ByteValue" smallint, "CharValue" char(1), @@ -173,12 +174,6 @@ CREATE TABLE "EntityWithEnumStoredAsInteger" "Enum" integer NULL ); - CREATE TABLE "EntityWithNonNullableProperty" - ( - "Id" bigint NOT NULL PRIMARY KEY, - "Value" bigint NULL - ); - CREATE TABLE "EntityWithNullableProperty" ( "Id" bigint NOT NULL PRIMARY KEY, @@ -255,7 +250,6 @@ DELETE FROM "Entity" TRUNCATE TABLE "Entity"; TRUNCATE TABLE "EntityWithEnumStoredAsString"; TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; - TRUNCATE TABLE "EntityWithNonNullableProperty"; TRUNCATE TABLE "EntityWithNullableProperty"; TRUNCATE TABLE "MappingTestEntity"; """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index ef616d6..fb7446a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -128,6 +128,7 @@ public void ResetDatabase() CREATE TABLE Entity ( Id INTEGER, + BytesValue BLOB, BooleanValue INTEGER, ByteValue INTEGER, CharValue TEXT, @@ -164,12 +165,6 @@ CREATE TABLE EntityWithEnumStoredAsInteger Enum INTEGER ); - CREATE TABLE EntityWithNonNullableProperty - ( - Id INTEGER NOT NULL, - Value INTEGER NULL - ); - CREATE TABLE EntityWithNullableProperty ( Id INTEGER NOT NULL, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index da00c8a..1544b93 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -166,6 +166,7 @@ private static void ExecuteScript(SqlConnection connection, String script) CREATE TABLE Entity ( Id BIGINT NOT NULL PRIMARY KEY, + BytesValue VARBINARY(MAX), BooleanValue BIT, ByteValue TINYINT, CharValue CHAR(1), @@ -206,13 +207,6 @@ Enum INT NULL ); GO - CREATE TABLE EntityWithNonNullableProperty - ( - Id BIGINT NOT NULL PRIMARY KEY, - Value BIGINT NULL - ); - GO - CREATE TABLE EntityWithNullableProperty ( Id BIGINT NOT NULL PRIMARY KEY, @@ -293,9 +287,6 @@ DELETE FROM Entity TRUNCATE TABLE EntityWithEnumStoredAsInteger; GO - TRUNCATE TABLE EntityWithNonNullableProperty; - GO - TRUNCATE TABLE EntityWithNullableProperty; GO diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs index 31eea9e..767dcb4 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -2,10 +2,31 @@ public class EntityPropertyBuilderTests : UnitTestsBase { + [Fact] + public void ColumnName_Configured_ShouldReturnColumnName() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.HasColumnName("Identifier"); + + ((IEntityPropertyBuilder)builder).ColumnName + .Should().Be("Identifier"); + } + + [Fact] + public void ColumnName_NotConfigured_ShouldReturnNull() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).ColumnName + .Should().BeNull(); + } + [Fact] public void Freeze_ShouldFreezeBuilder() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + ((IFreezable)builder).Freeze(); Invoking(() => builder.HasColumnName("Identifier")) @@ -16,6 +37,10 @@ public void Freeze_ShouldFreezeBuilder() .Should().Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + Invoking(() => builder.IsConcurrencyToken()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + Invoking(() => builder.IsIdentity()) .Should().Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); @@ -27,12 +52,17 @@ public void Freeze_ShouldFreezeBuilder() Invoking(() => builder.IsKey()) .Should().Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); + + Invoking(() => builder.IsRowVersion()) + .Should().Throw() + .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } [Fact] - public void GetColumnName_Configured_ShouldReturnColumnName() + public void HasColumnName_ShouldSetColumnName() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.HasColumnName("Identifier"); ((IEntityPropertyBuilder)builder).ColumnName @@ -40,16 +70,27 @@ public void GetColumnName_Configured_ShouldReturnColumnName() } [Fact] - public void GetColumnName_NotConfigured_ShouldReturnNull() + public void IsComputed_Configured_ShouldReturnTrue() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).ColumnName - .Should().BeNull(); + builder.IsComputed(); + + ((IEntityPropertyBuilder)builder).IsComputed + .Should().BeTrue(); } [Fact] - public void GetIsComputed_Configured_ShouldReturnTrue() + public void IsComputed_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsComputed + .Should().BeFalse(); + } + + [Fact] + public void IsComputed_ShouldMarkPropertyAsComputed() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); @@ -60,16 +101,38 @@ public void GetIsComputed_Configured_ShouldReturnTrue() } [Fact] - public void GetIsComputed_NotConfigured_ShouldReturnFalse() + public void IsConcurrencyToken_Configured_ShouldReturnTrue() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsComputed + builder.IsConcurrencyToken(); + + ((IEntityPropertyBuilder)builder).IsConcurrencyToken + .Should().BeTrue(); + } + + [Fact] + public void IsConcurrencyToken_NotConfigured_ShouldReturnFalse() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + ((IEntityPropertyBuilder)builder).IsConcurrencyToken .Should().BeFalse(); } [Fact] - public void GetIsIdentity_Configured_ShouldReturnTrue() + public void IsConcurrencyToken_ShouldMarkPropertyAsConcurrencyToken() + { + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsConcurrencyToken(); + + ((IEntityPropertyBuilder)builder).IsConcurrencyToken + .Should().BeTrue(); + } + + [Fact] + public void IsIdentity_Configured_ShouldReturnTrue() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); @@ -80,7 +143,7 @@ public void GetIsIdentity_Configured_ShouldReturnTrue() } [Fact] - public void GetIsIdentity_NotConfigured_ShouldReturnFalse() + public void IsIdentity_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); @@ -89,7 +152,35 @@ public void GetIsIdentity_NotConfigured_ShouldReturnFalse() } [Fact] - public void GetIsIgnored_Configured_ShouldReturnTrue() + public void IsIdentity_OtherPropertyIsAlreadyMarked_ShouldThrow() + { + var entityTypeBuilder = new EntityTypeBuilder(); + + entityTypeBuilder.Property(a => a.Id).IsIdentity(); + + var propertyBuilder = new EntityPropertyBuilder(entityTypeBuilder, "NotId"); + + Invoking(() => propertyBuilder.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_Configured_ShouldReturnTrue() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); @@ -100,7 +191,7 @@ public void GetIsIgnored_Configured_ShouldReturnTrue() } [Fact] - public void GetIsIgnored_NotConfigured_ShouldReturnFalse() + public void IsIgnored_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); @@ -109,90 +200,90 @@ public void GetIsIgnored_NotConfigured_ShouldReturnFalse() } [Fact] - public void GetIsKey_Configured_ShouldReturnTrue() + public void IsIgnored_ShouldMarkPropertyAsIgnored() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsIgnored(); - builder.IsKey(); - - ((IEntityPropertyBuilder)builder).IsKey + ((IEntityPropertyBuilder)builder).IsIgnored .Should().BeTrue(); } [Fact] - public void GetIsKey_NotConfigured_ShouldReturnFalse() + public void IsKey_Configured_ShouldReturnTrue() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + builder.IsKey(); + ((IEntityPropertyBuilder)builder).IsKey - .Should().BeFalse(); + .Should().BeTrue(); } [Fact] - public void HasColumnName_ShouldSetColumnName() + public void IsKey_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - builder.HasColumnName("Identifier"); - ((IEntityPropertyBuilder)builder).ColumnName - .Should().Be("Identifier"); + ((IEntityPropertyBuilder)builder).IsKey + .Should().BeFalse(); } [Fact] - public void IsComputed_ShouldMarkPropertyAsComputed() + public void IsKey_ShouldMarkPropertyAsKey() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); + + builder.IsKey(); - builder.IsComputed(); - - ((IEntityPropertyBuilder)builder).IsComputed + ((IEntityPropertyBuilder)builder).IsKey .Should().BeTrue(); } [Fact] - public void IsIdentity_OtherPropertyIsAlreadyMarked_ShouldThrow() + public void IsRowVersion_Configured_ShouldReturnTrue() { - var entityTypeBuilder = new EntityTypeBuilder(); - entityTypeBuilder.Property(a => a.Id).IsIdentity(); + var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - var builder = new EntityPropertyBuilder(entityTypeBuilder, "Property"); + builder.IsRowVersion(); - 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." - ); + ((IEntityPropertyBuilder)builder).IsRowVersion + .Should().BeTrue(); } [Fact] - public void IsIdentity_ShouldMarkPropertyAsIdentity() + public void IsRowVersion_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - builder.IsIdentity(); - - ((IEntityPropertyBuilder)builder).IsIdentity - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsRowVersion + .Should().BeFalse(); } [Fact] - public void IsIgnored_ShouldMarkPropertyAsIgnored() + public void IsRowVersion_ShouldMarkPropertyAsRowVersion() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - builder.IsIgnored(); + + builder.IsRowVersion(); - ((IEntityPropertyBuilder)builder).IsIgnored + ((IEntityPropertyBuilder)builder).IsRowVersion .Should().BeTrue(); } [Fact] - public void IsKey_ShouldMarkPropertyAsKey() + public void PropertyName_ShouldReturnPropertyName() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - builder.IsKey(); - ((IEntityPropertyBuilder)builder).IsKey - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).PropertyName + .Should().Be("Property"); } + + [Fact] + public void ShouldGuardAgainstNullArguments() => + ArgumentNullGuardVerifier.Verify(() => + new EntityPropertyBuilder(Substitute.For(), "Property") + ); } diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs index b499614..dbbc7d5 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs @@ -109,9 +109,9 @@ public void ToDebugString_ShouldReturnStringRepresentationOfValue() new Object().ToDebugString() .Should().Be("'{}' (System.Object)"); - new EntityWithStringProperty { String = "A String" }.ToDebugString() + new EntityWithEnumProperty { Enum = TestEnum.Value3 }.ToDebugString() .Should().Be( - """'{"String":"A String"}' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithStringProperty)""" + """'{"Enum":3}' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumProperty)""" ); } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index f1670a8..bc42851 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -33,15 +33,15 @@ public void GetMaterializer_DataReaderFieldTypeNotCompatibleWithEntityPropertyTy dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Char"); + dataReader.GetName(0).Returns("CharValue"); dataReader.GetFieldType(0).Returns(typeof(Guid)); - Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) + Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) .Should().Throw() .WithMessage( - $"The data type {typeof(Guid)} of the column 'Char' returned by the SQL statement is not compatible " + - $"with the property type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(EntityWithCharProperty)}.*" + $"The data type {typeof(Guid)} of the column 'CharValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(Char)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); } @@ -77,7 +77,6 @@ public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() ); } - [Fact] public void Materializer_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() { @@ -169,7 +168,7 @@ public void Materializer_EntityHasNoCorrespondingPropertyForDataReaderField_Shou var dataReader = Substitute.For(); var id = Generate.Id(); - var value = Generate.Id(); + var value = Generate.Single(); dataReader.FieldCount.Returns(3); @@ -178,10 +177,10 @@ public void Materializer_EntityHasNoCorrespondingPropertyForDataReaderField_Shou dataReader.IsDBNull(0).Returns(false); dataReader.GetInt64(0).Returns(id); - dataReader.GetFieldType(1).Returns(typeof(Int64)); - dataReader.GetName(1).Returns("Value"); + dataReader.GetFieldType(1).Returns(typeof(Int32)); + dataReader.GetName(1).Returns("Int32Value"); dataReader.IsDBNull(1).Returns(false); - dataReader.GetInt64(1).Returns(value); + dataReader.GetInt32(1).Returns(value); dataReader.GetFieldType(2).Returns(typeof(Int32)); dataReader.GetName(2).Returns("NonExistent"); @@ -189,7 +188,7 @@ public void Materializer_EntityHasNoCorrespondingPropertyForDataReaderField_Shou dataReader.GetInt64(2).Returns(Generate.SmallNumber()); var materializer = Invoking(() => - EntityMaterializerFactory.GetMaterializer(dataReader) + EntityMaterializerFactory.GetMaterializer(dataReader) ) .Should().NotThrow().Subject; @@ -198,7 +197,7 @@ public void Materializer_EntityHasNoCorrespondingPropertyForDataReaderField_Shou entity.Id .Should().Be(id); - entity.Value + entity.Int32Value .Should().Be(value); } @@ -582,25 +581,25 @@ public void [Fact] public void - Materializer_NonNullableCharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() + Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Char"); + dataReader.GetName(0).Returns("CharValue"); dataReader.GetFieldType(0).Returns(typeof(String)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(String.Empty); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( @@ -613,9 +612,9 @@ public void Invoking(() => materializer(dataReader)) .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.*" + "The column 'CharValue' 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(Entity)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( @@ -626,7 +625,7 @@ public void [Fact] public void - Materializer_NonNullableCharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() + Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() { var dataReader = Substitute.For(); @@ -634,16 +633,16 @@ public void var character = Generate.Single(); - dataReader.GetName(0).Returns("Char"); + dataReader.GetName(0).Returns("CharValue"); dataReader.GetFieldType(0).Returns(typeof(String)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(character.ToString()); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var entity = materializer(dataReader); - entity.Char + entity.CharValue .Should().Be(character); } @@ -669,74 +668,6 @@ public void ); } - [Fact] - public void - Materializer_NullableCharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(String.Empty); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - Invoking(() => materializer(dataReader)) - .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(EntityWithNullableCharProperty)}. 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." - ); - - dataReader.GetString(0).Returns("ab"); - - Invoking(() => materializer(dataReader)) - .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(EntityWithNullableCharProperty)}. 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 - Materializer_NullableCharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - var character = Generate.Single(); - - dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(character.ToString()); - - var materializer = EntityMaterializerFactory - .GetMaterializer(dataReader); - - var entity = materializer(dataReader); - - entity.Char - .Should().Be(character); - } - [Fact] public void Materializer_NullableEntityProperty_DataReaderFieldContainsNull_ShouldMaterializeNull() { @@ -777,28 +708,6 @@ public void Materializer_PropertiesWithDifferentCasing_ShouldMatchPropertiesCase .Should().BeEquivalentTo(entityWithDifferentCasingProperties); } - [Fact] - public void Materializer_ShouldMaterializeBinaryData() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - var bytes = Generate.Single(); - - dataReader.GetName(0).Returns("BinaryData"); - dataReader.GetFieldType(0).Returns(typeof(Byte[])); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetValue(0).Returns(bytes); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - var entity = materializer(dataReader); - - entity.BinaryData - .Should().BeEquivalentTo(bytes); - } - [Fact] public void Materializer_ShouldMaterializeDateTimeOffsetValue() { diff --git a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs index 7e7d49e..ad86432 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/EnumHandlingObjectReaderTests.cs @@ -7,11 +7,11 @@ public class EnumHandlingObjectReaderTests : UnitTestsBase [Fact] public void GetFieldType_CharProperty_ShouldReturnString() { - EntityWithCharProperty[] entities = [new()]; + Entity[] entities = [new()]; - var reader = new EnumHandlingObjectReader(typeof(EntityWithCharProperty), entities); + var reader = new EnumHandlingObjectReader(typeof(Entity), entities); - reader.GetFieldType(0) + reader.GetFieldType(reader.GetOrdinal("CharValue")) .Should().Be(typeof(String)); } @@ -61,14 +61,14 @@ public void GetInt32_EnumValues_ShouldReturnEnumAsInt32() [Fact] public void GetString_CharProperty_ShouldConvertToString() { - EntityWithCharProperty[] entities = [new() { Char = Generate.Single() }]; + Entity[] entities = [new() { CharValue = Generate.Single() }]; - var reader = new EnumHandlingObjectReader(typeof(EntityWithCharProperty), entities); + var reader = new EnumHandlingObjectReader(typeof(Entity), entities); reader.Read(); - reader.GetString(0) - .Should().Be(entities[0].Char.ToString()); + reader.GetString(reader.GetOrdinal("CharValue")) + .Should().Be(entities[0].CharValue.ToString()); } [Fact] @@ -91,18 +91,18 @@ public void GetString_EnumValues_ShouldReturnEnumAsString() [Fact] public void GetValues_CharProperty_ShouldConvertToString() { - EntityWithCharProperty[] entities = [new() { Char = Generate.Single() }]; + Entity[] entities = [new() { CharValue = Generate.Single() }]; - var reader = new EnumHandlingObjectReader(typeof(EntityWithCharProperty), entities); + var reader = new EnumHandlingObjectReader(typeof(Entity), entities); reader.Read(); - var values = new Object[1]; + var values = new Object[18]; reader.GetValues(values); - values[0] - .Should().Be(entities[0].Char.ToString()); + values[reader.GetOrdinal("CharValue")] + .Should().Be(entities[0].CharValue.ToString()); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 3f06d4c..985af42 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -2,6 +2,7 @@ public record Entity { + public Byte[] BytesValue { get; set; } = null!; public Boolean BooleanValue { get; set; } public Byte ByteValue { get; set; } public Char CharValue { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithBinaryProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithBinaryProperty.cs deleted file mode 100644 index a34de28..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithBinaryProperty.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public class EntityWithBinaryProperty -{ - public Byte[] BinaryData { get; set; } = null!; -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCharProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCharProperty.cs deleted file mode 100644 index 496a3dc..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithCharProperty.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithCharProperty -{ - public Char Char { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs index 1ad0353..f4dd76a 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs @@ -4,6 +4,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithDifferentCasingProperties { + public Byte[] BytesVALUE { get; set; } = null!; public Boolean BooleanVALUE { get; set; } public Byte ByteVALUE { get; set; } public Char CharVALUE { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNonNullableProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNonNullableProperty.cs deleted file mode 100644 index f1937ef..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNonNullableProperty.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithNonNullableProperty -{ - [Key] - public Int64 Id { get; set; } - - public Int64 Value { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableCharProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableCharProperty.cs deleted file mode 100644 index b65cff5..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableCharProperty.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public class EntityWithNullableCharProperty -{ - public Char? Char { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs index 90417f6..e14bf28 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs @@ -3,6 +3,7 @@ public record EntityWithPrivateConstructor { private EntityWithPrivateConstructor( + Byte[] bytesValue, Boolean booleanValue, Byte byteValue, Char charValue, @@ -22,6 +23,7 @@ private EntityWithPrivateConstructor( TimeSpan timeSpanValue ) { + this.BytesValue = bytesValue; this.BooleanValue = booleanValue; this.ByteValue = byteValue; this.CharValue = charValue; @@ -41,6 +43,7 @@ TimeSpan timeSpanValue this.TimeSpanValue = timeSpanValue; } + public Byte[] BytesValue { get; set; } = null!; public Boolean BooleanValue { get; } public Byte ByteValue { get; } public Char CharValue { get; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs index b95575a..50e6434 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs @@ -6,6 +6,7 @@ private EntityWithPrivateParameterlessConstructor() { } + public Byte[] BytesValue { get; set; } = null!; public Boolean BooleanValue { get; set; } public Byte ByteValue { get; set; } public Char CharValue { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index f2ddfec..b73b3bc 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -7,6 +7,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithPublicConstructor { public EntityWithPublicConstructor( + Byte[] bytesValue, Boolean booleanValue, Byte byteValue, Char charValue, @@ -26,6 +27,7 @@ public EntityWithPublicConstructor( TimeSpan timeSpanValue ) { + this.BytesValue = bytesValue; this.BooleanValue = booleanValue; this.ByteValue = byteValue; this.CharValue = charValue; @@ -45,6 +47,7 @@ TimeSpan timeSpanValue this.TimeSpanValue = timeSpanValue; } + public Byte[] BytesValue { get; set; } = null!; public Boolean BooleanValue { get; } public Byte ByteValue { get; } public Char CharValue { get; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithStringProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithStringProperty.cs deleted file mode 100644 index 4203553..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithStringProperty.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public class EntityWithStringProperty -{ - public String String { get; set; } = null!; -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs b/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs index 959f6c1..89501f2 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs @@ -1,8 +1,8 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; +#pragma warning disable RCS1042 + +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; -#pragma warning disable RCS1042 // Remove enum default underlying type public enum TestEnum : Int32 -#pragma warning restore RCS1042 // Remove enum default underlying type { Value1 = 1, Value2 = 2, From 24988d876a8b7bb923631c0755831b55ab8f0794 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 4 Feb 2026 14:58:47 +0100 Subject: [PATCH 07/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../TemporaryTableBuilderTests.cs | 12 ++++---- ...ConnectionExtensions.QueryFirstOfTTests.cs | 14 +++++----- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 14 +++++----- .../DbConnectionExtensions.QueryOfTTests.cs | 16 +++++------ ...onnectionExtensions.QuerySingleOfTTests.cs | 14 +++++----- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 14 +++++----- .../TestDatabase/MySqlTestDatabaseProvider.cs | 11 +------- .../OracleTestDatabaseProvider.cs | 14 +--------- .../PostgreSqlTestDatabaseProvider.cs | 8 +----- .../SQLiteTestDatabaseProvider.cs | 7 +---- .../SqlServerTestDatabaseProvider.cs | 11 +------- .../Extensions/ObjectExtensionsTests.cs | 4 +-- .../EntityMaterializerFactoryTests.cs | 28 +++++++++---------- .../TestData/Entity.cs | 4 ++- .../EntityWithDifferentCasingProperties.cs | 3 +- .../TestData/EntityWithEnumProperty.cs | 6 ---- .../TestData/EntityWithNullableProperty.cs | 9 ------ .../TestData/EntityWithPrivateConstructor.cs | 26 ++--------------- ...tityWithPrivateParameterlessConstructor.cs | 24 +--------------- .../TestData/EntityWithPublicConstructor.cs | 26 ++--------------- .../EntityWithUnsupportedPropertyType.cs | 8 ------ .../TestData/Generate.cs | 6 +++- 22 files changed, 80 insertions(+), 199 deletions(-) delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableProperty.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/TestData/EntityWithUnsupportedPropertyType.cs diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index 45b6001..efb49f7 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -71,7 +71,7 @@ Boolean useAsyncApi { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -79,7 +79,7 @@ Boolean useAsyncApi null, "Objects", entities, - typeof(EntityWithEnumProperty), + typeof(EntityWithEnumStoredAsInteger), TestContext.Current.CancellationToken ); @@ -116,7 +116,7 @@ Boolean useAsyncApi { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; - var entities = Generate.Multiple(); + var entities = Generate.Multiple(); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -124,7 +124,7 @@ Boolean useAsyncApi null, "Objects", entities, - typeof(EntityWithEnumProperty), + typeof(EntityWithEnumStoredAsString), TestContext.Current.CancellationToken ); @@ -168,8 +168,8 @@ Boolean useAsyncApi this.Connection, null, "Objects", - Generate.Multiple(), - typeof(EntityWithEnumProperty), + Generate.Multiple(), + typeof(EntityWithEnumStoredAsString), TestContext.Current.CancellationToken ); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 324b048..67d8340 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -750,16 +750,16 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); + .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] @@ -1150,13 +1150,13 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + (await CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index e12afec..bfa5e7c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -774,16 +774,16 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); + .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] @@ -1196,13 +1196,13 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + (await CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 638a571..6b4ff72 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -755,16 +755,16 @@ public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_Should 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)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new EntityWithNullableProperty { Id = 1, Value = null }]); + .Should().BeEquivalentTo([new Entity { Id = 1, NullableBooleanValue = null }]); } [Theory] @@ -1149,16 +1149,16 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + (await CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new ValueTuple(null)]); + .Should().BeEquivalentTo([new ValueTuple(null)]); } [Theory] diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index f045cc4..e6537d6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -751,16 +751,16 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); + .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] @@ -1173,13 +1173,13 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + (await CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 06b50f0..6b8f9e8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -778,16 +778,16 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + (await CallApi( useAsyncApi, this.Connection, - $"SELECT * FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().BeEquivalentTo(new EntityWithNullableProperty { Id = 1, Value = null }); + .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] @@ -1221,13 +1221,13 @@ Boolean useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( - $"INSERT INTO {Q("EntityWithNullableProperty")} ({Q("Id")}, {Q("Value")}) VALUES(1, NULL)" + $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + (await CallApi>( useAsyncApi, this.Connection, - $"SELECT {Q("Value")} FROM {Q("EntityWithNullableProperty")}", + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) .Should().Be(new(null)); diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index def0381..bfe8894 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -174,6 +174,7 @@ CREATE TABLE `Entity` `Int16Value` SMALLINT, `Int32Value` INT, `Int64Value` BIGINT, + `NullableBooleanValue` TINYINT(1) NULL, `SingleValue` FLOAT, `StringValue` TEXT, `TimeOnlyValue` TIME, @@ -195,13 +196,6 @@ CREATE TABLE `EntityWithEnumStoredAsInteger` ); GO - CREATE TABLE `EntityWithNullableProperty` - ( - `Id` BIGINT NOT NULL PRIMARY KEY, - `Value` BIGINT NULL - ); - GO - CREATE TABLE `MappingTestEntity` ( `Computed` INT AS (`Value`+999), @@ -281,9 +275,6 @@ FOR EACH ROW TRUNCATE TABLE `EntityWithEnumStoredAsInteger`; GO - TRUNCATE TABLE `EntityWithNullableProperty`; - GO - TRUNCATE TABLE `MappingTestEntity`; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 09e4b89..1ceec26 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -155,6 +155,7 @@ CREATE TABLE "Entity" "Int16Value" NUMBER(5), "Int32Value" NUMBER(10), "Int64Value" NUMBER(19), + "NullableBooleanValue" NUMBER(1) NULL, "SingleValue" BINARY_FLOAT, "StringValue" NVARCHAR2(2000), "TimeOnlyValue" INTERVAL DAY TO SECOND, @@ -183,13 +184,6 @@ CREATE TABLE "EntityWithEnumStoredAsInteger" ); GO - CREATE TABLE "EntityWithNullableProperty" - ( - "Id" NUMBER(19) NOT NULL PRIMARY KEY, - "Value" NUMBER(19) NULL - ); - GO - CREATE TABLE "MappingTestEntity" ( "Computed" GENERATED ALWAYS AS (("Value"+999)), @@ -234,9 +228,6 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS DROP TABLE IF EXISTS "EntityWithEnumStoredAsInteger" PURGE; GO - DROP TABLE IF EXISTS "EntityWithNullableProperty" PURGE; - GO - DROP TABLE IF EXISTS "MappingTestEntity" PURGE; GO @@ -258,9 +249,6 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; GO - TRUNCATE TABLE "EntityWithNullableProperty"; - GO - TRUNCATE TABLE "MappingTestEntity"; GO """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index 117f167..ddeea0d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -156,6 +156,7 @@ CREATE TABLE "Entity" "Int16Value" smallint, "Int32Value" integer, "Int64Value" bigint, + "NullableBooleanValue" boolean NULL, "SingleValue" real, "StringValue" text, "TimeOnlyValue" time, @@ -174,12 +175,6 @@ CREATE TABLE "EntityWithEnumStoredAsInteger" "Enum" integer NULL ); - CREATE TABLE "EntityWithNullableProperty" - ( - "Id" bigint NOT NULL PRIMARY KEY, - "Value" bigint NULL - ); - CREATE TABLE "MappingTestEntity" ( "Computed" integer GENERATED ALWAYS AS ("Value"+(999)), @@ -250,7 +245,6 @@ DELETE FROM "Entity" TRUNCATE TABLE "Entity"; TRUNCATE TABLE "EntityWithEnumStoredAsString"; TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; - TRUNCATE TABLE "EntityWithNullableProperty"; TRUNCATE TABLE "MappingTestEntity"; """; diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index fb7446a..ab523a6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -141,6 +141,7 @@ CREATE TABLE Entity Int16Value INTEGER, Int32Value INTEGER, Int64Value INTEGER, + NullableBooleanValue INTEGER NULL, SingleValue REAL, StringValue TEXT, TimeOnlyValue TEXT, @@ -165,12 +166,6 @@ CREATE TABLE EntityWithEnumStoredAsInteger Enum INTEGER ); - CREATE TABLE EntityWithNullableProperty - ( - Id INTEGER NOT NULL, - Value INTEGER NULL - ); - CREATE TABLE MappingTestEntity ( Computed INTEGER GENERATED ALWAYS AS (Value+999) VIRTUAL, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index 1544b93..c05a5ea 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -179,6 +179,7 @@ EnumValue NVARCHAR(200), Int16Value SMALLINT, Int32Value INT, Int64Value BIGINT, + NullableBooleanValue BIT NULL, SingleValue REAL, StringValue NVARCHAR(MAX), TimeOnlyValue TIME, @@ -207,13 +208,6 @@ Enum INT NULL ); GO - CREATE TABLE EntityWithNullableProperty - ( - Id BIGINT NOT NULL PRIMARY KEY, - Value BIGINT NULL - ); - GO - CREATE TABLE MappingTestEntity ( Computed AS ([Value]+(999)), @@ -287,9 +281,6 @@ DELETE FROM Entity TRUNCATE TABLE EntityWithEnumStoredAsInteger; GO - TRUNCATE TABLE EntityWithNullableProperty; - GO - TRUNCATE TABLE MappingTestEntity; GO """; diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs index dbbc7d5..aafc5d4 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs @@ -109,9 +109,9 @@ public void ToDebugString_ShouldReturnStringRepresentationOfValue() new Object().ToDebugString() .Should().Be("'{}' (System.Object)"); - new EntityWithEnumProperty { Enum = TestEnum.Value3 }.ToDebugString() + new EntityWithEnumStoredAsString { Enum = TestEnum.Value3, Id = 1}.ToDebugString() .Should().Be( - """'{"Enum":3}' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumProperty)""" + """'{"Enum":3,"Id":1}' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumStoredAsString)""" ); } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index bc42851..95caf1a 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -64,15 +64,15 @@ public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Id"); + dataReader.GetName(0).Returns("Value"); dataReader.GetFieldType(0).Returns(typeof(BigInteger)); Invoking(() => - EntityMaterializerFactory.GetMaterializer(dataReader) + EntityMaterializerFactory.GetMaterializer(dataReader) ) .Should().Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the column 'Id' returned by the SQL statement is not " + + $"The data type {typeof(BigInteger)} of the column 'Value' returned by the SQL statement is not " + "supported.*" ); } @@ -215,7 +215,7 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsInteger_Shoul dataReader.IsDBNull(0).Returns(false); dataReader.GetInt32(0).Returns((Int32)enumValue); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var entity = materializer(dataReader); @@ -236,14 +236,14 @@ public void dataReader.IsDBNull(0).Returns(false); dataReader.GetInt32(0).Returns(999); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) .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(EntityWithEnumProperty)}. See inner exception for details.*" + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( @@ -266,7 +266,7 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsString_Should dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(enumValue.ToString()); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var entity = materializer(dataReader); @@ -286,14 +286,14 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsStringNotMatc dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns("NonExistent"); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) .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(EntityWithEnumProperty)}. See inner exception for details.*" + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( @@ -675,18 +675,18 @@ public void Materializer_NullableEntityProperty_DataReaderFieldContainsNull_Shou dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Value"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); + dataReader.GetName(0).Returns("NullableBooleanValue"); + dataReader.GetFieldType(0).Returns(typeof(Boolean)); dataReader.IsDBNull(0).Returns(true); - dataReader.GetInt32(0).Throws(new SqlNullValueException()); + dataReader.GetBoolean(0).Throws(new SqlNullValueException()); var materializer = EntityMaterializerFactory - .GetMaterializer(dataReader); + .GetMaterializer(dataReader); var entity = Invoking(() => materializer(dataReader)) .Should().NotThrow().Subject; - entity.Value + entity.NullableBooleanValue .Should().BeNull(); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 985af42..212eff0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -2,8 +2,8 @@ public record Entity { - public Byte[] BytesValue { get; set; } = null!; public Boolean BooleanValue { get; set; } + public Byte[] BytesValue { get; set; } = null!; public Byte ByteValue { get; set; } public Char CharValue { get; set; } public DateOnly DateOnlyValue { get; set; } @@ -19,6 +19,8 @@ public record Entity public Int16 Int16Value { get; set; } public Int32 Int32Value { get; set; } public Int64 Int64Value { get; set; } + + public Boolean? NullableBooleanValue { get; set; } public Single SingleValue { get; set; } public String StringValue { get; set; } = null!; public TimeOnly TimeOnlyValue { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs index f4dd76a..86a2f9b 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs @@ -4,8 +4,8 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithDifferentCasingProperties { - public Byte[] BytesVALUE { get; set; } = null!; public Boolean BooleanVALUE { get; set; } + public Byte[] BytesVALUE { get; set; } = null!; public Byte ByteVALUE { get; set; } public Char CharVALUE { get; set; } public DateOnly DateOnlyVALUE { get; set; } @@ -25,6 +25,7 @@ public record EntityWithDifferentCasingProperties [NotMapped] public String? NotMappedProperty { get; set; } + public Boolean? NullableBooleanVALUE { get; set; } public Single SingleVALUE { get; set; } public String StringVALUE { get; set; } = null!; public TimeOnly TimeOnlyVALUE { get; set; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumProperty.cs deleted file mode 100644 index 4315e59..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumProperty.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public class EntityWithEnumProperty -{ - public TestEnum Enum { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableProperty.cs deleted file mode 100644 index 4cb1705..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithNullableProperty.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public record EntityWithNullableProperty -{ - [Key] - public Int64 Id { get; set; } - - public Int32? Value { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs index e14bf28..7542fbf 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs @@ -1,6 +1,6 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; -public record EntityWithPrivateConstructor +public record EntityWithPrivateConstructor : Entity { private EntityWithPrivateConstructor( Byte[] bytesValue, @@ -17,6 +17,7 @@ private EntityWithPrivateConstructor( Int16 int16Value, Int32 int32Value, Int64 int64Value, + Boolean? nullableBooleanValue, Single singleValue, String stringValue, TimeOnly timeOnlyValue, @@ -37,31 +38,10 @@ TimeSpan timeSpanValue this.Int16Value = int16Value; this.Int32Value = int32Value; this.Int64Value = int64Value; + this.NullableBooleanValue = nullableBooleanValue; this.SingleValue = singleValue; this.StringValue = stringValue; this.TimeOnlyValue = timeOnlyValue; this.TimeSpanValue = timeSpanValue; } - - public Byte[] BytesValue { get; set; } = null!; - public Boolean BooleanValue { get; } - public Byte ByteValue { get; } - public Char CharValue { get; } - public DateOnly DateOnlyValue { get; } - public DateTime DateTimeValue { get; } - public Decimal DecimalValue { get; } - public Double DoubleValue { get; } - public TestEnum EnumValue { get; } - public Guid GuidValue { get; } - - [Key] - public Int64 Id { get; } - - public Int16 Int16Value { get; } - public Int32 Int32Value { get; } - public Int64 Int64Value { get; } - public Single SingleValue { get; } - public String StringValue { get; } - public TimeOnly TimeOnlyValue { get; } - public TimeSpan TimeSpanValue { get; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs index 50e6434..94b700a 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs @@ -1,30 +1,8 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; -public record EntityWithPrivateParameterlessConstructor +public record EntityWithPrivateParameterlessConstructor : Entity { private EntityWithPrivateParameterlessConstructor() { } - - public Byte[] BytesValue { get; set; } = null!; - public Boolean BooleanValue { get; set; } - public Byte ByteValue { get; set; } - public Char CharValue { get; set; } - public DateOnly DateOnlyValue { get; set; } - public DateTime DateTimeValue { get; set; } - public Decimal DecimalValue { get; set; } - public Double DoubleValue { get; set; } - public TestEnum EnumValue { get; set; } - public Guid GuidValue { get; set; } - - [Key] - public Int64 Id { get; set; } - - public Int16 Int16Value { get; set; } - public Int32 Int32Value { get; set; } - public Int64 Int64Value { get; set; } - public Single SingleValue { get; set; } - public String StringValue { get; set; } = null!; - public TimeOnly TimeOnlyValue { get; set; } - public TimeSpan TimeSpanValue { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index b73b3bc..6113a44 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -4,7 +4,7 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; -public record EntityWithPublicConstructor +public record EntityWithPublicConstructor : Entity { public EntityWithPublicConstructor( Byte[] bytesValue, @@ -21,6 +21,7 @@ public EntityWithPublicConstructor( Int16 int16Value, Int32 int32Value, Int64 int64Value, + Boolean? nullableBooleanValue, Single singleValue, String stringValue, TimeOnly timeOnlyValue, @@ -41,31 +42,10 @@ TimeSpan timeSpanValue this.Int16Value = int16Value; this.Int32Value = int32Value; this.Int64Value = int64Value; + this.NullableBooleanValue = nullableBooleanValue; this.SingleValue = singleValue; this.StringValue = stringValue; this.TimeOnlyValue = timeOnlyValue; this.TimeSpanValue = timeSpanValue; } - - public Byte[] BytesValue { get; set; } = null!; - public Boolean BooleanValue { get; } - public Byte ByteValue { get; } - public Char CharValue { get; } - public DateOnly DateOnlyValue { get; } - public DateTime DateTimeValue { get; } - public Decimal DecimalValue { get; } - public Double DoubleValue { get; } - public TestEnum EnumValue { get; } - public Guid GuidValue { get; } - - [Key] - public Int64 Id { get; } - - public Int16 Int16Value { get; } - public Int32 Int32Value { get; } - public Int64 Int64Value { get; } - public Single SingleValue { get; } - public String StringValue { get; } - public TimeOnly TimeOnlyValue { get; } - public TimeSpan TimeSpanValue { get; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithUnsupportedPropertyType.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithUnsupportedPropertyType.cs deleted file mode 100644 index f7750eb..0000000 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithUnsupportedPropertyType.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Numerics; - -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; - -public class EntityWithUnsupportedPropertyType -{ - public BigInteger Id { get; set; } -} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 0326a9d..f70f344 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -338,7 +338,11 @@ public Object Create(Object request, ISpecimenContext context) var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(propertyInfo.DeclaringType!); if ( - entityTypeMetadata.AllPropertiesByPropertyName.TryGetValue(propertyInfo.Name, out var propertyMetadata) && + entityTypeMetadata.AllPropertiesByPropertyName.TryGetValue( + propertyInfo.Name, + out var propertyMetadata + ) + && propertyMetadata.IsIgnored ) { From 5664a4ecc1ffa2481cf280876a06a9e5cae0e3c8 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 4 Feb 2026 15:31:43 +0100 Subject: [PATCH 08/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 29 ++++ .../Configuration/EntityPropertyBuilder.cs | 6 +- ...ConnectionExtensions.QueryFirstOfTTests.cs | 10 +- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 10 +- .../DbConnectionExtensions.QueryOfTTests.cs | 8 +- ...onnectionExtensions.QuerySingleOfTTests.cs | 8 +- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 8 +- .../EntityPropertyBuilderTests.cs | 14 +- .../Extensions/ObjectExtensionsTests.cs | 2 +- .../EntityMaterializerFactoryTests.cs | 134 +++++++++--------- 10 files changed, 129 insertions(+), 100 deletions(-) diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index 06797d5..4241eb3 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -235,6 +235,7 @@ public List ExecuteReader_Manually() TOP ({ExecuteReader_EntitiesPerOperation}) [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -265,6 +266,7 @@ public List ExecuteReader_Manually() { Id = dataReader.GetInt64(ordinal++), BooleanValue = dataReader.GetBoolean(ordinal++), + BytesValue = (Byte[])dataReader.GetValue(ordinal++), ByteValue = dataReader.GetByte(ordinal++), CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), @@ -305,6 +307,7 @@ public List ExecuteReader_DbConnectionPlus() TOP ({ExecuteReader_EntitiesPerOperation}) [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -334,6 +337,7 @@ public List ExecuteReader_DbConnectionPlus() { Id = dataReader.GetInt64(ordinal++), BooleanValue = dataReader.GetBoolean(ordinal++), + BytesValue = (Byte[])dataReader.GetValue(ordinal++), ByteValue = dataReader.GetByte(ordinal++), CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), @@ -494,6 +498,7 @@ INSERT INTO [Entity] ( [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -514,6 +519,7 @@ INSERT INTO [Entity] ( @Id, @BooleanValue, + @BytesValue, @ByteValue, @CharValue, @DateOnlyValue, @@ -538,6 +544,9 @@ INSERT INTO [Entity] var booleanValueParameter = new SqlParameter(); booleanValueParameter.ParameterName = "@BooleanValue"; + var bytesValueParameter = new SqlParameter(); + bytesValueParameter.ParameterName = "@BytesValue"; + var byteValueParameter = new SqlParameter(); byteValueParameter.ParameterName = "@ByteValue"; @@ -585,6 +594,7 @@ INSERT INTO [Entity] command.Parameters.Add(idParameter); command.Parameters.Add(booleanValueParameter); + command.Parameters.Add(bytesValueParameter); command.Parameters.Add(byteValueParameter); command.Parameters.Add(charValueParameter); command.Parameters.Add(dateOnlyParameter); @@ -605,6 +615,7 @@ INSERT INTO [Entity] { idParameter.Value = entity.Id; booleanValueParameter.Value = entity.BooleanValue; + bytesValueParameter.Value = entity.BytesValue; byteValueParameter.Value = entity.ByteValue; charValueParameter.Value = entity.CharValue; dateOnlyParameter.Value = entity.DateOnlyValue; @@ -668,6 +679,7 @@ INSERT INTO [Entity] ( [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -688,6 +700,7 @@ INSERT INTO [Entity] ( @Id, @BooleanValue, + @BytesValue, @ByteValue, @CharValue, @DateOnlyValue, @@ -707,6 +720,7 @@ INSERT INTO [Entity] """; command.Parameters.Add(new("@Id", entity.Id)); command.Parameters.Add(new("@BooleanValue", entity.BooleanValue)); + command.Parameters.Add(new("@BytesValue", entity.BytesValue)); command.Parameters.Add(new("@ByteValue", entity.ByteValue)); command.Parameters.Add(new("@CharValue", entity.CharValue)); command.Parameters.Add(new("@DateOnlyValue", entity.DateOnlyValue)); @@ -849,6 +863,7 @@ public List Query_Dynamic_Manually() TOP ({Query_Dynamic_EntitiesPerOperation}) [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -878,6 +893,7 @@ public List Query_Dynamic_Manually() entity.Id = dataReader.GetInt64(ordinal++); entity.BooleanValue = dataReader.GetBoolean(ordinal++); + entity.BytesValue = (Byte[])dataReader.GetValue(ordinal++); entity.ByteValue = dataReader.GetByte(ordinal++); entity.CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] @@ -1011,6 +1027,7 @@ public List Query_Entities_Manually() TOP ({Query_Entities_EntitiesPerOperation}) [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -1040,6 +1057,7 @@ public List Query_Entities_Manually() { Id = dataReader.GetInt64(ordinal++), BooleanValue = dataReader.GetBoolean(ordinal++), + BytesValue = (Byte[])dataReader.GetValue(ordinal++), ByteValue = dataReader.GetByte(ordinal++), CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), @@ -1202,6 +1220,7 @@ public List TemporaryTable_ComplexObjects_Manually() $""" CREATE TABLE [#Entities] ( [BooleanValue] BIT, + [BytesValue] VARBINARY(MAX), [ByteValue] TINYINT, [CharValue] CHAR(1), [DateOnlyValue] DATE, @@ -1234,6 +1253,7 @@ [TimeSpanValue] TIME SELECT [Id], [BooleanValue], + [BytesValue], [ByteValue], [CharValue], [DateOnlyValue], @@ -1264,6 +1284,7 @@ [TimeSpanValue] TIME { Id = dataReader.GetInt64(ordinal++), BooleanValue = dataReader.GetBoolean(ordinal++), + BytesValue = (Byte[])dataReader.GetValue(ordinal++), ByteValue = dataReader.GetByte(ordinal++), CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), DateOnlyValue = DateOnly.FromDateTime((DateTime)dataReader.GetValue(ordinal++)), @@ -1427,6 +1448,7 @@ public void UpdateEntities_Manually() command.CommandText = """ UPDATE [Entity] SET [BooleanValue] = @BooleanValue, + [BytesValue] = @BytesValue, [ByteValue] = @ByteValue, [CharValue] = @CharValue, [DateOnlyValue] = @DateOnlyValue, @@ -1451,6 +1473,9 @@ UPDATE [Entity] var booleanValueParameter = new SqlParameter(); booleanValueParameter.ParameterName = "@BooleanValue"; + var bytesValueParameter = new SqlParameter(); + bytesValueParameter.ParameterName = "@BytesValue"; + var byteValueParameter = new SqlParameter(); byteValueParameter.ParameterName = "@ByteValue"; @@ -1498,6 +1523,7 @@ UPDATE [Entity] command.Parameters.Add(idParameter); command.Parameters.Add(booleanValueParameter); + command.Parameters.Add(bytesValueParameter); command.Parameters.Add(byteValueParameter); command.Parameters.Add(charValueParameter); command.Parameters.Add(dateOnlyValueParameter); @@ -1518,6 +1544,7 @@ UPDATE [Entity] { idParameter.Value = updatedEntity.Id; booleanValueParameter.Value = updatedEntity.BooleanValue; + bytesValueParameter.Value = updatedEntity.BytesValue; byteValueParameter.Value = updatedEntity.ByteValue; charValueParameter.Value = updatedEntity.CharValue; dateOnlyValueParameter.Value = updatedEntity.DateOnlyValue; @@ -1584,6 +1611,7 @@ public void UpdateEntity_Manually() command.CommandText = """ UPDATE [Entity] SET [BooleanValue] = @BooleanValue, + [BytesValue] = @BytesValue, [ByteValue] = @ByteValue, [CharValue] = @CharValue, [DateOnlyValue] = @DateOnlyValue, @@ -1603,6 +1631,7 @@ UPDATE [Entity] """; command.Parameters.Add(new("@Id", updatedEntity.Id)); command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue)); + command.Parameters.Add(new("@BytesValue", updatedEntity.BytesValue)); command.Parameters.Add(new("@ByteValue", updatedEntity.ByteValue)); command.Parameters.Add(new("@CharValue", updatedEntity.CharValue)); command.Parameters.Add(new("@DateOnlyValue", updatedEntity.DateOnlyValue)); diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs index cc4abb5..7559013 100644 --- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs +++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs @@ -14,17 +14,17 @@ public sealed class EntityPropertyBuilder : IEntityPropertyBuilder /// /// /// - /// is . + /// is . /// /// /// /// - /// is . + /// is . /// /// /// /// - /// is whitespace. + /// is whitespace. internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String propertyName) { ArgumentNullException.ThrowIfNull(entityTypeBuilder); diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 67d8340..95ef969 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -517,7 +517,7 @@ Boolean useAsyncApi $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entitiesWithDifferentCasingProperties[0]); + .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); } [Theory] @@ -737,8 +737,8 @@ public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_S ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -1137,8 +1137,8 @@ public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsN ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index bfa5e7c..d96273d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -535,7 +535,7 @@ Boolean useAsyncApi $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken )) - .Should().Be(entitiesWithDifferentCasingProperties[0]); + .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); } [Theory] @@ -761,8 +761,8 @@ Boolean useAsyncApi ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -1182,8 +1182,8 @@ Boolean useAsyncApi ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 6b4ff72..9197b1a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -744,8 +744,8 @@ public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_Should ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -1136,8 +1136,8 @@ public Task Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_S ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index e6537d6..426d8c1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -738,8 +738,8 @@ public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -1160,8 +1160,8 @@ Boolean useAsyncApi ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 6b8f9e8..e025054 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -765,8 +765,8 @@ Boolean useAsyncApi ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -1207,8 +1207,8 @@ Boolean useAsyncApi ) .Should().ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the corresponding " + - $"field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs index 767dcb4..730a52e 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -6,7 +6,7 @@ public class EntityPropertyBuilderTests : UnitTestsBase public void ColumnName_Configured_ShouldReturnColumnName() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + builder.HasColumnName("Identifier"); ((IEntityPropertyBuilder)builder).ColumnName @@ -26,7 +26,7 @@ public void ColumnName_NotConfigured_ShouldReturnNull() public void Freeze_ShouldFreezeBuilder() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + ((IFreezable)builder).Freeze(); Invoking(() => builder.HasColumnName("Identifier")) @@ -62,7 +62,7 @@ public void Freeze_ShouldFreezeBuilder() public void HasColumnName_ShouldSetColumnName() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + builder.HasColumnName("Identifier"); ((IEntityPropertyBuilder)builder).ColumnName @@ -155,7 +155,7 @@ public void IsIdentity_NotConfigured_ShouldReturnFalse() public void IsIdentity_OtherPropertyIsAlreadyMarked_ShouldThrow() { var entityTypeBuilder = new EntityTypeBuilder(); - + entityTypeBuilder.Property(a => a.Id).IsIdentity(); var propertyBuilder = new EntityPropertyBuilder(entityTypeBuilder, "NotId"); @@ -203,7 +203,7 @@ public void IsIgnored_NotConfigured_ShouldReturnFalse() public void IsIgnored_ShouldMarkPropertyAsIgnored() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + builder.IsIgnored(); ((IEntityPropertyBuilder)builder).IsIgnored @@ -234,7 +234,7 @@ public void IsKey_NotConfigured_ShouldReturnFalse() public void IsKey_ShouldMarkPropertyAsKey() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + builder.IsKey(); ((IEntityPropertyBuilder)builder).IsKey @@ -265,7 +265,7 @@ public void IsRowVersion_NotConfigured_ShouldReturnFalse() public void IsRowVersion_ShouldMarkPropertyAsRowVersion() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - + builder.IsRowVersion(); ((IEntityPropertyBuilder)builder).IsRowVersion diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs index aafc5d4..193f410 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs @@ -109,7 +109,7 @@ public void ToDebugString_ShouldReturnStringRepresentationOfValue() new Object().ToDebugString() .Should().Be("'{}' (System.Object)"); - new EntityWithEnumStoredAsString { Enum = TestEnum.Value3, Id = 1}.ToDebugString() + new EntityWithEnumStoredAsString { Enum = TestEnum.Value3, Id = 1 }.ToDebugString() .Should().Be( """'{"Enum":3,"Id":1}' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumStoredAsString)""" ); diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index 95caf1a..40acbc6 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -77,6 +77,73 @@ public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() ); } + [Fact] + public void + Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() + { + var dataReader = Substitute.For(); + + dataReader.FieldCount.Returns(1); + + dataReader.GetName(0).Returns("CharValue"); + dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(String.Empty); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + Invoking(() => materializer(dataReader)) + .Should().Throw() + .WithMessage( + "The column 'CharValue' 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(Entity)}. 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." + ); + + dataReader.GetString(0).Returns("ab"); + + Invoking(() => materializer(dataReader)) + .Should().Throw() + .WithMessage( + "The column 'CharValue' 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(Entity)}. 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 + Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() + { + var dataReader = Substitute.For(); + + dataReader.FieldCount.Returns(1); + + var character = Generate.Single(); + + dataReader.GetName(0).Returns("CharValue"); + dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(character.ToString()); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + var entity = materializer(dataReader); + + entity.CharValue + .Should().Be(character); + } + [Fact] public void Materializer_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() { @@ -579,73 +646,6 @@ public void .Should().BeEquivalentTo(entities[0]); } - [Fact] - public void - Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(String.Empty); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - Invoking(() => materializer(dataReader)) - .Should().Throw() - .WithMessage( - "The column 'CharValue' 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(Entity)}. 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." - ); - - dataReader.GetString(0).Returns("ab"); - - Invoking(() => materializer(dataReader)) - .Should().Throw() - .WithMessage( - "The column 'CharValue' 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(Entity)}. 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 - Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - var character = Generate.Single(); - - dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(character.ToString()); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - var entity = materializer(dataReader); - - entity.CharValue - .Should().Be(character); - } - [Fact] public void Materializer_NonNullableEntityProperty_DataReaderFieldContainsNull_ShouldThrow() From 751c91b2507de2eb69d8667babdbd2369798424b Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 6 Feb 2026 18:05:23 +0100 Subject: [PATCH 09/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- README.md | 128 +- .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 1373 ++++++++++------- .../DapperTypeHandlers/GuidTypeHandler.cs | 14 + .../DapperTypeHandlers/TimeSpanTypeHandler.cs | 14 + .../DbConnectionPlus.Benchmarks.csproj | 2 + .../DbConnectionPlus.Benchmarks/Program.cs | 169 +- .../TestHelpers/DelayDbCommandFactory.cs | 1 + .../TestData/BenchmarkEntity.cs | 26 + .../TestData/Entity.cs | 2 +- 9 files changed, 1116 insertions(+), 613 deletions(-) create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs create mode 100644 tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs diff --git a/README.md b/README.md index cc1ee23..de1247d 100644 --- a/README.md +++ b/README.md @@ -1204,10 +1204,14 @@ See [SqlServerDatabaseAdapter](https://github.com/rent-a-developer/DbConnectionP for an example implementation of a database adapter. ## Benchmarks -DbConnectionPlus is designed to have a minimal performance and allocation overhead compared to using -`DbCommand` manually. +DbConnectionPlus is designed to have a minimal performance and allocation overhead compared to using `DbCommand` +manually. + +All benchmarks are performed using SQLite in-memory databases, which is a worst-case scenario for DbConnectionPlus +because the overhead of using DbConnectionPlus is more noticeable when the executed SQL statements are very fast. ``` + BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.7623/24H2/2024Update/HudsonValley) 12th Gen Intel Core i9-12900K 3.19GHz, 1 CPU, 24 logical and 16 physical cores .NET SDK 10.0.102 @@ -1217,75 +1221,63 @@ BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.7623/24H2/2024Update/HudsonValle MinIterationTime=100ms OutlierMode=DontRemove Server=True InvocationCount=1 MaxIterationCount=20 UnrollFactor=1 WarmupCount=10 -``` - -| Method | Mean | Error | StdDev | Median | P90 | P95 | Ratio | RatioSD | Allocated | Alloc Ratio | -|----------------------------------------------- |-------------:|-------------:|-------------:|-------------:|-------------:|-------------:|-------------:|--------:|----------:|------------:| -| **DeleteEntities_Manually** | **14,672.73 μs** | **3,387.316 μs** | **3,900.839 μs** | **14,243.07 μs** | **19,825.26 μs** | **20,144.38 μs** | **baseline** | **** | **101.62 KB** | **** | -| DeleteEntities_DbConnectionPlus | 6,717.47 μs | 721.336 μs | 830.692 μs | 6,372.96 μs | 7,698.83 μs | 8,539.65 μs | 2.21x faster | 0.62x | 17.17 KB | 5.92x less | -| | | | | | | | | | | | -| **DeleteEntity_Manually** | **188.68 μs** | **24.244 μs** | **27.920 μs** | **198.98 μs** | **212.97 μs** | **217.73 μs** | **baseline** | **** | **2.1 KB** | **** | -| DeleteEntity_DbConnectionPlus | 191.09 μs | 27.642 μs | 31.833 μs | 197.78 μs | 230.76 μs | 235.60 μs | 1.04x slower | 0.24x | 2.1 KB | 1.00x more | -| | | | | | | | | | | | -| **ExecuteNonQuery_Manually** | **158.13 μs** | **24.189 μs** | **27.856 μs** | **157.52 μs** | **169.30 μs** | **178.27 μs** | **baseline** | **** | **2.1 KB** | **** | -| ExecuteNonQuery_DbConnectionPlus | 165.12 μs | 13.165 μs | 15.161 μs | 166.52 μs | 177.50 μs | 180.82 μs | 1.07x slower | 0.19x | 2.81 KB | 1.33x more | -| | | | | | | | | | | | -| **ExecuteReader_Manually** | **183.91 μs** | **9.815 μs** | **11.303 μs** | **179.93 μs** | **203.46 μs** | **211.49 μs** | **baseline** | **** | **50.54 KB** | **** | -| ExecuteReader_DbConnectionPlus | 173.84 μs | 4.810 μs | 5.539 μs | 173.00 μs | 180.74 μs | 186.21 μs | 1.06x faster | 0.07x | 50.83 KB | 1.01x more | -| | | | | | | | | | | | -| **ExecuteScalar_Manually** | **73.79 μs** | **2.411 μs** | **2.777 μs** | **73.54 μs** | **78.35 μs** | **78.58 μs** | **baseline** | **** | **3.04 KB** | **** | -| ExecuteScalar_DbConnectionPlus | 77.81 μs | 5.661 μs | 6.519 μs | 76.63 μs | 81.00 μs | 87.09 μs | 1.06x slower | 0.09x | 3.77 KB | 1.24x more | -| | | | | | | | | | | | -| **Exists_Manually** | **56.36 μs** | **13.725 μs** | **15.806 μs** | **48.61 μs** | **78.16 μs** | **86.30 μs** | **baseline** | **** | **2.63 KB** | **** | -| Exists_DbConnectionPlus | 51.36 μs | 2.946 μs | 3.392 μs | 50.43 μs | 53.15 μs | 55.69 μs | 1.10x faster | 0.31x | 3.34 KB | 1.27x more | -| | | | | | | | | | | | -| **InsertEntities_Manually** | **17,619.46 μs** | **2,472.686 μs** | **2,847.548 μs** | **18,691.91 μs** | **20,290.38 μs** | **20,702.41 μs** | **baseline** | **** | **517.03 KB** | **** | -| InsertEntities_DbConnectionPlus | 21,575.08 μs | 2,280.957 μs | 2,626.754 μs | 23,062.28 μs | 23,656.92 μs | 24,692.07 μs | 1.25x slower | 0.24x | 437.87 KB | 1.18x less | -| | | | | | | | | | | | -| **InsertEntity_Manually** | **256.13 μs** | **16.084 μs** | **18.522 μs** | **257.27 μs** | **264.82 μs** | **285.02 μs** | **baseline** | **** | **8.57 KB** | **** | -| InsertEntity_DbConnectionPlus | 280.06 μs | 37.113 μs | 42.740 μs | 259.51 μs | 341.86 μs | 355.55 μs | 1.10x slower | 0.18x | 8.72 KB | 1.02x more | -| | | | | | | | | | | | -| **Parameter_Manually** | **57.55 μs** | **10.088 μs** | **11.618 μs** | **56.99 μs** | **65.92 μs** | **67.72 μs** | **baseline** | **** | **5.43 KB** | **** | -| Parameter_DbConnectionPlus | 52.35 μs | 5.561 μs | 6.404 μs | 50.31 μs | 55.65 μs | 57.76 μs | 1.11x faster | 0.24x | 7.34 KB | 1.35x more | -| | | | | | | | | | | | -| **Query_Dynamic_Manually** | **315.14 μs** | **12.468 μs** | **14.358 μs** | **312.52 μs** | **327.40 μs** | **333.20 μs** | **baseline** | **** | **195.41 KB** | **** | -| Query_Dynamic_DbConnectionPlus | 203.51 μs | 16.883 μs | 19.442 μs | 197.45 μs | 215.26 μs | 224.23 μs | 1.56x faster | 0.13x | 136.38 KB | 1.43x less | -| | | | | | | | | | | | -| **Query_Scalars_Manually** | **74.03 μs** | **2.179 μs** | **2.510 μs** | **73.53 μs** | **77.74 μs** | **77.97 μs** | **baseline** | **** | **2.11 KB** | **** | -| Query_Scalars_DbConnectionPlus | 90.07 μs | 11.385 μs | 13.111 μs | 89.36 μs | 102.01 μs | 104.18 μs | 1.22x slower | 0.18x | 7.26 KB | 3.44x more | -| | | | | | | | | | | | -| **Query_Entities_Manually** | **251.81 μs** | **6.020 μs** | **6.933 μs** | **250.85 μs** | **260.06 μs** | **262.91 μs** | **baseline** | **** | **51.3 KB** | **** | -| Query_Entities_DbConnectionPlus | 263.71 μs | 6.792 μs | 7.822 μs | 260.52 μs | 271.74 μs | 274.68 μs | 1.05x slower | 0.04x | 54.37 KB | 1.06x more | -| | | | | | | | | | | | -| **Query_ValueTuples_Manually** | **180.00 μs** | **8.115 μs** | **9.345 μs** | **177.02 μs** | **185.67 μs** | **194.46 μs** | **baseline** | **** | **18.07 KB** | **** | -| Query_ValueTuples_DbConnectionPlus | 190.84 μs | 9.986 μs | 11.499 μs | 188.72 μs | 200.44 μs | 217.74 μs | 1.06x slower | 0.08x | 29.45 KB | 1.63x more | -| | | | | | | | | | | | -| **TemporaryTable_ComplexObjects_Manually** | **8,267.76 μs** | **2,480.979 μs** | **2,857.099 μs** | **7,983.17 μs** | **11,502.49 μs** | **11,944.48 μs** | **baseline** | **** | **132.52 KB** | **** | -| TemporaryTable_ComplexObjects_DbConnectionPlus | 6,636.36 μs | 614.018 μs | 707.104 μs | 6,582.66 μs | 7,309.96 μs | 7,595.85 μs | 1.26x faster | 0.44x | 137.92 KB | 1.04x more | -| | | | | | | | | | | | -| **TemporaryTable_ScalarValues_Manually** | **4,784.75 μs** | **566.815 μs** | **652.745 μs** | **4,620.07 μs** | **4,950.02 μs** | **5,609.07 μs** | **baseline** | **** | **177.18 KB** | **** | -| TemporaryTable_ScalarValues_DbConnectionPlus | 4,897.28 μs | 393.307 μs | 452.933 μs | 4,735.95 μs | 5,696.50 μs | 5,701.09 μs | 1.04x slower | 0.14x | 304.21 KB | 1.72x more | -| | | | | | | | | | | | -| **UpdateEntities_Manually** | **23,744.24 μs** | **3,367.021 μs** | **3,877.466 μs** | **22,203.37 μs** | **30,059.37 μs** | **32,188.80 μs** | **baseline** | **** | **530.26 KB** | **** | -| UpdateEntities_DbConnectionPlus | 34,624.61 μs | 3,734.617 μs | 4,300.790 μs | 34,084.29 μs | 35,478.88 μs | 39,301.47 μs | 1.49x slower | 0.28x | 450.27 KB | 1.18x less | -| | | | | | | | | | | | -| **UpdateEntity_Manually** | **300.87 μs** | **28.337 μs** | **32.633 μs** | **291.67 μs** | **350.76 μs** | **366.50 μs** | **baseline** | **** | **9.5 KB** | **** | -| UpdateEntity_DbConnectionPlus | 344.98 μs | 49.278 μs | 56.749 μs | 356.24 μs | 393.93 μs | 408.69 μs | 1.16x slower | 0.22x | 9.67 KB | 1.02x more | - -Please keep in mind that benchmarking is tricky when SQL Server is involved. -So take these benchmark results with a grain of salt. -### Running the benchmarks -To run the benchmarks, ensure you have an SQL Server instance available. -The benchmarks will create a database named `DbConnectionPlusTests`, so make sure your SQL user has the necessary -rights. - -Set the environment variable `ConnectionString_SqlServer` to the connection string to the SQL Server instance: -```shell -set ConnectionString_SqlServer="Data Source=.\SqlServer;Integrated Security=True;Encrypt=False;MultipleActiveResultSets=True" ``` +| Method | Mean | Error | StdDev | Median | P90 | P95 | Ratio | RatioSD | Allocated | Alloc Ratio | +|----------------------------------------------- |-------------:|--------------:|--------------:|-------------:|-------------:|-------------:|-------------:|--------:|-----------:|------------:| +| **DeleteEntities_Manually** | **15,812.58 μs** | **3,269.782 μs** | **3,765.486 μs** | **15,096.66 μs** | **20,554.52 μs** | **23,427.08 μs** | **baseline** | **** | **19.32 KB** | **** | +| DeleteEntities_DbConnectionPlus | 26,344.72 μs | 7,115.300 μs | 8,193.990 μs | 24,756.73 μs | 30,942.44 μs | 44,994.48 μs | 1.75x slower | 0.65x | 19.69 KB | 1.02x more | +| | | | | | | | | | | | +| **DeleteEntity_Manually** | **187.61 μs** | **37.759 μs** | **43.483 μs** | **171.45 μs** | **271.18 μs** | **283.79 μs** | **baseline** | **** | **1.29 KB** | **** | +| DeleteEntity_DbConnectionPlus | 231.79 μs | 77.386 μs | 89.118 μs | 207.50 μs | 360.04 μs | 411.14 μs | 1.29x slower | 0.54x | 1.62 KB | 1.25x more | +| | | | | | | | | | | | +| **ExecuteNonQuery_Manually** | **227.38 μs** | **44.374 μs** | **51.101 μs** | **216.12 μs** | **283.89 μs** | **298.40 μs** | **baseline** | **** | **1.29 KB** | **** | +| ExecuteNonQuery_DbConnectionPlus | 215.31 μs | 52.704 μs | 60.694 μs | 188.86 μs | 291.85 μs | 297.67 μs | 1.13x faster | 0.39x | 2 KB | 1.55x more | +| | | | | | | | | | | | +| **ExecuteReader_Manually** | **249.64 μs** | **45.099 μs** | **51.936 μs** | **239.96 μs** | **319.38 μs** | **352.49 μs** | **baseline** | **** | **60.46 KB** | **** | +| ExecuteReader_DbConnectionPlus | 299.73 μs | 80.094 μs | 92.236 μs | 293.57 μs | 408.75 μs | 440.39 μs | 1.24x slower | 0.44x | 61.46 KB | 1.02x more | +| | | | | | | | | | | | +| **ExecuteScalar_Manually** | **77.82 μs** | **17.007 μs** | **19.586 μs** | **71.95 μs** | **101.45 μs** | **124.05 μs** | **baseline** | **** | **2.12 KB** | **** | +| ExecuteScalar_DbConnectionPlus | 97.83 μs | 7.468 μs | 8.600 μs | 97.02 μs | 108.74 μs | 111.83 μs | 1.32x slower | 0.28x | 2.85 KB | 1.35x more | +| | | | | | | | | | | | +| **Exists_Manually** | **61.17 μs** | **7.029 μs** | **8.095 μs** | **59.87 μs** | **70.26 μs** | **72.24 μs** | **baseline** | **** | **1.96 KB** | **** | +| Exists_DbConnectionPlus | 80.57 μs | 17.980 μs | 20.706 μs | 74.52 μs | 93.71 μs | 97.91 μs | 1.34x slower | 0.38x | 2.67 KB | 1.36x more | +| | | | | | | | | | | | +| **InsertEntities_Manually** | **37,252.98 μs** | **16,064.582 μs** | **18,499.997 μs** | **31,081.23 μs** | **45,555.75 μs** | **88,905.67 μs** | **baseline** | **** | **5726.4 KB** | **** | +| InsertEntities_DbConnectionPlus | 28,925.35 μs | 5,155.758 μs | 5,937.379 μs | 28,238.99 μs | 34,674.36 μs | 40,572.46 μs | 1.34x faster | 0.70x | 5760.81 KB | 1.01x more | +| | | | | | | | | | | | +| **InsertEntity_Manually** | **509.25 μs** | **137.899 μs** | **158.805 μs** | **480.71 μs** | **691.81 μs** | **716.65 μs** | **baseline** | **** | **61.42 KB** | **** | +| InsertEntity_DbConnectionPlus | 407.38 μs | 47.504 μs | 54.705 μs | 388.55 μs | 468.85 μs | 528.37 μs | 1.27x faster | 0.42x | 62.49 KB | 1.02x more | +| | | | | | | | | | | | +| **Parameter_Manually** | **104.04 μs** | **38.319 μs** | **44.128 μs** | **79.73 μs** | **177.14 μs** | **190.65 μs** | **baseline** | **** | **4.38 KB** | **** | +| Parameter_DbConnectionPlus | 110.40 μs | 43.333 μs | 49.902 μs | 106.35 μs | 155.04 μs | 163.53 μs | 1.21x slower | 0.66x | 6.31 KB | 1.44x more | +| | | | | | | | | | | | +| **Query_Dynamic_Manually** | **683.58 μs** | **172.616 μs** | **198.785 μs** | **656.92 μs** | **817.59 μs** | **846.18 μs** | **baseline** | **** | **215.27 KB** | **** | +| Query_Dynamic_DbConnectionPlus | 278.25 μs | 79.237 μs | 91.250 μs | 240.19 μs | 359.03 μs | 530.59 μs | 2.62x faster | 0.92x | 162.94 KB | 1.32x less | +| | | | | | | | | | | | +| **Query_Scalars_Manually** | **117.42 μs** | **12.106 μs** | **13.941 μs** | **119.76 μs** | **128.44 μs** | **135.42 μs** | **baseline** | **** | **1.07 KB** | **** | +| Query_Scalars_DbConnectionPlus | 116.26 μs | 23.145 μs | 26.653 μs | 118.95 μs | 146.82 μs | 155.42 μs | 1.00x slower | 0.26x | 6.24 KB | 5.85x more | +| | | | | | | | | | | | +| **Query_Entities_Manually** | **391.99 μs** | **32.586 μs** | **37.526 μs** | **383.52 μs** | **442.81 μs** | **459.92 μs** | **baseline** | **** | **61.92 KB** | **** | +| Query_Entities_DbConnectionPlus | 497.88 μs | 169.969 μs | 195.737 μs | 495.08 μs | 768.03 μs | 773.60 μs | 1.28x slower | 0.51x | 72.26 KB | 1.17x more | +| | | | | | | | | | | | +| **Query_ValueTuples_Manually** | **182.67 μs** | **40.380 μs** | **46.502 μs** | **169.54 μs** | **247.10 μs** | **263.69 μs** | **baseline** | **** | **16.73 KB** | **** | +| Query_ValueTuples_DbConnectionPlus | 183.50 μs | 34.047 μs | 39.209 μs | 182.46 μs | 228.38 μs | 246.76 μs | 1.06x slower | 0.33x | 28.67 KB | 1.71x more | +| | | | | | | | | | | | +| **TemporaryTable_ComplexObjects_Manually** | **9,132.97 μs** | **1,686.089 μs** | **1,941.703 μs** | **8,385.77 μs** | **11,329.97 μs** | **13,415.61 μs** | **baseline** | **** | **360 KB** | **** | +| TemporaryTable_ComplexObjects_DbConnectionPlus | 9,045.06 μs | 1,076.753 μs | 1,239.991 μs | 8,996.34 μs | 10,270.86 μs | 10,485.35 μs | 1.03x slower | 0.23x | 373.24 KB | 1.04x more | +| | | | | | | | | | | | +| **TemporaryTable_ScalarValues_Manually** | **7,537.41 μs** | **936.994 μs** | **1,079.044 μs** | **7,167.69 μs** | **8,462.54 μs** | **9,957.15 μs** | **baseline** | **** | **176.13 KB** | **** | +| TemporaryTable_ScalarValues_DbConnectionPlus | 5,852.64 μs | 836.596 μs | 963.425 μs | 5,843.33 μs | 7,027.70 μs | 7,382.98 μs | 1.32x faster | 0.29x | 303.31 KB | 1.72x more | +| | | | | | | | | | | | +| **UpdateEntities_Manually** | **40,746.85 μs** | **12,388.517 μs** | **14,266.635 μs** | **34,575.09 μs** | **59,380.97 μs** | **61,547.65 μs** | **baseline** | **** | **5708.12 KB** | **** | +| UpdateEntities_DbConnectionPlus | 33,742.35 μs | 10,522.508 μs | 12,117.736 μs | 29,627.05 μs | 38,988.11 μs | 43,043.74 μs | 1.29x faster | 0.51x | 5743.07 KB | 1.01x more | +| | | | | | | | | | | | +| **UpdateEntity_Manually** | **368.24 μs** | **53.938 μs** | **62.115 μs** | **346.09 μs** | **470.61 μs** | **487.82 μs** | **baseline** | **** | **61.61 KB** | **** | +| UpdateEntity_DbConnectionPlus | 397.23 μs | 36.233 μs | 41.726 μs | 394.08 μs | 435.77 μs | 452.75 μs | 1.10x slower | 0.20x | 62.65 KB | 1.02x more | -Then run the following command: +### Running the benchmarks +To run the benchmarks, run the following command: ```shell dotnet run --configuration Release --project benchmarks\DbConnectionPlus.Benchmarks\DbConnectionPlus.Benchmarks.csproj ``` diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index 4241eb3..156b58e 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -1,15 +1,18 @@ // @formatter:off // ReSharper disable InconsistentNaming -#pragma warning disable IDE0017, IDE0305 +// ReSharper disable InvokeAsExtensionMethod +#pragma warning disable RCS1196, IDE0017, IDE0305 -using System.Dynamic; +using System.Data; using BenchmarkDotNet.Attributes; -using FastMember; -using Microsoft.Data.SqlClient; -using RentADeveloper.DbConnectionPlus.Entities; +using Microsoft.Data.Sqlite; using RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; -using RentADeveloper.DbConnectionPlus.Readers; using RentADeveloper.DbConnectionPlus.UnitTests.TestData; +using System.Dynamic; +using System.Globalization; +using Dapper; +using Dapper.Contrib.Extensions; +using RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; namespace RentADeveloper.DbConnectionPlus.Benchmarks; @@ -21,64 +24,59 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks; [Config(typeof(BenchmarksConfig))] public class Benchmarks { + static Benchmarks() + { + SqlMapper.AddTypeHandler(new GuidTypeHandler()); + SqlMapper.AddTypeHandler(new TimeSpanTypeHandler()); + } + [GlobalSetup] public void Setup_Global() { + this.testDatabaseProvider = new(); this.testDatabaseProvider.ResetDatabase(); - - // Warm up connection pool. - for (var i = 0; i < 20; i++) - { - using var warmUpConnection = this.CreateConnection(); - warmUpConnection.ExecuteScalar("SELECT 1"); - } - - using var connection = this.CreateConnection(); - connection.ExecuteNonQuery("CHECKPOINT"); - connection.ExecuteNonQuery("DBCC DROPCLEANBUFFERS"); - connection.ExecuteNonQuery("DBCC FREEPROCCACHE"); } - private SqlConnection CreateConnection() => - (SqlConnection)this.testDatabaseProvider.CreateConnection(); + private SqliteConnection CreateConnection() => + (SqliteConnection)this.testDatabaseProvider!.CreateConnection(); private void PrepareEntitiesInDb(Int32 numberOfEntities) { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); using var transaction = connection.BeginTransaction(); connection.ExecuteNonQuery("DELETE FROM Entity", transaction); - this.entitiesInDb = Generate.Multiple(numberOfEntities); + this.entitiesInDb = Generate.Multiple(numberOfEntities); connection.InsertEntities(this.entitiesInDb, transaction); transaction.Commit(); } - private List entitiesInDb = []; + private List entitiesInDb = []; #region DeleteEntities private const String DeleteEntities_Category = "DeleteEntities"; - private const Int32 DeleteEntities_EntitiesPerOperation = 100; + private const Int32 DeleteEntities_EntitiesPerOperation = 250; private const Int32 DeleteEntities_OperationsPerInvoke = 20; - [IterationSetup(Targets = [nameof(DeleteEntities_Manually), nameof(DeleteEntities_DbConnectionPlus)])] + [IterationSetup(Targets = [nameof(DeleteEntities_DbCommand), nameof(DeleteEntities_DbConnectionPlus)])] public void DeleteEntities_Setup() => this.PrepareEntitiesInDb(DeleteEntities_OperationsPerInvoke * DeleteEntities_EntitiesPerOperation); [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_Manually() + public void DeleteEntities_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - var idParameter = new SqlParameter(); + var idParameter = command.CreateParameter(); idParameter.ParameterName = "@Id"; command.Parameters.Add(idParameter); @@ -93,11 +91,30 @@ public void DeleteEntities_Manually() } } + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] + [BenchmarkCategory(DeleteEntities_Category)] + public void DeleteEntities_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + { + var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); + + SqlMapperExtensions.Delete(connection, entities); + + foreach (var entity in entities) + { + this.entitiesInDb.Remove(entity); + } + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntities_Category)] public void DeleteEntities_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { @@ -115,17 +132,17 @@ public void DeleteEntities_DbConnectionPlus() #region DeleteEntity private const String DeleteEntity_Category = "DeleteEntity"; - private const Int32 DeleteEntity_OperationsPerInvoke = 1200; + private const Int32 DeleteEntity_OperationsPerInvoke = 8000; - [IterationSetup(Targets = [nameof(DeleteEntity_Manually), nameof(DeleteEntity_DbConnectionPlus)])] + [IterationSetup(Targets = [nameof(DeleteEntity_DbCommand), nameof(DeleteEntity_DbConnectionPlus)])] public void DeleteEntity_Setup() => this.PrepareEntitiesInDb(DeleteEntity_OperationsPerInvoke); [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_Manually() + public void DeleteEntity_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) { @@ -142,11 +159,27 @@ public void DeleteEntity_Manually() } } + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [BenchmarkCategory(DeleteEntity_Category)] + public void DeleteEntity_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) + { + var entityToDelete = this.entitiesInDb[0]; + + SqlMapperExtensions.Delete(connection, entityToDelete); + + this.entitiesInDb.Remove(entityToDelete); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntity_Category)] public void DeleteEntity_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) { @@ -161,17 +194,17 @@ public void DeleteEntity_DbConnectionPlus() #region ExecuteNonQuery private const String ExecuteNonQuery_Category = "ExecuteNonQuery"; - private const Int32 ExecuteNonQuery_OperationsPerInvoke = 1100; + private const Int32 ExecuteNonQuery_OperationsPerInvoke = 7700; - [IterationSetup(Targets = [nameof(ExecuteNonQuery_Manually), nameof(ExecuteNonQuery_DbConnectionPlus)])] + [IterationSetup(Targets = [nameof(ExecuteNonQuery_DbCommand), nameof(ExecuteNonQuery_DbConnectionPlus)])] public void ExecuteNonQuery_Setup() => this.PrepareEntitiesInDb(ExecuteNonQuery_OperationsPerInvoke); [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_Manually() + public void ExecuteNonQuery_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) { @@ -188,11 +221,27 @@ public void ExecuteNonQuery_Manually() } } + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteNonQuery_Category)] + public void ExecuteNonQuery_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[0]; + + SqlMapper.Execute(connection, "DELETE FROM Entity WHERE Id = @Id", new { entity.Id }); + + this.entitiesInDb.Remove(entity); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] [BenchmarkCategory(ExecuteNonQuery_Category)] public void ExecuteNonQuery_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) { @@ -210,7 +259,7 @@ public void ExecuteNonQuery_DbConnectionPlus() private const Int32 ExecuteReader_OperationsPerInvoke = 700; private const Int32 ExecuteReader_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(ExecuteReader_Manually), nameof(ExecuteReader_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(ExecuteReader_DbCommand), nameof(ExecuteReader_DbConnectionPlus)])] public void ExecuteReader_Setup() { this.Setup_Global(); @@ -219,11 +268,11 @@ public void ExecuteReader_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_Manually() + public List ExecuteReader_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - var entities = new List(); + var entities = new List(); for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) { @@ -232,57 +281,79 @@ public List ExecuteReader_Manually() using var command = connection.CreateCommand(); command.CommandText = $""" SELECT - TOP ({ExecuteReader_EntitiesPerOperation}) - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue FROM Entity + LIMIT {ExecuteReader_EntitiesPerOperation} """; using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { - var charBuffer = new Char[1]; + entities.Add(ReadEntity(dataReader)); + } + } - var ordinal = 0; - entities.Add(new() - { - Id = dataReader.GetInt64(ordinal++), - BooleanValue = dataReader.GetBoolean(ordinal++), - BytesValue = (Byte[])dataReader.GetValue(ordinal++), - ByteValue = dataReader.GetByte(ordinal++), - CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), - DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), - DateTimeValue = dataReader.GetDateTime(ordinal++), - DecimalValue = dataReader.GetDecimal(ordinal++), - DoubleValue = dataReader.GetDouble(ordinal++), - EnumValue = Enum.Parse(dataReader.GetString(ordinal++)), - GuidValue = dataReader.GetGuid(ordinal++), - Int16Value = dataReader.GetInt16(ordinal++), - Int32Value = dataReader.GetInt32(ordinal++), - Int64Value = dataReader.GetInt64(ordinal++), - SingleValue = dataReader.GetFloat(ordinal++), - StringValue = dataReader.GetString(ordinal++), - TimeOnlyValue = TimeOnly.FromTimeSpan((TimeSpan)dataReader.GetValue(ordinal++)), - TimeSpanValue = (TimeSpan)dataReader.GetValue(ordinal) - }); + return entities; + } + + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteReader_Category)] + public List ExecuteReader_Dapper() + { + var connection = this.CreateConnection(); + + var entities = new List(); + + for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) + { + entities.Clear(); + + using var dataReader = SqlMapper.ExecuteReader( + connection, + $""" + SELECT + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + FROM + Entity + LIMIT {ExecuteReader_EntitiesPerOperation} + """ + ); + + while (dataReader.Read()) + { + entities.Add(ReadEntity(dataReader)); } } @@ -291,11 +362,11 @@ public List ExecuteReader_Manually() [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_DbConnectionPlus() + public List ExecuteReader_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - var entities = new List(); + var entities = new List(); for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) { @@ -304,56 +375,31 @@ public List ExecuteReader_DbConnectionPlus() using var dataReader = connection.ExecuteReader( $""" SELECT - TOP ({ExecuteReader_EntitiesPerOperation}) - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue FROM Entity + LIMIT {ExecuteReader_EntitiesPerOperation} """ ); while (dataReader.Read()) { - var charBuffer = new Char[1]; - - var ordinal = 0; - entities.Add(new() - { - Id = dataReader.GetInt64(ordinal++), - BooleanValue = dataReader.GetBoolean(ordinal++), - BytesValue = (Byte[])dataReader.GetValue(ordinal++), - ByteValue = dataReader.GetByte(ordinal++), - CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), - DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), - DateTimeValue = dataReader.GetDateTime(ordinal++), - DecimalValue = dataReader.GetDecimal(ordinal++), - DoubleValue = dataReader.GetDouble(ordinal++), - EnumValue = Enum.Parse(dataReader.GetString(ordinal++)), - GuidValue = dataReader.GetGuid(ordinal++), - Int16Value = dataReader.GetInt16(ordinal++), - Int32Value = dataReader.GetInt32(ordinal++), - Int64Value = dataReader.GetInt64(ordinal++), - SingleValue = dataReader.GetFloat(ordinal++), - StringValue = dataReader.GetString(ordinal++), - TimeOnlyValue = TimeOnly.FromTimeSpan((TimeSpan)dataReader.GetValue(ordinal++)), - TimeSpanValue = (TimeSpan)dataReader.GetValue(ordinal) - }); + entities.Add(ReadEntity(dataReader)); } } @@ -365,7 +411,7 @@ public List ExecuteReader_DbConnectionPlus() private const String ExecuteScalar_Category = "ExecuteScalar"; private const Int32 ExecuteScalar_OperationsPerInvoke = 5000; - [GlobalSetup(Targets = [nameof(ExecuteScalar_Manually), nameof(ExecuteScalar_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(ExecuteScalar_DbCommand), nameof(ExecuteScalar_DbConnectionPlus)])] public void ExecuteScalar_Setup() { this.Setup_Global(); @@ -374,9 +420,9 @@ public void ExecuteScalar_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_Manually() + public String ExecuteScalar_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); String result = null!; @@ -395,11 +441,33 @@ public String ExecuteScalar_Manually() return result; } + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteScalar_Category)] + public String ExecuteScalar_Dapper() + { + var connection = this.CreateConnection(); + + String result = null!; + + for (var i = 0; i < ExecuteScalar_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[i]; + + result = SqlMapper.ExecuteScalar( + connection, + "SELECT StringValue FROM Entity WHERE Id = @Id", + new { entity.Id } + )!; + } + + return result; + } + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] [BenchmarkCategory(ExecuteScalar_Category)] public String ExecuteScalar_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); String result = null!; @@ -420,7 +488,7 @@ public String ExecuteScalar_DbConnectionPlus() private const String Exists_Category = "Exists"; private const Int32 Exists_OperationsPerInvoke = 5000; - [GlobalSetup(Targets = [nameof(Exists_Manually), nameof(Exists_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Exists_DbCommand), nameof(Exists_DbConnectionPlus)])] public void Exists_Setup() { this.Setup_Global(); @@ -429,9 +497,9 @@ public void Exists_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Exists_OperationsPerInvoke)] [BenchmarkCategory(Exists_Category)] - public Boolean Exists_Manually() + public Boolean Exists_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var result = false; @@ -455,7 +523,7 @@ public Boolean Exists_Manually() [BenchmarkCategory(Exists_Category)] public Boolean Exists_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var result = false; @@ -473,9 +541,9 @@ public Boolean Exists_DbConnectionPlus() #region InsertEntities private const String InsertEntities_Category = "InsertEntities"; private const Int32 InsertEntities_OperationsPerInvoke = 20; - private const Int32 InsertEntities_EntitiesPerOperation = 100; + private const Int32 InsertEntities_EntitiesPerOperation = 140; - [GlobalSetup(Targets = [nameof(InsertEntities_Manually), nameof(InsertEntities_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(InsertEntities_DbCommand), nameof(InsertEntities_DbConnectionPlus)])] public void InsertEntities_Setup() { this.Setup_Global(); @@ -484,36 +552,34 @@ public void InsertEntities_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_Manually() + public void InsertEntities_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) { - var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); + var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); using var command = connection.CreateCommand(); command.CommandText = """ - INSERT INTO [Entity] + INSERT INTO Entity ( - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue ) VALUES ( @@ -522,7 +588,6 @@ INSERT INTO [Entity] @BytesValue, @ByteValue, @CharValue, - @DateOnlyValue, @DateTimeValue, @DecimalValue, @DoubleValue, @@ -533,63 +598,56 @@ INSERT INTO [Entity] @Int64Value, @SingleValue, @StringValue, - @TimeOnlyValue, @TimeSpanValue ) """; - var idParameter = new SqlParameter(); + var idParameter = new SqliteParameter(); idParameter.ParameterName = "@Id"; - var booleanValueParameter = new SqlParameter(); + var booleanValueParameter = new SqliteParameter(); booleanValueParameter.ParameterName = "@BooleanValue"; - var bytesValueParameter = new SqlParameter(); + var bytesValueParameter = new SqliteParameter(); bytesValueParameter.ParameterName = "@BytesValue"; - var byteValueParameter = new SqlParameter(); + var byteValueParameter = new SqliteParameter(); byteValueParameter.ParameterName = "@ByteValue"; - var charValueParameter = new SqlParameter(); + var charValueParameter = new SqliteParameter(); charValueParameter.ParameterName = "@CharValue"; - var dateOnlyParameter = new SqlParameter(); - dateOnlyParameter.ParameterName = "@DateOnlyValue"; - - var dateTimeValueParameter = new SqlParameter(); + var dateTimeValueParameter = new SqliteParameter(); dateTimeValueParameter.ParameterName = "@DateTimeValue"; - var decimalValueParameter = new SqlParameter(); + var decimalValueParameter = new SqliteParameter(); decimalValueParameter.ParameterName = "@DecimalValue"; - var doubleValueParameter = new SqlParameter(); + var doubleValueParameter = new SqliteParameter(); doubleValueParameter.ParameterName = "@DoubleValue"; - var enumValueParameter = new SqlParameter(); + var enumValueParameter = new SqliteParameter(); enumValueParameter.ParameterName = "@EnumValue"; - var guidValueParameter = new SqlParameter(); + var guidValueParameter = new SqliteParameter(); guidValueParameter.ParameterName = "@GuidValue"; - var int16ValueParameter = new SqlParameter(); + var int16ValueParameter = new SqliteParameter(); int16ValueParameter.ParameterName = "@Int16Value"; - var int32ValueParameter = new SqlParameter(); + var int32ValueParameter = new SqliteParameter(); int32ValueParameter.ParameterName = "@Int32Value"; - var int64ValueParameter = new SqlParameter(); + var int64ValueParameter = new SqliteParameter(); int64ValueParameter.ParameterName = "@Int64Value"; - var singleValueParameter = new SqlParameter(); + var singleValueParameter = new SqliteParameter(); singleValueParameter.ParameterName = "@SingleValue"; - var stringValueParameter = new SqlParameter(); + var stringValueParameter = new SqliteParameter(); stringValueParameter.ParameterName = "@StringValue"; - var timeOnlyValueParameter = new SqlParameter(); - timeOnlyValueParameter.ParameterName = "@TimeOnlyValue"; - - var timeSpanValueParameter = new SqlParameter(); + var timeSpanValueParameter = new SqliteParameter(); timeSpanValueParameter.ParameterName = "@TimeSpanValue"; command.Parameters.Add(idParameter); @@ -597,7 +655,6 @@ INSERT INTO [Entity] command.Parameters.Add(bytesValueParameter); command.Parameters.Add(byteValueParameter); command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateOnlyParameter); command.Parameters.Add(dateTimeValueParameter); command.Parameters.Add(decimalValueParameter); command.Parameters.Add(doubleValueParameter); @@ -608,44 +665,55 @@ INSERT INTO [Entity] command.Parameters.Add(int64ValueParameter); command.Parameters.Add(singleValueParameter); command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeOnlyValueParameter); command.Parameters.Add(timeSpanValueParameter); foreach (var entity in entities) { idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue; + booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; bytesValueParameter.Value = entity.BytesValue; byteValueParameter.Value = entity.ByteValue; charValueParameter.Value = entity.CharValue; - dateOnlyParameter.Value = entity.DateOnlyValue; - dateTimeValueParameter.Value = entity.DateTimeValue; - decimalValueParameter.Value = entity.DecimalValue; + dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); doubleValueParameter.Value = entity.DoubleValue; enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue; + guidValueParameter.Value = entity.GuidValue.ToString(); int16ValueParameter.Value = entity.Int16Value; int32ValueParameter.Value = entity.Int32Value; int64ValueParameter.Value = entity.Int64Value; singleValueParameter.Value = entity.SingleValue; stringValueParameter.Value = entity.StringValue; - timeOnlyValueParameter.Value = entity.TimeOnlyValue; - timeSpanValueParameter.Value = entity.TimeSpanValue; + timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); command.ExecuteNonQuery(); } } } + [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] + [BenchmarkCategory(InsertEntities_Category)] + public void InsertEntities_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) + { + var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); + + SqlMapperExtensions.Insert(connection, entitiesToInsert); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] [BenchmarkCategory(InsertEntities_Category)] public void InsertEntities_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) { - var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); + var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); connection.InsertEntities(entitiesToInsert); } @@ -654,9 +722,9 @@ public void InsertEntities_DbConnectionPlus() #region InsertEntity private const String InsertEntity_Category = "InsertEntity"; - private const Int32 InsertEntity_OperationsPerInvoke = 700; + private const Int32 InsertEntity_OperationsPerInvoke = 2500; - [GlobalSetup(Targets = [nameof(InsertEntity_Manually), nameof(InsertEntity_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(InsertEntity_DbCommand), nameof(InsertEntity_DbConnectionPlus)])] public void InsertEntity_Setup() { this.Setup_Global(); @@ -665,36 +733,34 @@ public void InsertEntity_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_Manually() + public void InsertEntity_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) { - var entity = Generate.Single(); + var entity = Generate.Single(); using var command = connection.CreateCommand(); command.CommandText = """ - INSERT INTO [Entity] + INSERT INTO Entity ( - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue ) VALUES ( @@ -703,7 +769,6 @@ INSERT INTO [Entity] @BytesValue, @ByteValue, @CharValue, - @DateOnlyValue, @DateTimeValue, @DecimalValue, @DoubleValue, @@ -714,42 +779,53 @@ INSERT INTO [Entity] @Int64Value, @SingleValue, @StringValue, - @TimeOnlyValue, @TimeSpanValue ) """; command.Parameters.Add(new("@Id", entity.Id)); - command.Parameters.Add(new("@BooleanValue", entity.BooleanValue)); + command.Parameters.Add(new("@BooleanValue", entity.BooleanValue ? 1 : 0)); command.Parameters.Add(new("@BytesValue", entity.BytesValue)); command.Parameters.Add(new("@ByteValue", entity.ByteValue)); command.Parameters.Add(new("@CharValue", entity.CharValue)); - command.Parameters.Add(new("@DateOnlyValue", entity.DateOnlyValue)); - command.Parameters.Add(new("@DateTimeValue", entity.DateTimeValue)); + command.Parameters.Add(new("@DateTimeValue", entity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); command.Parameters.Add(new("@DecimalValue", entity.DecimalValue)); command.Parameters.Add(new("@DoubleValue", entity.DoubleValue)); command.Parameters.Add(new("@EnumValue", entity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", entity.GuidValue)); + command.Parameters.Add(new("@GuidValue", entity.GuidValue.ToString())); command.Parameters.Add(new("@Int16Value", entity.Int16Value)); command.Parameters.Add(new("@Int32Value", entity.Int32Value)); command.Parameters.Add(new("@Int64Value", entity.Int64Value)); command.Parameters.Add(new("@SingleValue", entity.SingleValue)); command.Parameters.Add(new("@StringValue", entity.StringValue)); - command.Parameters.Add(new("@TimeOnlyValue", entity.TimeOnlyValue)); - command.Parameters.Add(new("@TimeSpanValue", entity.TimeSpanValue)); + command.Parameters.Add(new("@TimeSpanValue", entity.TimeSpanValue.ToString())); command.ExecuteNonQuery(); } } + [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] + [BenchmarkCategory(InsertEntity_Category)] + public void InsertEntity_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) + { + var entity = Generate.Single(); + + SqlMapperExtensions.Insert(connection, entity); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] [BenchmarkCategory(InsertEntity_Category)] public void InsertEntity_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) { - var entity = Generate.Single(); + var entity = Generate.Single(); connection.InsertEntity(entity); } @@ -758,9 +834,9 @@ public void InsertEntity_DbConnectionPlus() #region Parameter private const String Parameter_Category = "Parameter"; - private const Int32 Parameter_OperationsPerInvoke = 2500; + private const Int32 Parameter_OperationsPerInvoke = 35_000; - [GlobalSetup(Targets = [nameof(Parameter_Manually), nameof(Parameter_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Parameter_DbCommand), nameof(Parameter_DbConnectionPlus)])] public void Parameter_Setup() { this.Setup_Global(); @@ -769,9 +845,9 @@ public void Parameter_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Parameter_OperationsPerInvoke)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_Manually() + public Object Parameter_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var result = new List(); @@ -790,7 +866,45 @@ public Object Parameter_Manually() using var dataReader = command.ExecuteReader(); dataReader.Read(); - result.Add(dataReader.GetInt32(0)); + + result.Add((Int32) dataReader.GetInt64(0)); + result.Add(dataReader.GetString(1)); + result.Add(dataReader.GetDateTime(2)); + result.Add(dataReader.GetGuid(3)); + result.Add(dataReader.GetBoolean(4)); + } + + return result; + } + + [Benchmark(Baseline = false, OperationsPerInvoke = Parameter_OperationsPerInvoke)] + [BenchmarkCategory(Parameter_Category)] + public Object Parameter_Dapper() + { + var connection = this.CreateConnection(); + + var result = new List(); + + for (var i = 0; i < Parameter_OperationsPerInvoke; i++) + { + result.Clear(); + + using var dataReader = SqlMapper.ExecuteReader( + connection, + "SELECT @P1, @P2, @P3, @P4, @P5", + new + { + P1 = 1, + P2 = "Test", + P3 = DateTime.UtcNow, + P4 = Guid.NewGuid(), + P5 = true + } + ); + + dataReader.Read(); + + result.Add((Int32) dataReader.GetInt64(0)); result.Add(dataReader.GetString(1)); result.Add(dataReader.GetDateTime(2)); result.Add(dataReader.GetGuid(3)); @@ -804,7 +918,7 @@ public Object Parameter_Manually() [BenchmarkCategory(Parameter_Category)] public Object Parameter_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var result = new List(); @@ -822,7 +936,8 @@ public Object Parameter_DbConnectionPlus() """); dataReader.Read(); - result.Add(dataReader.GetInt32(0)); + + result.Add((Int32) dataReader.GetInt64(0)); result.Add(dataReader.GetString(1)); result.Add(dataReader.GetDateTime(2)); result.Add(dataReader.GetGuid(3)); @@ -838,7 +953,7 @@ public Object Parameter_DbConnectionPlus() private const Int32 Query_Dynamic_OperationsPerInvoke = 600; private const Int32 Query_Dynamic_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(Query_Dynamic_Manually), nameof(Query_Dynamic_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Query_Dynamic_DbCommand), nameof(Query_Dynamic_DbConnectionPlus)])] public void Query_Dynamic_Setup() { this.Setup_Global(); @@ -847,9 +962,9 @@ public void Query_Dynamic_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Query_Dynamic_OperationsPerInvoke)] [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_Manually() + public List Query_Dynamic_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var entities = new List(); @@ -860,27 +975,25 @@ public List Query_Dynamic_Manually() using var dataReader = connection.ExecuteReader( $""" SELECT - TOP ({Query_Dynamic_EntitiesPerOperation}) - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue FROM Entity + LIMIT {Query_Dynamic_EntitiesPerOperation} """ ); @@ -892,25 +1005,23 @@ public List Query_Dynamic_Manually() dynamic entity = new ExpandoObject(); entity.Id = dataReader.GetInt64(ordinal++); - entity.BooleanValue = dataReader.GetBoolean(ordinal++); + entity.BooleanValue = dataReader.GetInt64(ordinal++) == 1; entity.BytesValue = (Byte[])dataReader.GetValue(ordinal++); entity.ByteValue = dataReader.GetByte(ordinal++); entity.CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(); - entity.DateOnlyValue = DateOnly.FromDateTime((DateTime)dataReader.GetValue(ordinal++)); - entity.DateTimeValue = dataReader.GetDateTime(ordinal++); - entity.DecimalValue = dataReader.GetDecimal(ordinal++); + entity.DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); + entity.DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); entity.DoubleValue = dataReader.GetDouble(ordinal++); entity.EnumValue = Enum.Parse(dataReader.GetString(ordinal++)); - entity.GuidValue = dataReader.GetGuid(ordinal++); - entity.Int16Value = dataReader.GetInt16(ordinal++); - entity.Int32Value = dataReader.GetInt32(ordinal++); + entity.GuidValue = Guid.Parse(dataReader.GetString(ordinal++)); + entity.Int16Value = (Int16) dataReader.GetInt64(ordinal++); + entity.Int32Value = (Int32) dataReader.GetInt64(ordinal++); entity.Int64Value = dataReader.GetInt64(ordinal++); entity.SingleValue = dataReader.GetFloat(ordinal++); entity.StringValue = dataReader.GetString(ordinal++); - entity.TimeOnlyValue = TimeOnly.FromTimeSpan((TimeSpan)dataReader.GetValue(ordinal++)); - entity.TimeSpanValue = (TimeSpan)dataReader.GetValue(ordinal); + entity.TimeSpanValue = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); entities.Add(entity); } @@ -923,14 +1034,31 @@ public List Query_Dynamic_Manually() [BenchmarkCategory(Query_Dynamic_Category)] public List Query_Dynamic_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); List entities = []; for (var i = 0; i < Query_Dynamic_OperationsPerInvoke; i++) { entities = connection - .Query($"SELECT TOP ({Query_Dynamic_EntitiesPerOperation}) * FROM Entity") + .Query($"SELECT * FROM Entity LIMIT {Query_Dynamic_EntitiesPerOperation}") + .ToList(); + } + + return entities; + } + + [Benchmark(Baseline = false, OperationsPerInvoke = Query_Dynamic_OperationsPerInvoke)] + [BenchmarkCategory(Query_Dynamic_Category)] + public List Query_Dynamic_Dapper() + { + var connection = this.CreateConnection(); + + List entities = []; + + for (var i = 0; i < Query_Dynamic_OperationsPerInvoke; i++) + { + entities = SqlMapper.Query(connection, $"SELECT * FROM Entity LIMIT {Query_Dynamic_EntitiesPerOperation}") .ToList(); } @@ -941,9 +1069,9 @@ public List Query_Dynamic_DbConnectionPlus() #region Query_Scalars private const String Query_Scalars_Category = "Query_Scalars"; private const Int32 Query_Scalars_OperationsPerInvoke = 1500; - private const Int32 Query_Scalars_EntitiesPerOperation = 100; + private const Int32 Query_Scalars_EntitiesPerOperation = 500; - [GlobalSetup(Targets = [nameof(Query_Scalars_Manually), nameof(Query_Scalars_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Query_Scalars_DbCommand), nameof(Query_Scalars_DbConnectionPlus)])] public void Query_Scalars_Setup() { this.Setup_Global(); @@ -952,9 +1080,9 @@ public void Query_Scalars_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_Manually() + public List Query_Scalars_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var data = new List(); @@ -963,7 +1091,7 @@ public List Query_Scalars_Manually() data.Clear(); using var command = connection.CreateCommand(); - command.CommandText = $"SELECT TOP ({Query_Scalars_EntitiesPerOperation}) Id FROM Entity"; + command.CommandText = $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}"; using var dataReader = command.ExecuteReader(); @@ -978,18 +1106,38 @@ public List Query_Scalars_Manually() return data; } + [Benchmark(Baseline = false, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] + [BenchmarkCategory(Query_Scalars_Category)] + public List Query_Scalars_Dapper() + { + var connection = this.CreateConnection(); + + List data = []; + + for (var i = 0; i < Query_Scalars_OperationsPerInvoke; i++) + { + data = SqlMapper.Query( + connection, + $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}" + ) + .ToList(); + } + + return data; + } + [Benchmark(Baseline = false, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] [BenchmarkCategory(Query_Scalars_Category)] public List Query_Scalars_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); List data = []; for (var i = 0; i < Query_Scalars_OperationsPerInvoke; i++) { data = connection - .Query($"SELECT TOP ({Query_Scalars_EntitiesPerOperation}) Id FROM Entity") + .Query($"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}") .ToList(); } @@ -1002,7 +1150,7 @@ public List Query_Scalars_DbConnectionPlus() private const Int32 Query_Entities_OperationsPerInvoke = 600; private const Int32 Query_Entities_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(Query_Entities_Manually), nameof(Query_Entities_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Query_Entities_DbCommand), nameof(Query_Entities_DbConnectionPlus)])] public void Query_Entities_Setup() { this.Setup_Global(); @@ -1011,11 +1159,11 @@ public void Query_Entities_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_Manually() + public List Query_Entities_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - var entities = new List(); + var entities = new List(); for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) { @@ -1024,56 +1172,31 @@ public List Query_Entities_Manually() using var dataReader = connection.ExecuteReader( $""" SELECT - TOP ({Query_Entities_EntitiesPerOperation}) - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue FROM Entity + LIMIT {Query_Entities_EntitiesPerOperation} """ ); while (dataReader.Read()) { - var charBuffer = new Char[1]; - - var ordinal = 0; - entities.Add(new() - { - Id = dataReader.GetInt64(ordinal++), - BooleanValue = dataReader.GetBoolean(ordinal++), - BytesValue = (Byte[])dataReader.GetValue(ordinal++), - ByteValue = dataReader.GetByte(ordinal++), - CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), - DateOnlyValue = DateOnly.FromDateTime((DateTime) dataReader.GetValue(ordinal++)), - DateTimeValue = dataReader.GetDateTime(ordinal++), - DecimalValue = dataReader.GetDecimal(ordinal++), - DoubleValue = dataReader.GetDouble(ordinal++), - EnumValue = Enum.Parse(dataReader.GetString(ordinal++)), - GuidValue = dataReader.GetGuid(ordinal++), - Int16Value = dataReader.GetInt16(ordinal++), - Int32Value = dataReader.GetInt32(ordinal++), - Int64Value = dataReader.GetInt64(ordinal++), - SingleValue = dataReader.GetFloat(ordinal++), - StringValue = dataReader.GetString(ordinal++), - TimeOnlyValue = TimeOnly.FromTimeSpan((TimeSpan)dataReader.GetValue(ordinal++)), - TimeSpanValue = (TimeSpan)dataReader.GetValue(ordinal) - }); + entities.Add(ReadEntity(dataReader)); } } @@ -1082,16 +1205,37 @@ public List Query_Entities_Manually() [Benchmark(Baseline = false, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_DbConnectionPlus() + public List Query_Entities_Dapper() + { + var connection = this.CreateConnection(); + + List entities = []; + + for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) + { + entities = SqlMapper + .Query( + connection, + $"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}" + ) + .ToList(); + } + + return entities; + } + + [Benchmark(Baseline = false, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] + [BenchmarkCategory(Query_Entities_Category)] + public List Query_Entities_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - List entities = []; + List entities = []; for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) { entities = connection - .Query($"SELECT TOP ({Query_Entities_EntitiesPerOperation}) * FROM Entity") + .Query($"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}") .ToList(); } @@ -1101,10 +1245,10 @@ public List Query_Entities_DbConnectionPlus() #region Query_ValueTuples private const String Query_ValueTuples_Category = "Query_ValueTuples"; - private const Int32 Query_ValueTuples_OperationsPerInvoke = 900; - private const Int32 Query_ValueTuples_EntitiesPerOperation = 100; + private const Int32 Query_ValueTuples_OperationsPerInvoke = 1_000; + private const Int32 Query_ValueTuples_EntitiesPerOperation = 150; - [GlobalSetup(Targets = [nameof(Query_ValueTuples_Manually), nameof(Query_ValueTuples_DbConnectionPlus)])] + [GlobalSetup(Targets = [nameof(Query_ValueTuples_DbCommand), nameof(Query_ValueTuples_DbConnectionPlus)])] public void Query_ValueTuples_Setup() { this.Setup_Global(); @@ -1113,9 +1257,9 @@ public void Query_ValueTuples_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] [BenchmarkCategory(Query_ValueTuples_Category)] - public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> Query_ValueTuples_Manually() + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> Query_ValueTuples_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var tuples = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); @@ -1125,9 +1269,9 @@ public void Query_ValueTuples_Setup() using var command = connection.CreateCommand(); command.CommandText = $""" - SELECT TOP ({Query_ValueTuples_EntitiesPerOperation}) - Id, DateTimeValue, EnumValue, StringValue + SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity + LIMIT {Query_ValueTuples_EntitiesPerOperation} """; using var dataReader = command.ExecuteReader(); @@ -1137,7 +1281,7 @@ FROM Entity tuples.Add( ( dataReader.GetInt64(0), - dataReader.GetDateTime(1), + DateTime.Parse(dataReader.GetString(1), CultureInfo.InvariantCulture), Enum.Parse(dataReader.GetString(2)), dataReader.GetString(3) ) @@ -1148,12 +1292,38 @@ FROM Entity return tuples; } + [Benchmark(Baseline = false, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] + [BenchmarkCategory(Query_ValueTuples_Category)] + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> + Query_ValueTuples_Dapper() + { + var connection = this.CreateConnection(); + + List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> tuples = []; + + for (var i = 0; i < Query_ValueTuples_OperationsPerInvoke; i++) + { + tuples = SqlMapper + .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( + connection, + $""" + SELECT Id, DateTimeValue, EnumValue, StringValue + FROM Entity + LIMIT {Query_ValueTuples_EntitiesPerOperation} + """ + ) + .ToList(); + } + + return tuples; + } + [Benchmark(Baseline = false, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] [BenchmarkCategory(Query_ValueTuples_Category)] public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> Query_ValueTuples_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> tuples = []; @@ -1162,9 +1332,9 @@ FROM Entity tuples = connection .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( $""" - SELECT TOP ({Query_ValueTuples_EntitiesPerOperation}) - Id, DateTimeValue, EnumValue, StringValue + SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity + LIMIT {Query_ValueTuples_EntitiesPerOperation} """ ) .ToList(); @@ -1176,11 +1346,11 @@ FROM Entity #region TemporaryTable_ComplexObjects private const String TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects"; - private const Int32 TemporaryTable_ComplexObjects_OperationsPerInvoke = 25; - private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 100; + private const Int32 TemporaryTable_ComplexObjects_OperationsPerInvoke = 50; + private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 200; [GlobalSetup(Targets = [ - nameof(TemporaryTable_ComplexObjects_Manually), + nameof(TemporaryTable_ComplexObjects_DbCommand), nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) ])] public void TemporaryTable_ComplexObjects_Setup() @@ -1191,120 +1361,203 @@ public void TemporaryTable_ComplexObjects_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = TemporaryTable_ComplexObjects_OperationsPerInvoke)] [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_Manually() + public List TemporaryTable_ComplexObjects_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - var result = new List(); + var result = new List(); for (var i = 0; i < TemporaryTable_ComplexObjects_OperationsPerInvoke; i++) { result.Clear(); - using var entitiesReader = new ObjectReader( - typeof(Entity), - entities, - EntityHelper.GetEntityTypeMetadata(typeof(Entity)). - MappedProperties.Where(a => a.CanRead).Select(a => a.PropertyName).ToArray() - ); - - using var getCollationCommand = connection.CreateCommand(); - getCollationCommand.CommandText = - "SELECT CONVERT (VARCHAR(256), DATABASEPROPERTYEX(DB_NAME(), 'collation'))"; - var databaseCollation = (String)getCollationCommand.ExecuteScalar()!; - using var createTableCommand = connection.CreateCommand(); createTableCommand.CommandText = - $""" - CREATE TABLE [#Entities] ( - [BooleanValue] BIT, - [BytesValue] VARBINARY(MAX), - [ByteValue] TINYINT, - [CharValue] CHAR(1), - [DateOnlyValue] DATE, - [DateTimeValue] DATETIME2, - [DecimalValue] DECIMAL(28, 10), - [DoubleValue] FLOAT, - [EnumValue] NVARCHAR(200) COLLATE {databaseCollation}, - [GuidValue] UNIQUEIDENTIFIER, - [Id] BIGINT, - [Int16Value] SMALLINT, - [Int32Value] INT, - [Int64Value] BIGINT, - [SingleValue] REAL, - [StringValue] NVARCHAR(MAX) COLLATE {databaseCollation}, - [TimeOnlyValue] TIME, - [TimeSpanValue] TIME + """ + CREATE TEMP TABLE Entities ( + Id INTEGER, + BytesValue BLOB, + BooleanValue INTEGER, + ByteValue INTEGER, + CharValue TEXT, + DateTimeValue TEXT, + DecimalValue TEXT, + DoubleValue REAL, + EnumValue TEXT, + GuidValue TEXT, + Int16Value INTEGER, + Int32Value INTEGER, + Int64Value INTEGER, + SingleValue REAL, + StringValue TEXT, + TimeSpanValue TEXT ) """; createTableCommand.ExecuteNonQuery(); - using (var bulkCopy = new SqlBulkCopy(connection)) + using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = + """ + INSERT INTO temp.Entities ( + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + ) + VALUES ( + @Id, + @BooleanValue, + @BytesValue, + @ByteValue, + @CharValue, + @DateTimeValue, + @DecimalValue, + @DoubleValue, + @EnumValue, + @GuidValue, + @Int16Value, + @Int32Value, + @Int64Value, + @SingleValue, + @StringValue, + @TimeSpanValue + ) + """; + + var idParameter = new SqliteParameter(); + idParameter.ParameterName = "@Id"; + + var booleanValueParameter = new SqliteParameter(); + booleanValueParameter.ParameterName = "@BooleanValue"; + + var bytesValueParameter = new SqliteParameter(); + bytesValueParameter.ParameterName = "@BytesValue"; + + var byteValueParameter = new SqliteParameter(); + byteValueParameter.ParameterName = "@ByteValue"; + + var charValueParameter = new SqliteParameter(); + charValueParameter.ParameterName = "@CharValue"; + + var dateTimeValueParameter = new SqliteParameter(); + dateTimeValueParameter.ParameterName = "@DateTimeValue"; + + var decimalValueParameter = new SqliteParameter(); + decimalValueParameter.ParameterName = "@DecimalValue"; + + var doubleValueParameter = new SqliteParameter(); + doubleValueParameter.ParameterName = "@DoubleValue"; + + var enumValueParameter = new SqliteParameter(); + enumValueParameter.ParameterName = "@EnumValue"; + + var guidValueParameter = new SqliteParameter(); + guidValueParameter.ParameterName = "@GuidValue"; + + var int16ValueParameter = new SqliteParameter(); + int16ValueParameter.ParameterName = "@Int16Value"; + + var int32ValueParameter = new SqliteParameter(); + int32ValueParameter.ParameterName = "@Int32Value"; + + var int64ValueParameter = new SqliteParameter(); + int64ValueParameter.ParameterName = "@Int64Value"; + + var singleValueParameter = new SqliteParameter(); + singleValueParameter.ParameterName = "@SingleValue"; + + var stringValueParameter = new SqliteParameter(); + stringValueParameter.ParameterName = "@StringValue"; + + var timeSpanValueParameter = new SqliteParameter(); + timeSpanValueParameter.ParameterName = "@TimeSpanValue"; + + insertCommand.Parameters.Add(idParameter); + insertCommand.Parameters.Add(booleanValueParameter); + insertCommand.Parameters.Add(bytesValueParameter); + insertCommand.Parameters.Add(byteValueParameter); + insertCommand.Parameters.Add(charValueParameter); + insertCommand.Parameters.Add(dateTimeValueParameter); + insertCommand.Parameters.Add(decimalValueParameter); + insertCommand.Parameters.Add(doubleValueParameter); + insertCommand.Parameters.Add(enumValueParameter); + insertCommand.Parameters.Add(guidValueParameter); + insertCommand.Parameters.Add(int16ValueParameter); + insertCommand.Parameters.Add(int32ValueParameter); + insertCommand.Parameters.Add(int64ValueParameter); + insertCommand.Parameters.Add(singleValueParameter); + insertCommand.Parameters.Add(stringValueParameter); + insertCommand.Parameters.Add(timeSpanValueParameter); + + foreach (var entity in entities) { - bulkCopy.DestinationTableName = "#Entities"; - bulkCopy.WriteToServer(entitiesReader); + idParameter.Value = entity.Id; + booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; + bytesValueParameter.Value = entity.BytesValue; + byteValueParameter.Value = entity.ByteValue; + charValueParameter.Value = entity.CharValue; + dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); + doubleValueParameter.Value = entity.DoubleValue; + enumValueParameter.Value = entity.EnumValue.ToString(); + guidValueParameter.Value = entity.GuidValue.ToString(); + int16ValueParameter.Value = entity.Int16Value; + int32ValueParameter.Value = entity.Int32Value; + int64ValueParameter.Value = entity.Int64Value; + singleValueParameter.Value = entity.SingleValue; + stringValueParameter.Value = entity.StringValue; + timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + + insertCommand.ExecuteNonQuery(); } using var selectCommand = connection.CreateCommand(); selectCommand.CommandText = """ SELECT - [Id], - [BooleanValue], - [BytesValue], - [ByteValue], - [CharValue], - [DateOnlyValue], - [DateTimeValue], - [DecimalValue], - [DoubleValue], - [EnumValue], - [GuidValue], - [Int16Value], - [Int32Value], - [Int64Value], - [SingleValue], - [StringValue], - [TimeOnlyValue], - [TimeSpanValue] + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue FROM - #Entities + temp.Entities """; using var dataReader = selectCommand.ExecuteReader(); while (dataReader.Read()) { - var charBuffer = new Char[1]; - - var ordinal = 0; - result.Add(new() - { - Id = dataReader.GetInt64(ordinal++), - BooleanValue = dataReader.GetBoolean(ordinal++), - BytesValue = (Byte[])dataReader.GetValue(ordinal++), - ByteValue = dataReader.GetByte(ordinal++), - CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), - DateOnlyValue = DateOnly.FromDateTime((DateTime)dataReader.GetValue(ordinal++)), - DateTimeValue = dataReader.GetDateTime(ordinal++), - DecimalValue = dataReader.GetDecimal(ordinal++), - DoubleValue = dataReader.GetDouble(ordinal++), - EnumValue = Enum.Parse(dataReader.GetString(ordinal++)), - GuidValue = dataReader.GetGuid(ordinal++), - Int16Value = dataReader.GetInt16(ordinal++), - Int32Value = dataReader.GetInt32(ordinal++), - Int64Value = dataReader.GetInt64(ordinal++), - SingleValue = dataReader.GetFloat(ordinal++), - StringValue = dataReader.GetString(ordinal++), - TimeOnlyValue = TimeOnly.FromTimeSpan((TimeSpan)dataReader.GetValue(ordinal++)), - TimeSpanValue = (TimeSpan)dataReader.GetValue(ordinal) - }); + result.Add(ReadEntity(dataReader)); } using var dropTableCommand = connection.CreateCommand(); - dropTableCommand.CommandText = "DROP TABLE #Entities"; + dropTableCommand.CommandText = "DROP TABLE temp.Entities"; dropTableCommand.ExecuteNonQuery(); } @@ -1313,17 +1566,17 @@ [TimeSpanValue] TIME [Benchmark(Baseline = false, OperationsPerInvoke = TemporaryTable_ComplexObjects_OperationsPerInvoke)] [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_DbConnectionPlus() + public List TemporaryTable_ComplexObjects_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - List result = []; + List result = []; for (var i = 0; i < TemporaryTable_ComplexObjects_OperationsPerInvoke; i++) { - result = connection.Query($"SELECT * FROM {TemporaryTable(entities)}").ToList(); + result = connection.Query($"SELECT * FROM {TemporaryTable(entities)}").ToList(); } return result; @@ -1336,7 +1589,7 @@ public List TemporaryTable_ComplexObjects_DbConnectionPlus() private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000; [GlobalSetup(Targets = [ - nameof(TemporaryTable_ScalarValues_Manually), + nameof(TemporaryTable_ScalarValues_DbCommand), nameof(TemporaryTable_ScalarValues_DbConnectionPlus) ])] public void TemporaryTable_ScalarValues_Setup() @@ -1347,13 +1600,13 @@ public void TemporaryTable_ScalarValues_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = TemporaryTable_ScalarValues_OperationsPerInvoke)] [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_Manually() + public List TemporaryTable_ScalarValues_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var scalarValues = Enumerable .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString()) + .Select(a => a.ToString(CultureInfo.InvariantCulture)) .ToList(); var result = new List(); @@ -1362,25 +1615,27 @@ public List TemporaryTable_ScalarValues_Manually() { result.Clear(); - using var valuesReader = new EnumerableReader(scalarValues, typeof(String), "Value"); - - using var getCollationCommand = connection.CreateCommand(); - getCollationCommand.CommandText = - "SELECT CONVERT (VARCHAR(256), DATABASEPROPERTYEX(DB_NAME(), 'collation'))"; - var databaseCollation = (String)getCollationCommand.ExecuteScalar()!; - using var createTableCommand = connection.CreateCommand(); - createTableCommand.CommandText = $"CREATE TABLE #Values (Value NVARCHAR(4) COLLATE {databaseCollation})"; + createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value TEXT)"; createTableCommand.ExecuteNonQuery(); - using (var bulkCopy = new SqlBulkCopy(connection)) + using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)"; + + var valueParameter = new SqliteParameter(); + valueParameter.ParameterName = "@Value"; + + insertCommand.Parameters.Add(valueParameter); + + foreach (var value in scalarValues) { - bulkCopy.DestinationTableName = "#Values"; - bulkCopy.WriteToServer(valuesReader); + valueParameter.Value = value; + + insertCommand.ExecuteNonQuery(); } using var selectCommand = connection.CreateCommand(); - selectCommand.CommandText = "SELECT Value FROM #Values"; + selectCommand.CommandText = "SELECT Value FROM temp.\"Values\""; using var dataReader = selectCommand.ExecuteReader(); @@ -1390,7 +1645,7 @@ public List TemporaryTable_ScalarValues_Manually() } using var dropTableCommand = connection.CreateCommand(); - dropTableCommand.CommandText = "DROP TABLE #Values"; + dropTableCommand.CommandText = "DROP TABLE temp.\"Values\""; dropTableCommand.ExecuteNonQuery(); } @@ -1401,11 +1656,11 @@ public List TemporaryTable_ScalarValues_Manually() [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] public List TemporaryTable_ScalarValues_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); var scalarValues = Enumerable .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString()) + .Select(a => a.ToString(CultureInfo.InvariantCulture)) .ToList(); List result = []; @@ -1421,11 +1676,11 @@ public List TemporaryTable_ScalarValues_DbConnectionPlus() #region UpdateEntities private const String UpdateEntities_Category = "UpdateEntities"; - private const Int32 UpdateEntities_OperationsPerInvoke = 10; + private const Int32 UpdateEntities_OperationsPerInvoke = 25; private const Int32 UpdateEntities_EntitiesPerOperation = 100; [GlobalSetup(Targets = [ - nameof(UpdateEntities_Manually), + nameof(UpdateEntities_DbCommand), nameof(UpdateEntities_DbConnectionPlus) ])] public void UpdateEntities_Setup() @@ -1436,9 +1691,9 @@ public void UpdateEntities_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_Manually() + public void UpdateEntities_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) { @@ -1446,79 +1701,71 @@ public void UpdateEntities_Manually() using var command = connection.CreateCommand(); command.CommandText = """ - UPDATE [Entity] - SET [BooleanValue] = @BooleanValue, - [BytesValue] = @BytesValue, - [ByteValue] = @ByteValue, - [CharValue] = @CharValue, - [DateOnlyValue] = @DateOnlyValue, - [DateTimeValue] = @DateTimeValue, - [DecimalValue] = @DecimalValue, - [DoubleValue] = @DoubleValue, - [EnumValue] = @EnumValue, - [GuidValue] = @GuidValue, - [Int16Value] = @Int16Value, - [Int32Value] = @Int32Value, - [Int64Value] = @Int64Value, - [SingleValue] = @SingleValue, - [StringValue] = @StringValue, - [TimeOnlyValue] = @TimeOnlyValue, - [TimeSpanValue] = @TimeSpanValue - WHERE [Id] = @Id + UPDATE Entity + SET BooleanValue = @BooleanValue, + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue + WHERE Id = @Id """; - var idParameter = new SqlParameter(); + var idParameter = new SqliteParameter(); idParameter.ParameterName = "@Id"; - var booleanValueParameter = new SqlParameter(); + var booleanValueParameter = new SqliteParameter(); booleanValueParameter.ParameterName = "@BooleanValue"; - var bytesValueParameter = new SqlParameter(); + var bytesValueParameter = new SqliteParameter(); bytesValueParameter.ParameterName = "@BytesValue"; - var byteValueParameter = new SqlParameter(); + var byteValueParameter = new SqliteParameter(); byteValueParameter.ParameterName = "@ByteValue"; - var charValueParameter = new SqlParameter(); + var charValueParameter = new SqliteParameter(); charValueParameter.ParameterName = "@CharValue"; - var dateOnlyValueParameter = new SqlParameter(); - dateOnlyValueParameter.ParameterName = "@DateOnlyValue"; - - var dateTimeValueParameter = new SqlParameter(); + var dateTimeValueParameter = new SqliteParameter(); dateTimeValueParameter.ParameterName = "@DateTimeValue"; - var decimalValueParameter = new SqlParameter(); + var decimalValueParameter = new SqliteParameter(); decimalValueParameter.ParameterName = "@DecimalValue"; - var doubleValueParameter = new SqlParameter(); + var doubleValueParameter = new SqliteParameter(); doubleValueParameter.ParameterName = "@DoubleValue"; - var enumValueParameter = new SqlParameter(); + var enumValueParameter = new SqliteParameter(); enumValueParameter.ParameterName = "@EnumValue"; - var guidValueParameter = new SqlParameter(); + var guidValueParameter = new SqliteParameter(); guidValueParameter.ParameterName = "@GuidValue"; - var int16ValueParameter = new SqlParameter(); + var int16ValueParameter = new SqliteParameter(); int16ValueParameter.ParameterName = "@Int16Value"; - var int32ValueParameter = new SqlParameter(); + var int32ValueParameter = new SqliteParameter(); int32ValueParameter.ParameterName = "@Int32Value"; - var int64ValueParameter = new SqlParameter(); + var int64ValueParameter = new SqliteParameter(); int64ValueParameter.ParameterName = "@Int64Value"; - var singleValueParameter = new SqlParameter(); + var singleValueParameter = new SqliteParameter(); singleValueParameter.ParameterName = "@SingleValue"; - var stringValueParameter = new SqlParameter(); + var stringValueParameter = new SqliteParameter(); stringValueParameter.ParameterName = "@StringValue"; - var timeOnlyValueParameter = new SqlParameter(); - timeOnlyValueParameter.ParameterName = "@TimeOnlyValue"; - - var timeSpanValueParameter = new SqlParameter(); + var timeSpanValueParameter = new SqliteParameter(); timeSpanValueParameter.ParameterName = "@TimeSpanValue"; command.Parameters.Add(idParameter); @@ -1526,7 +1773,6 @@ UPDATE [Entity] command.Parameters.Add(bytesValueParameter); command.Parameters.Add(byteValueParameter); command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateOnlyValueParameter); command.Parameters.Add(dateTimeValueParameter); command.Parameters.Add(decimalValueParameter); command.Parameters.Add(doubleValueParameter); @@ -1537,40 +1783,51 @@ UPDATE [Entity] command.Parameters.Add(int64ValueParameter); command.Parameters.Add(singleValueParameter); command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeOnlyValueParameter); command.Parameters.Add(timeSpanValueParameter); foreach (var updatedEntity in updatedEntities) { idParameter.Value = updatedEntity.Id; - booleanValueParameter.Value = updatedEntity.BooleanValue; + booleanValueParameter.Value = updatedEntity.BooleanValue ? 1 : 0; bytesValueParameter.Value = updatedEntity.BytesValue; byteValueParameter.Value = updatedEntity.ByteValue; charValueParameter.Value = updatedEntity.CharValue; - dateOnlyValueParameter.Value = updatedEntity.DateOnlyValue; - dateTimeValueParameter.Value = updatedEntity.DateTimeValue; - decimalValueParameter.Value = updatedEntity.DecimalValue; + dateTimeValueParameter.Value = updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture); doubleValueParameter.Value = updatedEntity.DoubleValue; enumValueParameter.Value = updatedEntity.EnumValue.ToString(); - guidValueParameter.Value = updatedEntity.GuidValue; + guidValueParameter.Value = updatedEntity.GuidValue.ToString(); int16ValueParameter.Value = updatedEntity.Int16Value; int32ValueParameter.Value = updatedEntity.Int32Value; int64ValueParameter.Value = updatedEntity.Int64Value; singleValueParameter.Value = updatedEntity.SingleValue; stringValueParameter.Value = updatedEntity.StringValue; - timeOnlyValueParameter.Value = updatedEntity.TimeOnlyValue; - timeSpanValueParameter.Value = updatedEntity.TimeSpanValue; + timeSpanValueParameter.Value = updatedEntity.TimeSpanValue.ToString(); command.ExecuteNonQuery(); } } } + [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] + [BenchmarkCategory(UpdateEntities_Category)] + public void UpdateEntities_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) + { + var updatesEntities = Generate.UpdateFor(this.entitiesInDb); + + SqlMapperExtensions.Update(connection, updatesEntities); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] [BenchmarkCategory(UpdateEntities_Category)] public void UpdateEntities_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) { @@ -1583,10 +1840,10 @@ public void UpdateEntities_DbConnectionPlus() #region UpdateEntity private const String UpdateEntity_Category = "UpdateEntity"; - private const Int32 UpdateEntity_OperationsPerInvoke = 700; + private const Int32 UpdateEntity_OperationsPerInvoke = 1_600; [GlobalSetup(Targets = [ - nameof(UpdateEntity_Manually), + nameof(UpdateEntity_DbCommand), nameof(UpdateEntity_DbConnectionPlus) ])] public void UpdateEntity_Setup() @@ -1597,9 +1854,9 @@ public void UpdateEntity_Setup() [Benchmark(Baseline = true, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_Manually() + public void UpdateEntity_DbCommand() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) { @@ -1609,54 +1866,66 @@ public void UpdateEntity_Manually() using var command = connection.CreateCommand(); command.CommandText = """ - UPDATE [Entity] - SET [BooleanValue] = @BooleanValue, - [BytesValue] = @BytesValue, - [ByteValue] = @ByteValue, - [CharValue] = @CharValue, - [DateOnlyValue] = @DateOnlyValue, - [DateTimeValue] = @DateTimeValue, - [DecimalValue] = @DecimalValue, - [DoubleValue] = @DoubleValue, - [EnumValue] = @EnumValue, - [GuidValue] = @GuidValue, - [Int16Value] = @Int16Value, - [Int32Value] = @Int32Value, - [Int64Value] = @Int64Value, - [SingleValue] = @SingleValue, - [StringValue] = @StringValue, - [TimeOnlyValue] = @TimeOnlyValue, - [TimeSpanValue] = @TimeSpanValue - WHERE [Id] = @Id + UPDATE Entity + SET BooleanValue = @BooleanValue, + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue + WHERE Id = @Id """; command.Parameters.Add(new("@Id", updatedEntity.Id)); - command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue)); + command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue ? 1 :0)); command.Parameters.Add(new("@BytesValue", updatedEntity.BytesValue)); command.Parameters.Add(new("@ByteValue", updatedEntity.ByteValue)); command.Parameters.Add(new("@CharValue", updatedEntity.CharValue)); - command.Parameters.Add(new("@DateOnlyValue", updatedEntity.DateOnlyValue)); - command.Parameters.Add(new("@DateTimeValue", updatedEntity.DateTimeValue)); - command.Parameters.Add(new("@DecimalValue", updatedEntity.DecimalValue)); + command.Parameters.Add(new("@DateTimeValue", updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); + command.Parameters.Add(new("@DecimalValue", updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture))); command.Parameters.Add(new("@DoubleValue", updatedEntity.DoubleValue)); command.Parameters.Add(new("@EnumValue", updatedEntity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", updatedEntity.GuidValue)); + command.Parameters.Add(new("@GuidValue", updatedEntity.GuidValue.ToString())); command.Parameters.Add(new("@Int16Value", updatedEntity.Int16Value)); command.Parameters.Add(new("@Int32Value", updatedEntity.Int32Value)); command.Parameters.Add(new("@Int64Value", updatedEntity.Int64Value)); command.Parameters.Add(new("@SingleValue", updatedEntity.SingleValue)); command.Parameters.Add(new("@StringValue", updatedEntity.StringValue)); - command.Parameters.Add(new("@TimeOnlyValue", updatedEntity.TimeOnlyValue)); - command.Parameters.Add(new("@TimeSpanValue", updatedEntity.TimeSpanValue)); + command.Parameters.Add(new("@TimeSpanValue", updatedEntity.TimeSpanValue.ToString())); command.ExecuteNonQuery(); } } + [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] + [BenchmarkCategory(UpdateEntity_Category)] + public void UpdateEntity_Dapper() + { + var connection = this.CreateConnection(); + + for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[i]; + + var updatedEntity = Generate.UpdateFor(entity); + + SqlMapperExtensions.Update(connection, updatedEntity); + } + } + [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] [BenchmarkCategory(UpdateEntity_Category)] public void UpdateEntity_DbConnectionPlus() { - using var connection = this.CreateConnection(); + var connection = this.CreateConnection(); for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) { @@ -1669,5 +1938,31 @@ public void UpdateEntity_DbConnectionPlus() } #endregion UpdateEntity - private readonly SqlServerTestDatabaseProvider testDatabaseProvider = new(); -} + private static BenchmarkEntity ReadEntity(IDataReader dataReader) + { + var charBuffer = new Char[1]; + + var ordinal = 0; + return new() + { + Id = dataReader.GetInt64(ordinal++), + BooleanValue = dataReader.GetInt64(ordinal++) == 1, + BytesValue = (Byte[])dataReader.GetValue(ordinal++), + ByteValue = dataReader.GetByte(ordinal++), + CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(), + DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture), + DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture), + DoubleValue = dataReader.GetDouble(ordinal++), + EnumValue = Enum.Parse(dataReader.GetString(ordinal++)), + GuidValue = Guid.Parse(dataReader.GetString(ordinal++)), + Int16Value = (Int16)dataReader.GetInt64(ordinal++), + Int32Value = (Int32)dataReader.GetInt64(ordinal++), + Int64Value = dataReader.GetInt64(ordinal++), + SingleValue = dataReader.GetFloat(ordinal++), + StringValue = dataReader.GetString(ordinal++), + TimeSpanValue = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture) + }; + } + + private SqliteTestDatabaseProvider? testDatabaseProvider; +} \ No newline at end of file diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs new file mode 100644 index 0000000..810fb34 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs @@ -0,0 +1,14 @@ +using Dapper; + +namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; + +public class GuidTypeHandler : SqlMapper.StringTypeHandler +{ + /// + protected override Guid Parse(String xml) => + Guid.Parse(xml); + + /// + protected override String Format(Guid xml) => + xml.ToString(); +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs new file mode 100644 index 0000000..f12cd64 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs @@ -0,0 +1,14 @@ +using Dapper; + +namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; + +public class TimeSpanTypeHandler : SqlMapper.StringTypeHandler +{ + /// + protected override TimeSpan Parse(String xml) => + TimeSpan.Parse(xml); + + /// + protected override String Format(TimeSpan xml) => + xml.ToString(); +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj b/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj index 410c65c..b451269 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj +++ b/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj @@ -18,6 +18,8 @@ + + diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index 162cb6e..adcdab0 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -1,11 +1,170 @@ -using BenchmarkDotNet.Running; +#pragma warning disable RCS1163, IDE0022 + +using BenchmarkDotNet.Running; namespace RentADeveloper.DbConnectionPlus.Benchmarks; -public class Program +public static class Program { - public static void Main(String[] args) => - BenchmarkSwitcher + public static void Main(String[] args) + { + /*BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) - .Run(args); + .Run(args);*/ + + var benchmarks = new Benchmarks(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntities_Setup(); + benchmarks.DeleteEntities_DbCommand(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntities_Setup(); + benchmarks.DeleteEntities_Dapper(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntities_Setup(); + benchmarks.DeleteEntities_DbConnectionPlus(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntity_Setup(); + benchmarks.DeleteEntity_DbCommand(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntity_Setup(); + benchmarks.DeleteEntity_Dapper(); + + benchmarks.Setup_Global(); + benchmarks.DeleteEntity_Setup(); + benchmarks.DeleteEntity_DbConnectionPlus(); + + benchmarks.Setup_Global(); + benchmarks.ExecuteNonQuery_Setup(); + benchmarks.ExecuteNonQuery_DbCommand(); + + benchmarks.Setup_Global(); + benchmarks.ExecuteNonQuery_Setup(); + benchmarks.ExecuteNonQuery_Dapper(); + + benchmarks.Setup_Global(); + benchmarks.ExecuteNonQuery_Setup(); + benchmarks.ExecuteNonQuery_DbConnectionPlus(); + + benchmarks.ExecuteReader_Setup(); + benchmarks.ExecuteReader_DbCommand(); + + benchmarks.ExecuteReader_Setup(); + benchmarks.ExecuteReader_Dapper(); + + benchmarks.ExecuteReader_Setup(); + benchmarks.ExecuteReader_DbConnectionPlus(); + + benchmarks.ExecuteScalar_Setup(); + benchmarks.ExecuteScalar_DbCommand(); + + benchmarks.ExecuteScalar_Setup(); + benchmarks.ExecuteScalar_Dapper(); + + benchmarks.ExecuteScalar_Setup(); + benchmarks.ExecuteScalar_DbConnectionPlus(); + + benchmarks.Exists_Setup(); + benchmarks.Exists_DbCommand(); + + benchmarks.Exists_Setup(); + benchmarks.Exists_DbConnectionPlus(); + + benchmarks.InsertEntities_Setup(); + benchmarks.InsertEntities_DbCommand(); + + benchmarks.InsertEntities_Setup(); + benchmarks.InsertEntities_Dapper(); + + benchmarks.InsertEntities_Setup(); + benchmarks.InsertEntities_DbConnectionPlus(); + + benchmarks.InsertEntity_Setup(); + benchmarks.InsertEntity_DbCommand(); + + benchmarks.InsertEntity_Setup(); + benchmarks.InsertEntity_Dapper(); + + benchmarks.InsertEntity_Setup(); + benchmarks.InsertEntity_DbConnectionPlus(); + + benchmarks.Parameter_Setup(); + benchmarks.Parameter_DbCommand(); + + benchmarks.Parameter_Setup(); + benchmarks.Parameter_Dapper(); + + benchmarks.Parameter_Setup(); + benchmarks.Parameter_DbConnectionPlus(); + + benchmarks.Query_Dynamic_Setup(); + benchmarks.Query_Dynamic_DbCommand(); + + benchmarks.Query_Dynamic_Setup(); + benchmarks.Query_Dynamic_Dapper(); + + benchmarks.Query_Scalars_Setup(); + benchmarks.Query_Scalars_DbConnectionPlus(); + + benchmarks.Query_Entities_Setup(); + benchmarks.Query_Entities_DbCommand(); + + benchmarks.Query_Entities_Setup(); + benchmarks.Query_Entities_Dapper(); + + benchmarks.Query_Entities_Setup(); + benchmarks.Query_Entities_DbConnectionPlus(); + + benchmarks.Query_Dynamic_Setup(); + benchmarks.Query_Dynamic_DbConnectionPlus(); + + benchmarks.Query_Scalars_Setup(); + benchmarks.Query_Scalars_DbCommand(); + + benchmarks.Query_Scalars_Setup(); + benchmarks.Query_Scalars_Dapper(); + + benchmarks.Query_ValueTuples_Setup(); + benchmarks.Query_ValueTuples_DbCommand(); + + benchmarks.Query_ValueTuples_Setup(); + benchmarks.Query_ValueTuples_Dapper(); + + benchmarks.Query_ValueTuples_Setup(); + benchmarks.Query_ValueTuples_DbConnectionPlus(); + + benchmarks.TemporaryTable_ComplexObjects_Setup(); + benchmarks.TemporaryTable_ComplexObjects_DbCommand(); + + benchmarks.TemporaryTable_ComplexObjects_Setup(); + benchmarks.TemporaryTable_ComplexObjects_DbConnectionPlus(); + + benchmarks.TemporaryTable_ScalarValues_Setup(); + benchmarks.TemporaryTable_ScalarValues_DbCommand(); + + benchmarks.TemporaryTable_ScalarValues_Setup(); + benchmarks.TemporaryTable_ScalarValues_DbConnectionPlus(); + + benchmarks.UpdateEntities_Setup(); + benchmarks.UpdateEntities_DbCommand(); + + benchmarks.UpdateEntities_Setup(); + benchmarks.UpdateEntities_Dapper(); + + benchmarks.UpdateEntities_Setup(); + benchmarks.UpdateEntities_DbConnectionPlus(); + + benchmarks.UpdateEntity_Setup(); + benchmarks.UpdateEntity_DbCommand(); + + benchmarks.UpdateEntity_Setup(); + benchmarks.UpdateEntity_Dapper(); + + benchmarks.UpdateEntity_Setup(); + benchmarks.UpdateEntity_DbConnectionPlus(); + } } \ No newline at end of file diff --git a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs b/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs index 0178be6..d16308e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs @@ -5,6 +5,7 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestHelpers; /// /// An implementation of that supports delaying created commands. /// +/// The provider for the current test database. public class DelayDbCommandFactory(ITestDatabaseProvider testDatabaseProvider) : IDbCommandFactory { /// diff --git a/tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs new file mode 100644 index 0000000..74be75e --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs @@ -0,0 +1,26 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; + +[Table("Entity")] +public record BenchmarkEntity +{ + public Boolean BooleanValue { get; set; } + public Byte[] BytesValue { get; set; } = null!; + public Byte ByteValue { get; set; } + public Char CharValue { get; set; } + public DateTime DateTimeValue { get; set; } + public Decimal DecimalValue { get; set; } + public Double DoubleValue { get; set; } + public TestEnum EnumValue { get; set; } + public Guid GuidValue { get; set; } + + [Key] + public Int64 Id { get; set; } + + public Int16 Int16Value { get; set; } + public Int32 Int32Value { get; set; } + public Int64 Int64Value { get; set; } + + public Single SingleValue { get; set; } + public String StringValue { get; set; } = null!; + public TimeSpan TimeSpanValue { get; set; } +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 212eff0..81824d0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -25,4 +25,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 From abf72cd59ec004557326fa3aeb51d387e893fbba Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Fri, 6 Feb 2026 18:33:52 +0100 Subject: [PATCH 10/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- CHANGELOG.md | 3 + .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 365 +++++++++--------- .../DbConnectionPlus.Benchmarks/Program.cs | 6 +- 3 files changed, 192 insertions(+), 182 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d860b..6243047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ TODO: Update date ### Added - Support for Optimistic Concurrency Support via Concurrency Tokens (Fixes [issue #5](https://github.com/rent-a-developer/DbConnectionPlus/issues/5)) +### Changed +- Switched benchmarks to SQLite for more stable results. + ## [1.1.0] - 2026-02-01 ### Added diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index 156b58e..9dfc223 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -61,7 +61,13 @@ private void PrepareEntitiesInDb(Int32 numberOfEntities) private const Int32 DeleteEntities_EntitiesPerOperation = 250; private const Int32 DeleteEntities_OperationsPerInvoke = 20; - [IterationSetup(Targets = [nameof(DeleteEntities_DbCommand), nameof(DeleteEntities_DbConnectionPlus)])] + [IterationSetup( + Targets = [ + nameof(DeleteEntities_DbCommand), + nameof(DeleteEntities_Dapper), + nameof(DeleteEntities_DbConnectionPlus) + ] + )] public void DeleteEntities_Setup() => this.PrepareEntitiesInDb(DeleteEntities_OperationsPerInvoke * DeleteEntities_EntitiesPerOperation); @@ -134,7 +140,13 @@ public void DeleteEntities_DbConnectionPlus() private const String DeleteEntity_Category = "DeleteEntity"; private const Int32 DeleteEntity_OperationsPerInvoke = 8000; - [IterationSetup(Targets = [nameof(DeleteEntity_DbCommand), nameof(DeleteEntity_DbConnectionPlus)])] + [IterationSetup( + Targets = [ + nameof(DeleteEntity_DbCommand), + nameof(DeleteEntity_Dapper), + nameof(DeleteEntity_DbConnectionPlus) + ] + )] public void DeleteEntity_Setup() => this.PrepareEntitiesInDb(DeleteEntity_OperationsPerInvoke); @@ -196,7 +208,13 @@ public void DeleteEntity_DbConnectionPlus() private const String ExecuteNonQuery_Category = "ExecuteNonQuery"; private const Int32 ExecuteNonQuery_OperationsPerInvoke = 7700; - [IterationSetup(Targets = [nameof(ExecuteNonQuery_DbCommand), nameof(ExecuteNonQuery_DbConnectionPlus)])] + [IterationSetup( + Targets = [ + nameof(ExecuteNonQuery_DbCommand), + nameof(ExecuteNonQuery_Dapper), + nameof(ExecuteNonQuery_DbConnectionPlus) + ] + )] public void ExecuteNonQuery_Setup() => this.PrepareEntitiesInDb(ExecuteNonQuery_OperationsPerInvoke); @@ -259,7 +277,13 @@ public void ExecuteNonQuery_DbConnectionPlus() private const Int32 ExecuteReader_OperationsPerInvoke = 700; private const Int32 ExecuteReader_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(ExecuteReader_DbCommand), nameof(ExecuteReader_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(ExecuteReader_DbCommand), + nameof(ExecuteReader_Dapper), + nameof(ExecuteReader_DbConnectionPlus) + ] + )] public void ExecuteReader_Setup() { this.Setup_Global(); @@ -279,28 +303,7 @@ public List ExecuteReader_DbCommand() entities.Clear(); using var command = connection.CreateCommand(); - command.CommandText = $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {ExecuteReader_EntitiesPerOperation} - """; + command.CommandText = ExecuteReaderSql; using var dataReader = command.ExecuteReader(); @@ -325,31 +328,7 @@ public List ExecuteReader_Dapper() { entities.Clear(); - using var dataReader = SqlMapper.ExecuteReader( - connection, - $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {ExecuteReader_EntitiesPerOperation} - """ - ); + using var dataReader = SqlMapper.ExecuteReader(connection, ExecuteReaderSql); while (dataReader.Read()) { @@ -372,30 +351,7 @@ public List ExecuteReader_DbConnectionPlus() { entities.Clear(); - using var dataReader = connection.ExecuteReader( - $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {ExecuteReader_EntitiesPerOperation} - """ - ); + using var dataReader = connection.ExecuteReader(ExecuteReaderSql); while (dataReader.Read()) { @@ -411,7 +367,13 @@ public List ExecuteReader_DbConnectionPlus() private const String ExecuteScalar_Category = "ExecuteScalar"; private const Int32 ExecuteScalar_OperationsPerInvoke = 5000; - [GlobalSetup(Targets = [nameof(ExecuteScalar_DbCommand), nameof(ExecuteScalar_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(ExecuteScalar_DbCommand), + nameof(ExecuteScalar_Dapper), + nameof(ExecuteScalar_DbConnectionPlus) + ] + )] public void ExecuteScalar_Setup() { this.Setup_Global(); @@ -488,7 +450,12 @@ public String ExecuteScalar_DbConnectionPlus() private const String Exists_Category = "Exists"; private const Int32 Exists_OperationsPerInvoke = 5000; - [GlobalSetup(Targets = [nameof(Exists_DbCommand), nameof(Exists_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Exists_DbCommand), + nameof(Exists_DbConnectionPlus) + ] + )] public void Exists_Setup() { this.Setup_Global(); @@ -543,7 +510,13 @@ public Boolean Exists_DbConnectionPlus() private const Int32 InsertEntities_OperationsPerInvoke = 20; private const Int32 InsertEntities_EntitiesPerOperation = 140; - [GlobalSetup(Targets = [nameof(InsertEntities_DbCommand), nameof(InsertEntities_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(InsertEntities_DbCommand), + nameof(InsertEntities_Dapper), + nameof(InsertEntities_DbConnectionPlus) + ] + )] public void InsertEntities_Setup() { this.Setup_Global(); @@ -561,46 +534,7 @@ public void InsertEntities_DbCommand() var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); using var command = connection.CreateCommand(); - command.CommandText = """ - INSERT INTO Entity - ( - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - ) - VALUES - ( - @Id, - @BooleanValue, - @BytesValue, - @ByteValue, - @CharValue, - @DateTimeValue, - @DecimalValue, - @DoubleValue, - @EnumValue, - @GuidValue, - @Int16Value, - @Int32Value, - @Int64Value, - @SingleValue, - @StringValue, - @TimeSpanValue - ) - """; + command.CommandText = InsertEntitySql; var idParameter = new SqliteParameter(); idParameter.ParameterName = "@Id"; @@ -724,7 +658,13 @@ public void InsertEntities_DbConnectionPlus() private const String InsertEntity_Category = "InsertEntity"; private const Int32 InsertEntity_OperationsPerInvoke = 2500; - [GlobalSetup(Targets = [nameof(InsertEntity_DbCommand), nameof(InsertEntity_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(InsertEntity_DbCommand), + nameof(InsertEntity_Dapper), + nameof(InsertEntity_DbConnectionPlus) + ] + )] public void InsertEntity_Setup() { this.Setup_Global(); @@ -742,46 +682,7 @@ public void InsertEntity_DbCommand() var entity = Generate.Single(); using var command = connection.CreateCommand(); - command.CommandText = """ - INSERT INTO Entity - ( - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - ) - VALUES - ( - @Id, - @BooleanValue, - @BytesValue, - @ByteValue, - @CharValue, - @DateTimeValue, - @DecimalValue, - @DoubleValue, - @EnumValue, - @GuidValue, - @Int16Value, - @Int32Value, - @Int64Value, - @SingleValue, - @StringValue, - @TimeSpanValue - ) - """; + command.CommandText = InsertEntitySql; command.Parameters.Add(new("@Id", entity.Id)); command.Parameters.Add(new("@BooleanValue", entity.BooleanValue ? 1 : 0)); command.Parameters.Add(new("@BytesValue", entity.BytesValue)); @@ -836,7 +737,13 @@ public void InsertEntity_DbConnectionPlus() private const String Parameter_Category = "Parameter"; private const Int32 Parameter_OperationsPerInvoke = 35_000; - [GlobalSetup(Targets = [nameof(Parameter_DbCommand), nameof(Parameter_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Parameter_DbCommand), + nameof(Parameter_Dapper), + nameof(Parameter_DbConnectionPlus) + ] + )] public void Parameter_Setup() { this.Setup_Global(); @@ -953,7 +860,13 @@ public Object Parameter_DbConnectionPlus() private const Int32 Query_Dynamic_OperationsPerInvoke = 600; private const Int32 Query_Dynamic_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(Query_Dynamic_DbCommand), nameof(Query_Dynamic_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Query_Dynamic_DbCommand), + nameof(Query_Dynamic_Dapper), + nameof(Query_Dynamic_DbConnectionPlus) + ] + )] public void Query_Dynamic_Setup() { this.Setup_Global(); @@ -1071,7 +984,13 @@ public List Query_Dynamic_Dapper() private const Int32 Query_Scalars_OperationsPerInvoke = 1500; private const Int32 Query_Scalars_EntitiesPerOperation = 500; - [GlobalSetup(Targets = [nameof(Query_Scalars_DbCommand), nameof(Query_Scalars_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Query_Scalars_DbCommand), + nameof(Query_Scalars_Dapper), + nameof(Query_Scalars_DbConnectionPlus) + ] + )] public void Query_Scalars_Setup() { this.Setup_Global(); @@ -1150,7 +1069,13 @@ public List Query_Scalars_DbConnectionPlus() private const Int32 Query_Entities_OperationsPerInvoke = 600; private const Int32 Query_Entities_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [nameof(Query_Entities_DbCommand), nameof(Query_Entities_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Query_Entities_DbCommand), + nameof(Query_Entities_Dapper), + nameof(Query_Entities_DbConnectionPlus) + ] + )] public void Query_Entities_Setup() { this.Setup_Global(); @@ -1248,7 +1173,13 @@ public List Query_Entities_DbConnectionPlus() private const Int32 Query_ValueTuples_OperationsPerInvoke = 1_000; private const Int32 Query_ValueTuples_EntitiesPerOperation = 150; - [GlobalSetup(Targets = [nameof(Query_ValueTuples_DbCommand), nameof(Query_ValueTuples_DbConnectionPlus)])] + [GlobalSetup( + Targets = [ + nameof(Query_ValueTuples_DbCommand), + nameof(Query_ValueTuples_Dapper), + nameof(Query_ValueTuples_DbConnectionPlus) + ] + )] public void Query_ValueTuples_Setup() { this.Setup_Global(); @@ -1349,10 +1280,12 @@ FROM Entity private const Int32 TemporaryTable_ComplexObjects_OperationsPerInvoke = 50; private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 200; - [GlobalSetup(Targets = [ - nameof(TemporaryTable_ComplexObjects_DbCommand), - nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) - ])] + [GlobalSetup( + Targets = [ + nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) + ] + )] public void TemporaryTable_ComplexObjects_Setup() { this.Setup_Global(); @@ -1588,10 +1521,12 @@ public List TemporaryTable_ComplexObjects_DbConnectionPlus() private const Int32 TemporaryTable_ScalarValues_OperationsPerInvoke = 30; private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000; - [GlobalSetup(Targets = [ - nameof(TemporaryTable_ScalarValues_DbCommand), - nameof(TemporaryTable_ScalarValues_DbConnectionPlus) - ])] + [GlobalSetup( + Targets = [ + nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_DbConnectionPlus) + ] + )] public void TemporaryTable_ScalarValues_Setup() { this.Setup_Global(); @@ -1679,10 +1614,13 @@ public List TemporaryTable_ScalarValues_DbConnectionPlus() private const Int32 UpdateEntities_OperationsPerInvoke = 25; private const Int32 UpdateEntities_EntitiesPerOperation = 100; - [GlobalSetup(Targets = [ - nameof(UpdateEntities_DbCommand), - nameof(UpdateEntities_DbConnectionPlus) - ])] + [GlobalSetup( + Targets = [ + nameof(UpdateEntities_DbCommand), + nameof(UpdateEntities_Dapper), + nameof(UpdateEntities_DbConnectionPlus) + ] + )] public void UpdateEntities_Setup() { this.Setup_Global(); @@ -1842,10 +1780,13 @@ public void UpdateEntities_DbConnectionPlus() private const String UpdateEntity_Category = "UpdateEntity"; private const Int32 UpdateEntity_OperationsPerInvoke = 1_600; - [GlobalSetup(Targets = [ - nameof(UpdateEntity_DbCommand), - nameof(UpdateEntity_DbConnectionPlus) - ])] + [GlobalSetup( + Targets = [ + nameof(UpdateEntity_DbCommand), + nameof(UpdateEntity_Dapper), + nameof(UpdateEntity_DbConnectionPlus) + ] + )] public void UpdateEntity_Setup() { this.Setup_Global(); @@ -1965,4 +1906,68 @@ private static BenchmarkEntity ReadEntity(IDataReader dataReader) } private SqliteTestDatabaseProvider? testDatabaseProvider; + + private const String InsertEntitySql = """ + INSERT INTO Entity + ( + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + ) + VALUES + ( + @Id, + @BooleanValue, + @BytesValue, + @ByteValue, + @CharValue, + @DateTimeValue, + @DecimalValue, + @DoubleValue, + @EnumValue, + @GuidValue, + @Int16Value, + @Int32Value, + @Int64Value, + @SingleValue, + @StringValue, + @TimeSpanValue + ) + """; + + private static readonly String ExecuteReaderSql = $""" + SELECT + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + FROM + Entity + LIMIT {ExecuteReader_EntitiesPerOperation} + """; } \ No newline at end of file diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index adcdab0..de04b28 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -8,10 +8,11 @@ public static class Program { public static void Main(String[] args) { - /*BenchmarkSwitcher + BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) - .Run(args);*/ + .Run(args); + /* var benchmarks = new Benchmarks(); benchmarks.Setup_Global(); @@ -166,5 +167,6 @@ public static void Main(String[] args) benchmarks.UpdateEntity_Setup(); benchmarks.UpdateEntity_DbConnectionPlus(); + */ } } \ No newline at end of file From 8f0e61389adeb6c51c299dfe51da71fdf03348ac Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 7 Feb 2026 00:10:31 +0100 Subject: [PATCH 11/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../Benchmarks.DeleteEntities.cs | 93 + .../Benchmarks.DeleteEntity.cs | 81 + .../Benchmarks.ExecuteNonQuery.cs | 81 + .../Benchmarks.ExecuteReader.cs | 106 + .../Benchmarks.ExecuteScalar.cs | 71 + .../Benchmarks.Exists.cs | 70 + .../Benchmarks.InsertEntities.cs | 222 ++ .../Benchmarks.InsertEntity.cs | 81 + .../Benchmarks.Parameter.cs | 117 + .../Benchmarks.Query_Dynamic.cs | 86 + .../Benchmarks.Query_Entities.cs | 90 + .../Benchmarks.Query_Scalars.cs | 67 + .../Benchmarks.Query_ValueTuples.cs | 82 + ...enchmarks.TemporaryTable_ComplexObjects.cs | 411 ++++ .../Benchmarks.TemporaryTable_ScalarValues.cs | 128 ++ .../Benchmarks.UpdateEntities.cs | 199 ++ .../Benchmarks.UpdateEntity.cs | 105 + .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 1984 +---------------- .../BenchmarksConfig.cs | 14 +- .../DapperTypeHandlers/GuidTypeHandler.cs | 12 +- .../DapperTypeHandlers/TimeSpanTypeHandler.cs | 12 +- .../GlobalUsings.cs | 10 + .../DbConnectionPlus.Benchmarks/Program.cs | 165 +- .../TestData/BenchmarkEntity.cs | 6 +- 24 files changed, 2165 insertions(+), 2128 deletions(-) create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs create mode 100644 benchmarks/DbConnectionPlus.Benchmarks/GlobalUsings.cs rename {tests/DbConnectionPlus.UnitTests => benchmarks/DbConnectionPlus.Benchmarks}/TestData/BenchmarkEntity.cs (80%) diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs new file mode 100644 index 0000000..2d78458 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs @@ -0,0 +1,93 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [IterationCleanup( + Targets = + [ + nameof(DeleteEntities_DbCommand), + nameof(DeleteEntities_Dapper), + nameof(DeleteEntities_DbConnectionPlus) + ] + )] + public void DeleteEntities__Cleanup() => + this.connection.Dispose(); + + [IterationSetup( + Targets = + [ + nameof(DeleteEntities_DbCommand), + nameof(DeleteEntities_Dapper), + nameof(DeleteEntities_DbConnectionPlus) + ] + )] + public void DeleteEntities__Setup() => + this.SetupDatabase(DeleteEntities_EntitiesPerOperation * DeleteEntities_OperationsPerInvoke); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(DeleteEntities_Category)] + public void DeleteEntities_Dapper() + { + for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + { + var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); + + SqlMapperExtensions.Delete(this.connection, entities); + + foreach (var entity in entities) + { + this.entitiesInDb.Remove(entity); + } + } + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(DeleteEntities_Category)] + public void DeleteEntities_DbCommand() + { + for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + { + using var command = this.connection.CreateCommand(); + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; + + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "@Id"; + command.Parameters.Add(idParameter); + + foreach (var entity in this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList()) + { + idParameter.Value = entity.Id; + + command.ExecuteNonQuery(); + + this.entitiesInDb.Remove(entity); + } + } + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(DeleteEntities_Category)] + public void DeleteEntities_DbConnectionPlus() + { + for (int i = 0; i < DeleteEntities_EntitiesPerOperation; i++) + { + var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); + + this.connection.DeleteEntities(entities); + + foreach (var entity in entities) + { + this.entitiesInDb.Remove(entity); + } + } + } + + private const String DeleteEntities_Category = "DeleteEntities"; + private const Int32 DeleteEntities_EntitiesPerOperation = 250; + private const Int32 DeleteEntities_OperationsPerInvoke = 20; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs new file mode 100644 index 0000000..8f3d818 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs @@ -0,0 +1,81 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [IterationCleanup( + Targets = + [ + nameof(DeleteEntity_DbCommand), + nameof(DeleteEntity_Dapper), + nameof(DeleteEntity_DbConnectionPlus) + ] + )] + public void DeleteEntity__Cleanup() => + this.SetupDatabase(DeleteEntity_OperationsPerInvoke); + + [IterationSetup( + Targets = + [ + nameof(DeleteEntity_DbCommand), + nameof(DeleteEntity_Dapper), + nameof(DeleteEntity_DbConnectionPlus) + ] + )] + public void DeleteEntity__Setup() => + this.SetupDatabase(DeleteEntity_OperationsPerInvoke); + + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [BenchmarkCategory(DeleteEntity_Category)] + public void DeleteEntity_Dapper() + { + for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) + { + var entityToDelete = this.entitiesInDb[0]; + + SqlMapperExtensions.Delete(this.connection, entityToDelete); + + this.entitiesInDb.Remove(entityToDelete); + } + } + + [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [BenchmarkCategory(DeleteEntity_Category)] + public void DeleteEntity_DbCommand() + { + for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) + { + var entityToDelete = this.entitiesInDb[0]; + + using var command = this.connection.CreateCommand(); + + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; + command.Parameters.Add(new("@Id", entityToDelete.Id)); + + command.ExecuteNonQuery(); + + this.entitiesInDb.Remove(entityToDelete); + } + } + + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [BenchmarkCategory(DeleteEntity_Category)] + public void DeleteEntity_DbConnectionPlus() + { + for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) + { + var entityToDelete = this.entitiesInDb[0]; + + this.connection.DeleteEntity(entityToDelete); + + this.entitiesInDb.Remove(entityToDelete); + } + } + + private const String DeleteEntity_Category = "DeleteEntity"; + private const Int32 DeleteEntity_OperationsPerInvoke = 8000; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs new file mode 100644 index 0000000..7f80332 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs @@ -0,0 +1,81 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [IterationCleanup( + Targets = + [ + nameof(ExecuteNonQuery_DbCommand), + nameof(ExecuteNonQuery_Dapper), + nameof(ExecuteNonQuery_DbConnectionPlus) + ] + )] + public void ExecuteNonQuery__Cleanup() => + this.connection.Dispose(); + + [IterationSetup( + Targets = + [ + nameof(ExecuteNonQuery_DbCommand), + nameof(ExecuteNonQuery_Dapper), + nameof(ExecuteNonQuery_DbConnectionPlus) + ] + )] + public void ExecuteNonQuery__Setup() => + this.SetupDatabase(ExecuteNonQuery_OperationsPerInvoke); + + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteNonQuery_Category)] + public void ExecuteNonQuery_Dapper() + { + for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[0]; + + SqlMapper.Execute(this.connection, "DELETE FROM Entity WHERE Id = @Id", new { entity.Id }); + + this.entitiesInDb.Remove(entity); + } + } + + [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteNonQuery_Category)] + public void ExecuteNonQuery_DbCommand() + { + for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[0]; + + using var command = this.connection.CreateCommand(); + + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; + command.Parameters.Add(new("@Id", entity.Id)); + + command.ExecuteNonQuery(); + + this.entitiesInDb.Remove(entity); + } + } + + [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [BenchmarkCategory(ExecuteNonQuery_Category)] + public void ExecuteNonQuery_DbConnectionPlus() + { + for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) + { + var entity = this.entitiesInDb[0]; + + this.connection.ExecuteNonQuery($"DELETE FROM Entity WHERE Id = {Parameter(entity.Id)}"); + + this.entitiesInDb.Remove(entity); + } + } + + private const String ExecuteNonQuery_Category = "ExecuteNonQuery"; + private const Int32 ExecuteNonQuery_OperationsPerInvoke = 7700; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs new file mode 100644 index 0000000..0cd94dc --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs @@ -0,0 +1,106 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(ExecuteReader_DbCommand), + nameof(ExecuteReader_Dapper), + nameof(ExecuteReader_DbConnectionPlus) + ] + )] + public void ExecuteReader__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(ExecuteReader_DbCommand), + nameof(ExecuteReader_Dapper), + nameof(ExecuteReader_DbConnectionPlus) + ] + )] + public void ExecuteReader__Setup() => + this.SetupDatabase(100); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteReader_Category)] + public List ExecuteReader_Dapper() + { + var result = new List(); + + using var dataReader = SqlMapper.ExecuteReader(this.connection, ExecuteReaderSql); + + while (dataReader.Read()) + { + result.Add(ReadEntity(dataReader)); + } + + return result; + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(ExecuteReader_Category)] + public List ExecuteReader_DbCommand() + { + var result = new List(); + + using var command = this.connection.CreateCommand(); + command.CommandText = ExecuteReaderSql; + + using var dataReader = command.ExecuteReader(); + + while (dataReader.Read()) + { + result.Add(ReadEntity(dataReader)); + } + + return result; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteReader_Category)] + public List ExecuteReader_DbConnectionPlus() + { + var result = new List(); + + using var dataReader = this.connection.ExecuteReader(ExecuteReaderSql); + + while (dataReader.Read()) + { + result.Add(ReadEntity(dataReader)); + } + + return result; + } + + private const String ExecuteReader_Category = "ExecuteReader"; + + private const String ExecuteReaderSql = """ + SELECT + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + FROM + Entity + """; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs new file mode 100644 index 0000000..d6bf6d6 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs @@ -0,0 +1,71 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(ExecuteScalar_DbCommand), + nameof(ExecuteScalar_Dapper), + nameof(ExecuteScalar_DbConnectionPlus) + ] + )] + public void ExecuteScalar__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(ExecuteScalar_DbCommand), + nameof(ExecuteScalar_Dapper), + nameof(ExecuteScalar_DbConnectionPlus) + ] + )] + public void ExecuteScalar__Setup() => + this.SetupDatabase(1); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteScalar_Category)] + public String ExecuteScalar_Dapper() + { + var entity = this.entitiesInDb[0]; + + return SqlMapper.ExecuteScalar( + this.connection, + "SELECT StringValue FROM Entity WHERE Id = @Id", + new { entity.Id } + )!; + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(ExecuteScalar_Category)] + public String ExecuteScalar_DbCommand() + { + var entity = this.entitiesInDb[0]; + + using var command = this.connection.CreateCommand(); + + command.CommandText = "SELECT StringValue FROM Entity WHERE Id = @Id"; + command.Parameters.Add(new("@Id", entity.Id)); + + return (String)command.ExecuteScalar()!; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteScalar_Category)] + public String ExecuteScalar_DbConnectionPlus() + { + var entity = this.entitiesInDb[0]; + + return this.connection.ExecuteScalar( + $"SELECT StringValue FROM Entity WHERE Id = {Parameter(entity.Id)}" + ); + } + + private const String ExecuteScalar_Category = "ExecuteScalar"; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs new file mode 100644 index 0000000..207a648 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs @@ -0,0 +1,70 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Exists_DbCommand), + nameof(Exists_DbConnectionPlus) + ] + )] + public void Exists__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Exists_DbCommand), + nameof(Exists_DbConnectionPlus) + ] + )] + public void Exists__Setup() => + this.SetupDatabase(1); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Exists_Category)] + public Boolean Exists_Dapper() + { + var entityId = this.entitiesInDb[0].Id; + + using var dataReader = SqlMapper.ExecuteReader( + this.connection, + "SELECT 1 FROM Entity WHERE Id = @Id", + new { Id = entityId } + ); + + return dataReader.Read(); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Exists_Category)] + public Boolean Exists_DbCommand() + { + var entityId = this.entitiesInDb[0].Id; + + using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT 1 FROM Entity WHERE Id = @Id"; + command.Parameters.Add(new("@Id", entityId)); + + using var dataReader = command.ExecuteReader(); + + return dataReader.HasRows; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Exists_Category)] + public Boolean Exists_DbConnectionPlus() + { + var entityId = this.entitiesInDb[0].Id; + + return this.connection.Exists($"SELECT 1 FROM Entity WHERE Id = {Parameter(entityId)}"); + } + + private const String Exists_Category = "Exists"; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs new file mode 100644 index 0000000..a12bf62 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs @@ -0,0 +1,222 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(InsertEntities_DbCommand), + nameof(InsertEntities_Dapper), + nameof(InsertEntities_DbConnectionPlus) + ] + )] + public void InsertEntities__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(InsertEntities_DbCommand), + nameof(InsertEntities_Dapper), + nameof(InsertEntities_DbConnectionPlus) + ] + )] + public void InsertEntities__Setup() => + this.SetupDatabase(0); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntities_Category)] + public void InsertEntities_Dapper() + { + var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); + + SqlMapperExtensions.Insert(this.connection, entitiesToInsert); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(InsertEntities_Category)] + public void InsertEntities_DbCommand() + { + var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); + + using var command = this.connection.CreateCommand(); + command.CommandText = InsertEntitySql; + + var idParameter = new SqliteParameter + { + ParameterName = "@Id" + }; + + var booleanValueParameter = new SqliteParameter + { + ParameterName = "@BooleanValue" + }; + + var bytesValueParameter = new SqliteParameter + { + ParameterName = "@BytesValue" + }; + + var byteValueParameter = new SqliteParameter + { + ParameterName = "@ByteValue" + }; + + var charValueParameter = new SqliteParameter + { + ParameterName = "@CharValue" + }; + + var dateTimeValueParameter = new SqliteParameter + { + ParameterName = "@DateTimeValue" + }; + + var decimalValueParameter = new SqliteParameter + { + ParameterName = "@DecimalValue" + }; + + var doubleValueParameter = new SqliteParameter + { + ParameterName = "@DoubleValue" + }; + + var enumValueParameter = new SqliteParameter + { + ParameterName = "@EnumValue" + }; + + var guidValueParameter = new SqliteParameter + { + ParameterName = "@GuidValue" + }; + + var int16ValueParameter = new SqliteParameter + { + ParameterName = "@Int16Value" + }; + + var int32ValueParameter = new SqliteParameter + { + ParameterName = "@Int32Value" + }; + + var int64ValueParameter = new SqliteParameter + { + ParameterName = "@Int64Value" + }; + + var singleValueParameter = new SqliteParameter + { + ParameterName = "@SingleValue" + }; + + var stringValueParameter = new SqliteParameter + { + ParameterName = "@StringValue" + }; + + var timeSpanValueParameter = new SqliteParameter + { + ParameterName = "@TimeSpanValue" + }; + + command.Parameters.Add(idParameter); + command.Parameters.Add(booleanValueParameter); + command.Parameters.Add(bytesValueParameter); + command.Parameters.Add(byteValueParameter); + command.Parameters.Add(charValueParameter); + command.Parameters.Add(dateTimeValueParameter); + command.Parameters.Add(decimalValueParameter); + command.Parameters.Add(doubleValueParameter); + command.Parameters.Add(enumValueParameter); + command.Parameters.Add(guidValueParameter); + command.Parameters.Add(int16ValueParameter); + command.Parameters.Add(int32ValueParameter); + command.Parameters.Add(int64ValueParameter); + command.Parameters.Add(singleValueParameter); + command.Parameters.Add(stringValueParameter); + command.Parameters.Add(timeSpanValueParameter); + + foreach (var entity in entities) + { + idParameter.Value = entity.Id; + booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; + bytesValueParameter.Value = entity.BytesValue; + byteValueParameter.Value = entity.ByteValue; + charValueParameter.Value = entity.CharValue; + dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); + doubleValueParameter.Value = entity.DoubleValue; + enumValueParameter.Value = entity.EnumValue.ToString(); + guidValueParameter.Value = entity.GuidValue.ToString(); + int16ValueParameter.Value = entity.Int16Value; + int32ValueParameter.Value = entity.Int32Value; + int64ValueParameter.Value = entity.Int64Value; + singleValueParameter.Value = entity.SingleValue; + stringValueParameter.Value = entity.StringValue; + timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + + command.ExecuteNonQuery(); + } + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntities_Category)] + public void InsertEntities_DbConnectionPlus() + { + var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); + + this.connection.InsertEntities(entitiesToInsert); + } + + private const String InsertEntities_Category = "InsertEntities"; + private const Int32 InsertEntities_EntitiesPerOperation = 200; + + private const String InsertEntitySql = """ + INSERT INTO Entity + ( + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + ) + VALUES + ( + @Id, + @BooleanValue, + @BytesValue, + @ByteValue, + @CharValue, + @DateTimeValue, + @DecimalValue, + @DoubleValue, + @EnumValue, + @GuidValue, + @Int16Value, + @Int32Value, + @Int64Value, + @SingleValue, + @StringValue, + @TimeSpanValue + ) + """; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs new file mode 100644 index 0000000..31a7d97 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs @@ -0,0 +1,81 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(InsertEntity_DbCommand), + nameof(InsertEntity_Dapper), + nameof(InsertEntity_DbConnectionPlus) + ] + )] + public void InsertEntity__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(InsertEntity_DbCommand), + nameof(InsertEntity_Dapper), + nameof(InsertEntity_DbConnectionPlus) + ] + )] + public void InsertEntity__Setup() => + this.SetupDatabase(0); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntity_Category)] + public void InsertEntity_Dapper() + { + var entity = Generate.Single(); + + SqlMapperExtensions.Insert(this.connection, entity); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(InsertEntity_Category)] + public void InsertEntity_DbCommand() + { + var entity = Generate.Single(); + + using var command = this.connection.CreateCommand(); + + command.CommandText = InsertEntitySql; + + command.Parameters.Add(new("@Id", entity.Id)); + command.Parameters.Add(new("@BooleanValue", entity.BooleanValue ? 1 : 0)); + command.Parameters.Add(new("@BytesValue", entity.BytesValue)); + command.Parameters.Add(new("@ByteValue", entity.ByteValue)); + command.Parameters.Add(new("@CharValue", entity.CharValue)); + command.Parameters.Add(new("@DateTimeValue", entity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); + command.Parameters.Add(new("@DecimalValue", entity.DecimalValue)); + command.Parameters.Add(new("@DoubleValue", entity.DoubleValue)); + command.Parameters.Add(new("@EnumValue", entity.EnumValue.ToString())); + command.Parameters.Add(new("@GuidValue", entity.GuidValue.ToString())); + command.Parameters.Add(new("@Int16Value", entity.Int16Value)); + command.Parameters.Add(new("@Int32Value", entity.Int32Value)); + command.Parameters.Add(new("@Int64Value", entity.Int64Value)); + command.Parameters.Add(new("@SingleValue", entity.SingleValue)); + command.Parameters.Add(new("@StringValue", entity.StringValue)); + command.Parameters.Add(new("@TimeSpanValue", entity.TimeSpanValue.ToString())); + + command.ExecuteNonQuery(); + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntity_Category)] + public void InsertEntity_DbConnectionPlus() + { + var entity = Generate.Single(); + + this.connection.InsertEntity(entity); + } + + private const String InsertEntity_Category = "InsertEntity"; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs new file mode 100644 index 0000000..d56a4f1 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -0,0 +1,117 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Parameter_DbCommand), + nameof(Parameter_Dapper), + nameof(Parameter_DbConnectionPlus) + ] + )] + public void Parameter__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Parameter_DbCommand), + nameof(Parameter_Dapper), + nameof(Parameter_DbConnectionPlus) + ] + )] + public void Parameter__Setup() => + this.SetupDatabase(0); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Parameter_Category)] + public Object Parameter_Dapper() + { + var result = new List(); + + using var dataReader = SqlMapper.ExecuteReader( + this.connection, + "SELECT @P1, @P2, @P3, @P4, @P5", + new + { + P1 = 1, + P2 = "Test", + P3 = DateTime.UtcNow, + P4 = Guid.NewGuid(), + P5 = true + } + ); + + dataReader.Read(); + + result.Add((Int32)dataReader.GetInt64(0)); + result.Add(dataReader.GetString(1)); + result.Add(dataReader.GetDateTime(2)); + result.Add(dataReader.GetGuid(3)); + result.Add(dataReader.GetBoolean(4)); + + return result; + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Parameter_Category)] + public Object Parameter_DbCommand() + { + var result = new List(); + + using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; + command.Parameters.Add(new("@P1", 1)); + command.Parameters.Add(new("@P2", "Test")); + command.Parameters.Add(new("@P3", DateTime.UtcNow)); + command.Parameters.Add(new("@P4", Guid.NewGuid())); + command.Parameters.Add(new("@P5", true)); + + using var dataReader = command.ExecuteReader(); + + dataReader.Read(); + + result.Add((Int32)dataReader.GetInt64(0)); + result.Add(dataReader.GetString(1)); + result.Add(dataReader.GetDateTime(2)); + result.Add(dataReader.GetGuid(3)); + result.Add(dataReader.GetBoolean(4)); + + return result; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Parameter_Category)] + public Object Parameter_DbConnectionPlus() + { + var result = new List(); + + using var dataReader = this.connection.ExecuteReader( + $""" + SELECT {Parameter(1)}, + {Parameter("Test")}, + {Parameter(DateTime.UtcNow)}, + {Parameter(Guid.NewGuid())}, + {Parameter(true)} + """ + ); + + dataReader.Read(); + + result.Add((Int32)dataReader.GetInt64(0)); + result.Add(dataReader.GetString(1)); + result.Add(dataReader.GetDateTime(2)); + result.Add(dataReader.GetGuid(3)); + result.Add(dataReader.GetBoolean(4)); + + return result; + } + + private const String Parameter_Category = "Parameter"; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs new file mode 100644 index 0000000..9365069 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs @@ -0,0 +1,86 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +using System.Dynamic; + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Query_Dynamic_DbCommand), + nameof(Query_Dynamic_Dapper), + nameof(Query_Dynamic_DbConnectionPlus) + ] + )] + public void Query_Dynamic__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Query_Dynamic_DbCommand), + nameof(Query_Dynamic_Dapper), + nameof(Query_Dynamic_DbConnectionPlus) + ] + )] + public void Query_Dynamic__Setup() => + this.SetupDatabase(Query_Dynamic_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Dynamic_Category)] + public List Query_Dynamic_Dapper() => + SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Query_Dynamic_Category)] + public List Query_Dynamic_DbCommand() + { + var entities = new List(); + + using var dataReader = this.connection.ExecuteReader("SELECT * FROM Entity"); + + while (dataReader.Read()) + { + var charBuffer = new Char[1]; + + var ordinal = 0; + dynamic entity = new ExpandoObject(); + + entity.Id = dataReader.GetInt64(ordinal++); + entity.BooleanValue = dataReader.GetInt64(ordinal++) == 1; + entity.BytesValue = (Byte[])dataReader.GetValue(ordinal++); + entity.ByteValue = dataReader.GetByte(ordinal++); + entity.CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 + ? charBuffer[0] + : throw new(); + entity.DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); + entity.DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); + entity.DoubleValue = dataReader.GetDouble(ordinal++); + entity.EnumValue = Enum.Parse(dataReader.GetString(ordinal++)); + entity.GuidValue = Guid.Parse(dataReader.GetString(ordinal++)); + entity.Int16Value = (Int16)dataReader.GetInt64(ordinal++); + entity.Int32Value = (Int32)dataReader.GetInt64(ordinal++); + entity.Int64Value = dataReader.GetInt64(ordinal++); + entity.SingleValue = dataReader.GetFloat(ordinal++); + entity.StringValue = dataReader.GetString(ordinal++); + entity.TimeSpanValue = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); + + entities.Add(entity); + } + + return entities; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Dynamic_Category)] + public List Query_Dynamic_DbConnectionPlus() => + this.connection.Query("SELECT * FROM Entity").ToList(); + + private const String Query_Dynamic_Category = "Query_Dynamic"; + private const Int32 Query_Dynamic_EntitiesPerOperation = 100; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs new file mode 100644 index 0000000..d66e9f6 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs @@ -0,0 +1,90 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Query_Entities_DbCommand), + nameof(Query_Entities_Dapper), + nameof(Query_Entities_DbConnectionPlus) + ] + )] + public void Query_Entities__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Query_Entities_DbCommand), + nameof(Query_Entities_Dapper), + nameof(Query_Entities_DbConnectionPlus) + ] + )] + public void Query_Entities__Setup() => + this.SetupDatabase(Query_Entities_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Entities_Category)] + public List Query_Entities_Dapper() => + SqlMapper + .Query( + this.connection, + $"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}" + ) + .ToList(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Query_Entities_Category)] + public List Query_Entities_DbCommand() + { + var entities = new List(); + + using var dataReader = this.connection.ExecuteReader( + $""" + SELECT + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + FROM + Entity + LIMIT {Query_Entities_EntitiesPerOperation} + """ + ); + + while (dataReader.Read()) + { + entities.Add(ReadEntity(dataReader)); + } + + return entities; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Entities_Category)] + public List Query_Entities_DbConnectionPlus() => + this.connection + .Query($"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}") + .ToList(); + + private const String Query_Entities_Category = "Query_Entities"; + private const Int32 Query_Entities_EntitiesPerOperation = 100; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs new file mode 100644 index 0000000..9a65a89 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs @@ -0,0 +1,67 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Query_Scalars_DbCommand), + nameof(Query_Scalars_Dapper), + nameof(Query_Scalars_DbConnectionPlus) + ] + )] + public void Query_Scalars__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Query_Scalars_DbCommand), + nameof(Query_Scalars_Dapper), + nameof(Query_Scalars_DbConnectionPlus) + ] + )] + public void Query_Scalars__Setup() => + this.SetupDatabase(Query_Scalars_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Scalars_Category)] + public List Query_Scalars_Dapper() => + SqlMapper.Query(this.connection, $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}") + .ToList(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Query_Scalars_Category)] + public List Query_Scalars_DbCommand() + { + var data = new List(); + + using var command = this.connection.CreateCommand(); + command.CommandText = $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}"; + + using var dataReader = command.ExecuteReader(); + + while (dataReader.Read()) + { + var id = dataReader.GetInt64(0); + + data.Add(id); + } + + return data; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Scalars_Category)] + public List Query_Scalars_DbConnectionPlus() => + this.connection + .Query($"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}").ToList(); + + private const String Query_Scalars_Category = "Query_Scalars"; + private const Int32 Query_Scalars_EntitiesPerOperation = 600; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs new file mode 100644 index 0000000..25efc04 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs @@ -0,0 +1,82 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(Query_ValueTuples_DbCommand), + nameof(Query_ValueTuples_Dapper), + nameof(Query_ValueTuples_DbConnectionPlus) + ] + )] + public void Query_ValueTuples__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(Query_ValueTuples_DbCommand), + nameof(Query_ValueTuples_Dapper), + nameof(Query_ValueTuples_DbConnectionPlus) + ] + )] + public void Query_ValueTuples__Setup() => + this.SetupDatabase(Query_ValueTuples_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_ValueTuples_Category)] + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> + Query_ValueTuples_Dapper() => + SqlMapper + .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( + this.connection, + "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity" + ) + .ToList(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory(Query_ValueTuples_Category)] + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> + Query_ValueTuples_DbCommand() + { + var tuples = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); + + using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity"; + + using var dataReader = command.ExecuteReader(); + + while (dataReader.Read()) + { + tuples.Add( + ( + dataReader.GetInt64(0), + DateTime.Parse(dataReader.GetString(1), CultureInfo.InvariantCulture), + Enum.Parse(dataReader.GetString(2)), + dataReader.GetString(3) + ) + ); + } + + return tuples; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_ValueTuples_Category)] + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> + Query_ValueTuples_DbConnectionPlus() => + this.connection + .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( + "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity" + ) + .ToList(); + + private const String Query_ValueTuples_Category = "Query_ValueTuples"; + private const Int32 Query_ValueTuples_EntitiesPerOperation = 150; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs new file mode 100644 index 0000000..c5d5f28 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs @@ -0,0 +1,411 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) + ] + )] + public void TemporaryTable_ComplexObjects__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) + ] + )] + public void TemporaryTable_ComplexObjects__Setup() => + this.SetupDatabase(TemporaryTable_ComplexObjects_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] + public List TemporaryTable_ComplexObjects_Dapper() + { + var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + + SqlMapper.Execute(this.connection, CreateTempEntitiesTableSql); + + using var insertCommand = this.connection.CreateCommand(); + insertCommand.CommandText = InsertIntoTempEntities; + + var idParameter = new SqliteParameter + { + ParameterName = "@Id" + }; + + var booleanValueParameter = new SqliteParameter + { + ParameterName = "@BooleanValue" + }; + + var bytesValueParameter = new SqliteParameter + { + ParameterName = "@BytesValue" + }; + + var byteValueParameter = new SqliteParameter + { + ParameterName = "@ByteValue" + }; + + var charValueParameter = new SqliteParameter + { + ParameterName = "@CharValue" + }; + + var dateTimeValueParameter = new SqliteParameter + { + ParameterName = "@DateTimeValue" + }; + + var decimalValueParameter = new SqliteParameter + { + ParameterName = "@DecimalValue" + }; + + var doubleValueParameter = new SqliteParameter + { + ParameterName = "@DoubleValue" + }; + + var enumValueParameter = new SqliteParameter + { + ParameterName = "@EnumValue" + }; + + var guidValueParameter = new SqliteParameter + { + ParameterName = "@GuidValue" + }; + + var int16ValueParameter = new SqliteParameter + { + ParameterName = "@Int16Value" + }; + + var int32ValueParameter = new SqliteParameter + { + ParameterName = "@Int32Value" + }; + + var int64ValueParameter = new SqliteParameter + { + ParameterName = "@Int64Value" + }; + + var singleValueParameter = new SqliteParameter + { + ParameterName = "@SingleValue" + }; + + var stringValueParameter = new SqliteParameter + { + ParameterName = "@StringValue" + }; + + var timeSpanValueParameter = new SqliteParameter + { + ParameterName = "@TimeSpanValue" + }; + + insertCommand.Parameters.Add(idParameter); + insertCommand.Parameters.Add(booleanValueParameter); + insertCommand.Parameters.Add(bytesValueParameter); + insertCommand.Parameters.Add(byteValueParameter); + insertCommand.Parameters.Add(charValueParameter); + insertCommand.Parameters.Add(dateTimeValueParameter); + insertCommand.Parameters.Add(decimalValueParameter); + insertCommand.Parameters.Add(doubleValueParameter); + insertCommand.Parameters.Add(enumValueParameter); + insertCommand.Parameters.Add(guidValueParameter); + insertCommand.Parameters.Add(int16ValueParameter); + insertCommand.Parameters.Add(int32ValueParameter); + insertCommand.Parameters.Add(int64ValueParameter); + insertCommand.Parameters.Add(singleValueParameter); + insertCommand.Parameters.Add(stringValueParameter); + insertCommand.Parameters.Add(timeSpanValueParameter); + + foreach (var entity in entities) + { + idParameter.Value = entity.Id; + booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; + bytesValueParameter.Value = entity.BytesValue; + byteValueParameter.Value = entity.ByteValue; + charValueParameter.Value = entity.CharValue; + dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); + doubleValueParameter.Value = entity.DoubleValue; + enumValueParameter.Value = entity.EnumValue.ToString(); + guidValueParameter.Value = entity.GuidValue.ToString(); + int16ValueParameter.Value = entity.Int16Value; + int32ValueParameter.Value = entity.Int32Value; + int64ValueParameter.Value = entity.Int64Value; + singleValueParameter.Value = entity.SingleValue; + stringValueParameter.Value = entity.StringValue; + timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + + insertCommand.ExecuteNonQuery(); + } + + var result = SqlMapper.Query(this.connection, SelectTempEntitiesSql).ToList(); + + SqlMapper.Execute(this.connection, "DROP TABLE temp.Entities"); + + return result; + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] + public List TemporaryTable_ComplexObjects_DbCommand() + { + var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + + var result = new List(); + + using var createTableCommand = this.connection.CreateCommand(); + createTableCommand.CommandText = CreateTempEntitiesTableSql; + createTableCommand.ExecuteNonQuery(); + + using var insertCommand = this.connection.CreateCommand(); + insertCommand.CommandText = InsertIntoTempEntities; + + var idParameter = new SqliteParameter + { + ParameterName = "@Id" + }; + + var booleanValueParameter = new SqliteParameter + { + ParameterName = "@BooleanValue" + }; + + var bytesValueParameter = new SqliteParameter + { + ParameterName = "@BytesValue" + }; + + var byteValueParameter = new SqliteParameter + { + ParameterName = "@ByteValue" + }; + + var charValueParameter = new SqliteParameter + { + ParameterName = "@CharValue" + }; + + var dateTimeValueParameter = new SqliteParameter + { + ParameterName = "@DateTimeValue" + }; + + var decimalValueParameter = new SqliteParameter + { + ParameterName = "@DecimalValue" + }; + + var doubleValueParameter = new SqliteParameter + { + ParameterName = "@DoubleValue" + }; + + var enumValueParameter = new SqliteParameter + { + ParameterName = "@EnumValue" + }; + + var guidValueParameter = new SqliteParameter + { + ParameterName = "@GuidValue" + }; + + var int16ValueParameter = new SqliteParameter + { + ParameterName = "@Int16Value" + }; + + var int32ValueParameter = new SqliteParameter + { + ParameterName = "@Int32Value" + }; + + var int64ValueParameter = new SqliteParameter + { + ParameterName = "@Int64Value" + }; + + var singleValueParameter = new SqliteParameter + { + ParameterName = "@SingleValue" + }; + + var stringValueParameter = new SqliteParameter + { + ParameterName = "@StringValue" + }; + + var timeSpanValueParameter = new SqliteParameter + { + ParameterName = "@TimeSpanValue" + }; + + insertCommand.Parameters.Add(idParameter); + insertCommand.Parameters.Add(booleanValueParameter); + insertCommand.Parameters.Add(bytesValueParameter); + insertCommand.Parameters.Add(byteValueParameter); + insertCommand.Parameters.Add(charValueParameter); + insertCommand.Parameters.Add(dateTimeValueParameter); + insertCommand.Parameters.Add(decimalValueParameter); + insertCommand.Parameters.Add(doubleValueParameter); + insertCommand.Parameters.Add(enumValueParameter); + insertCommand.Parameters.Add(guidValueParameter); + insertCommand.Parameters.Add(int16ValueParameter); + insertCommand.Parameters.Add(int32ValueParameter); + insertCommand.Parameters.Add(int64ValueParameter); + insertCommand.Parameters.Add(singleValueParameter); + insertCommand.Parameters.Add(stringValueParameter); + insertCommand.Parameters.Add(timeSpanValueParameter); + + foreach (var entity in entities) + { + idParameter.Value = entity.Id; + booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; + bytesValueParameter.Value = entity.BytesValue; + byteValueParameter.Value = entity.ByteValue; + charValueParameter.Value = entity.CharValue; + dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); + doubleValueParameter.Value = entity.DoubleValue; + enumValueParameter.Value = entity.EnumValue.ToString(); + guidValueParameter.Value = entity.GuidValue.ToString(); + int16ValueParameter.Value = entity.Int16Value; + int32ValueParameter.Value = entity.Int32Value; + int64ValueParameter.Value = entity.Int64Value; + singleValueParameter.Value = entity.SingleValue; + stringValueParameter.Value = entity.StringValue; + timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + + insertCommand.ExecuteNonQuery(); + } + + using var selectCommand = this.connection.CreateCommand(); + selectCommand.CommandText = SelectTempEntitiesSql; + + using var dataReader = selectCommand.ExecuteReader(); + + while (dataReader.Read()) + { + result.Add(ReadEntity(dataReader)); + } + + using var dropTableCommand = this.connection.CreateCommand(); + dropTableCommand.CommandText = "DROP TABLE temp.Entities"; + dropTableCommand.ExecuteNonQuery(); + + return result; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] + public List TemporaryTable_ComplexObjects_DbConnectionPlus() + { + var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + + return this.connection.Query($"SELECT * FROM {TemporaryTable(entities)}").ToList(); + } + + private const String CreateTempEntitiesTableSql = """ + CREATE TEMP TABLE Entities ( + Id INTEGER, + BytesValue BLOB, + BooleanValue INTEGER, + ByteValue INTEGER, + CharValue TEXT, + DateTimeValue TEXT, + DecimalValue TEXT, + DoubleValue REAL, + EnumValue TEXT, + GuidValue TEXT, + Int16Value INTEGER, + Int32Value INTEGER, + Int64Value INTEGER, + SingleValue REAL, + StringValue TEXT, + TimeSpanValue TEXT + ) + """; + + private const String InsertIntoTempEntities = """ + INSERT INTO temp.Entities ( + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + ) + VALUES ( + @Id, + @BooleanValue, + @BytesValue, + @ByteValue, + @CharValue, + @DateTimeValue, + @DecimalValue, + @DoubleValue, + @EnumValue, + @GuidValue, + @Int16Value, + @Int32Value, + @Int64Value, + @SingleValue, + @StringValue, + @TimeSpanValue + ) + """; + + private const String SelectTempEntitiesSql = """ + SELECT + Id, + BooleanValue, + BytesValue, + ByteValue, + CharValue, + DateTimeValue, + DecimalValue, + DoubleValue, + EnumValue, + GuidValue, + Int16Value, + Int32Value, + Int64Value, + SingleValue, + StringValue, + TimeSpanValue + FROM + temp.Entities + """; + + private const String TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects"; + private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 250; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs new file mode 100644 index 0000000..1527de5 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs @@ -0,0 +1,128 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_DbConnectionPlus) + ] + )] + public void TemporaryTable_ScalarValues__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_DbConnectionPlus) + ] + )] + public void TemporaryTable_ScalarValues__Setup() => + this.SetupDatabase(0); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] + public List TemporaryTable_ScalarValues_Dapper() + { + var scalarValues = Enumerable + .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) + .Select(a => a.ToString(CultureInfo.InvariantCulture)) + .ToList(); + + SqlMapper.Execute(this.connection, "CREATE TEMP TABLE \"Values\" (Value TEXT)"); + + using var insertCommand = this.connection.CreateCommand(); + insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)"; + + var valueParameter = new SqliteParameter + { + ParameterName = "@Value" + }; + + insertCommand.Parameters.Add(valueParameter); + + foreach (var value in scalarValues) + { + valueParameter.Value = value; + + insertCommand.ExecuteNonQuery(); + } + + var result = SqlMapper.Query(this.connection, "SELECT Value FROM temp.\"Values\"").ToList(); + + SqlMapper.Execute(this.connection, "DROP TABLE temp.\"Values\""); + + return result; + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] + public List TemporaryTable_ScalarValues_DbCommand() + { + var scalarValues = Enumerable + .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) + .Select(a => a.ToString(CultureInfo.InvariantCulture)) + .ToList(); + + var result = new List(); + + using var createTableCommand = this.connection.CreateCommand(); + createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value TEXT)"; + createTableCommand.ExecuteNonQuery(); + + using var insertCommand = this.connection.CreateCommand(); + insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)"; + + var valueParameter = new SqliteParameter + { + ParameterName = "@Value" + }; + + insertCommand.Parameters.Add(valueParameter); + + foreach (var value in scalarValues) + { + valueParameter.Value = value; + + insertCommand.ExecuteNonQuery(); + } + + using var selectCommand = this.connection.CreateCommand(); + selectCommand.CommandText = "SELECT Value FROM temp.\"Values\""; + + using var dataReader = selectCommand.ExecuteReader(); + + while (dataReader.Read()) + { + result.Add(dataReader.GetString(0)); + } + + using var dropTableCommand = this.connection.CreateCommand(); + dropTableCommand.CommandText = "DROP TABLE temp.\"Values\""; + dropTableCommand.ExecuteNonQuery(); + + return result; + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] + public List TemporaryTable_ScalarValues_DbConnectionPlus() + { + var scalarValues = Enumerable + .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) + .Select(a => a.ToString(CultureInfo.InvariantCulture)) + .ToList(); + + return this.connection.Query($"SELECT Value FROM {TemporaryTable(scalarValues)}").ToList(); + } + + private const String TemporaryTable_ScalarValues_Category = "TemporaryTable_ScalarValues"; + private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs new file mode 100644 index 0000000..d8c1cee --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs @@ -0,0 +1,199 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(UpdateEntities_DbCommand), + nameof(UpdateEntities_Dapper), + nameof(UpdateEntities_DbConnectionPlus) + ] + )] + public void UpdateEntities__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(UpdateEntities_DbCommand), + nameof(UpdateEntities_Dapper), + nameof(UpdateEntities_DbConnectionPlus) + ] + )] + public void UpdateEntities__Setup() => + this.SetupDatabase(UpdateEntities_EntitiesPerOperation); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntities_Category)] + public void UpdateEntities_Dapper() + { + var updatesEntities = Generate.UpdateFor(this.entitiesInDb); + + SqlMapperExtensions.Update(this.connection, updatesEntities); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(UpdateEntities_Category)] + public void UpdateEntities_DbCommand() + { + var updatedEntities = Generate.UpdateFor(this.entitiesInDb); + + using var command = this.connection.CreateCommand(); + command.CommandText = """ + UPDATE Entity + SET BooleanValue = @BooleanValue, + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue + WHERE Id = @Id + """; + + var idParameter = new SqliteParameter + { + ParameterName = "@Id" + }; + + var booleanValueParameter = new SqliteParameter + { + ParameterName = "@BooleanValue" + }; + + var bytesValueParameter = new SqliteParameter + { + ParameterName = "@BytesValue" + }; + + var byteValueParameter = new SqliteParameter + { + ParameterName = "@ByteValue" + }; + + var charValueParameter = new SqliteParameter + { + ParameterName = "@CharValue" + }; + + var dateTimeValueParameter = new SqliteParameter + { + ParameterName = "@DateTimeValue" + }; + + var decimalValueParameter = new SqliteParameter + { + ParameterName = "@DecimalValue" + }; + + var doubleValueParameter = new SqliteParameter + { + ParameterName = "@DoubleValue" + }; + + var enumValueParameter = new SqliteParameter + { + ParameterName = "@EnumValue" + }; + + var guidValueParameter = new SqliteParameter + { + ParameterName = "@GuidValue" + }; + + var int16ValueParameter = new SqliteParameter + { + ParameterName = "@Int16Value" + }; + + var int32ValueParameter = new SqliteParameter + { + ParameterName = "@Int32Value" + }; + + var int64ValueParameter = new SqliteParameter + { + ParameterName = "@Int64Value" + }; + + var singleValueParameter = new SqliteParameter + { + ParameterName = "@SingleValue" + }; + + var stringValueParameter = new SqliteParameter + { + ParameterName = "@StringValue" + }; + + var timeSpanValueParameter = new SqliteParameter + { + ParameterName = "@TimeSpanValue" + }; + + command.Parameters.Add(idParameter); + command.Parameters.Add(booleanValueParameter); + command.Parameters.Add(bytesValueParameter); + command.Parameters.Add(byteValueParameter); + command.Parameters.Add(charValueParameter); + command.Parameters.Add(dateTimeValueParameter); + command.Parameters.Add(decimalValueParameter); + command.Parameters.Add(doubleValueParameter); + command.Parameters.Add(enumValueParameter); + command.Parameters.Add(guidValueParameter); + command.Parameters.Add(int16ValueParameter); + command.Parameters.Add(int32ValueParameter); + command.Parameters.Add(int64ValueParameter); + command.Parameters.Add(singleValueParameter); + command.Parameters.Add(stringValueParameter); + command.Parameters.Add(timeSpanValueParameter); + + foreach (var updatedEntity in updatedEntities) + { + idParameter.Value = updatedEntity.Id; + booleanValueParameter.Value = updatedEntity.BooleanValue ? 1 : 0; + bytesValueParameter.Value = updatedEntity.BytesValue; + byteValueParameter.Value = updatedEntity.ByteValue; + charValueParameter.Value = updatedEntity.CharValue; + dateTimeValueParameter.Value = updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + decimalValueParameter.Value = updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture); + doubleValueParameter.Value = updatedEntity.DoubleValue; + enumValueParameter.Value = updatedEntity.EnumValue.ToString(); + guidValueParameter.Value = updatedEntity.GuidValue.ToString(); + int16ValueParameter.Value = updatedEntity.Int16Value; + int32ValueParameter.Value = updatedEntity.Int32Value; + int64ValueParameter.Value = updatedEntity.Int64Value; + singleValueParameter.Value = updatedEntity.SingleValue; + stringValueParameter.Value = updatedEntity.StringValue; + timeSpanValueParameter.Value = updatedEntity.TimeSpanValue.ToString(); + + command.ExecuteNonQuery(); + } + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntities_Category)] + public void UpdateEntities_DbConnectionPlus() + { + var updatesEntities = Generate.UpdateFor(this.entitiesInDb); + + this.connection.UpdateEntities(updatesEntities); + } + + private const String UpdateEntities_Category = "UpdateEntities"; + private const Int32 UpdateEntities_EntitiesPerOperation = 100; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs new file mode 100644 index 0000000..28ab986 --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs @@ -0,0 +1,105 @@ +// ReSharper disable InvokeAsExtensionMethod +// ReSharper disable InconsistentNaming + +#pragma warning disable RCS1196 + +namespace RentADeveloper.DbConnectionPlus.Benchmarks; + +public partial class Benchmarks +{ + [GlobalCleanup( + Targets = + [ + nameof(UpdateEntity_DbCommand), + nameof(UpdateEntity_Dapper), + nameof(UpdateEntity_DbConnectionPlus) + ] + )] + public void UpdateEntity__Cleanup() => + this.connection.Dispose(); + + [GlobalSetup( + Targets = + [ + nameof(UpdateEntity_DbCommand), + nameof(UpdateEntity_Dapper), + nameof(UpdateEntity_DbConnectionPlus) + ] + )] + public void UpdateEntity__Setup() => + this.SetupDatabase(1); + + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntity_Category)] + public void UpdateEntity_Dapper() + { + var entity = this.entitiesInDb[0]; + + var updatedEntity = Generate.UpdateFor(entity); + + SqlMapperExtensions.Update(this.connection, updatedEntity); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory(UpdateEntity_Category)] + public void UpdateEntity_DbCommand() + { + var entity = this.entitiesInDb[0]; + + var updatedEntity = Generate.UpdateFor(entity); + + using var command = this.connection.CreateCommand(); + command.CommandText = """ + UPDATE Entity + SET BooleanValue = @BooleanValue, + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue + WHERE Id = @Id + """; + command.Parameters.Add(new("@Id", updatedEntity.Id)); + command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue ? 1 : 0)); + command.Parameters.Add(new("@BytesValue", updatedEntity.BytesValue)); + command.Parameters.Add(new("@ByteValue", updatedEntity.ByteValue)); + command.Parameters.Add(new("@CharValue", updatedEntity.CharValue)); + command.Parameters.Add( + new("@DateTimeValue", updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture)) + ); + command.Parameters.Add(new("@DecimalValue", updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture))); + command.Parameters.Add(new("@DoubleValue", updatedEntity.DoubleValue)); + command.Parameters.Add(new("@EnumValue", updatedEntity.EnumValue.ToString())); + command.Parameters.Add(new("@GuidValue", updatedEntity.GuidValue.ToString())); + command.Parameters.Add(new("@Int16Value", updatedEntity.Int16Value)); + command.Parameters.Add(new("@Int32Value", updatedEntity.Int32Value)); + command.Parameters.Add(new("@Int64Value", updatedEntity.Int64Value)); + command.Parameters.Add(new("@SingleValue", updatedEntity.SingleValue)); + command.Parameters.Add(new("@StringValue", updatedEntity.StringValue)); + command.Parameters.Add(new("@TimeSpanValue", updatedEntity.TimeSpanValue.ToString())); + + command.ExecuteNonQuery(); + } + + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntity_Category)] + public void UpdateEntity_DbConnectionPlus() + { + var entity = this.entitiesInDb[0]; + + var updatedEntity = Generate.UpdateFor(entity); + + this.connection.UpdateEntity(updatedEntity); + } + + private const String UpdateEntity_Category = "UpdateEntity"; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index 9dfc223..ff2d304 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -1,1883 +1,34 @@ -// @formatter:off -// ReSharper disable InconsistentNaming -// ReSharper disable InvokeAsExtensionMethod -#pragma warning disable RCS1196, IDE0017, IDE0305 +namespace RentADeveloper.DbConnectionPlus.Benchmarks; -using System.Data; -using BenchmarkDotNet.Attributes; -using Microsoft.Data.Sqlite; -using RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; -using RentADeveloper.DbConnectionPlus.UnitTests.TestData; -using System.Dynamic; -using System.Globalization; -using Dapper; -using Dapper.Contrib.Extensions; -using RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; -using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; - -namespace RentADeveloper.DbConnectionPlus.Benchmarks; - -// Note: All settings (i.e. *_EntitiesPerOperation and *_OperationsPerInvoke) are chosen so that each invoke -// takes at least 100 milliseconds to complete on a reasonably fast machine. - -[MemoryDiagnoser] -[Config(typeof(BenchmarksConfig))] -public class Benchmarks -{ - static Benchmarks() - { - SqlMapper.AddTypeHandler(new GuidTypeHandler()); - SqlMapper.AddTypeHandler(new TimeSpanTypeHandler()); - } - - [GlobalSetup] - public void Setup_Global() - { - this.testDatabaseProvider = new(); - this.testDatabaseProvider.ResetDatabase(); - } - - private SqliteConnection CreateConnection() => - (SqliteConnection)this.testDatabaseProvider!.CreateConnection(); - - private void PrepareEntitiesInDb(Int32 numberOfEntities) - { - var connection = this.CreateConnection(); - - using var transaction = connection.BeginTransaction(); - - connection.ExecuteNonQuery("DELETE FROM Entity", transaction); - - this.entitiesInDb = Generate.Multiple(numberOfEntities); - connection.InsertEntities(this.entitiesInDb, transaction); - - transaction.Commit(); - } - - private List entitiesInDb = []; - - #region DeleteEntities - private const String DeleteEntities_Category = "DeleteEntities"; - private const Int32 DeleteEntities_EntitiesPerOperation = 250; - private const Int32 DeleteEntities_OperationsPerInvoke = 20; - - [IterationSetup( - Targets = [ - nameof(DeleteEntities_DbCommand), - nameof(DeleteEntities_Dapper), - nameof(DeleteEntities_DbConnectionPlus) - ] - )] - public void DeleteEntities_Setup() => - this.PrepareEntitiesInDb(DeleteEntities_OperationsPerInvoke * DeleteEntities_EntitiesPerOperation); - - [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) - { - using var command = connection.CreateCommand(); - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - - var idParameter = command.CreateParameter(); - idParameter.ParameterName = "@Id"; - command.Parameters.Add(idParameter); - - foreach (var entity in this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList()) - { - idParameter.Value = entity.Id; - - command.ExecuteNonQuery(); - - this.entitiesInDb.Remove(entity); - } - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) - { - var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); - - SqlMapperExtensions.Delete(connection, entities); - - foreach (var entity in entities) - { - this.entitiesInDb.Remove(entity); - } - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) - { - var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); - - connection.DeleteEntities(entities); - - foreach (var entity in entities) - { - this.entitiesInDb.Remove(entity); - } - } - } - #endregion DeleteEntities - - #region DeleteEntity - private const String DeleteEntity_Category = "DeleteEntity"; - private const Int32 DeleteEntity_OperationsPerInvoke = 8000; - - [IterationSetup( - Targets = [ - nameof(DeleteEntity_DbCommand), - nameof(DeleteEntity_Dapper), - nameof(DeleteEntity_DbConnectionPlus) - ] - )] - public void DeleteEntity_Setup() => - this.PrepareEntitiesInDb(DeleteEntity_OperationsPerInvoke); - - [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) - { - var entityToDelete = this.entitiesInDb[0]; - - using var command = connection.CreateCommand(); - - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entityToDelete.Id)); - - command.ExecuteNonQuery(); - - this.entitiesInDb.Remove(entityToDelete); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) - { - var entityToDelete = this.entitiesInDb[0]; - - SqlMapperExtensions.Delete(connection, entityToDelete); - - this.entitiesInDb.Remove(entityToDelete); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] - [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) - { - var entityToDelete = this.entitiesInDb[0]; - - connection.DeleteEntity(entityToDelete); - - this.entitiesInDb.Remove(entityToDelete); - } - } - #endregion DeleteEntity - - #region ExecuteNonQuery - private const String ExecuteNonQuery_Category = "ExecuteNonQuery"; - private const Int32 ExecuteNonQuery_OperationsPerInvoke = 7700; - - [IterationSetup( - Targets = [ - nameof(ExecuteNonQuery_DbCommand), - nameof(ExecuteNonQuery_Dapper), - nameof(ExecuteNonQuery_DbConnectionPlus) - ] - )] - public void ExecuteNonQuery_Setup() => - this.PrepareEntitiesInDb(ExecuteNonQuery_OperationsPerInvoke); - - [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - using var command = connection.CreateCommand(); - - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entity.Id)); - - command.ExecuteNonQuery(); - - this.entitiesInDb.Remove(entity); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - SqlMapper.Execute(connection, "DELETE FROM Entity WHERE Id = @Id", new { entity.Id }); - - this.entitiesInDb.Remove(entity); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - connection.ExecuteNonQuery($"DELETE FROM Entity WHERE Id = {Parameter(entity.Id)}"); - - this.entitiesInDb.Remove(entity); - } - } - #endregion ExecuteNonQuery - - #region ExecuteReader - private const String ExecuteReader_Category = "ExecuteReader"; - private const Int32 ExecuteReader_OperationsPerInvoke = 700; - private const Int32 ExecuteReader_EntitiesPerOperation = 100; - - [GlobalSetup( - Targets = [ - nameof(ExecuteReader_DbCommand), - nameof(ExecuteReader_Dapper), - nameof(ExecuteReader_DbConnectionPlus) - ] - )] - public void ExecuteReader_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(ExecuteReader_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_DbCommand() - { - var connection = this.CreateConnection(); - - var entities = new List(); - - for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) - { - entities.Clear(); - - using var command = connection.CreateCommand(); - command.CommandText = ExecuteReaderSql; - - using var dataReader = command.ExecuteReader(); - - while (dataReader.Read()) - { - entities.Add(ReadEntity(dataReader)); - } - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_Dapper() - { - var connection = this.CreateConnection(); - - var entities = new List(); - - for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) - { - entities.Clear(); - - using var dataReader = SqlMapper.ExecuteReader(connection, ExecuteReaderSql); - - while (dataReader.Read()) - { - entities.Add(ReadEntity(dataReader)); - } - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteReader_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - var entities = new List(); - - for (var i = 0; i < ExecuteReader_OperationsPerInvoke; i++) - { - entities.Clear(); - - using var dataReader = connection.ExecuteReader(ExecuteReaderSql); - - while (dataReader.Read()) - { - entities.Add(ReadEntity(dataReader)); - } - } - - return entities; - } - #endregion ExecuteReader - - #region ExecuteScalar - private const String ExecuteScalar_Category = "ExecuteScalar"; - private const Int32 ExecuteScalar_OperationsPerInvoke = 5000; - - [GlobalSetup( - Targets = [ - nameof(ExecuteScalar_DbCommand), - nameof(ExecuteScalar_Dapper), - nameof(ExecuteScalar_DbConnectionPlus) - ] - )] - public void ExecuteScalar_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(ExecuteScalar_OperationsPerInvoke); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_DbCommand() - { - var connection = this.CreateConnection(); - - String result = null!; - - for (var i = 0; i < ExecuteScalar_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; - - using var command = connection.CreateCommand(); - - command.CommandText = "SELECT StringValue FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entity.Id)); - - result = (String)command.ExecuteScalar()!; - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_Dapper() - { - var connection = this.CreateConnection(); - - String result = null!; - - for (var i = 0; i < ExecuteScalar_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; - - result = SqlMapper.ExecuteScalar( - connection, - "SELECT StringValue FROM Entity WHERE Id = @Id", - new { entity.Id } - )!; - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteScalar_OperationsPerInvoke)] - [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - String result = null!; - - for (var i = 0; i < ExecuteScalar_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; - - result = connection.ExecuteScalar( - $"SELECT StringValue FROM Entity WHERE Id = {Parameter(entity.Id)}" - ); - } - - return result; - } - #endregion ExecuteScalar - - #region Exists - private const String Exists_Category = "Exists"; - private const Int32 Exists_OperationsPerInvoke = 5000; - - [GlobalSetup( - Targets = [ - nameof(Exists_DbCommand), - nameof(Exists_DbConnectionPlus) - ] - )] - public void Exists_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(Exists_OperationsPerInvoke); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Exists_OperationsPerInvoke)] - [BenchmarkCategory(Exists_Category)] - public Boolean Exists_DbCommand() - { - var connection = this.CreateConnection(); - - var result = false; - - for (var i = 0; i < Exists_OperationsPerInvoke; i++) - { - var entityId = this.entitiesInDb[i].Id; - - using var command = connection.CreateCommand(); - command.CommandText = "SELECT 1 FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entityId)); - - using var dataReader = command.ExecuteReader(); - - result = dataReader.HasRows; - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Exists_OperationsPerInvoke)] - [BenchmarkCategory(Exists_Category)] - public Boolean Exists_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - var result = false; - - for (var i = 0; i < Exists_OperationsPerInvoke; i++) - { - var entityId = this.entitiesInDb[i].Id; - - result = connection.Exists($"SELECT 1 FROM Entity WHERE Id = {Parameter(entityId)}"); - } - - return result; - } - #endregion Exists - - #region InsertEntities - private const String InsertEntities_Category = "InsertEntities"; - private const Int32 InsertEntities_OperationsPerInvoke = 20; - private const Int32 InsertEntities_EntitiesPerOperation = 140; - - [GlobalSetup( - Targets = [ - nameof(InsertEntities_DbCommand), - nameof(InsertEntities_Dapper), - nameof(InsertEntities_DbConnectionPlus) - ] - )] - public void InsertEntities_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(0); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) - { - var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); - - using var command = connection.CreateCommand(); - command.CommandText = InsertEntitySql; - - var idParameter = new SqliteParameter(); - idParameter.ParameterName = "@Id"; - - var booleanValueParameter = new SqliteParameter(); - booleanValueParameter.ParameterName = "@BooleanValue"; - - var bytesValueParameter = new SqliteParameter(); - bytesValueParameter.ParameterName = "@BytesValue"; - - var byteValueParameter = new SqliteParameter(); - byteValueParameter.ParameterName = "@ByteValue"; - - var charValueParameter = new SqliteParameter(); - charValueParameter.ParameterName = "@CharValue"; - - var dateTimeValueParameter = new SqliteParameter(); - dateTimeValueParameter.ParameterName = "@DateTimeValue"; - - var decimalValueParameter = new SqliteParameter(); - decimalValueParameter.ParameterName = "@DecimalValue"; - - var doubleValueParameter = new SqliteParameter(); - doubleValueParameter.ParameterName = "@DoubleValue"; - - var enumValueParameter = new SqliteParameter(); - enumValueParameter.ParameterName = "@EnumValue"; - - var guidValueParameter = new SqliteParameter(); - guidValueParameter.ParameterName = "@GuidValue"; - - var int16ValueParameter = new SqliteParameter(); - int16ValueParameter.ParameterName = "@Int16Value"; - - var int32ValueParameter = new SqliteParameter(); - int32ValueParameter.ParameterName = "@Int32Value"; - - var int64ValueParameter = new SqliteParameter(); - int64ValueParameter.ParameterName = "@Int64Value"; - - var singleValueParameter = new SqliteParameter(); - singleValueParameter.ParameterName = "@SingleValue"; - - var stringValueParameter = new SqliteParameter(); - stringValueParameter.ParameterName = "@StringValue"; - - var timeSpanValueParameter = new SqliteParameter(); - timeSpanValueParameter.ParameterName = "@TimeSpanValue"; - - command.Parameters.Add(idParameter); - command.Parameters.Add(booleanValueParameter); - command.Parameters.Add(bytesValueParameter); - command.Parameters.Add(byteValueParameter); - command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateTimeValueParameter); - command.Parameters.Add(decimalValueParameter); - command.Parameters.Add(doubleValueParameter); - command.Parameters.Add(enumValueParameter); - command.Parameters.Add(guidValueParameter); - command.Parameters.Add(int16ValueParameter); - command.Parameters.Add(int32ValueParameter); - command.Parameters.Add(int64ValueParameter); - command.Parameters.Add(singleValueParameter); - command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeSpanValueParameter); - - foreach (var entity in entities) - { - idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = entity.BytesValue; - byteValueParameter.Value = entity.ByteValue; - charValueParameter.Value = entity.CharValue; - dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = entity.DoubleValue; - enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue.ToString(); - int16ValueParameter.Value = entity.Int16Value; - int32ValueParameter.Value = entity.Int32Value; - int64ValueParameter.Value = entity.Int64Value; - singleValueParameter.Value = entity.SingleValue; - stringValueParameter.Value = entity.StringValue; - timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); - - command.ExecuteNonQuery(); - } - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) - { - var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); - - SqlMapperExtensions.Insert(connection, entitiesToInsert); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntities_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntities_OperationsPerInvoke; i++) - { - var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); - - connection.InsertEntities(entitiesToInsert); - } - } - #endregion InsertEntities - - #region InsertEntity - private const String InsertEntity_Category = "InsertEntity"; - private const Int32 InsertEntity_OperationsPerInvoke = 2500; - - [GlobalSetup( - Targets = [ - nameof(InsertEntity_DbCommand), - nameof(InsertEntity_Dapper), - nameof(InsertEntity_DbConnectionPlus) - ] - )] - public void InsertEntity_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(0); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) - { - var entity = Generate.Single(); - - using var command = connection.CreateCommand(); - command.CommandText = InsertEntitySql; - command.Parameters.Add(new("@Id", entity.Id)); - command.Parameters.Add(new("@BooleanValue", entity.BooleanValue ? 1 : 0)); - command.Parameters.Add(new("@BytesValue", entity.BytesValue)); - command.Parameters.Add(new("@ByteValue", entity.ByteValue)); - command.Parameters.Add(new("@CharValue", entity.CharValue)); - command.Parameters.Add(new("@DateTimeValue", entity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); - command.Parameters.Add(new("@DecimalValue", entity.DecimalValue)); - command.Parameters.Add(new("@DoubleValue", entity.DoubleValue)); - command.Parameters.Add(new("@EnumValue", entity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", entity.GuidValue.ToString())); - command.Parameters.Add(new("@Int16Value", entity.Int16Value)); - command.Parameters.Add(new("@Int32Value", entity.Int32Value)); - command.Parameters.Add(new("@Int64Value", entity.Int64Value)); - command.Parameters.Add(new("@SingleValue", entity.SingleValue)); - command.Parameters.Add(new("@StringValue", entity.StringValue)); - command.Parameters.Add(new("@TimeSpanValue", entity.TimeSpanValue.ToString())); - - command.ExecuteNonQuery(); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) - { - var entity = Generate.Single(); - - SqlMapperExtensions.Insert(connection, entity); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = InsertEntity_OperationsPerInvoke)] - [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < InsertEntity_OperationsPerInvoke; i++) - { - var entity = Generate.Single(); - - connection.InsertEntity(entity); - } - } - #endregion InsertEntity - - #region Parameter - private const String Parameter_Category = "Parameter"; - private const Int32 Parameter_OperationsPerInvoke = 35_000; - - [GlobalSetup( - Targets = [ - nameof(Parameter_DbCommand), - nameof(Parameter_Dapper), - nameof(Parameter_DbConnectionPlus) - ] - )] - public void Parameter_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(0); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Parameter_OperationsPerInvoke)] - [BenchmarkCategory(Parameter_Category)] - public Object Parameter_DbCommand() - { - var connection = this.CreateConnection(); - - var result = new List(); - - for (var i = 0; i < Parameter_OperationsPerInvoke; i++) - { - result.Clear(); - - using var command = connection.CreateCommand(); - command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; - command.Parameters.Add(new("@P1", 1)); - command.Parameters.Add(new("@P2", "Test")); - command.Parameters.Add(new("@P3", DateTime.UtcNow)); - command.Parameters.Add(new("@P4", Guid.NewGuid())); - command.Parameters.Add(new("@P5", true)); - - using var dataReader = command.ExecuteReader(); - - dataReader.Read(); - - result.Add((Int32) dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Parameter_OperationsPerInvoke)] - [BenchmarkCategory(Parameter_Category)] - public Object Parameter_Dapper() - { - var connection = this.CreateConnection(); - - var result = new List(); - - for (var i = 0; i < Parameter_OperationsPerInvoke; i++) - { - result.Clear(); - - using var dataReader = SqlMapper.ExecuteReader( - connection, - "SELECT @P1, @P2, @P3, @P4, @P5", - new - { - P1 = 1, - P2 = "Test", - P3 = DateTime.UtcNow, - P4 = Guid.NewGuid(), - P5 = true - } - ); - - dataReader.Read(); - - result.Add((Int32) dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Parameter_OperationsPerInvoke)] - [BenchmarkCategory(Parameter_Category)] - public Object Parameter_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - var result = new List(); - - for (var i = 0; i < Parameter_OperationsPerInvoke; i++) - { - result.Clear(); - - using var dataReader = connection.ExecuteReader( - $""" - SELECT {Parameter(1)}, - {Parameter("Test")}, - {Parameter(DateTime.UtcNow)}, - {Parameter(Guid.NewGuid())}, - {Parameter(true)} - """); - - dataReader.Read(); - - result.Add((Int32) dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); - } - - return result; - } - #endregion Parameter - - #region Query_Dynamic - private const String Query_Dynamic_Category = "Query_Dynamic"; - private const Int32 Query_Dynamic_OperationsPerInvoke = 600; - private const Int32 Query_Dynamic_EntitiesPerOperation = 100; - - [GlobalSetup( - Targets = [ - nameof(Query_Dynamic_DbCommand), - nameof(Query_Dynamic_Dapper), - nameof(Query_Dynamic_DbConnectionPlus) - ] - )] - public void Query_Dynamic_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(Query_Dynamic_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Query_Dynamic_OperationsPerInvoke)] - [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_DbCommand() - { - var connection = this.CreateConnection(); - - var entities = new List(); - - for (var i = 0; i < Query_Dynamic_OperationsPerInvoke; i++) - { - entities.Clear(); - - using var dataReader = connection.ExecuteReader( - $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {Query_Dynamic_EntitiesPerOperation} - """ - ); - - while (dataReader.Read()) - { - var charBuffer = new Char[1]; - - var ordinal = 0; - dynamic entity = new ExpandoObject(); - - entity.Id = dataReader.GetInt64(ordinal++); - entity.BooleanValue = dataReader.GetInt64(ordinal++) == 1; - entity.BytesValue = (Byte[])dataReader.GetValue(ordinal++); - entity.ByteValue = dataReader.GetByte(ordinal++); - entity.CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 - ? charBuffer[0] - : throw new(); - entity.DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - entity.DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - entity.DoubleValue = dataReader.GetDouble(ordinal++); - entity.EnumValue = Enum.Parse(dataReader.GetString(ordinal++)); - entity.GuidValue = Guid.Parse(dataReader.GetString(ordinal++)); - entity.Int16Value = (Int16) dataReader.GetInt64(ordinal++); - entity.Int32Value = (Int32) dataReader.GetInt64(ordinal++); - entity.Int64Value = dataReader.GetInt64(ordinal++); - entity.SingleValue = dataReader.GetFloat(ordinal++); - entity.StringValue = dataReader.GetString(ordinal++); - entity.TimeSpanValue = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); - - entities.Add(entity); - } - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Dynamic_OperationsPerInvoke)] - [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - List entities = []; - - for (var i = 0; i < Query_Dynamic_OperationsPerInvoke; i++) - { - entities = connection - .Query($"SELECT * FROM Entity LIMIT {Query_Dynamic_EntitiesPerOperation}") - .ToList(); - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Dynamic_OperationsPerInvoke)] - [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_Dapper() - { - var connection = this.CreateConnection(); - - List entities = []; - - for (var i = 0; i < Query_Dynamic_OperationsPerInvoke; i++) - { - entities = SqlMapper.Query(connection, $"SELECT * FROM Entity LIMIT {Query_Dynamic_EntitiesPerOperation}") - .ToList(); - } - - return entities; - } - #endregion Query_Dynamic - - #region Query_Scalars - private const String Query_Scalars_Category = "Query_Scalars"; - private const Int32 Query_Scalars_OperationsPerInvoke = 1500; - private const Int32 Query_Scalars_EntitiesPerOperation = 500; - - [GlobalSetup( - Targets = [ - nameof(Query_Scalars_DbCommand), - nameof(Query_Scalars_Dapper), - nameof(Query_Scalars_DbConnectionPlus) - ] - )] - public void Query_Scalars_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(Query_Scalars_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] - [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_DbCommand() - { - var connection = this.CreateConnection(); - - var data = new List(); - - for (var i = 0; i < Query_Scalars_OperationsPerInvoke; i++) - { - data.Clear(); - - using var command = connection.CreateCommand(); - command.CommandText = $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}"; - - using var dataReader = command.ExecuteReader(); - - while (dataReader.Read()) - { - var id = dataReader.GetInt64(0); - - data.Add(id); - } - } - - return data; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] - [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_Dapper() - { - var connection = this.CreateConnection(); - - List data = []; - - for (var i = 0; i < Query_Scalars_OperationsPerInvoke; i++) - { - data = SqlMapper.Query( - connection, - $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}" - ) - .ToList(); - } - - return data; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Scalars_OperationsPerInvoke)] - [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - List data = []; - - for (var i = 0; i < Query_Scalars_OperationsPerInvoke; i++) - { - data = connection - .Query($"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}") - .ToList(); - } - - return data; - } - #endregion Query_Scalars - - #region Query_Entities - private const String Query_Entities_Category = "Query_Entities"; - private const Int32 Query_Entities_OperationsPerInvoke = 600; - private const Int32 Query_Entities_EntitiesPerOperation = 100; - - [GlobalSetup( - Targets = [ - nameof(Query_Entities_DbCommand), - nameof(Query_Entities_Dapper), - nameof(Query_Entities_DbConnectionPlus) - ] - )] - public void Query_Entities_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(Query_Entities_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] - [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_DbCommand() - { - var connection = this.CreateConnection(); - - var entities = new List(); - - for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) - { - entities.Clear(); - - using var dataReader = connection.ExecuteReader( - $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {Query_Entities_EntitiesPerOperation} - """ - ); - - while (dataReader.Read()) - { - entities.Add(ReadEntity(dataReader)); - } - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] - [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_Dapper() - { - var connection = this.CreateConnection(); - - List entities = []; - - for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) - { - entities = SqlMapper - .Query( - connection, - $"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}" - ) - .ToList(); - } - - return entities; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_Entities_OperationsPerInvoke)] - [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - List entities = []; - - for (var i = 0; i < Query_Entities_OperationsPerInvoke; i++) - { - entities = connection - .Query($"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}") - .ToList(); - } - - return entities; - } - #endregion Query_Entities - - #region Query_ValueTuples - private const String Query_ValueTuples_Category = "Query_ValueTuples"; - private const Int32 Query_ValueTuples_OperationsPerInvoke = 1_000; - private const Int32 Query_ValueTuples_EntitiesPerOperation = 150; - - [GlobalSetup( - Targets = [ - nameof(Query_ValueTuples_DbCommand), - nameof(Query_ValueTuples_Dapper), - nameof(Query_ValueTuples_DbConnectionPlus) - ] - )] - public void Query_ValueTuples_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(Query_ValueTuples_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] - [BenchmarkCategory(Query_ValueTuples_Category)] - public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> Query_ValueTuples_DbCommand() - { - var connection = this.CreateConnection(); - - var tuples = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); - - for (var i = 0; i < Query_ValueTuples_OperationsPerInvoke; i++) - { - tuples.Clear(); - - using var command = connection.CreateCommand(); - command.CommandText = $""" - SELECT Id, DateTimeValue, EnumValue, StringValue - FROM Entity - LIMIT {Query_ValueTuples_EntitiesPerOperation} - """; - - using var dataReader = command.ExecuteReader(); - - while (dataReader.Read()) - { - tuples.Add( - ( - dataReader.GetInt64(0), - DateTime.Parse(dataReader.GetString(1), CultureInfo.InvariantCulture), - Enum.Parse(dataReader.GetString(2)), - dataReader.GetString(3) - ) - ); - } - } - - return tuples; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] - [BenchmarkCategory(Query_ValueTuples_Category)] - public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> - Query_ValueTuples_Dapper() - { - var connection = this.CreateConnection(); - - List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> tuples = []; - - for (var i = 0; i < Query_ValueTuples_OperationsPerInvoke; i++) - { - tuples = SqlMapper - .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( - connection, - $""" - SELECT Id, DateTimeValue, EnumValue, StringValue - FROM Entity - LIMIT {Query_ValueTuples_EntitiesPerOperation} - """ - ) - .ToList(); - } - - return tuples; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = Query_ValueTuples_OperationsPerInvoke)] - [BenchmarkCategory(Query_ValueTuples_Category)] - public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> - Query_ValueTuples_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> tuples = []; - - for (var i = 0; i < Query_ValueTuples_OperationsPerInvoke; i++) - { - tuples = connection - .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( - $""" - SELECT Id, DateTimeValue, EnumValue, StringValue - FROM Entity - LIMIT {Query_ValueTuples_EntitiesPerOperation} - """ - ) - .ToList(); - } - - return tuples; - } - #endregion Query_ValueTuples - - #region TemporaryTable_ComplexObjects - private const String TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects"; - private const Int32 TemporaryTable_ComplexObjects_OperationsPerInvoke = 50; - private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 200; - - [GlobalSetup( - Targets = [ - nameof(TemporaryTable_ComplexObjects_DbCommand), - nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) - ] - )] - public void TemporaryTable_ComplexObjects_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(0); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = TemporaryTable_ComplexObjects_OperationsPerInvoke)] - [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_DbCommand() - { - var connection = this.CreateConnection(); - - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - - var result = new List(); - - for (var i = 0; i < TemporaryTable_ComplexObjects_OperationsPerInvoke; i++) - { - result.Clear(); - - using var createTableCommand = connection.CreateCommand(); - createTableCommand.CommandText = - """ - CREATE TEMP TABLE Entities ( - Id INTEGER, - BytesValue BLOB, - BooleanValue INTEGER, - ByteValue INTEGER, - CharValue TEXT, - DateTimeValue TEXT, - DecimalValue TEXT, - DoubleValue REAL, - EnumValue TEXT, - GuidValue TEXT, - Int16Value INTEGER, - Int32Value INTEGER, - Int64Value INTEGER, - SingleValue REAL, - StringValue TEXT, - TimeSpanValue TEXT - ) - """; - createTableCommand.ExecuteNonQuery(); - - using var insertCommand = connection.CreateCommand(); - insertCommand.CommandText = - """ - INSERT INTO temp.Entities ( - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - ) - VALUES ( - @Id, - @BooleanValue, - @BytesValue, - @ByteValue, - @CharValue, - @DateTimeValue, - @DecimalValue, - @DoubleValue, - @EnumValue, - @GuidValue, - @Int16Value, - @Int32Value, - @Int64Value, - @SingleValue, - @StringValue, - @TimeSpanValue - ) - """; - - var idParameter = new SqliteParameter(); - idParameter.ParameterName = "@Id"; - - var booleanValueParameter = new SqliteParameter(); - booleanValueParameter.ParameterName = "@BooleanValue"; - - var bytesValueParameter = new SqliteParameter(); - bytesValueParameter.ParameterName = "@BytesValue"; - - var byteValueParameter = new SqliteParameter(); - byteValueParameter.ParameterName = "@ByteValue"; - - var charValueParameter = new SqliteParameter(); - charValueParameter.ParameterName = "@CharValue"; - - var dateTimeValueParameter = new SqliteParameter(); - dateTimeValueParameter.ParameterName = "@DateTimeValue"; - - var decimalValueParameter = new SqliteParameter(); - decimalValueParameter.ParameterName = "@DecimalValue"; - - var doubleValueParameter = new SqliteParameter(); - doubleValueParameter.ParameterName = "@DoubleValue"; - - var enumValueParameter = new SqliteParameter(); - enumValueParameter.ParameterName = "@EnumValue"; - - var guidValueParameter = new SqliteParameter(); - guidValueParameter.ParameterName = "@GuidValue"; - - var int16ValueParameter = new SqliteParameter(); - int16ValueParameter.ParameterName = "@Int16Value"; - - var int32ValueParameter = new SqliteParameter(); - int32ValueParameter.ParameterName = "@Int32Value"; - - var int64ValueParameter = new SqliteParameter(); - int64ValueParameter.ParameterName = "@Int64Value"; - - var singleValueParameter = new SqliteParameter(); - singleValueParameter.ParameterName = "@SingleValue"; - - var stringValueParameter = new SqliteParameter(); - stringValueParameter.ParameterName = "@StringValue"; - - var timeSpanValueParameter = new SqliteParameter(); - timeSpanValueParameter.ParameterName = "@TimeSpanValue"; - - insertCommand.Parameters.Add(idParameter); - insertCommand.Parameters.Add(booleanValueParameter); - insertCommand.Parameters.Add(bytesValueParameter); - insertCommand.Parameters.Add(byteValueParameter); - insertCommand.Parameters.Add(charValueParameter); - insertCommand.Parameters.Add(dateTimeValueParameter); - insertCommand.Parameters.Add(decimalValueParameter); - insertCommand.Parameters.Add(doubleValueParameter); - insertCommand.Parameters.Add(enumValueParameter); - insertCommand.Parameters.Add(guidValueParameter); - insertCommand.Parameters.Add(int16ValueParameter); - insertCommand.Parameters.Add(int32ValueParameter); - insertCommand.Parameters.Add(int64ValueParameter); - insertCommand.Parameters.Add(singleValueParameter); - insertCommand.Parameters.Add(stringValueParameter); - insertCommand.Parameters.Add(timeSpanValueParameter); - - foreach (var entity in entities) - { - idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = entity.BytesValue; - byteValueParameter.Value = entity.ByteValue; - charValueParameter.Value = entity.CharValue; - dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = entity.DoubleValue; - enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue.ToString(); - int16ValueParameter.Value = entity.Int16Value; - int32ValueParameter.Value = entity.Int32Value; - int64ValueParameter.Value = entity.Int64Value; - singleValueParameter.Value = entity.SingleValue; - stringValueParameter.Value = entity.StringValue; - timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); - - insertCommand.ExecuteNonQuery(); - } - - using var selectCommand = connection.CreateCommand(); - selectCommand.CommandText = - """ - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - temp.Entities - """; - - using var dataReader = selectCommand.ExecuteReader(); - - while (dataReader.Read()) - { - result.Add(ReadEntity(dataReader)); - } - - using var dropTableCommand = connection.CreateCommand(); - dropTableCommand.CommandText = "DROP TABLE temp.Entities"; - dropTableCommand.ExecuteNonQuery(); - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = TemporaryTable_ComplexObjects_OperationsPerInvoke)] - [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - - List result = []; - - for (var i = 0; i < TemporaryTable_ComplexObjects_OperationsPerInvoke; i++) - { - result = connection.Query($"SELECT * FROM {TemporaryTable(entities)}").ToList(); - } - - return result; - } - #endregion TemporaryTable_ComplexObjects - - #region TemporaryTable_ScalarValues - private const String TemporaryTable_ScalarValues_Category = "TemporaryTable_ScalarValues"; - private const Int32 TemporaryTable_ScalarValues_OperationsPerInvoke = 30; - private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000; - - [GlobalSetup( - Targets = [ - nameof(TemporaryTable_ScalarValues_DbCommand), - nameof(TemporaryTable_ScalarValues_DbConnectionPlus) - ] - )] - public void TemporaryTable_ScalarValues_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(0); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = TemporaryTable_ScalarValues_OperationsPerInvoke)] - [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_DbCommand() - { - var connection = this.CreateConnection(); - - var scalarValues = Enumerable - .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString(CultureInfo.InvariantCulture)) - .ToList(); - - var result = new List(); - - for (var i = 0; i < TemporaryTable_ScalarValues_OperationsPerInvoke; i++) - { - result.Clear(); - - using var createTableCommand = connection.CreateCommand(); - createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value TEXT)"; - createTableCommand.ExecuteNonQuery(); - - using var insertCommand = connection.CreateCommand(); - insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)"; - - var valueParameter = new SqliteParameter(); - valueParameter.ParameterName = "@Value"; - - insertCommand.Parameters.Add(valueParameter); - - foreach (var value in scalarValues) - { - valueParameter.Value = value; - - insertCommand.ExecuteNonQuery(); - } - - using var selectCommand = connection.CreateCommand(); - selectCommand.CommandText = "SELECT Value FROM temp.\"Values\""; - - using var dataReader = selectCommand.ExecuteReader(); - - while (dataReader.Read()) - { - result.Add(dataReader.GetString(0)); - } - - using var dropTableCommand = connection.CreateCommand(); - dropTableCommand.CommandText = "DROP TABLE temp.\"Values\""; - dropTableCommand.ExecuteNonQuery(); - } - - return result; - } - - [Benchmark(Baseline = false, OperationsPerInvoke = TemporaryTable_ScalarValues_OperationsPerInvoke)] - [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - var scalarValues = Enumerable - .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString(CultureInfo.InvariantCulture)) - .ToList(); - - List result = []; - - for (var i = 0; i < TemporaryTable_ScalarValues_OperationsPerInvoke; i++) - { - result = connection.Query($"SELECT Value FROM {TemporaryTable(scalarValues)}").ToList(); - } - - return result; - } - #endregion TemporaryTable_ScalarValues - - #region UpdateEntities - private const String UpdateEntities_Category = "UpdateEntities"; - private const Int32 UpdateEntities_OperationsPerInvoke = 25; - private const Int32 UpdateEntities_EntitiesPerOperation = 100; - - [GlobalSetup( - Targets = [ - nameof(UpdateEntities_DbCommand), - nameof(UpdateEntities_Dapper), - nameof(UpdateEntities_DbConnectionPlus) - ] - )] - public void UpdateEntities_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(UpdateEntities_EntitiesPerOperation); - } - - [Benchmark(Baseline = true, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_DbCommand() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) - { - var updatedEntities = Generate.UpdateFor(this.entitiesInDb); - - using var command = connection.CreateCommand(); - command.CommandText = """ - UPDATE Entity - SET BooleanValue = @BooleanValue, - BytesValue = @BytesValue, - ByteValue = @ByteValue, - CharValue = @CharValue, - DateTimeValue = @DateTimeValue, - DecimalValue = @DecimalValue, - DoubleValue = @DoubleValue, - EnumValue = @EnumValue, - GuidValue = @GuidValue, - Int16Value = @Int16Value, - Int32Value = @Int32Value, - Int64Value = @Int64Value, - SingleValue = @SingleValue, - StringValue = @StringValue, - TimeSpanValue = @TimeSpanValue - WHERE Id = @Id - """; - - var idParameter = new SqliteParameter(); - idParameter.ParameterName = "@Id"; - - var booleanValueParameter = new SqliteParameter(); - booleanValueParameter.ParameterName = "@BooleanValue"; - - var bytesValueParameter = new SqliteParameter(); - bytesValueParameter.ParameterName = "@BytesValue"; - - var byteValueParameter = new SqliteParameter(); - byteValueParameter.ParameterName = "@ByteValue"; - - var charValueParameter = new SqliteParameter(); - charValueParameter.ParameterName = "@CharValue"; - - var dateTimeValueParameter = new SqliteParameter(); - dateTimeValueParameter.ParameterName = "@DateTimeValue"; - - var decimalValueParameter = new SqliteParameter(); - decimalValueParameter.ParameterName = "@DecimalValue"; - - var doubleValueParameter = new SqliteParameter(); - doubleValueParameter.ParameterName = "@DoubleValue"; - - var enumValueParameter = new SqliteParameter(); - enumValueParameter.ParameterName = "@EnumValue"; - - var guidValueParameter = new SqliteParameter(); - guidValueParameter.ParameterName = "@GuidValue"; - - var int16ValueParameter = new SqliteParameter(); - int16ValueParameter.ParameterName = "@Int16Value"; - - var int32ValueParameter = new SqliteParameter(); - int32ValueParameter.ParameterName = "@Int32Value"; - - var int64ValueParameter = new SqliteParameter(); - int64ValueParameter.ParameterName = "@Int64Value"; - - var singleValueParameter = new SqliteParameter(); - singleValueParameter.ParameterName = "@SingleValue"; - - var stringValueParameter = new SqliteParameter(); - stringValueParameter.ParameterName = "@StringValue"; - - var timeSpanValueParameter = new SqliteParameter(); - timeSpanValueParameter.ParameterName = "@TimeSpanValue"; - - command.Parameters.Add(idParameter); - command.Parameters.Add(booleanValueParameter); - command.Parameters.Add(bytesValueParameter); - command.Parameters.Add(byteValueParameter); - command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateTimeValueParameter); - command.Parameters.Add(decimalValueParameter); - command.Parameters.Add(doubleValueParameter); - command.Parameters.Add(enumValueParameter); - command.Parameters.Add(guidValueParameter); - command.Parameters.Add(int16ValueParameter); - command.Parameters.Add(int32ValueParameter); - command.Parameters.Add(int64ValueParameter); - command.Parameters.Add(singleValueParameter); - command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeSpanValueParameter); - - foreach (var updatedEntity in updatedEntities) - { - idParameter.Value = updatedEntity.Id; - booleanValueParameter.Value = updatedEntity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = updatedEntity.BytesValue; - byteValueParameter.Value = updatedEntity.ByteValue; - charValueParameter.Value = updatedEntity.CharValue; - dateTimeValueParameter.Value = updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = updatedEntity.DoubleValue; - enumValueParameter.Value = updatedEntity.EnumValue.ToString(); - guidValueParameter.Value = updatedEntity.GuidValue.ToString(); - int16ValueParameter.Value = updatedEntity.Int16Value; - int32ValueParameter.Value = updatedEntity.Int32Value; - int64ValueParameter.Value = updatedEntity.Int64Value; - singleValueParameter.Value = updatedEntity.SingleValue; - stringValueParameter.Value = updatedEntity.StringValue; - timeSpanValueParameter.Value = updatedEntity.TimeSpanValue.ToString(); - - command.ExecuteNonQuery(); - } - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_Dapper() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) - { - var updatesEntities = Generate.UpdateFor(this.entitiesInDb); - - SqlMapperExtensions.Update(connection, updatesEntities); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntities_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_DbConnectionPlus() - { - var connection = this.CreateConnection(); - - for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++) - { - var updatesEntities = Generate.UpdateFor(this.entitiesInDb); - - connection.UpdateEntities(updatesEntities); - } - } - #endregion UpdateEntities - - #region UpdateEntity - private const String UpdateEntity_Category = "UpdateEntity"; - private const Int32 UpdateEntity_OperationsPerInvoke = 1_600; - - [GlobalSetup( - Targets = [ - nameof(UpdateEntity_DbCommand), - nameof(UpdateEntity_Dapper), - nameof(UpdateEntity_DbConnectionPlus) - ] - )] - public void UpdateEntity_Setup() - { - this.Setup_Global(); - this.PrepareEntitiesInDb(UpdateEntity_OperationsPerInvoke); - } +// Note: All benchmark settings (i.e. *_EntitiesPerOperation and *_OperationsPerInvoke) are chosen so that each invoke +// takes at least 100 milliseconds to complete on a reasonably fast machine. - [Benchmark(Baseline = true, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_DbCommand() +[MemoryDiagnoser] +[Config(typeof(BenchmarksConfig))] +public partial class Benchmarks +{ + static Benchmarks() { - var connection = this.CreateConnection(); - - for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; - - var updatedEntity = Generate.UpdateFor(entity); - - using var command = connection.CreateCommand(); - command.CommandText = """ - UPDATE Entity - SET BooleanValue = @BooleanValue, - BytesValue = @BytesValue, - ByteValue = @ByteValue, - CharValue = @CharValue, - DateTimeValue = @DateTimeValue, - DecimalValue = @DecimalValue, - DoubleValue = @DoubleValue, - EnumValue = @EnumValue, - GuidValue = @GuidValue, - Int16Value = @Int16Value, - Int32Value = @Int32Value, - Int64Value = @Int64Value, - SingleValue = @SingleValue, - StringValue = @StringValue, - TimeSpanValue = @TimeSpanValue - WHERE Id = @Id - """; - command.Parameters.Add(new("@Id", updatedEntity.Id)); - command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue ? 1 :0)); - command.Parameters.Add(new("@BytesValue", updatedEntity.BytesValue)); - command.Parameters.Add(new("@ByteValue", updatedEntity.ByteValue)); - command.Parameters.Add(new("@CharValue", updatedEntity.CharValue)); - command.Parameters.Add(new("@DateTimeValue", updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); - command.Parameters.Add(new("@DecimalValue", updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture))); - command.Parameters.Add(new("@DoubleValue", updatedEntity.DoubleValue)); - command.Parameters.Add(new("@EnumValue", updatedEntity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", updatedEntity.GuidValue.ToString())); - command.Parameters.Add(new("@Int16Value", updatedEntity.Int16Value)); - command.Parameters.Add(new("@Int32Value", updatedEntity.Int32Value)); - command.Parameters.Add(new("@Int64Value", updatedEntity.Int64Value)); - command.Parameters.Add(new("@SingleValue", updatedEntity.SingleValue)); - command.Parameters.Add(new("@StringValue", updatedEntity.StringValue)); - command.Parameters.Add(new("@TimeSpanValue", updatedEntity.TimeSpanValue.ToString())); - - command.ExecuteNonQuery(); - } + SqlMapper.AddTypeHandler(new GuidTypeHandler()); + SqlMapper.AddTypeHandler(new TimeSpanTypeHandler()); } - [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_Dapper() + private void SetupDatabase(Int32 numberOfEntities) { - var connection = this.CreateConnection(); - - for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; - - var updatedEntity = Generate.UpdateFor(entity); + this.connection = new("Data Source=:memory:"); + this.connection.Open(); - SqlMapperExtensions.Update(connection, updatedEntity); - } - } - - [Benchmark(Baseline = false, OperationsPerInvoke = UpdateEntity_OperationsPerInvoke)] - [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_DbConnectionPlus() - { - var connection = this.CreateConnection(); + using var createEntityTableCommand = this.connection.CreateCommand(); + createEntityTableCommand.CommandText = CreateEntityTableSql; + createEntityTableCommand.ExecuteNonQuery(); - for (var i = 0; i < UpdateEntity_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[i]; + using var transaction = this.connection.BeginTransaction(); - var updatedEntity = Generate.UpdateFor(entity); + this.entitiesInDb = Generate.Multiple(numberOfEntities); + this.connection.InsertEntities(this.entitiesInDb, transaction); - connection.UpdateEntity(updatedEntity); - } + transaction.Commit(); } - #endregion UpdateEntity private static BenchmarkEntity ReadEntity(IDataReader dataReader) { @@ -1905,69 +56,32 @@ private static BenchmarkEntity ReadEntity(IDataReader dataReader) }; } - private SqliteTestDatabaseProvider? testDatabaseProvider; - - private const String InsertEntitySql = """ - INSERT INTO Entity - ( - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - ) - VALUES - ( - @Id, - @BooleanValue, - @BytesValue, - @ByteValue, - @CharValue, - @DateTimeValue, - @DecimalValue, - @DoubleValue, - @EnumValue, - @GuidValue, - @Int16Value, - @Int32Value, - @Int64Value, - @SingleValue, - @StringValue, - @TimeSpanValue - ) - """; - - private static readonly String ExecuteReaderSql = $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {ExecuteReader_EntitiesPerOperation} - """; -} \ No newline at end of file + private SqliteConnection connection = null!; + private List entitiesInDb = null!; + + private const String CreateEntityTableSql = + """ + CREATE TABLE Entity + ( + Id INTEGER, + BytesValue BLOB, + BooleanValue INTEGER, + ByteValue INTEGER, + CharValue TEXT, + DateOnlyValue TEXT, + DateTimeValue TEXT, + DecimalValue TEXT, + DoubleValue REAL, + EnumValue TEXT, + GuidValue TEXT, + Int16Value INTEGER, + Int32Value INTEGER, + Int64Value INTEGER, + NullableBooleanValue INTEGER NULL, + SingleValue REAL, + StringValue TEXT, + TimeOnlyValue TEXT, + TimeSpanValue TEXT + ); + """; +} diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs index 71f9e42..0a9a2e4 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs @@ -4,7 +4,6 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; using Perfolizer.Horology; -using Perfolizer.Mathematics.OutlierDetection; namespace RentADeveloper.DbConnectionPlus.Benchmarks; @@ -16,25 +15,14 @@ public BenchmarksConfig() this.Orderer = new BenchmarksOrderer(); this.SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); - this.AddColumn(StatisticColumn.Median); - this.AddColumn(StatisticColumn.P90); - this.AddColumn(StatisticColumn.P95); - - this.AddExporter(PlainExporter.Default); this.AddExporter(MarkdownExporter.Default); this.AddJob( Job.Default - .WithWarmupCount(10) - .WithMinIterationTime(TimeInterval.FromMilliseconds(100)) - .WithMaxIterationCount(20) - .WithInvocationCount(1) - .WithUnrollFactor(1) + .WithMinIterationTime(TimeInterval.FromMilliseconds(150)) // Since DbConnectionPlus will mostly be used in server applications, we test with server GC. .WithGcServer(true) - - .WithOutlierMode(OutlierMode.DontRemove) ); } } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs index 810fb34..db2a668 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/GuidTypeHandler.cs @@ -1,14 +1,12 @@ -using Dapper; - -namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; +namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; public class GuidTypeHandler : SqlMapper.StringTypeHandler { - /// - protected override Guid Parse(String xml) => - Guid.Parse(xml); - /// protected override String Format(Guid xml) => xml.ToString(); + + /// + protected override Guid Parse(String xml) => + Guid.Parse(xml); } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs index f12cd64..a33b695 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/DapperTypeHandlers/TimeSpanTypeHandler.cs @@ -1,14 +1,12 @@ -using Dapper; - -namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; +namespace RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; public class TimeSpanTypeHandler : SqlMapper.StringTypeHandler { - /// - protected override TimeSpan Parse(String xml) => - TimeSpan.Parse(xml); - /// protected override String Format(TimeSpan xml) => xml.ToString(); + + /// + protected override TimeSpan Parse(String xml) => + TimeSpan.Parse(xml); } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/GlobalUsings.cs b/benchmarks/DbConnectionPlus.Benchmarks/GlobalUsings.cs new file mode 100644 index 0000000..4fe9b5d --- /dev/null +++ b/benchmarks/DbConnectionPlus.Benchmarks/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using System.Data; +global using System.Globalization; +global using BenchmarkDotNet.Attributes; +global using Dapper; +global using Dapper.Contrib.Extensions; +global using Microsoft.Data.Sqlite; +global using RentADeveloper.DbConnectionPlus.Benchmarks.DapperTypeHandlers; +global using RentADeveloper.DbConnectionPlus.Benchmarks.TestData; +global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; +global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index de04b28..3d2df03 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -1,172 +1,11 @@ -#pragma warning disable RCS1163, IDE0022 - using BenchmarkDotNet.Running; namespace RentADeveloper.DbConnectionPlus.Benchmarks; public static class Program { - public static void Main(String[] args) - { + public static void Main(String[] args) => BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) .Run(args); - - /* - var benchmarks = new Benchmarks(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntities_Setup(); - benchmarks.DeleteEntities_DbCommand(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntities_Setup(); - benchmarks.DeleteEntities_Dapper(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntities_Setup(); - benchmarks.DeleteEntities_DbConnectionPlus(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntity_Setup(); - benchmarks.DeleteEntity_DbCommand(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntity_Setup(); - benchmarks.DeleteEntity_Dapper(); - - benchmarks.Setup_Global(); - benchmarks.DeleteEntity_Setup(); - benchmarks.DeleteEntity_DbConnectionPlus(); - - benchmarks.Setup_Global(); - benchmarks.ExecuteNonQuery_Setup(); - benchmarks.ExecuteNonQuery_DbCommand(); - - benchmarks.Setup_Global(); - benchmarks.ExecuteNonQuery_Setup(); - benchmarks.ExecuteNonQuery_Dapper(); - - benchmarks.Setup_Global(); - benchmarks.ExecuteNonQuery_Setup(); - benchmarks.ExecuteNonQuery_DbConnectionPlus(); - - benchmarks.ExecuteReader_Setup(); - benchmarks.ExecuteReader_DbCommand(); - - benchmarks.ExecuteReader_Setup(); - benchmarks.ExecuteReader_Dapper(); - - benchmarks.ExecuteReader_Setup(); - benchmarks.ExecuteReader_DbConnectionPlus(); - - benchmarks.ExecuteScalar_Setup(); - benchmarks.ExecuteScalar_DbCommand(); - - benchmarks.ExecuteScalar_Setup(); - benchmarks.ExecuteScalar_Dapper(); - - benchmarks.ExecuteScalar_Setup(); - benchmarks.ExecuteScalar_DbConnectionPlus(); - - benchmarks.Exists_Setup(); - benchmarks.Exists_DbCommand(); - - benchmarks.Exists_Setup(); - benchmarks.Exists_DbConnectionPlus(); - - benchmarks.InsertEntities_Setup(); - benchmarks.InsertEntities_DbCommand(); - - benchmarks.InsertEntities_Setup(); - benchmarks.InsertEntities_Dapper(); - - benchmarks.InsertEntities_Setup(); - benchmarks.InsertEntities_DbConnectionPlus(); - - benchmarks.InsertEntity_Setup(); - benchmarks.InsertEntity_DbCommand(); - - benchmarks.InsertEntity_Setup(); - benchmarks.InsertEntity_Dapper(); - - benchmarks.InsertEntity_Setup(); - benchmarks.InsertEntity_DbConnectionPlus(); - - benchmarks.Parameter_Setup(); - benchmarks.Parameter_DbCommand(); - - benchmarks.Parameter_Setup(); - benchmarks.Parameter_Dapper(); - - benchmarks.Parameter_Setup(); - benchmarks.Parameter_DbConnectionPlus(); - - benchmarks.Query_Dynamic_Setup(); - benchmarks.Query_Dynamic_DbCommand(); - - benchmarks.Query_Dynamic_Setup(); - benchmarks.Query_Dynamic_Dapper(); - - benchmarks.Query_Scalars_Setup(); - benchmarks.Query_Scalars_DbConnectionPlus(); - - benchmarks.Query_Entities_Setup(); - benchmarks.Query_Entities_DbCommand(); - - benchmarks.Query_Entities_Setup(); - benchmarks.Query_Entities_Dapper(); - - benchmarks.Query_Entities_Setup(); - benchmarks.Query_Entities_DbConnectionPlus(); - - benchmarks.Query_Dynamic_Setup(); - benchmarks.Query_Dynamic_DbConnectionPlus(); - - benchmarks.Query_Scalars_Setup(); - benchmarks.Query_Scalars_DbCommand(); - - benchmarks.Query_Scalars_Setup(); - benchmarks.Query_Scalars_Dapper(); - - benchmarks.Query_ValueTuples_Setup(); - benchmarks.Query_ValueTuples_DbCommand(); - - benchmarks.Query_ValueTuples_Setup(); - benchmarks.Query_ValueTuples_Dapper(); - - benchmarks.Query_ValueTuples_Setup(); - benchmarks.Query_ValueTuples_DbConnectionPlus(); - - benchmarks.TemporaryTable_ComplexObjects_Setup(); - benchmarks.TemporaryTable_ComplexObjects_DbCommand(); - - benchmarks.TemporaryTable_ComplexObjects_Setup(); - benchmarks.TemporaryTable_ComplexObjects_DbConnectionPlus(); - - benchmarks.TemporaryTable_ScalarValues_Setup(); - benchmarks.TemporaryTable_ScalarValues_DbCommand(); - - benchmarks.TemporaryTable_ScalarValues_Setup(); - benchmarks.TemporaryTable_ScalarValues_DbConnectionPlus(); - - benchmarks.UpdateEntities_Setup(); - benchmarks.UpdateEntities_DbCommand(); - - benchmarks.UpdateEntities_Setup(); - benchmarks.UpdateEntities_Dapper(); - - benchmarks.UpdateEntities_Setup(); - benchmarks.UpdateEntities_DbConnectionPlus(); - - benchmarks.UpdateEntity_Setup(); - benchmarks.UpdateEntity_DbCommand(); - - benchmarks.UpdateEntity_Setup(); - benchmarks.UpdateEntity_Dapper(); - - benchmarks.UpdateEntity_Setup(); - benchmarks.UpdateEntity_DbConnectionPlus(); - */ - } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs similarity index 80% rename from tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs rename to benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs index 74be75e..36c9d27 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/BenchmarkEntity.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs @@ -1,6 +1,6 @@ -namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; +namespace RentADeveloper.DbConnectionPlus.Benchmarks.TestData; -[Table("Entity")] +[System.ComponentModel.DataAnnotations.Schema.Table("Entity")] public record BenchmarkEntity { public Boolean BooleanValue { get; set; } @@ -13,7 +13,7 @@ public record BenchmarkEntity public TestEnum EnumValue { get; set; } public Guid GuidValue { get; set; } - [Key] + [System.ComponentModel.DataAnnotations.Key] public Int64 Id { get; set; } public Int16 Int16Value { get; set; } From e52ecc623ebdbe58eabd310adae1ebfb321008d8 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sun, 8 Feb 2026 00:15:50 +0100 Subject: [PATCH 12/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .editorconfig | 3 + .../Benchmarks.Exists.cs | 2 + .../Benchmarks.Parameter.cs | 60 ++++---- ...enchmarks.TemporaryTable_ComplexObjects.cs | 2 + .../Benchmarks.TemporaryTable_ScalarValues.cs | 2 + .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 5 +- .../BenchmarksConfig.cs | 3 - .../BenchmarksOrderer.cs | 21 ++- .../MySql/MySqlEntityManipulator.cs | 27 ++-- .../MySql/MySqlTemporaryTableBuilder.cs | 75 +++++----- .../Oracle/OracleEntityManipulator.cs | 27 ++-- .../Oracle/OracleTemporaryTableBuilder.cs | 90 ++++++------ .../PostgreSql/PostgreSqlEntityManipulator.cs | 27 ++-- .../PostgreSqlTemporaryTableBuilder.cs | 82 ++++++----- .../SqlServer/SqlServerEntityManipulator.cs | 27 ++-- .../SqlServerTemporaryTableBuilder.cs | 113 +++++++--------- .../Sqlite/SqliteEntityManipulator.cs | 27 ++-- .../Sqlite/SqliteTemporaryTableBuilder.cs | 82 ++++++----- .../DbCommands/DbCommandBuilder.cs | 100 +++++++------- .../DbCommands/DefaultDbCommandFactory.cs | 39 ------ .../DbCommands/IDbCommandFactory.cs | 41 ------ .../DbConnectionExtensions.Configuration.cs | 9 -- .../DbConnectionExtensions.Parameter.cs | 2 +- src/DbConnectionPlus/Helpers/NameHelper.cs | 48 +++++-- .../EntityManipulator.DeleteEntitiesTests.cs | 2 +- .../EntityManipulator.DeleteEntityTests.cs | 2 +- .../EntityManipulator.InsertEntitiesTests.cs | 2 +- .../EntityManipulator.InsertEntityTests.cs | 2 +- .../EntityManipulator.UpdateEntitiesTests.cs | 2 +- .../EntityManipulator.UpdateEntityTests.cs | 2 +- .../DefaultDbCommandFactoryTests.cs | 71 ---------- ...nnectionExtensions.ExecuteNonQueryTests.cs | 2 +- ...ConnectionExtensions.ExecuteReaderTests.cs | 2 +- ...ConnectionExtensions.ExecuteScalarTests.cs | 2 +- .../DbConnectionExtensions.ExistsTests.cs | 2 +- ...ConnectionExtensions.QueryFirstOfTTests.cs | 2 +- ...nExtensions.QueryFirstOrDefaultOfTTests.cs | 2 +- ...tionExtensions.QueryFirstOrDefaultTests.cs | 2 +- .../DbConnectionExtensions.QueryFirstTests.cs | 2 +- .../DbConnectionExtensions.QueryOfTTests.cs | 2 +- ...onnectionExtensions.QuerySingleOfTTests.cs | 2 +- ...Extensions.QuerySingleOrDefaultOfTTests.cs | 2 +- ...ionExtensions.QuerySingleOrDefaultTests.cs | 2 +- ...DbConnectionExtensions.QuerySingleTests.cs | 2 +- .../DbConnectionExtensions.QueryTests.cs | 2 +- .../GlobalUsings.cs | 1 - .../IntegrationTestsBase.cs | 128 ++++++++++++++---- .../TestDatabase/MySqlTestDatabaseProvider.cs | 2 +- .../OracleTestDatabaseProvider.cs | 2 +- .../PostgreSqlTestDatabaseProvider.cs | 2 +- .../SQLiteTestDatabaseProvider.cs | 2 +- .../SqlServerTestDatabaseProvider.cs | 2 +- .../TestHelpers/DbCommandLogger.cs | 81 ----------- .../TestHelpers/DelayDbCommandFactory.cs | 55 -------- .../DbConnectionPlusConfigurationTests.cs | 8 +- .../Oracle/OracleDatabaseAdapterTests.cs | 16 +-- .../DefaultDbCommandFactoryTests.cs | 54 -------- .../Mocks/MockDbParameterCollection.cs | 91 +++++++++++++ ...piTest.PublicApiHasNotChanged.verified.txt | 7 - .../StatementMethodTestsBase.cs | 50 +++---- .../UnitTestsBase.cs | 82 ++++++----- 61 files changed, 681 insertions(+), 927 deletions(-) delete mode 100644 src/DbConnectionPlus/DbCommands/DefaultDbCommandFactory.cs delete mode 100644 src/DbConnectionPlus/DbCommands/IDbCommandFactory.cs delete mode 100644 tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs delete mode 100644 tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs delete mode 100644 tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs delete mode 100644 tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs create mode 100644 tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs diff --git a/.editorconfig b/.editorconfig index 133850c..fcf36f6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -50,6 +50,9 @@ dotnet_diagnostic.RCS1124.severity = suggestion # IDE0290: Use primary constructor dotnet_diagnostic.IDE0290.severity = none +# CA2100: Review SQL queries for security vulnerabilities +dotnet_diagnostic.CA2100.severity = none + [*.{cs,vb}] #### Naming styles #### diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs index 207a648..137104e 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs @@ -11,6 +11,7 @@ public partial class Benchmarks Targets = [ nameof(Exists_DbCommand), + nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus) ] )] @@ -21,6 +22,7 @@ public void Exists__Cleanup() => Targets = [ nameof(Exists_DbCommand), + nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus) ] )] diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs index d56a4f1..19852e0 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -33,7 +33,7 @@ public void Parameter__Setup() => [BenchmarkCategory(Parameter_Category)] public Object Parameter_Dapper() { - var result = new List(); + var result = new Int32[5]; using var dataReader = SqlMapper.ExecuteReader( this.connection, @@ -41,20 +41,20 @@ public Object Parameter_Dapper() new { P1 = 1, - P2 = "Test", - P3 = DateTime.UtcNow, - P4 = Guid.NewGuid(), - P5 = true + P2 = 2, + P3 = 3, + P4 = 4, + P5 = 5 } ); dataReader.Read(); - result.Add((Int32)dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); + result[0] = dataReader.GetInt32(0); + result[1] = dataReader.GetInt32(1); + result[2] = dataReader.GetInt32(2); + result[3] = dataReader.GetInt32(3); + result[4] = dataReader.GetInt32(4); return result; } @@ -63,25 +63,25 @@ public Object Parameter_Dapper() [BenchmarkCategory(Parameter_Category)] public Object Parameter_DbCommand() { - var result = new List(); + var result = new Int32[5]; using var command = this.connection.CreateCommand(); command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; command.Parameters.Add(new("@P1", 1)); - command.Parameters.Add(new("@P2", "Test")); - command.Parameters.Add(new("@P3", DateTime.UtcNow)); - command.Parameters.Add(new("@P4", Guid.NewGuid())); - command.Parameters.Add(new("@P5", true)); + command.Parameters.Add(new("@P2", 2)); + command.Parameters.Add(new("@P3", 3)); + command.Parameters.Add(new("@P4", 4)); + command.Parameters.Add(new("@P5", 5)); using var dataReader = command.ExecuteReader(); dataReader.Read(); - result.Add((Int32)dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); + result[0] = dataReader.GetInt32(0); + result[1] = dataReader.GetInt32(1); + result[2] = dataReader.GetInt32(2); + result[3] = dataReader.GetInt32(3); + result[4] = dataReader.GetInt32(4); return result; } @@ -90,25 +90,19 @@ public Object Parameter_DbCommand() [BenchmarkCategory(Parameter_Category)] public Object Parameter_DbConnectionPlus() { - var result = new List(); + var result = new Int32[5]; using var dataReader = this.connection.ExecuteReader( - $""" - SELECT {Parameter(1)}, - {Parameter("Test")}, - {Parameter(DateTime.UtcNow)}, - {Parameter(Guid.NewGuid())}, - {Parameter(true)} - """ + $"SELECT {Parameter(1)}, {Parameter(2)}, {Parameter(3)}, {Parameter(4)}, {Parameter(5)}" ); dataReader.Read(); - result.Add((Int32)dataReader.GetInt64(0)); - result.Add(dataReader.GetString(1)); - result.Add(dataReader.GetDateTime(2)); - result.Add(dataReader.GetGuid(3)); - result.Add(dataReader.GetBoolean(4)); + result[0] = dataReader.GetInt32(0); + result[1] = dataReader.GetInt32(1); + result[2] = dataReader.GetInt32(2); + result[3] = dataReader.GetInt32(3); + result[4] = dataReader.GetInt32(4); return result; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs index c5d5f28..c47796d 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs @@ -11,6 +11,7 @@ public partial class Benchmarks Targets = [ nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_Dapper), nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) ] )] @@ -21,6 +22,7 @@ public void TemporaryTable_ComplexObjects__Cleanup() => Targets = [ nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_Dapper), nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) ] )] diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs index 1527de5..eaa011f 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs @@ -11,6 +11,7 @@ public partial class Benchmarks Targets = [ nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_Dapper), nameof(TemporaryTable_ScalarValues_DbConnectionPlus) ] )] @@ -21,6 +22,7 @@ public void TemporaryTable_ScalarValues__Cleanup() => Targets = [ nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_Dapper), nameof(TemporaryTable_ScalarValues_DbConnectionPlus) ] )] diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index ff2d304..52ef7e5 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -64,11 +64,10 @@ private static BenchmarkEntity ReadEntity(IDataReader dataReader) CREATE TABLE Entity ( Id INTEGER, - BytesValue BLOB, BooleanValue INTEGER, + BytesValue BLOB, ByteValue INTEGER, CharValue TEXT, - DateOnlyValue TEXT, DateTimeValue TEXT, DecimalValue TEXT, DoubleValue REAL, @@ -77,10 +76,8 @@ CREATE TABLE Entity Int16Value INTEGER, Int32Value INTEGER, Int64Value INTEGER, - NullableBooleanValue INTEGER NULL, SingleValue REAL, StringValue TEXT, - TimeOnlyValue TEXT, TimeSpanValue TEXT ); """; diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs index 0a9a2e4..b697ffc 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs @@ -3,7 +3,6 @@ using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; -using Perfolizer.Horology; namespace RentADeveloper.DbConnectionPlus.Benchmarks; @@ -19,8 +18,6 @@ public BenchmarksConfig() this.AddJob( Job.Default - .WithMinIterationTime(TimeInterval.FromMilliseconds(150)) - // Since DbConnectionPlus will mostly be used in server applications, we test with server GC. .WithGcServer(true) ); diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs index 58f6f14..2db0da3 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs @@ -17,7 +17,10 @@ public IEnumerable GetExecutionOrder( IEnumerable? order = null ) => benchmarksCase - .OrderBy(a => + .OrderByDescending(a => + a.Descriptor.Baseline + ) + .ThenBy(a => a.Descriptor.WorkloadMethod.DeclaringType! .GetMethods() .ToList() @@ -47,10 +50,14 @@ public IEnumerable GetSummaryOrder( ImmutableArray benchmarksCases, Summary summary ) => - benchmarksCases.OrderBy(a => - a.Descriptor.WorkloadMethod.DeclaringType! - .GetMethods() - .ToList() - .IndexOf(a.Descriptor.WorkloadMethod) - ); + benchmarksCases + .OrderByDescending(a => + a.Descriptor.Baseline + ) + .ThenBy(a => + a.Descriptor.WorkloadMethod.DeclaringType! + .GetMethods() + .ToList() + .IndexOf(a.Descriptor.WorkloadMethod) + ); } diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index 5f4f772..8a94a35 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -741,11 +741,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetDeleteEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -782,11 +781,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetInsertEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -819,11 +817,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetUpdateEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs index a9f51bf..644b94d 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs @@ -60,15 +60,14 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -79,15 +78,14 @@ public TemporaryTableDisposer BuildTemporaryTable( } else { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -162,17 +160,16 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = #pragma warning disable CA2007 DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -185,16 +182,16 @@ public async Task BuildTemporaryTableAsync( else { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( + await using var createCommand = connection.CreateCommand(); +#pragma warning restore CA2007 + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( name, valuesType, DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); -#pragma warning restore CA2007 + ); + + createCommand.Transaction = transaction; await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken) @@ -400,12 +397,11 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu /// The transaction within to drop the table. private static void DropTemporaryTable(String name, MySqlConnection connection, MySqlTransaction? transaction) { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TEMPORARY TABLE IF EXISTS `{name}`", - transaction - ); + using var command = connection.CreateCommand(); + command.CommandText = $"DROP TEMPORARY TABLE IF EXISTS `{name}`"; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); command.ExecuteNonQuery(); @@ -425,13 +421,12 @@ private static async ValueTask DropTemporaryTableAsync( ) { #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TEMPORARY TABLE IF EXISTS `{name}`", - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + command.CommandText = $"DROP TEMPORARY TABLE IF EXISTS `{name}`"; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); await command.ExecuteNonQueryAsync().ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs index 98bdaea..aeb1486 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleEntityManipulator.cs @@ -709,11 +709,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetDeleteEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -750,11 +749,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetInsertEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -810,11 +808,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetUpdateEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs index a7c35dd..0adc5a1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs @@ -69,17 +69,16 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - quotedTableName, - // ReSharper disable once PossibleMultipleEnumeration - values, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + quotedTableName, + // ReSharper disable once PossibleMultipleEnumeration + values, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -90,15 +89,14 @@ public TemporaryTableDisposer BuildTemporaryTable( } else { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - quotedTableName, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + quotedTableName, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -166,19 +164,18 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - quotedTableName, - // ReSharper disable once PossibleMultipleEnumeration - values, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + quotedTableName, + // ReSharper disable once PossibleMultipleEnumeration + values, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -189,17 +186,16 @@ public async Task BuildTemporaryTableAsync( else { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - quotedTableName, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + quotedTableName, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -556,11 +552,10 @@ private static void DropTemporaryTable( OracleTransaction? transaction ) { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE {quotedTableName}", - transaction - ); + using var command = connection.CreateCommand(); + + command.CommandText = $"DROP TABLE {quotedTableName}"; + command.Transaction = transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); @@ -581,13 +576,12 @@ private static async ValueTask DropTemporaryTableAsync( ) { #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE {quotedTableName}", - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + command.CommandText = $"DROP TABLE {quotedTableName}"; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); await command.ExecuteNonQueryAsync().ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs index f639ed4..39edd3e 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlEntityManipulator.cs @@ -736,11 +736,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetDeleteEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -777,11 +776,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetInsertEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -814,11 +812,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetUpdateEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs index aa78493..6a643a7 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs @@ -60,15 +60,14 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -79,15 +78,14 @@ public TemporaryTableDisposer BuildTemporaryTable( } else { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -137,17 +135,16 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -158,17 +155,16 @@ public async Task BuildTemporaryTableAsync( else { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -419,11 +415,10 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu /// The transaction within to drop the table. private static void DropTemporaryTable(String name, NpgsqlConnection connection, NpgsqlTransaction? transaction) { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE IF EXISTS \"{name}\"", - transaction - ); + using var command = connection.CreateCommand(); + + command.CommandText = $"DROP TABLE IF EXISTS \"{name}\""; + command.Transaction = transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); @@ -444,13 +439,12 @@ private static async ValueTask DropTemporaryTableAsync( ) { #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE IF EXISTS \"{name}\"", - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + command.CommandText = $"DROP TABLE IF EXISTS \"{name}\""; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); await command.ExecuteNonQueryAsync().ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs index 74e7650..da65721 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerEntityManipulator.cs @@ -736,11 +736,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetDeleteEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -777,11 +776,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetInsertEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -814,11 +812,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetUpdateEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs index 260cd00..e6a4035 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs @@ -64,18 +64,17 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - // ReSharper disable once PossibleMultipleEnumeration - values, - valuesType, - databaseCollation, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + // ReSharper disable once PossibleMultipleEnumeration + values, + valuesType, + databaseCollation, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -86,16 +85,15 @@ public TemporaryTableDisposer BuildTemporaryTable( } else { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - databaseCollation, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + databaseCollation, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -177,18 +175,16 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - // ReSharper disable once PossibleMultipleEnumeration - values, - valuesType, - databaseCollation, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + await using var createCommand = connection.CreateCommand(); + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + // ReSharper disable once PossibleMultipleEnumeration + values, + valuesType, + databaseCollation, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; #pragma warning restore CA2007 await using var cancellationTokenRegistration = @@ -201,18 +197,17 @@ public async Task BuildTemporaryTableAsync( else { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - databaseCollation, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + databaseCollation, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -443,11 +438,10 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu /// The transaction within to drop the table. private static void DropTemporaryTable(String name, SqlConnection connection, SqlTransaction? transaction) { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]", - transaction - ); + using var command = connection.CreateCommand(); + + command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]"; + command.Transaction = transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); @@ -468,13 +462,12 @@ private static async ValueTask DropTemporaryTableAsync( ) { #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]", - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]"; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); await command.ExecuteNonQueryAsync().ConfigureAwait(false); @@ -494,11 +487,10 @@ private static String GetCurrentDatabaseCollation( (connection.DataSource, connection.Database), static (_, args) => { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - args.connection, - GetCurrentDatabaseCollationQuery, - args.transaction - ); + using var command = args.connection.CreateCommand(); + + command.CommandText = GetCurrentDatabaseCollationQuery; + command.Transaction = args.transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); @@ -528,13 +520,12 @@ private static async ValueTask GetCurrentDatabaseCollationAsync( } #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - GetCurrentDatabaseCollationQuery, - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + command.CommandText = GetCurrentDatabaseCollationQuery; + command.Transaction = transaction; + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); collation = (String)(await command.ExecuteScalarAsync().ConfigureAwait(false))!; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs index 4e7f4a8..c95e0b7 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteEntityManipulator.cs @@ -740,11 +740,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetDeleteEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -781,11 +780,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetInsertEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); @@ -818,11 +816,10 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(entityTypeMetadata); - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.GetUpdateEntitySqlCode(entityTypeMetadata), - transaction - ); + var command = connection.CreateCommand(); + + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); + command.Transaction = transaction; var parameters = new List(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs index 501793f..d7ce45d 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs @@ -60,15 +60,14 @@ public TemporaryTableDisposer BuildTemporaryTable( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -79,15 +78,14 @@ public TemporaryTableDisposer BuildTemporaryTable( } else { - using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction + using var createCommand = connection.CreateCommand(); + + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode ); + createCommand.Transaction = transaction; using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); @@ -137,17 +135,16 @@ public async Task BuildTemporaryTableAsync( if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateSingleColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateSingleColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -158,17 +155,16 @@ public async Task BuildTemporaryTableAsync( else { #pragma warning disable CA2007 - await using var createCommand = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ), - transaction - ); + await using var createCommand = connection.CreateCommand(); #pragma warning restore CA2007 + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + createCommand.Transaction = transaction; + await using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false); @@ -389,11 +385,10 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu /// The transaction within to drop the table. private static void DropTemporaryTable(String name, SqliteConnection connection, SqliteTransaction? transaction) { - using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE IF EXISTS temp.\"{name}\"", - transaction - ); + using var command = connection.CreateCommand(); + + command.CommandText = $"DROP TABLE IF EXISTS temp.\"{name}\""; + command.Transaction = transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); @@ -414,12 +409,11 @@ private static async ValueTask DropTemporaryTableAsync( ) { #pragma warning disable CA2007 - await using var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - $"DROP TABLE IF EXISTS temp.\"{name}\"", - transaction - ); + await using var command = connection.CreateCommand(); #pragma warning restore CA2007 + + command.CommandText = $"DROP TABLE IF EXISTS temp.\"{name}\""; + command.Transaction = transaction; DbConnectionExtensions.OnBeforeExecutingCommand(command, []); diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index a079b78..9529f38 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -60,7 +60,7 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand( ArgumentNullException.ThrowIfNull(databaseAdapter); ArgumentNullException.ThrowIfNull(connection); - var (command, temporaryTables, cancellationTokenRegistration) = BuildDbCommandCore( + var (command, cancellationTokenRegistration) = BuildDbCommandCore( statement, databaseAdapter, connection, @@ -72,10 +72,10 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand( TemporaryTableDisposer[] temporaryTableDisposers = []; - if (temporaryTables.Length > 0) + if (statement.TemporaryTables.Count > 0) { temporaryTableDisposers = BuildTemporaryTables( - temporaryTables, + statement.TemporaryTables, databaseAdapter, connection, transaction, @@ -135,7 +135,7 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand( ArgumentNullException.ThrowIfNull(connection); ArgumentNullException.ThrowIfNull(databaseAdapter); - var (command, temporaryTables, cancellationTokenRegistration) = BuildDbCommandCore( + var (command, cancellationTokenRegistration) = BuildDbCommandCore( statement, databaseAdapter, connection, @@ -147,10 +147,10 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand( TemporaryTableDisposer[] temporaryTableDisposers = []; - if (temporaryTables.Length > 0) + if (statement.TemporaryTables.Count > 0) { temporaryTableDisposers = await BuildTemporaryTablesAsync( - temporaryTables, + statement.TemporaryTables, databaseAdapter, connection, transaction, @@ -174,10 +174,9 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand( /// The to assign to the command. /// A token that can be used to cancel the command. /// - /// A tuple containing the created , the temporary tables for the statement, and the - /// cancellation token registration for the command. + /// A tuple containing the created and the cancellation token registration for the command. /// - private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegistration) BuildDbCommandCore( + private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( InterpolatedSqlStatement statement, IDatabaseAdapter databaseAdapter, DbConnection connection, @@ -188,8 +187,17 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist ) { using var codeBuilder = new ValueStringBuilder(stackalloc Char[500]); - var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var temporaryTables = new List(); + var parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + var command = connection.CreateCommand(); + + command.Transaction = transaction; + command.CommandType = commandType; + + if (commandTimeout is not null) + { + command.CommandTimeout = (Int32)commandTimeout.Value.TotalSeconds; + } foreach (var fragment in statement.Fragments) { @@ -207,32 +215,32 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist ); } - parameters.Add(parameter.Name, parameterValue); + var dbParameter = command.CreateParameter(); + dbParameter.ParameterName = parameter.Name; + databaseAdapter.BindParameterValue(dbParameter, parameterValue); + command.Parameters.Add(dbParameter); + + parameterNames.Add(parameter.Name); break; } case InterpolatedParameter interpolatedParameter: { - var parameterName = interpolatedParameter.InferredName; + var parameterName = interpolatedParameter.InferredName ?? + "Parameter_" + (parameterNames.Count + 1); - if (String.IsNullOrWhiteSpace(parameterName)) - { - parameterName = "Parameter_" + (parameters.Count + 1).ToString(CultureInfo.InvariantCulture); - } - - if (parameters.ContainsKey(parameterName)) + if (parameterNames.Contains(parameterName)) { var suffix = 2; + String candidate; - var newParameterName = parameterName + suffix.ToString(CultureInfo.InvariantCulture); - - while (parameters.ContainsKey(newParameterName)) + do { + candidate = parameterName + suffix; suffix++; - newParameterName = parameterName + suffix.ToString(CultureInfo.InvariantCulture); - } + } while (parameterNames.Contains(candidate)); - parameterName = newParameterName; + parameterName = candidate; } var parameterValue = interpolatedParameter.Value; @@ -245,13 +253,18 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist ); } - parameters.Add(parameterName, parameterValue); + var dbParameter = command.CreateParameter(); + dbParameter.ParameterName = parameterName; + databaseAdapter.BindParameterValue(dbParameter, parameterValue); + command.Parameters.Add(dbParameter); + + parameterNames.Add(parameterName); + codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName)); break; } case InterpolatedTemporaryTable interpolatedTemporaryTable: - temporaryTables.Add(interpolatedTemporaryTable); codeBuilder.Append( databaseAdapter.QuoteTemporaryTableName(interpolatedTemporaryTable.Name, connection) ); @@ -263,31 +276,14 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist } } - var command = DbConnectionExtensions.DbCommandFactory.CreateDbCommand( - connection, - codeBuilder.ToString(), - transaction, - commandTimeout, - commandType - ); + command.CommandText = codeBuilder.ToString(); var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( command, cancellationToken ); - foreach (var (name, value) in parameters) - { - var parameter = command.CreateParameter(); - - parameter.ParameterName = name; - - databaseAdapter.BindParameterValue(parameter, value); - - command.Parameters.Add(parameter); - } - - return (command, temporaryTables.ToArray(), cancellationTokenRegistration); + return (command, cancellationTokenRegistration); } /// @@ -309,7 +305,7 @@ private static (DbCommand, InterpolatedTemporaryTable[], CancellationTokenRegist /// The operation was cancelled via . /// private static TemporaryTableDisposer[] BuildTemporaryTables( - InterpolatedTemporaryTable[] temporaryTables, + IReadOnlyList temporaryTables, IDatabaseAdapter databaseAdapter, DbConnection connection, DbTransaction? transaction, @@ -321,11 +317,11 @@ CancellationToken cancellationToken ThrowHelper.ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(databaseAdapter); } - var temporaryTableDisposers = new TemporaryTableDisposer?[temporaryTables.Length]; + var temporaryTableDisposers = new TemporaryTableDisposer?[temporaryTables.Count]; try { - for (var i = 0; i < temporaryTables.Length; i++) + for (var i = 0; i < temporaryTables.Count; i++) { var interpolatedTemporaryTable = temporaryTables[i]; @@ -374,7 +370,7 @@ CancellationToken cancellationToken /// The operation was cancelled via . /// private static async Task BuildTemporaryTablesAsync( - InterpolatedTemporaryTable[] temporaryTables, + IReadOnlyList temporaryTables, IDatabaseAdapter databaseAdapter, DbConnection connection, DbTransaction? transaction, @@ -386,11 +382,11 @@ CancellationToken cancellationToken ThrowHelper.ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(databaseAdapter); } - var temporaryTableDisposers = new TemporaryTableDisposer?[temporaryTables.Length]; + var temporaryTableDisposers = new TemporaryTableDisposer?[temporaryTables.Count]; try { - for (var i = 0; i < temporaryTables.Length; i++) + for (var i = 0; i < temporaryTables.Count; i++) { var interpolatedTemporaryTable = temporaryTables[i]; diff --git a/src/DbConnectionPlus/DbCommands/DefaultDbCommandFactory.cs b/src/DbConnectionPlus/DbCommands/DefaultDbCommandFactory.cs deleted file mode 100644 index 90abccb..0000000 --- a/src/DbConnectionPlus/DbCommands/DefaultDbCommandFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 David Liebeherr -// Licensed under the MIT License. See LICENSE.md in the project root for more information. - -namespace RentADeveloper.DbConnectionPlus.DbCommands; - -/// -/// The default implementation of . -/// -internal sealed class DefaultDbCommandFactory : IDbCommandFactory -{ - /// - public DbCommand CreateDbCommand( - DbConnection connection, - String commandText, - DbTransaction? transaction = null, - TimeSpan? commandTimeout = null, - CommandType commandType = CommandType.Text - ) - { - ArgumentNullException.ThrowIfNull(connection); - ArgumentNullException.ThrowIfNull(commandText); - - var command = connection.CreateCommand(); - -#pragma warning disable CA2100 - command.CommandText = commandText; -#pragma warning restore CA2100 - - command.Transaction = transaction; - command.CommandType = commandType; - - if (commandTimeout is not null) - { - command.CommandTimeout = (Int32)commandTimeout.Value.TotalSeconds; - } - - return command; - } -} diff --git a/src/DbConnectionPlus/DbCommands/IDbCommandFactory.cs b/src/DbConnectionPlus/DbCommands/IDbCommandFactory.cs deleted file mode 100644 index 2623278..0000000 --- a/src/DbConnectionPlus/DbCommands/IDbCommandFactory.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2026 David Liebeherr -// Licensed under the MIT License. See LICENSE.md in the project root for more information. - -namespace RentADeveloper.DbConnectionPlus.DbCommands; - -/// -/// Represents a factory that creates instances of . -/// -public interface IDbCommandFactory -{ - /// - /// Creates an instance of with the specified settings. - /// - /// The connection to use to create the . - /// The command text to assign to the . - /// The transaction to assign to the . - /// The command timeout to assign to the . - /// The to assign to the . - /// An instance of with the specified settings. - /// - /// - /// - /// - /// is . - /// - /// - /// - /// - /// is . - /// - /// - /// - /// - public DbCommand CreateDbCommand( - DbConnection connection, - String commandText, - DbTransaction? transaction = null, - TimeSpan? commandTimeout = null, - CommandType commandType = CommandType.Text - ); -} diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs index 9d68a24..91e7718 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs @@ -1,7 +1,6 @@ // 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; @@ -29,14 +28,6 @@ public static void Configure(Action configureActi 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. /// diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs index e343b8e..d886c8b 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs @@ -80,7 +80,7 @@ public static InterpolatedParameter Parameter( MaximumParameterNameLength ); - if (!String.IsNullOrWhiteSpace(nameFromCallerArgumentExpression)) + if (nameFromCallerArgumentExpression.Length > 0) { inferredParameterName = nameFromCallerArgumentExpression; } diff --git a/src/DbConnectionPlus/Helpers/NameHelper.cs b/src/DbConnectionPlus/Helpers/NameHelper.cs index 2d22824..cbca45e 100644 --- a/src/DbConnectionPlus/Helpers/NameHelper.cs +++ b/src/DbConnectionPlus/Helpers/NameHelper.cs @@ -21,42 +21,64 @@ internal static class NameHelper /// to the specified maximum length. /// The first character of the resulting name is converted to uppercase if it is a lowercase letter. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan expression, Int32 maximumLength) { - if (expression.StartsWith("this.", StringComparison.OrdinalIgnoreCase)) + // Remove "this.": + if ( + expression.Length >= 5 && + expression[0] == 't' && expression[1] == 'h' && + expression[2] == 'i' && expression[3] == 's' && + expression[4] == '.' + ) { expression = expression[5..]; } - if (expression.StartsWith("new", StringComparison.OrdinalIgnoreCase)) + // Remove "new": + if ( + expression.Length >= 3 && + expression[0] == 'n' && expression[1] == 'e' && expression[2] == 'w' + ) { expression = expression[3..]; } - if (expression.StartsWith("Get", StringComparison.OrdinalIgnoreCase)) + // Remove "Get": + if ( + expression.Length >= 3 && + expression[0] == 'G' && expression[1] == 'e' && expression[2] == 't' + ) { expression = expression[3..]; } - var buffer = expression.Length <= 512 ? stackalloc Char[expression.Length] : new Char[expression.Length]; + var length = Math.Min(expression.Length, maximumLength); + + var buffer = length <= 512 ? stackalloc Char[length] : new Char[length]; + var count = 0; - foreach (var character in expression) + for (var i = 0; i < expression.Length && count < maximumLength; i++) { - if (count >= maximumLength) - { - break; - } + var c = expression[i]; - if (character is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_') + // Only allow letters, digits, and underscores in the name. + if ( + (UInt32)(c - 'A') <= 'Z' - 'A' || // Uppercase letters + (UInt32)(c - 'a') <= 'z' - 'a' || // Lowercase letters + (UInt32)(c - '0') <= '9' - '0' || // Digits + c == '_' // Underscores + ) { - buffer[count++] = character; + buffer[count++] = c; } } - if (count > 0 && Char.IsLower(buffer[0])) + // Convert the first character to uppercase if it is a lowercase letter. + if (count > 0 && (UInt32)(buffer[0] - 'a') <= 'z' - 'a') { - buffer[0] = Char.ToUpper(buffer[0], CultureInfo.InvariantCulture); + buffer[0] = (Char)(buffer[0] - 32); } return new(buffer[..count]); diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs index 0912120..a08b27a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs @@ -46,7 +46,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs index 7941cfa..3197b77 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs @@ -43,7 +43,7 @@ public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancella var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs index 4ff17a3..185ec00 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs @@ -44,7 +44,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index 95e41f0..ce35e76 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -42,7 +42,7 @@ public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancella var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 3f9358e..9a14194 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -46,7 +46,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken) diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 9db1ef5..0e48679 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -44,7 +44,7 @@ public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancella var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs deleted file mode 100644 index 86bac48..0000000 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DefaultDbCommandFactoryTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DbCommands; - -public sealed class - DefaultDbCommandFactoryTests_MySql : - DefaultDbCommandFactoryTests; - -public sealed class - DefaultDbCommandFactoryTests_Oracle : - DefaultDbCommandFactoryTests; - -public sealed class - DefaultDbCommandFactoryTests_PostgreSql : - DefaultDbCommandFactoryTests; - -public sealed class - DefaultDbCommandFactoryTests_Sqlite : - DefaultDbCommandFactoryTests; - -public sealed class - DefaultDbCommandFactoryTests_SqlServer : - DefaultDbCommandFactoryTests; - -public abstract class DefaultDbCommandFactoryTests : IntegrationTestsBase - where TTestDatabaseProvider : ITestDatabaseProvider, new() -{ - [Fact] - public void CreateDbCommand_NoTimeout_ShouldUseDefaultTimeout() - { - var command = this.factory.CreateDbCommand(this.Connection, "SELECT 1"); - - command.CommandTimeout - .Should().Be(this.Connection.CreateCommand().CommandTimeout); - } - - [Fact] - public void CreateDbCommand_ShouldCreateDbCommandWithSpecifiedSettings() - { - var commandType = this.TestDatabaseProvider.SupportsStoredProcedures - ? CommandType.StoredProcedure - : CommandType.Text; - - using var transaction = this.Connection.BeginTransaction(); - - var timeout = Generate.Single(); - - var command = this.factory.CreateDbCommand( - this.Connection, - "SELECT 1", - transaction, - timeout, - commandType - ); - - command.Connection - .Should().BeSameAs(this.Connection); - - command.CommandText - .Should().Be("SELECT 1"); - - command.Transaction - .Should().BeSameAs(transaction); - - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); - - command.CommandType - .Should().Be(commandType); - } - - private readonly DefaultDbCommandFactory factory = new(); -} diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs index e31a439..8c07bea 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs @@ -39,7 +39,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( useAsyncApi, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs index 3879b34..0399f4f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs @@ -37,7 +37,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(async () => { diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs index 3839540..8d7baa5 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs @@ -37,7 +37,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs index dcb1211..8ee17ed 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs @@ -35,7 +35,7 @@ public async Task Exists_CancellationToken_ShouldCancelOperationIfCancellationIs var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi(useAsyncApi, this.Connection, "SELECT 1", cancellationToken: cancellationToken)) .Should().ThrowAsync() diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index 95ef969..de5ae7a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -242,7 +242,7 @@ public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellati var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index d96273d..073e68f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -254,7 +254,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs index c4e4101..a843ddd 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs @@ -39,7 +39,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs index 17a2a2c..6d4ccd8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs @@ -37,7 +37,7 @@ public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellati var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 9197b1a..7ad6a72 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -240,7 +240,7 @@ public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsR var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 426d8c1..9ed0dd6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -242,7 +242,7 @@ public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellat var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index e025054..9c36fb3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -254,7 +254,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs index 5616799..abb7f63 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs @@ -40,7 +40,7 @@ Boolean useAsyncApi var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs index 92e873b..a97795e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs @@ -37,7 +37,7 @@ public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellat var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs index 40d77e0..5614a58 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs @@ -37,7 +37,7 @@ public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsR var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - this.DbCommandFactory.DelayNextDbCommand = true; + this.DelayNextDbCommand = true; await Invoking(() => CallApi( diff --git a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs index d350e53..a22400f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs @@ -5,7 +5,6 @@ global using RentADeveloper.DbConnectionPlus.Configuration; global using RentADeveloper.DbConnectionPlus.DbCommands; global using RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; -global using RentADeveloper.DbConnectionPlus.IntegrationTests.TestHelpers; global using RentADeveloper.DbConnectionPlus.SqlStatements; global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; global using static AwesomeAssertions.FluentActions; diff --git a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs index 7931e13..53f6325 100644 --- a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs +++ b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs @@ -5,9 +5,11 @@ using System.Data.Common; using System.Globalization; +using LinkDotNet.StringBuilder; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; using RentADeveloper.DbConnectionPlus.Entities; +using RentADeveloper.DbConnectionPlus.Extensions; namespace RentADeveloper.DbConnectionPlus.IntegrationTests; @@ -26,7 +28,7 @@ protected IntegrationTestsBase() Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new("en-US"); - DbCommandLogger.LogCommands = false; + this.logDbCommands = false; this.TestDatabaseProvider = new(); this.TestDatabaseProvider.ResetDatabase(); @@ -37,10 +39,7 @@ protected IntegrationTestsBase() currentTestDatabaseConnection.Value = this.Connection; - this.DbCommandFactory = new(this.TestDatabaseProvider); - DbConnectionExtensions.DbCommandFactory = this.DbCommandFactory; - - DbCommandLogger.LogCommands = true; + this.logDbCommands = true; OracleDatabaseAdapter.AllowTemporaryTables = true; @@ -48,11 +47,19 @@ protected IntegrationTestsBase() DbConnectionPlusConfiguration.Instance = new() { EnumSerializationMode = EnumSerializationMode.Strings, - InterceptDbCommand = DbCommandLogger.LogDbCommand + InterceptDbCommand = this.InterceptDbCommand }; EntityHelper.ResetEntityTypeMetadataCache(); } + /// + /// Determines whether the next database command created by DbConnectionPlus will be delayed by 2 seconds. + /// If set to , a 2 second delay will be injected into the next database command created by + /// DbConnectionPlus. Subsequent commands will not be delayed unless this property is set to + /// again. + /// + public Boolean DelayNextDbCommand { get; set; } + /// public void Dispose() { @@ -134,7 +141,7 @@ public static String QT(String tableName) => /// The entities that were created and inserted. protected List CreateEntitiesInDb(Int32? numberOfEntities = null, DbTransaction? transaction = null) where T : class => - ExecuteWithoutDbCommandLogging(() => + this.ExecuteWithoutDbCommandLogging(() => { var entities = Generate.Multiple(numberOfEntities); @@ -164,7 +171,7 @@ protected List CreateEntitiesInDb(Int32? numberOfEntities = null, DbTransa protected T CreateEntityInDb(DbTransaction? transaction = null) where T : class => - ExecuteWithoutDbCommandLogging(() => + this.ExecuteWithoutDbCommandLogging(() => { var entity = Generate.Single(); @@ -218,7 +225,7 @@ [.. keyProperties.Select(p => $"{Q(p.ColumnName)} = {P(p.PropertyName)}")] keyProperties.Select(p => (p.PropertyName, p.PropertyGetter!(entity))).ToArray()! ); - return ExecuteWithoutDbCommandLogging(() => this.Connection.Exists( + return this.ExecuteWithoutDbCommandLogging(() => this.Connection.Exists( statement, transaction, cancellationToken: TestContext.Current.CancellationToken @@ -236,7 +243,7 @@ [.. keyProperties.Select(p => $"{Q(p.ColumnName)} = {P(p.PropertyName)}")] /// otherwise, . /// protected Boolean ExistsTemporaryTableInDb(String tableName, DbTransaction? transaction = null) => - ExecuteWithoutDbCommandLogging(() => + this.ExecuteWithoutDbCommandLogging(() => this.TestDatabaseProvider.ExistsTemporaryTable( tableName, this.Connection, @@ -251,7 +258,7 @@ protected Boolean ExistsTemporaryTableInDb(String tableName, DbTransaction? tran /// The name of the column of which to get the collation. /// The collation of the specified column of the specified temporary table. protected String GetCollationOfTemporaryTableColumn(String temporaryTableName, String columnName) => - ExecuteWithoutDbCommandLogging(() => + this.ExecuteWithoutDbCommandLogging(() => this.TestDatabaseProvider.GetCollationOfTemporaryTableColumn( temporaryTableName, columnName, @@ -269,7 +276,7 @@ protected String GetDataTypeOfTemporaryTableColumn( String temporaryTableName, String columnName ) => - ExecuteWithoutDbCommandLogging(() => + this.ExecuteWithoutDbCommandLogging(() => this.TestDatabaseProvider.GetDataTypeOfTemporaryTableColumn( temporaryTableName, columnName, @@ -277,6 +284,84 @@ String columnName ) ); + /// + /// Executes while disabling database command logging during the execution. + /// + /// The type of the return value of . + /// The function to execute. + /// The return value of . + private T ExecuteWithoutDbCommandLogging(Func func) + { + this.logDbCommands = false; + var result = func(); + this.logDbCommands = true; + return result; + } + + private void InterceptDbCommand(DbCommand command, IReadOnlyList temporaryTables) + { + if (this.DelayNextDbCommand) + { + command.CommandText = this.TestDatabaseProvider.DelayTwoSecondsStatement + command.CommandText; + this.DelayNextDbCommand = false; + } + + if (this.logDbCommands) + { + using var logMessageBuilder = new ValueStringBuilder(stackalloc Char[500]); + + logMessageBuilder.AppendLine(); + logMessageBuilder.AppendLine("-----------------"); + logMessageBuilder.AppendLine("Executing Command"); + logMessageBuilder.AppendLine("-----------------"); + logMessageBuilder.AppendLine(command.CommandText.Trim()); + + if (command.Parameters.Count > 0) + { + logMessageBuilder.AppendLine(); + logMessageBuilder.AppendLine("----------"); + logMessageBuilder.AppendLine("Parameters"); + logMessageBuilder.AppendLine("----------"); + + foreach (DbParameter parameter in command.Parameters) + { + logMessageBuilder.Append(" - "); + logMessageBuilder.Append(parameter.ParameterName); + + logMessageBuilder.Append(" ("); + logMessageBuilder.Append(parameter.Direction.ToString()); + logMessageBuilder.Append(")"); + + logMessageBuilder.Append(" = "); + logMessageBuilder.Append(parameter.Value.ToDebugString()); + logMessageBuilder.AppendLine(); + } + } + + if (temporaryTables.Count > 0) + { + logMessageBuilder.AppendLine(); + logMessageBuilder.AppendLine("----------------"); + logMessageBuilder.AppendLine("Temporary Tables"); + logMessageBuilder.AppendLine("----------------"); + + foreach (var temporaryTable in temporaryTables) + { + logMessageBuilder.AppendLine(); + logMessageBuilder.AppendLine(temporaryTable.Name); + logMessageBuilder.AppendLine(new String('-', temporaryTable.Name.Length)); + + foreach (var value in temporaryTable.Values) + { + logMessageBuilder.AppendLine(value.ToDebugString()); + } + } + } + + Console.WriteLine(logMessageBuilder.ToString()); + } + } + /// /// Creates a that will be cancelled after 100 milliseconds. /// @@ -288,19 +373,7 @@ protected static CancellationToken CreateCancellationTokenThatIsCancelledAfter10 return cancellationTokenSource.Token; } - /// - /// Executes while disabling database command logging during the execution. - /// - /// The type of the return value of . - /// The function to execute. - /// The return value of . - private static T ExecuteWithoutDbCommandLogging(Func func) - { - DbCommandLogger.LogCommands = false; - var result = func(); - DbCommandLogger.LogCommands = true; - return result; - } + private Boolean logDbCommands; /// /// The connection to the test database for the currently running integration test. @@ -311,9 +384,4 @@ private static T ExecuteWithoutDbCommandLogging(Func func) /// The test database provider for the currently running integration test. /// private static readonly AsyncLocal currentTestDatabaseProvider = new(); - - /// - /// The database command factory used for testing cancellation of SQL statements. - /// - protected readonly DelayDbCommandFactory DbCommandFactory; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index bfe8894..96a70d2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -161,8 +161,8 @@ private static void ExecuteScript(MySqlConnection connection, String script) CREATE TABLE `Entity` ( `Id` BIGINT, - `BytesValue` BLOB, `BooleanValue` TINYINT(1), + `BytesValue` BLOB, `ByteValue` TINYINT UNSIGNED, `CharValue` CHAR(1), `DateOnlyValue` DATE, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 1ceec26..6613109 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -142,8 +142,8 @@ private static void ExecuteScript(OracleConnection connection, String script) CREATE TABLE "Entity" ( "Id" NUMBER(19) NOT NULL PRIMARY KEY, - "BytesValue" RAW(2000), "BooleanValue" NUMBER(1), + "BytesValue" RAW(2000), "ByteValue" NUMBER(3), "CharValue" CHAR(1), "DateOnlyValue" DATE, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index ddeea0d..413d9a1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -143,8 +143,8 @@ public void ResetDatabase() CREATE TABLE "Entity" ( "Id" bigint NOT NULL PRIMARY KEY, - "BytesValue" bytea, "BooleanValue" boolean, + "BytesValue" bytea, "ByteValue" smallint, "CharValue" char(1), "DateOnlyValue" date, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index ab523a6..c81b471 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -128,8 +128,8 @@ public void ResetDatabase() CREATE TABLE Entity ( Id INTEGER, - BytesValue BLOB, BooleanValue INTEGER, + BytesValue BLOB, ByteValue INTEGER, CharValue TEXT, DateOnlyValue TEXT, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index c05a5ea..803b8c3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -166,8 +166,8 @@ private static void ExecuteScript(SqlConnection connection, String script) CREATE TABLE Entity ( Id BIGINT NOT NULL PRIMARY KEY, - BytesValue VARBINARY(MAX), BooleanValue BIT, + BytesValue VARBINARY(MAX), ByteValue TINYINT, CharValue CHAR(1), DateOnlyValue DATE, diff --git a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs b/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs deleted file mode 100644 index 90c9c76..0000000 --- a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Data.Common; -using LinkDotNet.StringBuilder; -using RentADeveloper.DbConnectionPlus.Extensions; - -namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestHelpers; - -/// -/// Logs database commands executed during tests for debugging purposes. -/// -public static class DbCommandLogger -{ - /// - /// Determines whether database commands should be logged. - /// - public static Boolean LogCommands { get; set; } = true; - - /// - /// Logs the specified database command and the temporary tables used in the command. - /// - /// The to log. - /// The temporary tables used in the command. - public static void LogDbCommand(DbCommand command, IReadOnlyList temporaryTables) - { - if (!LogCommands) - { - return; - } - - using var logMessageBuilder = new ValueStringBuilder(stackalloc Char[500]); - - logMessageBuilder.AppendLine(); - logMessageBuilder.AppendLine("-----------------"); - logMessageBuilder.AppendLine("Executing Command"); - logMessageBuilder.AppendLine("-----------------"); - logMessageBuilder.AppendLine(command.CommandText.Trim()); - - if (command.Parameters.Count > 0) - { - logMessageBuilder.AppendLine(); - logMessageBuilder.AppendLine("----------"); - logMessageBuilder.AppendLine("Parameters"); - logMessageBuilder.AppendLine("----------"); - - foreach (DbParameter parameter in command.Parameters) - { - logMessageBuilder.Append(" - "); - logMessageBuilder.Append(parameter.ParameterName); - - logMessageBuilder.Append(" ("); - logMessageBuilder.Append(parameter.Direction.ToString()); - logMessageBuilder.Append(")"); - - logMessageBuilder.Append(" = "); - logMessageBuilder.Append(parameter.Value.ToDebugString()); - logMessageBuilder.AppendLine(); - } - } - - if (temporaryTables.Count > 0) - { - logMessageBuilder.AppendLine(); - logMessageBuilder.AppendLine("----------------"); - logMessageBuilder.AppendLine("Temporary Tables"); - logMessageBuilder.AppendLine("----------------"); - - foreach (var temporaryTable in temporaryTables) - { - logMessageBuilder.AppendLine(); - logMessageBuilder.AppendLine(temporaryTable.Name); - logMessageBuilder.AppendLine(new String('-', temporaryTable.Name.Length)); - - foreach (var value in temporaryTable.Values) - { - logMessageBuilder.AppendLine(value.ToDebugString()); - } - } - } - - Console.WriteLine(logMessageBuilder.ToString()); - } -} diff --git a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs b/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs deleted file mode 100644 index d16308e..0000000 --- a/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DelayDbCommandFactory.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Data.Common; - -namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestHelpers; - -/// -/// An implementation of that supports delaying created commands. -/// -/// The provider for the current test database. -public class DelayDbCommandFactory(ITestDatabaseProvider testDatabaseProvider) : IDbCommandFactory -{ - /// - /// Determines whether the next database command created throw this instance will be delayed by 2 seconds. - /// If set to , a 2 second delay will be injected into the next database command created by - /// this factory. Subsequent commands will not be delayed unless this property is set to - /// again. - /// - public Boolean DelayNextDbCommand { get; set; } - - /// - public DbCommand CreateDbCommand( - DbConnection connection, - String commandText, - DbTransaction? transaction = null, - TimeSpan? commandTimeout = null, - CommandType commandType = CommandType.Text - ) - { - ArgumentNullException.ThrowIfNull(connection); - ArgumentNullException.ThrowIfNull(commandText); - - var command = connection.CreateCommand(); - -#pragma warning disable CA2100 - if (this.DelayNextDbCommand) - { - command.CommandText = testDatabaseProvider.DelayTwoSecondsStatement + commandText; - this.DelayNextDbCommand = false; - } - else - { - command.CommandText = commandText; - } -#pragma warning restore CA2100 - - command.Transaction = transaction; - command.CommandType = commandType; - - if (commandTimeout is not null) - { - command.CommandTimeout = (Int32)commandTimeout.Value.TotalSeconds; - } - - return command; - } -} diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index 6c2952c..bb9cbf6 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Data.Sqlite; +using Microsoft.Data.Sqlite; using MySqlConnector; using Npgsql; using NSubstitute.DbConnection; @@ -20,8 +20,6 @@ public void EnumSerializationMode_Integers_ShouldSerializeEnumAsInteger() { var enumValue = Generate.Single(); - this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); - DbParameter? interceptedDbParameter = null; DbConnectionPlusConfiguration.Instance.InterceptDbCommand = @@ -43,8 +41,6 @@ public void EnumSerializationMode_Strings_ShouldSerializeEnumAsString() { var enumValue = Generate.Single(); - this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); - DbParameter? interceptedDbParameter = null; DbConnectionPlusConfiguration.Instance.InterceptDbCommand = @@ -193,8 +189,6 @@ public void InterceptDbCommand_ShouldInterceptDbCommands() 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; diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs index 128f384..ce590a0 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs @@ -1,5 +1,4 @@ -using NSubstitute.DbConnection; -using Oracle.ManagedDataAccess.Client; +using Oracle.ManagedDataAccess.Client; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Oracle; @@ -303,8 +302,7 @@ public void QuoteIdentifier_ShouldQuoteIdentifier() => [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() { - this.MockDbConnection.SetupQuery("SELECT VALUE FROM v$parameter WHERE NAME = 'private_temp_table_prefix'") - .Returns(new { VALUE = "MockPrefix" }); + this.MockDbCommand.ExecuteScalar().Returns("MockPrefix"); this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) .Should().Be("\"MockPrefixTempTable\""); @@ -325,8 +323,9 @@ public void ShouldGuardAgainstNullArguments() [Fact] public void SupportsTemporaryTables_OracleVersionEqualToOrGreaterThan18_ShouldReturnTrue() { - this.MockDbConnection.SetupQuery("SELECT 1 FROM v$instance WHERE version >= '18'") - .Returns(new { Value = 1 }); + // Fake the query "SELECT 1 FROM v$instance WHERE version >= '18'" to return a result, which indicates that the + // Oracle version is 18 or higher. + this.MockDbDataReader.Read().Returns(true); this.adapter.SupportsTemporaryTables(this.MockDbConnection) .Should().BeTrue(); @@ -335,8 +334,9 @@ public void SupportsTemporaryTables_OracleVersionEqualToOrGreaterThan18_ShouldRe [Fact] public void SupportsTemporaryTables_OracleVersionLessThan18_ShouldReturnFalse() { - this.MockDbConnection.SetupQuery("SELECT 1 FROM v$instance WHERE version >= '18'") - .Returns(Array.Empty()); + // Fake the query "SELECT 1 FROM v$instance WHERE version >= '18'" to return no results, which indicates that + // the Oracle version is lower than 18. + this.MockDbDataReader.Read().Returns(false); this.adapter.SupportsTemporaryTables(this.MockDbConnection) .Should().BeFalse(); diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs deleted file mode 100644 index 9b19612..0000000 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DefaultDbCommandFactoryTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using RentADeveloper.DbConnectionPlus.DbCommands; - -namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; - -public class DefaultDbCommandFactoryTests : UnitTestsBase -{ - [Fact] - public void CreateDbCommand_NoTimeout_ShouldUseDefaultTimeout() - { - var command = this.factory.CreateDbCommand(this.MockDbConnection, "SELECT 1"); - - command.CommandTimeout - .Should().Be(0); - } - - [Fact] - public void CreateDbCommand_ShouldCreateDbCommandWithSpecifiedSettings() - { - using var transaction = this.MockDbConnection.BeginTransaction(); - - var timeout = Generate.Single(); - - var command = this.factory.CreateDbCommand( - this.MockDbConnection, - "SELECT 1", - transaction, - timeout, - CommandType.StoredProcedure - ); - - command.Connection - .Should().BeSameAs(this.MockDbConnection); - - command.CommandText - .Should().Be("SELECT 1"); - - command.Transaction - .Should().BeSameAs(transaction); - - command.CommandType - .Should().Be(CommandType.StoredProcedure); - - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); - } - - [Fact] - public void ShouldGuardAgainstNullArguments() => - ArgumentNullGuardVerifier.Verify(() => - this.factory.CreateDbCommand(this.MockDbConnection, "SELECT 1") - ); - - private readonly DefaultDbCommandFactory factory = new(); -} diff --git a/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs new file mode 100644 index 0000000..e53b006 --- /dev/null +++ b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs @@ -0,0 +1,91 @@ +namespace RentADeveloper.DbConnectionPlus.UnitTests.Mocks; + +/// +/// A simple mock implementation of . +/// +public class MockDbParameterCollection : DbParameterCollection +{ + /// + public override Int32 Count => this.parameters.Count; + + /// + public override Object SyncRoot => ((ICollection)this.parameters).SyncRoot; + + /// + public override Int32 Add(Object value) + { + this.parameters.Add((DbParameter)value); + return this.Count - 1; + } + + /// + public override void AddRange(Array values) => this.parameters.AddRange(values.Cast()); + + /// + public override void Clear() => this.parameters.Clear(); + + /// + public override bool Contains(Object value) => this.parameters.Contains(value); + + /// + public override bool Contains(String value) => this.IndexOf(value) != -1; + + /// + public override void CopyTo(Array array, Int32 index) => + this.parameters.CopyTo((DbParameter[])array, index); + + /// + public override IEnumerator GetEnumerator() => this.parameters.GetEnumerator(); + + /// + public override Int32 IndexOf(Object value) => this.parameters.IndexOf((DbParameter)value); + + /// + public override Int32 IndexOf(String parameterName) + { + for (Int32 index = 0; index < this.parameters.Count; ++index) + { + if (this.parameters[index].ParameterName == parameterName) + return index; + } + + return -1; + } + + /// + public override void Insert(Int32 index, Object value) => + this.parameters.Insert(index, (DbParameter)value); + + /// + public override void Remove(Object value) => this.parameters.Remove((DbParameter)value); + + /// + public override void RemoveAt(Int32 index) => this.parameters.RemoveAt(index); + + /// + public override void RemoveAt(String parameterName) => + this.RemoveAt(this.IndexOfChecked(parameterName)); + + /// + protected override DbParameter GetParameter(Int32 index) => this.parameters[index]; + + /// + protected override DbParameter GetParameter(String parameterName) => + this.GetParameter(this.IndexOfChecked(parameterName)); + + /// + protected override void SetParameter(Int32 index, DbParameter value) => + this.parameters[index] = value; + + /// + protected override void SetParameter(String parameterName, DbParameter value) => + this.SetParameter(this.IndexOfChecked(parameterName), value); + + private Int32 IndexOfChecked(String parameterName) + { + Int32 num = this.IndexOf(parameterName); + return num != -1 ? num : throw new IndexOutOfRangeException(); + } + + private readonly List parameters = []; +} diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index ca134b3..04dcd52 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -78,13 +78,6 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters public System.Threading.Tasks.ValueTask DisposeAsync() { } } } -namespace RentADeveloper.DbConnectionPlus.DbCommands -{ - public interface IDbCommandFactory - { - System.Data.Common.DbCommand CreateDbCommand(System.Data.Common.DbConnection connection, string commandText, System.Data.Common.DbTransaction? transaction = null, System.TimeSpan? commandTimeout = default, System.Data.CommandType commandType = 1); - } -} namespace RentADeveloper.DbConnectionPlus { public static class DbConnectionExtensions diff --git a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs index 8c2fcd0..ea3b51b 100644 --- a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs @@ -22,8 +22,6 @@ protected StatementMethodTestsBase( { this.asyncTestMethod = asyncTestMethod; this.syncTestMethod = syncTestMethod; - - this.MockDbConnection.SetupQuery(_ => true).Returns(new { Id = 1 }); } [Fact] @@ -40,11 +38,9 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - Arg.Any(), - timeout + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.CommandTimeout == (Int32) timeout.TotalSeconds), + Arg.Any>() ); } @@ -60,12 +56,9 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - Arg.Any(), - Arg.Any(), - CommandType.StoredProcedure + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), + Arg.Any>() ); } @@ -83,10 +76,9 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - transaction + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.Transaction == transaction), + Arg.Any>() ); } @@ -104,11 +96,9 @@ public void SyncMethod_ShouldUseCommandTimeout() TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - Arg.Any(), - timeout + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.CommandTimeout == (Int32) timeout.TotalSeconds), + Arg.Any>() ); } @@ -124,12 +114,9 @@ public void SyncMethod_ShouldUseCommandType() TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - Arg.Any(), - Arg.Any(), - CommandType.StoredProcedure + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), + Arg.Any>() ); } @@ -147,10 +134,9 @@ public void SyncMethod_ShouldUseTransaction() TestContext.Current.CancellationToken ); - this.MockCommandFactory.Received().CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - transaction + this.MockInterceptDbCommand.Received().Invoke( + Arg.Is(cmd => cmd.Transaction == transaction), + Arg.Any>() ); } diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index dfcd812..8370bc4 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -1,13 +1,14 @@ #pragma warning disable NS1004 using System.Globalization; +using NSubstitute.ClearExtensions; using NSubstitute.DbConnection; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; -using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Entities; using RentADeveloper.DbConnectionPlus.Extensions; +using RentADeveloper.DbConnectionPlus.UnitTests.Mocks; namespace RentADeveloper.DbConnectionPlus.UnitTests; @@ -25,49 +26,26 @@ public UnitTestsBase() Thread.CurrentThread.CurrentUICulture = new("en-US"); + this.MockDatabaseAdapter = Substitute.For(); + this.MockEntityManipulator = Substitute.For(); - // Reset all settings to defaults before each test. - DbConnectionPlusConfiguration.Instance = new() - { - EnumSerializationMode = EnumSerializationMode.Strings, - InterceptDbCommand = null - }; - EntityHelper.ResetEntityTypeMetadataCache(); - OracleDatabaseAdapter.AllowTemporaryTables = false; + this.MockDbConnection = Substitute.For(); - this.MockDbConnection = Substitute.For().SetupCommands(); - this.MockCommandFactory = Substitute.For(); + this.MockDbCommand = Substitute.For().SetupCommands().CreateCommand(); + this.MockDbCommand.ClearSubstitute(); - this.MockDatabaseAdapter = Substitute.For(); - this.MockEntityManipulator = Substitute.For(); + this.MockDbCommand.CreateParameter().Returns(_ => Substitute.For()); - typeof(DbConnectionPlusConfiguration).GetMethod(nameof(DbConnectionPlusConfiguration.RegisterDatabaseAdapter))! - .MakeGenericMethod(this.MockDbConnection.GetType()) - .Invoke(DbConnectionPlusConfiguration.Instance, [this.MockDatabaseAdapter]); + var dbParameterCollection = new MockDbParameterCollection(); + this.MockDbCommand.Parameters.Returns(dbParameterCollection); - DbCommandFactory = this.MockCommandFactory; + this.MockDbDataReader = Substitute.For(); - this.MockDbCommand = this.MockDbConnection.CreateCommand(); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(this.MockDbDataReader); + this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(this.MockDbDataReader)); - this.MockCommandFactory - .CreateDbCommand( - this.MockDbConnection, - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any() - ) - .Returns(info => - { - // ReSharper disable once InlineTemporaryVariable - var command = this.MockDbCommand; - command.CommandText = info.ArgAt(1); - command.Transaction = info.ArgAt(2); - command.CommandTimeout = (Int32)(info.ArgAt(3)?.TotalSeconds ?? 30); - command.CommandType = info.ArgAt(4); - return command; - } - ); + this.MockDbConnection.CreateCommand().Returns(this.MockDbCommand); this.MockTemporaryTableBuilder = Substitute.For(); @@ -141,12 +119,22 @@ public UnitTestsBase() ); this.MockDatabaseAdapter.EntityManipulator.Returns(this.MockEntityManipulator); - } - /// - /// The mocked to use in tests. - /// - protected IDbCommandFactory MockCommandFactory { get; } + this.MockInterceptDbCommand = Substitute.For(); + + // Reset all settings to defaults before each test. + DbConnectionPlusConfiguration.Instance = new() + { + EnumSerializationMode = EnumSerializationMode.Strings, + InterceptDbCommand = this.MockInterceptDbCommand + }; + EntityHelper.ResetEntityTypeMetadataCache(); + OracleDatabaseAdapter.AllowTemporaryTables = false; + + typeof(DbConnectionPlusConfiguration).GetMethod(nameof(DbConnectionPlusConfiguration.RegisterDatabaseAdapter))! + .MakeGenericMethod(this.MockDbConnection.GetType()) + .Invoke(DbConnectionPlusConfiguration.Instance, [this.MockDatabaseAdapter]); + } /// /// The mocked to use in tests. @@ -163,11 +151,21 @@ public UnitTestsBase() /// protected DbConnection MockDbConnection { get; } + /// + /// The mocked to use in tests. + /// + protected DbDataReader MockDbDataReader { get; } + /// /// The mocked to use in tests. /// protected IEntityManipulator MockEntityManipulator { get; } + /// + /// The mocked delegate to use in tests. + /// + protected InterceptDbCommand MockInterceptDbCommand { get; } + /// /// The mocked to use in tests. /// From fec5b9010d902a6998c3ca481830a85f9e093f1d Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sun, 8 Feb 2026 18:48:16 +0100 Subject: [PATCH 13/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .github/workflows/documentation.yml | 23 +++- CHANGELOG.md | 2 +- ...DESIGN-DECISIONS.md => DESIGN-DECISIONS.md | 0 README.md | 128 ++++++++++-------- .../Benchmarks.Query_Dynamic.cs | 35 ++--- .../Benchmarks.Query_Entities.cs | 36 +---- .../BenchmarksConfig.cs | 3 + .../BenchmarksOrderer.cs | 24 +--- .../DbConnectionPlus.Benchmarks.csproj | 2 +- .../DbConnectionPlus.Benchmarks/Program.cs | 6 +- docs/docfx.json | 4 +- .../DbConnectionPlusConfiguration.cs | 12 +- .../DbCommands/DbCommandBuilder.cs | 83 +++++------- .../IInterpolatedSqlStatementFragment.cs | 2 +- .../SqlStatements/InterpolatedSqlStatement.cs | 8 +- src/DbConnectionPlus/SqlStatements/Literal.cs | 1 + .../SqlStatements/Parameter.cs | 1 + .../TestDatabase/ITestDatabaseProvider.cs | 4 +- .../DbConnectionPlusConfigurationTests.cs | 1 - .../StatementMethodTestsBase.cs | 3 +- .../UnitTestsBase.cs | 2 +- 21 files changed, 182 insertions(+), 198 deletions(-) rename docs/DESIGN-DECISIONS.md => DESIGN-DECISIONS.md (100%) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 954fb3a..724859a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -25,20 +25,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 - - name: Dotnet Setup - uses: actions/setup-dotnet@v3 + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.x + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release - - run: dotnet tool update -g docfx - - run: docfx docs/docfx.json + - name: Install docfx + run: dotnet tool update -g docfx + + - name: Run docfx + run: docfx docs/docfx.json - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload entire repository path: 'docs/_site' + - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6243047..3437d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ TODO: Update date ## [1.2.0] - 2026-02-XX ### Added -- Support for Optimistic Concurrency Support via Concurrency Tokens (Fixes [issue #5](https://github.com/rent-a-developer/DbConnectionPlus/issues/5)) +- Optimistic Concurrency Support via Concurrency Tokens (Fixes [issue #5](https://github.com/rent-a-developer/DbConnectionPlus/issues/5)) ### Changed - Switched benchmarks to SQLite for more stable results. diff --git a/docs/DESIGN-DECISIONS.md b/DESIGN-DECISIONS.md similarity index 100% rename from docs/DESIGN-DECISIONS.md rename to DESIGN-DECISIONS.md diff --git a/README.md b/README.md index de1247d..3be7fb3 100644 --- a/README.md +++ b/README.md @@ -1216,65 +1216,81 @@ BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.7623/24H2/2024Update/HudsonValle 12th Gen Intel Core i9-12900K 3.19GHz, 1 CPU, 24 logical and 16 physical cores .NET SDK 10.0.102 [Host] : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v3 - Job-ADQEJE : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v3 + Job-HTGOBL : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v3 + Job-CYDVOR : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v3 -MinIterationTime=100ms OutlierMode=DontRemove Server=True -InvocationCount=1 MaxIterationCount=20 UnrollFactor=1 -WarmupCount=10 +Server=True MaxIterationCount=20 ``` -| Method | Mean | Error | StdDev | Median | P90 | P95 | Ratio | RatioSD | Allocated | Alloc Ratio | -|----------------------------------------------- |-------------:|--------------:|--------------:|-------------:|-------------:|-------------:|-------------:|--------:|-----------:|------------:| -| **DeleteEntities_Manually** | **15,812.58 μs** | **3,269.782 μs** | **3,765.486 μs** | **15,096.66 μs** | **20,554.52 μs** | **23,427.08 μs** | **baseline** | **** | **19.32 KB** | **** | -| DeleteEntities_DbConnectionPlus | 26,344.72 μs | 7,115.300 μs | 8,193.990 μs | 24,756.73 μs | 30,942.44 μs | 44,994.48 μs | 1.75x slower | 0.65x | 19.69 KB | 1.02x more | -| | | | | | | | | | | | -| **DeleteEntity_Manually** | **187.61 μs** | **37.759 μs** | **43.483 μs** | **171.45 μs** | **271.18 μs** | **283.79 μs** | **baseline** | **** | **1.29 KB** | **** | -| DeleteEntity_DbConnectionPlus | 231.79 μs | 77.386 μs | 89.118 μs | 207.50 μs | 360.04 μs | 411.14 μs | 1.29x slower | 0.54x | 1.62 KB | 1.25x more | -| | | | | | | | | | | | -| **ExecuteNonQuery_Manually** | **227.38 μs** | **44.374 μs** | **51.101 μs** | **216.12 μs** | **283.89 μs** | **298.40 μs** | **baseline** | **** | **1.29 KB** | **** | -| ExecuteNonQuery_DbConnectionPlus | 215.31 μs | 52.704 μs | 60.694 μs | 188.86 μs | 291.85 μs | 297.67 μs | 1.13x faster | 0.39x | 2 KB | 1.55x more | -| | | | | | | | | | | | -| **ExecuteReader_Manually** | **249.64 μs** | **45.099 μs** | **51.936 μs** | **239.96 μs** | **319.38 μs** | **352.49 μs** | **baseline** | **** | **60.46 KB** | **** | -| ExecuteReader_DbConnectionPlus | 299.73 μs | 80.094 μs | 92.236 μs | 293.57 μs | 408.75 μs | 440.39 μs | 1.24x slower | 0.44x | 61.46 KB | 1.02x more | -| | | | | | | | | | | | -| **ExecuteScalar_Manually** | **77.82 μs** | **17.007 μs** | **19.586 μs** | **71.95 μs** | **101.45 μs** | **124.05 μs** | **baseline** | **** | **2.12 KB** | **** | -| ExecuteScalar_DbConnectionPlus | 97.83 μs | 7.468 μs | 8.600 μs | 97.02 μs | 108.74 μs | 111.83 μs | 1.32x slower | 0.28x | 2.85 KB | 1.35x more | -| | | | | | | | | | | | -| **Exists_Manually** | **61.17 μs** | **7.029 μs** | **8.095 μs** | **59.87 μs** | **70.26 μs** | **72.24 μs** | **baseline** | **** | **1.96 KB** | **** | -| Exists_DbConnectionPlus | 80.57 μs | 17.980 μs | 20.706 μs | 74.52 μs | 93.71 μs | 97.91 μs | 1.34x slower | 0.38x | 2.67 KB | 1.36x more | -| | | | | | | | | | | | -| **InsertEntities_Manually** | **37,252.98 μs** | **16,064.582 μs** | **18,499.997 μs** | **31,081.23 μs** | **45,555.75 μs** | **88,905.67 μs** | **baseline** | **** | **5726.4 KB** | **** | -| InsertEntities_DbConnectionPlus | 28,925.35 μs | 5,155.758 μs | 5,937.379 μs | 28,238.99 μs | 34,674.36 μs | 40,572.46 μs | 1.34x faster | 0.70x | 5760.81 KB | 1.01x more | -| | | | | | | | | | | | -| **InsertEntity_Manually** | **509.25 μs** | **137.899 μs** | **158.805 μs** | **480.71 μs** | **691.81 μs** | **716.65 μs** | **baseline** | **** | **61.42 KB** | **** | -| InsertEntity_DbConnectionPlus | 407.38 μs | 47.504 μs | 54.705 μs | 388.55 μs | 468.85 μs | 528.37 μs | 1.27x faster | 0.42x | 62.49 KB | 1.02x more | -| | | | | | | | | | | | -| **Parameter_Manually** | **104.04 μs** | **38.319 μs** | **44.128 μs** | **79.73 μs** | **177.14 μs** | **190.65 μs** | **baseline** | **** | **4.38 KB** | **** | -| Parameter_DbConnectionPlus | 110.40 μs | 43.333 μs | 49.902 μs | 106.35 μs | 155.04 μs | 163.53 μs | 1.21x slower | 0.66x | 6.31 KB | 1.44x more | -| | | | | | | | | | | | -| **Query_Dynamic_Manually** | **683.58 μs** | **172.616 μs** | **198.785 μs** | **656.92 μs** | **817.59 μs** | **846.18 μs** | **baseline** | **** | **215.27 KB** | **** | -| Query_Dynamic_DbConnectionPlus | 278.25 μs | 79.237 μs | 91.250 μs | 240.19 μs | 359.03 μs | 530.59 μs | 2.62x faster | 0.92x | 162.94 KB | 1.32x less | -| | | | | | | | | | | | -| **Query_Scalars_Manually** | **117.42 μs** | **12.106 μs** | **13.941 μs** | **119.76 μs** | **128.44 μs** | **135.42 μs** | **baseline** | **** | **1.07 KB** | **** | -| Query_Scalars_DbConnectionPlus | 116.26 μs | 23.145 μs | 26.653 μs | 118.95 μs | 146.82 μs | 155.42 μs | 1.00x slower | 0.26x | 6.24 KB | 5.85x more | -| | | | | | | | | | | | -| **Query_Entities_Manually** | **391.99 μs** | **32.586 μs** | **37.526 μs** | **383.52 μs** | **442.81 μs** | **459.92 μs** | **baseline** | **** | **61.92 KB** | **** | -| Query_Entities_DbConnectionPlus | 497.88 μs | 169.969 μs | 195.737 μs | 495.08 μs | 768.03 μs | 773.60 μs | 1.28x slower | 0.51x | 72.26 KB | 1.17x more | -| | | | | | | | | | | | -| **Query_ValueTuples_Manually** | **182.67 μs** | **40.380 μs** | **46.502 μs** | **169.54 μs** | **247.10 μs** | **263.69 μs** | **baseline** | **** | **16.73 KB** | **** | -| Query_ValueTuples_DbConnectionPlus | 183.50 μs | 34.047 μs | 39.209 μs | 182.46 μs | 228.38 μs | 246.76 μs | 1.06x slower | 0.33x | 28.67 KB | 1.71x more | -| | | | | | | | | | | | -| **TemporaryTable_ComplexObjects_Manually** | **9,132.97 μs** | **1,686.089 μs** | **1,941.703 μs** | **8,385.77 μs** | **11,329.97 μs** | **13,415.61 μs** | **baseline** | **** | **360 KB** | **** | -| TemporaryTable_ComplexObjects_DbConnectionPlus | 9,045.06 μs | 1,076.753 μs | 1,239.991 μs | 8,996.34 μs | 10,270.86 μs | 10,485.35 μs | 1.03x slower | 0.23x | 373.24 KB | 1.04x more | -| | | | | | | | | | | | -| **TemporaryTable_ScalarValues_Manually** | **7,537.41 μs** | **936.994 μs** | **1,079.044 μs** | **7,167.69 μs** | **8,462.54 μs** | **9,957.15 μs** | **baseline** | **** | **176.13 KB** | **** | -| TemporaryTable_ScalarValues_DbConnectionPlus | 5,852.64 μs | 836.596 μs | 963.425 μs | 5,843.33 μs | 7,027.70 μs | 7,382.98 μs | 1.32x faster | 0.29x | 303.31 KB | 1.72x more | -| | | | | | | | | | | | -| **UpdateEntities_Manually** | **40,746.85 μs** | **12,388.517 μs** | **14,266.635 μs** | **34,575.09 μs** | **59,380.97 μs** | **61,547.65 μs** | **baseline** | **** | **5708.12 KB** | **** | -| UpdateEntities_DbConnectionPlus | 33,742.35 μs | 10,522.508 μs | 12,117.736 μs | 29,627.05 μs | 38,988.11 μs | 43,043.74 μs | 1.29x faster | 0.51x | 5743.07 KB | 1.01x more | -| | | | | | | | | | | | -| **UpdateEntity_Manually** | **368.24 μs** | **53.938 μs** | **62.115 μs** | **346.09 μs** | **470.61 μs** | **487.82 μs** | **baseline** | **** | **61.61 KB** | **** | -| UpdateEntity_DbConnectionPlus | 397.23 μs | 36.233 μs | 41.726 μs | 394.08 μs | 435.77 μs | 452.75 μs | 1.10x slower | 0.20x | 62.65 KB | 1.02x more | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|----------------------------------------------- |---------------:|--------------:|--------------:|-------------:|--------:|--------:|-----------:|------------:| +| **DeleteEntities_DbCommand** | **223,628.340 μs** | **3,095.0138 μs** | **2,895.0778 μs** | **baseline** | **** | **-** | **1412000 B** | **** | +| DeleteEntities_Dapper | 218,830.227 μs | 3,503.0632 μs | 3,276.7675 μs | 1.02x faster | 0.02x | - | 2710584 B | 1.92x more | +| DeleteEntities_DbConnectionPlus | 225,066.323 μs | 3,714.7493 μs | 3,474.7788 μs | 1.01x slower | 0.02x | - | 2560160 B | 1.81x more | +| | | | | | | | | | +| **DeleteEntity_DbCommand** | **73.778 μs** | **0.6793 μs** | **0.6022 μs** | **baseline** | **** | **-** | **768 B** | **** | +| DeleteEntity_Dapper | 75.820 μs | 1.1366 μs | 1.0632 μs | 1.03x slower | 0.02x | - | 1704 B | 2.22x more | +| DeleteEntity_DbConnectionPlus | 75.308 μs | 0.9598 μs | 0.8014 μs | 1.02x slower | 0.01x | - | 1312 B | 1.71x more | +| | | | | | | | | | +| **ExecuteNonQuery_DbCommand** | **88.582 μs** | **19.6701 μs** | **22.6521 μs** | **baseline** | **** | **-** | **768 B** | **** | +| ExecuteNonQuery_Dapper | 72.316 μs | 0.9734 μs | 0.9106 μs | 1.23x faster | 0.31x | - | 1072 B | 1.40x more | +| ExecuteNonQuery_DbConnectionPlus | 71.852 μs | 0.9360 μs | 0.8755 μs | 1.23x faster | 0.31x | - | 1704 B | 2.22x more | +| | | | | | | | | | +| **ExecuteReader_DbCommand** | **650.394 μs** | **5.8340 μs** | **5.4571 μs** | **baseline** | **** | **0.9766** | **857945 B** | **** | +| ExecuteReader_Dapper | 649.543 μs | 6.7084 μs | 6.2750 μs | 1.00x faster | 0.01x | 0.9766 | 857417 B | 1.00x less | +| ExecuteReader_DbConnectionPlus | 657.459 μs | 6.0157 μs | 5.6271 μs | 1.01x slower | 0.01x | - | 859169 B | 1.00x more | +| | | | | | | | | | +| **ExecuteScalar_DbCommand** | **2.090 μs** | **0.0199 μs** | **0.0186 μs** | **baseline** | **** | **-** | **1072 B** | **** | +| ExecuteScalar_Dapper | 2.410 μs | 0.0239 μs | 0.0199 μs | 1.15x slower | 0.01x | - | 1464 B | 1.37x more | +| ExecuteScalar_DbConnectionPlus | 2.779 μs | 0.0356 μs | 0.0333 μs | 1.33x slower | 0.02x | 0.0038 | 2128 B | 1.99x more | +| | | | | | | | | | +| **Exists_DbCommand** | **1.824 μs** | **0.0183 μs** | **0.0172 μs** | **baseline** | **** | **-** | **1000 B** | **** | +| Exists_Dapper | 2.102 μs | 0.0165 μs | 0.0138 μs | 1.15x slower | 0.01x | - | 1336 B | 1.34x more | +| Exists_DbConnectionPlus | 2.457 μs | 0.0416 μs | 0.0389 μs | 1.35x slower | 0.02x | 0.0038 | 1944 B | 1.94x more | +| | | | | | | | | | +| **InsertEntities_DbCommand** | **5,739.618 μs** | **29.8202 μs** | **26.4348 μs** | **baseline** | **** | **15.6250** | **8586537 B** | **** | +| InsertEntities_Dapper | 6,495.939 μs | 58.2679 μs | 54.5038 μs | 1.13x slower | 0.01x | 15.6250 | 9602018 B | 1.12x more | +| InsertEntities_DbConnectionPlus | 6,518.117 μs | 56.0311 μs | 52.4115 μs | 1.14x slower | 0.01x | 15.6250 | 9468391 B | 1.10x more | +| | | | | | | | | | +| **InsertEntity_DbCommand** | **32.660 μs** | **0.4831 μs** | **0.4282 μs** | **baseline** | **** | **-** | **45267 B** | **** | +| InsertEntity_Dapper | 43.533 μs | 0.6281 μs | 0.5875 μs | 1.33x slower | 0.02x | - | 60446 B | 1.34x more | +| InsertEntity_DbConnectionPlus | 37.122 μs | 0.4297 μs | 0.3809 μs | 1.14x slower | 0.02x | - | 50039 B | 1.11x more | +| | | | | | | | | | +| **Parameter_DbCommand** | **2.672 μs** | **0.0394 μs** | **0.0350 μs** | **baseline** | **** | **0.0038** | **1864 B** | **** | +| Parameter_Dapper | 3.395 μs | 0.0375 μs | 0.0333 μs | 1.27x slower | 0.02x | - | 2984 B | 1.60x more | +| Parameter_DbConnectionPlus | 4.102 μs | 0.0802 μs | 0.0711 μs | 1.54x slower | 0.03x | 0.0076 | 4464 B | 2.39x more | +| | | | | | | | | | +| **Query_Dynamic_DbCommand** | **793.944 μs** | **6.6057 μs** | **6.1790 μs** | **baseline** | **** | **1.9531** | **975378 B** | **** | +| Query_Dynamic_Dapper | 246.576 μs | 3.5537 μs | 3.3241 μs | 3.22x faster | 0.05x | - | 91560 B | 10.65x less | +| Query_Dynamic_DbConnectionPlus | 323.036 μs | 3.4959 μs | 3.2701 μs | 2.46x faster | 0.03x | - | 149368 B | 6.53x less | +| | | | | | | | | | +| **Query_Entities_DbCommand** | **652.021 μs** | **5.8298 μs** | **5.1680 μs** | **baseline** | **** | **0.9766** | **858825 B** | **** | +| Query_Entities_Dapper | 287.951 μs | 4.6809 μs | 4.3785 μs | 2.26x faster | 0.04x | - | 98056 B | 8.76x less | +| Query_Entities_DbConnectionPlus | 307.558 μs | 3.6913 μs | 3.4528 μs | 2.12x faster | 0.03x | - | 99560 B | 8.63x less | +| | | | | | | | | | +| **Query_Scalars_DbCommand** | **80.531 μs** | **0.6016 μs** | **0.5333 μs** | **baseline** | **** | **-** | **17384 B** | **** | +| Query_Scalars_Dapper | 113.098 μs | 1.2660 μs | 1.1842 μs | 1.40x slower | 0.02x | - | 37072 B | 2.13x more | +| Query_Scalars_DbConnectionPlus | 111.844 μs | 1.3095 μs | 1.2249 μs | 1.39x slower | 0.02x | - | 32464 B | 1.87x more | +| | | | | | | | | | +| **Query_ValueTuples_DbCommand** | **120.574 μs** | **1.3583 μs** | **1.2041 μs** | **baseline** | **** | **-** | **49200 B** | **** | +| Query_ValueTuples_Dapper | 142.430 μs | 1.7777 μs | 1.6628 μs | 1.18x slower | 0.02x | - | 73584 B | 1.50x more | +| Query_ValueTuples_DbConnectionPlus | 145.854 μs | 2.0993 μs | 1.9637 μs | 1.21x slower | 0.02x | - | 58752 B | 1.19x more | +| | | | | | | | | | +| **TemporaryTable_ComplexObjects_DbCommand** | **9,029.496 μs** | **109.0053 μs** | **101.9636 μs** | **baseline** | **** | **-** | **12899278 B** | **** | +| TemporaryTable_ComplexObjects_Dapper | 8,120.420 μs | 106.3880 μs | 99.5154 μs | 1.11x faster | 0.02x | 15.6250 | 10968565 B | 1.18x less | +| TemporaryTable_ComplexObjects_DbConnectionPlus | 9,348.308 μs | 104.4113 μs | 97.6664 μs | 1.04x slower | 0.02x | 15.6250 | 12074972 B | 1.07x less | +| | | | | | | | | | +| **TemporaryTable_ScalarValues_DbCommand** | **5,080.907 μs** | **101.4218 μs** | **99.6098 μs** | **baseline** | **** | **-** | **1723968 B** | **** | +| TemporaryTable_ScalarValues_Dapper | 5,165.566 μs | 48.4921 μs | 45.3595 μs | 1.02x slower | 0.02x | - | 1764568 B | 1.02x more | +| TemporaryTable_ScalarValues_DbConnectionPlus | 6,644.777 μs | 125.8456 μs | 123.5973 μs | 1.31x slower | 0.03x | - | 2806416 B | 1.63x more | +| | | | | | | | | | +| **UpdateEntities_DbCommand** | **3,113.671 μs** | **29.0143 μs** | **27.1400 μs** | **baseline** | **** | **-** | **4325925 B** | **** | +| UpdateEntities_Dapper | 3,467.305 μs | 39.2788 μs | 36.7414 μs | 1.11x slower | 0.01x | 7.8125 | 4872590 B | 1.13x more | +| UpdateEntities_DbConnectionPlus | 3,602.616 μs | 46.1440 μs | 43.1631 μs | 1.16x slower | 0.02x | - | 4766990 B | 1.10x more | +| | | | | | | | | | +| **UpdateEntity_DbCommand** | **35.296 μs** | **0.2973 μs** | **0.2636 μs** | **baseline** | **** | **-** | **45469 B** | **** | +| UpdateEntity_Dapper | 40.460 μs | 0.8024 μs | 0.7113 μs | 1.15x slower | 0.02x | - | 54299 B | 1.19x more | +| UpdateEntity_DbConnectionPlus | 38.464 μs | 0.3082 μs | 0.2883 μs | 1.09x slower | 0.01x | - | 50254 B | 1.11x more | ### Running the benchmarks To run the benchmarks, run the following command: diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs index 9365069..7f56f23 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs @@ -49,26 +49,27 @@ public List Query_Dynamic_DbCommand() var charBuffer = new Char[1]; var ordinal = 0; - dynamic entity = new ExpandoObject(); + var entity = new ExpandoObject(); + IDictionary dictionary = entity; - entity.Id = dataReader.GetInt64(ordinal++); - entity.BooleanValue = dataReader.GetInt64(ordinal++) == 1; - entity.BytesValue = (Byte[])dataReader.GetValue(ordinal++); - entity.ByteValue = dataReader.GetByte(ordinal++); - entity.CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 + dictionary["Id"] = dataReader.GetInt64(ordinal++); + dictionary["BooleanValue"] = dataReader.GetInt64(ordinal++) == 1; + dictionary["BytesValue"] = (Byte[])dataReader.GetValue(ordinal++); + dictionary["ByteValue"] = dataReader.GetByte(ordinal++); + dictionary["CharValue"] = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new(); - entity.DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - entity.DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - entity.DoubleValue = dataReader.GetDouble(ordinal++); - entity.EnumValue = Enum.Parse(dataReader.GetString(ordinal++)); - entity.GuidValue = Guid.Parse(dataReader.GetString(ordinal++)); - entity.Int16Value = (Int16)dataReader.GetInt64(ordinal++); - entity.Int32Value = (Int32)dataReader.GetInt64(ordinal++); - entity.Int64Value = dataReader.GetInt64(ordinal++); - entity.SingleValue = dataReader.GetFloat(ordinal++); - entity.StringValue = dataReader.GetString(ordinal++); - entity.TimeSpanValue = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); + dictionary["DateTimeValue"] = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); + dictionary["DecimalValue"] = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); + dictionary["DoubleValue"] = dataReader.GetDouble(ordinal++); + dictionary["EnumValue"] = Enum.Parse(dataReader.GetString(ordinal++)); + dictionary["GuidValue"] = Guid.Parse(dataReader.GetString(ordinal++)); + dictionary["Int16Value"] = (Int16)dataReader.GetInt64(ordinal++); + dictionary["Int32Value"] = (Int32)dataReader.GetInt64(ordinal++); + dictionary["Int64Value"] = dataReader.GetInt64(ordinal++); + dictionary["SingleValue"] = dataReader.GetFloat(ordinal++); + dictionary["StringValue"] = dataReader.GetString(ordinal++); + dictionary["TimeSpanValue"] = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); entities.Add(entity); } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs index d66e9f6..4482fe4 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs @@ -32,12 +32,7 @@ public void Query_Entities__Setup() => [Benchmark(Baseline = false)] [BenchmarkCategory(Query_Entities_Category)] public List Query_Entities_Dapper() => - SqlMapper - .Query( - this.connection, - $"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}" - ) - .ToList(); + SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); [Benchmark(Baseline = true)] [BenchmarkCategory(Query_Entities_Category)] @@ -45,30 +40,7 @@ public List Query_Entities_DbCommand() { var entities = new List(); - using var dataReader = this.connection.ExecuteReader( - $""" - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - LIMIT {Query_Entities_EntitiesPerOperation} - """ - ); + using var dataReader = this.connection.ExecuteReader("SELECT * FROM Entity"); while (dataReader.Read()) { @@ -81,9 +53,7 @@ public List Query_Entities_DbCommand() [Benchmark(Baseline = false)] [BenchmarkCategory(Query_Entities_Category)] public List Query_Entities_DbConnectionPlus() => - this.connection - .Query($"SELECT * FROM Entity LIMIT {Query_Entities_EntitiesPerOperation}") - .ToList(); + this.connection.Query("SELECT * FROM Entity").ToList(); private const String Query_Entities_Category = "Query_Entities"; private const Int32 Query_Entities_EntitiesPerOperation = 100; diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs index b697ffc..4433a2e 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs @@ -14,10 +14,13 @@ public BenchmarksConfig() this.Orderer = new BenchmarksOrderer(); this.SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); + this.HideColumns("Job", "InvocationCount", "UnrollFactor"); + this.AddExporter(MarkdownExporter.Default); this.AddJob( Job.Default + .WithMaxIterationCount(20) // Since DbConnectionPlus will mostly be used in server applications, we test with server GC. .WithGcServer(true) ); diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs index 2db0da3..a340ffb 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs @@ -17,15 +17,9 @@ public IEnumerable GetExecutionOrder( IEnumerable? order = null ) => benchmarksCase - .OrderByDescending(a => - a.Descriptor.Baseline - ) - .ThenBy(a => - a.Descriptor.WorkloadMethod.DeclaringType! - .GetMethods() - .ToList() - .IndexOf(a.Descriptor.WorkloadMethod) - ); + .OrderBy(a => a.Descriptor.Categories[0]) + .ThenByDescending(a => a.Descriptor.Baseline) + .ThenBy(a => a.Descriptor.WorkloadMethod.Name); /// public String? GetHighlightGroupKey(BenchmarkCase benchmarkCase) => @@ -51,13 +45,7 @@ public IEnumerable GetSummaryOrder( Summary summary ) => benchmarksCases - .OrderByDescending(a => - a.Descriptor.Baseline - ) - .ThenBy(a => - a.Descriptor.WorkloadMethod.DeclaringType! - .GetMethods() - .ToList() - .IndexOf(a.Descriptor.WorkloadMethod) - ); + .OrderBy(a => a.Descriptor.Categories[0]) + .ThenByDescending(a => a.Descriptor.Baseline) + .ThenBy(a => a.Descriptor.WorkloadMethod.Name); } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj b/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj index b451269..4a23953 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj +++ b/benchmarks/DbConnectionPlus.Benchmarks/DbConnectionPlus.Benchmarks.csproj @@ -37,7 +37,7 @@ - + diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index 3d2df03..c34a972 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -1,11 +1,15 @@ +#pragma warning disable RCS1163, IDE0022 + using BenchmarkDotNet.Running; namespace RentADeveloper.DbConnectionPlus.Benchmarks; public static class Program { - public static void Main(String[] args) => + public static void Main(String[] args) + { BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) .Run(args); + } } diff --git a/docs/docfx.json b/docs/docfx.json index 1533139..f88415b 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -4,9 +4,9 @@ { "src": [ { - "src": "../src/DbConnectionPlus/", + "src": "../src/DbConnectionPlus", "files": [ - "**/*.csproj" + "**/bin/Release/net8.0/RentADeveloper.DbConnectionPlus.dll" ] } ], diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs index 05ec157..30352b3 100644 --- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -66,13 +66,13 @@ internal DbConnectionPlusConfiguration() /// public EnumSerializationMode EnumSerializationMode { - get => this.enumSerializationMode; + get; set { this.EnsureNotFrozen(); - this.enumSerializationMode = value; + field = value; } - } + } = EnumSerializationMode.Strings; /// /// A function that can be used to intercept database commands executed via DbConnectionPlus. @@ -84,11 +84,11 @@ public EnumSerializationMode EnumSerializationMode /// public InterceptDbCommand? InterceptDbCommand { - get => this.interceptDbCommand; + get; set { this.EnsureNotFrozen(); - this.interceptDbCommand = value; + field = value; } } @@ -195,7 +195,5 @@ private void EnsureNotFrozen() private readonly ConcurrentDictionary databaseAdapters = []; private readonly ConcurrentDictionary entityTypeBuilders = new(); - private EnumSerializationMode enumSerializationMode = EnumSerializationMode.Strings; - private InterceptDbCommand? interceptDbCommand; private Boolean isFrozen; } diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index 9529f38..6c666a6 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -186,8 +186,9 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( CancellationToken cancellationToken = default ) { - using var codeBuilder = new ValueStringBuilder(stackalloc Char[500]); - var parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + using var codeBuilder = new ValueStringBuilder(stackalloc char[2048]); + var parameterNameOccurrences = new Dictionary(StringComparer.OrdinalIgnoreCase); + var enumSerializationMode = DbConnectionPlusConfiguration.Instance.EnumSerializationMode; var command = connection.CreateCommand(); @@ -199,68 +200,63 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( command.CommandTimeout = (Int32)commandTimeout.Value.TotalSeconds; } + var dbParameters = command.Parameters; + foreach (var fragment in statement.Fragments) { switch (fragment) { - case Parameter parameter: + case Literal literal: + codeBuilder.Append(literal.Value); + break; + + case InterpolatedParameter interpolatedParameter: { - var parameterValue = parameter.Value; + var parameterName = interpolatedParameter.InferredName ?? + "Parameter_" + (parameterNameOccurrences.Count + 1); + + if (parameterNameOccurrences.TryGetValue(parameterName, out var count)) + { + count++; + parameterNameOccurrences[parameterName] = count; + parameterName += count; + } + else + { + parameterNameOccurrences[parameterName] = 1; + } + + var parameterValue = interpolatedParameter.Value; if (parameterValue is Enum enumValue) { - parameterValue = EnumSerializer.SerializeEnum( - enumValue, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ); + parameterValue = EnumSerializer.SerializeEnum(enumValue, enumSerializationMode); } var dbParameter = command.CreateParameter(); - dbParameter.ParameterName = parameter.Name; + dbParameter.ParameterName = parameterName; databaseAdapter.BindParameterValue(dbParameter, parameterValue); - command.Parameters.Add(dbParameter); + dbParameters.Add(dbParameter); - parameterNames.Add(parameter.Name); + codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName)); break; } - case InterpolatedParameter interpolatedParameter: + case Parameter parameter: { - var parameterName = interpolatedParameter.InferredName ?? - "Parameter_" + (parameterNames.Count + 1); - - if (parameterNames.Contains(parameterName)) - { - var suffix = 2; - String candidate; - - do - { - candidate = parameterName + suffix; - suffix++; - } while (parameterNames.Contains(candidate)); - - parameterName = candidate; - } - - var parameterValue = interpolatedParameter.Value; + var parameterValue = parameter.Value; if (parameterValue is Enum enumValue) { - parameterValue = EnumSerializer.SerializeEnum( - enumValue, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ); + parameterValue = EnumSerializer.SerializeEnum(enumValue, enumSerializationMode); } var dbParameter = command.CreateParameter(); - dbParameter.ParameterName = parameterName; + dbParameter.ParameterName = parameter.Name; databaseAdapter.BindParameterValue(dbParameter, parameterValue); - command.Parameters.Add(dbParameter); - - parameterNames.Add(parameterName); + dbParameters.Add(dbParameter); - codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName)); + parameterNameOccurrences[parameter.Name] = 1; break; } @@ -269,19 +265,12 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( databaseAdapter.QuoteTemporaryTableName(interpolatedTemporaryTable.Name, connection) ); break; - - case Literal literal: - codeBuilder.Append(literal.Value); - break; } } command.CommandText = codeBuilder.ToString(); - var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( - command, - cancellationToken - ); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); return (command, cancellationTokenRegistration); } diff --git a/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs b/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs index 89c0497..5700758 100644 --- a/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs +++ b/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs @@ -6,4 +6,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// /// Represents a fragment of an SQL statement created from an interpolated string. /// -internal interface IInterpolatedSqlStatementFragment; +internal interface IInterpolatedSqlStatementFragment; \ No newline at end of file diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs index 9c24c8d..a64d33b 100644 --- a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs +++ b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs @@ -220,8 +220,8 @@ public override String ToString() { switch (fragment) { - case Parameter parameter: - parameters.Add(parameter.Name, parameter.Value); + case Literal literal: + stringBuilder.Append(literal.Value); break; case InterpolatedParameter interpolatedParameter: @@ -258,8 +258,8 @@ public override String ToString() interpolatedTemporaryTables.Add(interpolatedTemporaryTable); break; - case Literal literal: - stringBuilder.Append(literal.Value); + case Parameter parameter: + parameters.Add(parameter.Name, parameter.Value); break; } } diff --git a/src/DbConnectionPlus/SqlStatements/Literal.cs b/src/DbConnectionPlus/SqlStatements/Literal.cs index efe5c87..e266da0 100644 --- a/src/DbConnectionPlus/SqlStatements/Literal.cs +++ b/src/DbConnectionPlus/SqlStatements/Literal.cs @@ -8,3 +8,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// /// The literal string. internal readonly record struct Literal(String Value) : IInterpolatedSqlStatementFragment; + diff --git a/src/DbConnectionPlus/SqlStatements/Parameter.cs b/src/DbConnectionPlus/SqlStatements/Parameter.cs index 208feb4..445e8c0 100644 --- a/src/DbConnectionPlus/SqlStatements/Parameter.cs +++ b/src/DbConnectionPlus/SqlStatements/Parameter.cs @@ -9,3 +9,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// The name of the parameter. /// The value of the parameter. internal readonly record struct Parameter(String Name, Object? Value) : IInterpolatedSqlStatementFragment; + diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs index b441f78..91010a5 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs @@ -114,7 +114,9 @@ DbConnection connection /// /// Gets a literal representing a data type in the test database system that is not supported by DbConnectionPlus. /// - /// + /// + /// A literal representing a data type in the test database system that is not supported by DbConnectionPlus. + /// public String GetUnsupportedDataTypeLiteral(); /// diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index bb9cbf6..5ec0abf 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -1,7 +1,6 @@ using Microsoft.Data.Sqlite; using MySqlConnector; using Npgsql; -using NSubstitute.DbConnection; using Oracle.ManagedDataAccess.Client; using RentADeveloper.DbConnectionPlus.DatabaseAdapters; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; diff --git a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs index ea3b51b..dfe46b2 100644 --- a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs @@ -1,4 +1,5 @@ -using NSubstitute.DbConnection; +// ReSharper disable AccessToDisposedClosure + using RentADeveloper.DbConnectionPlus.SqlStatements; namespace RentADeveloper.DbConnectionPlus.UnitTests; diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index 8370bc4..d1dd551 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -1,4 +1,4 @@ -#pragma warning disable NS1004 +#pragma warning disable NS1004, NS1000 using System.Globalization; using NSubstitute.ClearExtensions; From fc804c13a18e1f215d11a68407d077f718d27bc7 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Mon, 9 Feb 2026 00:15:12 +0100 Subject: [PATCH 14/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- DbConnectionPlus.slnx | 2 +- .../Benchmarks.ExecuteNonQuery.cs | 54 +++++------------- .../Benchmarks.ExecuteScalar.cs | 7 ++- .../Benchmarks.Exists.cs | 11 +++- .../Benchmarks.Query_Entities.cs | 5 +- .../DbConnectionPlusConfiguration.cs | 28 ++++++---- .../MySql/MySqlDatabaseAdapter.cs | 10 ++-- .../Oracle/OracleDatabaseAdapter.cs | 20 +++---- .../PostgreSql/PostgreSqlDatabaseAdapter.cs | 10 ++-- .../SqlServer/SqlServerDatabaseAdapter.cs | 10 ++-- .../Sqlite/SqliteDatabaseAdapter.cs | 10 ++-- .../DbCommands/DbCommandBuilder.cs | 20 +------ .../DbCommands/DbCommandDisposer.cs | 2 +- .../DbConnectionExtensions.Configuration.cs | 15 +++-- .../DbConnectionExtensions.ExecuteReader.cs | 18 ++---- .../DbConnectionExtensions.Exists.cs | 4 +- .../DbConnectionExtensions.Parameter.cs | 2 +- .../DbConnectionExtensions.QueryFirst.cs | 4 +- .../DbConnectionExtensions.QueryFirstOfT.cs | 4 +- ...onnectionExtensions.QueryFirstOrDefault.cs | 4 +- ...ectionExtensions.QueryFirstOrDefaultOfT.cs | 4 +- src/DbConnectionPlus/Helpers/NameHelper.cs | 56 ++++++++----------- ...=> CommandDisposingDataReaderDecorator.cs} | 37 +++++------- ...mmandDisposingDataReaderDecoratorTests.cs} | 32 ++++++----- ...mmandDisposingDataReaderDecoratorTests.cs} | 51 ++++++++++------- 25 files changed, 197 insertions(+), 223 deletions(-) rename src/DbConnectionPlus/Readers/{DisposeSignalingDataReaderDecorator.cs => CommandDisposingDataReaderDecorator.cs} (91%) rename tests/DbConnectionPlus.IntegrationTests/Readers/{DisposeSignalingDataReaderDecoratorTests.cs => CommandDisposingDataReaderDecoratorTests.cs} (79%) rename tests/DbConnectionPlus.UnitTests/Readers/{DisposeSignalingDataReaderDecoratorTests.cs => CommandDisposingDataReaderDecoratorTests.cs} (61%) diff --git a/DbConnectionPlus.slnx b/DbConnectionPlus.slnx index bffd51f..8327e94 100644 --- a/DbConnectionPlus.slnx +++ b/DbConnectionPlus.slnx @@ -4,7 +4,7 @@ - + diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs index 7f80332..949966c 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs @@ -7,7 +7,7 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks; public partial class Benchmarks { - [IterationCleanup( + [GlobalCleanup( Targets = [ nameof(ExecuteNonQuery_DbCommand), @@ -18,7 +18,7 @@ public partial class Benchmarks public void ExecuteNonQuery__Cleanup() => this.connection.Dispose(); - [IterationSetup( + [GlobalSetup( Targets = [ nameof(ExecuteNonQuery_DbCommand), @@ -27,55 +27,29 @@ public void ExecuteNonQuery__Cleanup() => ] )] public void ExecuteNonQuery__Setup() => - this.SetupDatabase(ExecuteNonQuery_OperationsPerInvoke); + this.SetupDatabase(0); - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [Benchmark(Baseline = false)] [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_Dapper() - { - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - SqlMapper.Execute(this.connection, "DELETE FROM Entity WHERE Id = @Id", new { entity.Id }); + public void ExecuteNonQuery_Dapper() => + SqlMapper.Execute(this.connection, "DELETE FROM Entity WHERE Id = @Id", new { Id = -1 }); - this.entitiesInDb.Remove(entity); - } - } - - [Benchmark(Baseline = true, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [Benchmark(Baseline = true)] [BenchmarkCategory(ExecuteNonQuery_Category)] public void ExecuteNonQuery_DbCommand() { - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - using var command = this.connection.CreateCommand(); + using var command = this.connection.CreateCommand(); - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entity.Id)); + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; + command.Parameters.Add(new("@Id", -1)); - command.ExecuteNonQuery(); - - this.entitiesInDb.Remove(entity); - } + command.ExecuteNonQuery(); } - [Benchmark(Baseline = false, OperationsPerInvoke = ExecuteNonQuery_OperationsPerInvoke)] + [Benchmark(Baseline = false)] [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_DbConnectionPlus() - { - for (var i = 0; i < ExecuteNonQuery_OperationsPerInvoke; i++) - { - var entity = this.entitiesInDb[0]; - - this.connection.ExecuteNonQuery($"DELETE FROM Entity WHERE Id = {Parameter(entity.Id)}"); - - this.entitiesInDb.Remove(entity); - } - } + public void ExecuteNonQuery_DbConnectionPlus() => + this.connection.ExecuteNonQuery($"DELETE FROM Entity WHERE Id = {Parameter(-1)}"); private const String ExecuteNonQuery_Category = "ExecuteNonQuery"; - private const Int32 ExecuteNonQuery_OperationsPerInvoke = 7700; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs index d6bf6d6..6a78758 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs @@ -51,7 +51,12 @@ public String ExecuteScalar_DbCommand() using var command = this.connection.CreateCommand(); command.CommandText = "SELECT StringValue FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entity.Id)); + + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "@Id"; + idParameter.Value = entity.Id; + + command.Parameters.Add(idParameter); return (String)command.ExecuteScalar()!; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs index 137104e..604ff65 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs @@ -52,11 +52,16 @@ public Boolean Exists_DbCommand() using var command = this.connection.CreateCommand(); command.CommandText = "SELECT 1 FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entityId)); - using var dataReader = command.ExecuteReader(); + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "@Id"; + idParameter.Value = entityId; - return dataReader.HasRows; + command.Parameters.Add(idParameter); + + using var dataReader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); + + return dataReader.Read(); } [Benchmark(Baseline = false)] diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs index 4482fe4..a94eb1d 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs @@ -40,7 +40,10 @@ public List Query_Entities_DbCommand() { var entities = new List(); - using var dataReader = this.connection.ExecuteReader("SELECT * FROM Entity"); + using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT * FROM Entity"; + + using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs index 30352b3..fa62c32 100644 --- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -20,11 +20,11 @@ public sealed class DbConnectionPlusConfiguration : IFreezable /// 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()); + this.databaseAdapters.Add(typeof(MySqlConnection), new MySqlDatabaseAdapter()); + this.databaseAdapters.Add(typeof(OracleConnection), new OracleDatabaseAdapter()); + this.databaseAdapters.Add(typeof(NpgsqlConnection), new PostgreSqlDatabaseAdapter()); + this.databaseAdapters.Add(typeof(SqliteConnection), new SqliteDatabaseAdapter()); + this.databaseAdapters.Add(typeof(SqlConnection), new SqlServerDatabaseAdapter()); } /// @@ -104,10 +104,14 @@ public EntityTypeBuilder Entity() { this.EnsureNotFrozen(); - return (EntityTypeBuilder)this.entityTypeBuilders.GetOrAdd( - typeof(TEntity), - _ => new EntityTypeBuilder() - ); + if (!this.entityTypeBuilders.TryGetValue(typeof(TEntity), out var builder)) + { + builder = new EntityTypeBuilder(); + + this.entityTypeBuilders.Add(typeof(TEntity), builder); + } + + return (EntityTypeBuilder)builder; } /// @@ -127,7 +131,7 @@ public void RegisterDatabaseAdapter(IDatabaseAdapter adapter) { ArgumentNullException.ThrowIfNull(adapter); - this.databaseAdapters.AddOrUpdate(typeof(TConnection), adapter, (_, _) => adapter); + this.databaseAdapters[typeof(TConnection)] = adapter; } /// @@ -193,7 +197,7 @@ private void EnsureNotFrozen() } } - private readonly ConcurrentDictionary databaseAdapters = []; - private readonly ConcurrentDictionary entityTypeBuilders = new(); + private readonly Dictionary databaseAdapters = []; + private readonly Dictionary entityTypeBuilders = new(); private Boolean isFrozen; } diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs index e88a131..40b3106 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlDatabaseAdapter.cs @@ -33,6 +33,11 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { + case DateTime: + parameter.DbType = DbType.DateTime; + parameter.Value = value; + break; + case Enum enumValue: parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { @@ -54,11 +59,6 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case DateTime: - parameter.DbType = DbType.DateTime; - parameter.Value = value; - break; - case Byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs index 77677d7..4f02f89 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleDatabaseAdapter.cs @@ -44,6 +44,16 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { + case DateTime: + parameter.DbType = DbType.DateTime; + parameter.Value = value; + break; + + case Guid guid: + parameter.DbType = DbType.Binary; + parameter.Value = guid; + break; + case Enum enumValue: parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { @@ -65,16 +75,6 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case Guid guid: - parameter.DbType = DbType.Binary; - parameter.Value = guid; - break; - - case DateTime: - parameter.DbType = DbType.DateTime; - parameter.Value = value; - break; - case Byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs index 275adaa..6bce060 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapter.cs @@ -34,6 +34,11 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { + case DateTime: + parameter.DbType = DbType.DateTime2; + parameter.Value = value; + break; + case Enum enumValue: parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { @@ -55,11 +60,6 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case DateTime: - parameter.DbType = DbType.DateTime2; - parameter.Value = value; - break; - case Byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs index 554a70c..0c2182f 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs @@ -33,6 +33,11 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { + case DateTime: + parameter.DbType = DbType.DateTime2; + parameter.Value = value; + break; + case Enum enumValue: parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { @@ -54,11 +59,6 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case DateTime: - parameter.DbType = DbType.DateTime2; - parameter.Value = value; - break; - case Byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs index 3341b13..dc3a6f1 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteDatabaseAdapter.cs @@ -33,6 +33,11 @@ public void BindParameterValue(DbParameter parameter, Object? value) switch (value) { + case DateTime: + parameter.DbType = DbType.DateTime; + parameter.Value = value; + break; + case Enum enumValue: parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { @@ -54,11 +59,6 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case DateTime: - parameter.DbType = DbType.DateTime; - parameter.Value = value; - break; - case Byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index 6c666a6..d976abc 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -188,7 +188,6 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( { using var codeBuilder = new ValueStringBuilder(stackalloc char[2048]); var parameterNameOccurrences = new Dictionary(StringComparer.OrdinalIgnoreCase); - var enumSerializationMode = DbConnectionPlusConfiguration.Instance.EnumSerializationMode; var command = connection.CreateCommand(); @@ -217,6 +216,7 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( if (parameterNameOccurrences.TryGetValue(parameterName, out var count)) { + // Parameter name is already used, so we append a suffix to make it unique. count++; parameterNameOccurrences[parameterName] = count; parameterName += count; @@ -226,16 +226,9 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( parameterNameOccurrences[parameterName] = 1; } - var parameterValue = interpolatedParameter.Value; - - if (parameterValue is Enum enumValue) - { - parameterValue = EnumSerializer.SerializeEnum(enumValue, enumSerializationMode); - } - var dbParameter = command.CreateParameter(); dbParameter.ParameterName = parameterName; - databaseAdapter.BindParameterValue(dbParameter, parameterValue); + databaseAdapter.BindParameterValue(dbParameter, interpolatedParameter.Value); dbParameters.Add(dbParameter); codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName)); @@ -244,16 +237,9 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( case Parameter parameter: { - var parameterValue = parameter.Value; - - if (parameterValue is Enum enumValue) - { - parameterValue = EnumSerializer.SerializeEnum(enumValue, enumSerializationMode); - } - var dbParameter = command.CreateParameter(); dbParameter.ParameterName = parameter.Name; - databaseAdapter.BindParameterValue(dbParameter, parameterValue); + databaseAdapter.BindParameterValue(dbParameter, parameter.Value); dbParameters.Add(dbParameter); parameterNameOccurrences[parameter.Name] = 1; diff --git a/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs b/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs index 8e905ee..4056000 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs @@ -8,7 +8,7 @@ namespace RentADeveloper.DbConnectionPlus.DbCommands; /// When disposed, disposes the command, any temporary tables created for the command, and the cancellation token /// registration associated with the command. /// -internal sealed class DbCommandDisposer : IDisposable, IAsyncDisposable +internal class DbCommandDisposer : IDisposable, IAsyncDisposable { /// /// Initializes a new instance of the class. diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs index 91e7718..e35a0a6 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs @@ -19,13 +19,16 @@ public static void Configure(Action configureActi { ArgumentNullException.ThrowIfNull(configureAction); - configureAction(DbConnectionPlusConfiguration.Instance); + lock (configurationLockObject) + { + configureAction(DbConnectionPlusConfiguration.Instance); - ((IFreezable)DbConnectionPlusConfiguration.Instance).Freeze(); + ((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(); + // 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(); + } } /// @@ -38,4 +41,6 @@ internal static void OnBeforeExecutingCommand( IReadOnlyList temporaryTables ) => DbConnectionPlusConfiguration.Instance.InterceptDbCommand?.Invoke(command, temporaryTables); + + private static readonly Object configurationLockObject = new(); } diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs index 4e8cc70..9a3af8e 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs @@ -79,18 +79,12 @@ public static DbDataReader ExecuteReader( OnBeforeExecutingCommand(command, statement.TemporaryTables); dataReader = command.ExecuteReader(commandBehavior); - var disposeSignalingDecorator = new DisposeSignalingDataReaderDecorator( + return new CommandDisposingDataReaderDecorator( dataReader, databaseAdapter, + commandDisposer, cancellationToken ); - - // ReSharper disable AccessToDisposedClosure - disposeSignalingDecorator.OnDisposing = () => commandDisposer.Dispose(); - disposeSignalingDecorator.OnDisposingAsync = () => commandDisposer.DisposeAsync(); - // ReSharper restore AccessToDisposedClosure - - return disposeSignalingDecorator; } catch (Exception exception) when ( databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) @@ -174,16 +168,12 @@ public static async Task ExecuteReaderAsync( OnBeforeExecutingCommand(command, statement.TemporaryTables); dataReader = await command.ExecuteReaderAsync(commandBehavior, cancellationToken).ConfigureAwait(false); - var disposeSignalingDecorator = new DisposeSignalingDataReaderDecorator( + return new CommandDisposingDataReaderDecorator( dataReader, databaseAdapter, + commandDisposer, cancellationToken ); - - disposeSignalingDecorator.OnDisposing = () => commandDisposer.Dispose(); - disposeSignalingDecorator.OnDisposingAsync = () => commandDisposer.DisposeAsync(); - - return disposeSignalingDecorator; } catch (Exception exception) when ( databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs index 5d6cbb2..7b729d8 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs @@ -75,7 +75,7 @@ public static Boolean Exists( try { OnBeforeExecutingCommand(command, statement.TemporaryTables); - using var reader = command.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult); + using var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); return reader.Read(); } catch (Exception exception) when ( @@ -155,7 +155,7 @@ public static async Task ExistsAsync( OnBeforeExecutingCommand(command, statement.TemporaryTables); #pragma warning disable CA2007 await using var reader = await command.ExecuteReaderAsync( - CommandBehavior.SingleRow | CommandBehavior.SingleResult, + CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken ).ConfigureAwait(false); #pragma warning restore CA2007 diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs index d886c8b..941681c 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs @@ -73,7 +73,7 @@ public static InterpolatedParameter Parameter( { String? inferredParameterName = null; - if (!String.IsNullOrWhiteSpace(parameterValueExpression)) + if (parameterValueExpression?.Length > 0) { var nameFromCallerArgumentExpression = NameHelper.CreateNameFromCallerArgumentExpression( parameterValueExpression, diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs index 0e349a8..db42d58 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs @@ -74,7 +74,7 @@ public static dynamic QueryFirst( try { OnBeforeExecutingCommand(command, statement.TemporaryTables); - var reader = command.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult); + var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); using (reader) { @@ -160,7 +160,7 @@ public static async Task QueryFirstAsync( { OnBeforeExecutingCommand(command, statement.TemporaryTables); var reader = await command.ExecuteReaderAsync( - CommandBehavior.SingleRow | CommandBehavior.SingleResult, + CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken ) .ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs index 22118ef..619b5ab 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs @@ -205,7 +205,7 @@ public static T QueryFirst( try { OnBeforeExecutingCommand(command, statement.TemporaryTables); - var reader = command.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult); + var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); using (reader) { @@ -453,7 +453,7 @@ public static async Task QueryFirstAsync( { OnBeforeExecutingCommand(command, statement.TemporaryTables); var reader = await command.ExecuteReaderAsync( - CommandBehavior.SingleRow | CommandBehavior.SingleResult, + CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken ) .ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs index 08b6d3d..26c0a90 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs @@ -77,7 +77,7 @@ public static partial class DbConnectionExtensions try { OnBeforeExecutingCommand(command, statement.TemporaryTables); - var reader = command.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult); + var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); using (reader) { @@ -165,7 +165,7 @@ public static partial class DbConnectionExtensions { OnBeforeExecutingCommand(command, statement.TemporaryTables); var reader = await command.ExecuteReaderAsync( - CommandBehavior.SingleRow | CommandBehavior.SingleResult, + CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken ) .ConfigureAwait(false); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs index 30cadc3..7585a09 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs @@ -205,7 +205,7 @@ public static partial class DbConnectionExtensions try { OnBeforeExecutingCommand(command, statement.TemporaryTables); - var reader = command.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult); + var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow); using (reader) { @@ -457,7 +457,7 @@ public static partial class DbConnectionExtensions { OnBeforeExecutingCommand(command, statement.TemporaryTables); var reader = await command.ExecuteReaderAsync( - CommandBehavior.SingleRow | CommandBehavior.SingleResult, + CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken ) .ConfigureAwait(false); diff --git a/src/DbConnectionPlus/Helpers/NameHelper.cs b/src/DbConnectionPlus/Helpers/NameHelper.cs index cbca45e..ce28b3c 100644 --- a/src/DbConnectionPlus/Helpers/NameHelper.cs +++ b/src/DbConnectionPlus/Helpers/NameHelper.cs @@ -1,6 +1,8 @@ // Copyright (c) 2026 David Liebeherr // Licensed under the MIT License. See LICENSE.md in the project root for more information. +using System.Runtime.InteropServices; + namespace RentADeveloper.DbConnectionPlus.Helpers; /// @@ -24,63 +26,53 @@ internal static class NameHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan expression, Int32 maximumLength) { - // Remove "this.": - if ( - expression.Length >= 5 && - expression[0] == 't' && expression[1] == 'h' && - expression[2] == 'i' && expression[3] == 's' && - expression[4] == '.' - ) + // Remove common prefixes that are not relevant for the name. + + if (expression.StartsWith("this.", StringComparison.Ordinal)) { expression = expression[5..]; } - // Remove "new": - if ( - expression.Length >= 3 && - expression[0] == 'n' && expression[1] == 'e' && expression[2] == 'w' - ) + if (expression.StartsWith("new", StringComparison.Ordinal)) { expression = expression[3..]; } - // Remove "Get": - if ( - expression.Length >= 3 && - expression[0] == 'G' && expression[1] == 'e' && expression[2] == 't' - ) + if (expression.StartsWith("Get", StringComparison.Ordinal)) { expression = expression[3..]; } - var length = Math.Min(expression.Length, maximumLength); + var scanLength = Math.Min(expression.Length, maximumLength); - var buffer = length <= 512 ? stackalloc Char[length] : new Char[length]; - - var count = 0; + var buffer = scanLength <= 512 ? stackalloc Char[scanLength] : new Char[scanLength]; - for (var i = 0; i < expression.Length && count < maximumLength; i++) - { - var c = expression[i]; + ref var src = ref MemoryMarshal.GetReference(expression); + ref var dst = ref MemoryMarshal.GetReference(buffer); - // Only allow letters, digits, and underscores in the name. + var count = 0; + + for (var i = 0; i < scanLength; i++) + { + var character = Unsafe.Add(ref src, i); + if ( - (UInt32)(c - 'A') <= 'Z' - 'A' || // Uppercase letters - (UInt32)(c - 'a') <= 'z' - 'a' || // Lowercase letters - (UInt32)(c - '0') <= '9' - '0' || // Digits - c == '_' // Underscores + (UInt32)(character - '0') <= 9 || // Digits + (UInt32)(character - 'A') <= 25 || // Uppercase letters + (UInt32)(character - 'a') <= 25 || // Lowercase letters + character == '_' ) { - buffer[count++] = c; + Unsafe.Add(ref dst, count++) = character; } } // Convert the first character to uppercase if it is a lowercase letter. - if (count > 0 && (UInt32)(buffer[0] - 'a') <= 'z' - 'a') + if (count != 0 && (UInt32)(buffer[0] - 'a') <= 25) { buffer[0] = (Char)(buffer[0] - 32); } - return new(buffer[..count]); + return new(buffer.Slice(0, count)); } } diff --git a/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs similarity index 91% rename from src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs rename to src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs index 419ad41..7186210 100644 --- a/src/DbConnectionPlus/Readers/DisposeSignalingDataReaderDecorator.cs +++ b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs @@ -2,22 +2,26 @@ // Licensed under the MIT License. See LICENSE.md in the project root for more information. using System.Collections.ObjectModel; +using RentADeveloper.DbConnectionPlus.DbCommands; namespace RentADeveloper.DbConnectionPlus.Readers; /// -/// A decorator for a that signals when it is being disposed and handles the case when a -/// read operation is cancelled by a . +/// A decorator for a that disposes the associated when disposed +/// and handles the case when a read operation is cancelled by a . /// -internal sealed class DisposeSignalingDataReaderDecorator : DbDataReader +internal sealed class CommandDisposingDataReaderDecorator : DbDataReader { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The to decorate. /// /// The database adapter for the database for which was obtained. /// + /// + /// The that is responsible for disposing the associated . + /// /// /// The that is associated with the from which the /// to decorate was obtained. @@ -36,17 +40,20 @@ internal sealed class DisposeSignalingDataReaderDecorator : DbDataReader /// /// /// - public DisposeSignalingDataReaderDecorator( + public CommandDisposingDataReaderDecorator( DbDataReader dataReader, IDatabaseAdapter databaseAdapter, + DbCommandDisposer commandDisposer, CancellationToken commandCancellationToken ) { ArgumentNullException.ThrowIfNull(dataReader); ArgumentNullException.ThrowIfNull(databaseAdapter); + ArgumentNullException.ThrowIfNull(commandDisposer); this.dataReader = dataReader; this.databaseAdapter = databaseAdapter; + this.commandDisposer = commandDisposer; this.commandCancellationToken = commandCancellationToken; } @@ -94,11 +101,7 @@ public override async ValueTask DisposeAsync() await base.DisposeAsync().ConfigureAwait(false); await this.dataReader.DisposeAsync().ConfigureAwait(false); - - if (this.OnDisposingAsync is not null) - { - await this.OnDisposingAsync().ConfigureAwait(false); - } + await this.commandDisposer.DisposeAsync().ConfigureAwait(false); } /// @@ -314,16 +317,6 @@ public override async Task ReadAsync(CancellationToken cancellationToke public override String? ToString() => this.dataReader.ToString(); - /// - /// A function that is invoked when this instance is being disposed synchronously. - /// - internal Action? OnDisposing { get; set; } - - /// - /// A function that is invoked when this instance is being disposed asynchronously. - /// - internal Func? OnDisposingAsync { get; set; } - /// protected override void Dispose(Boolean disposing) { @@ -339,13 +332,13 @@ protected override void Dispose(Boolean disposing) if (disposing) { this.dataReader.Dispose(); - - this.OnDisposing?.Invoke(); + this.commandDisposer.Dispose(); } } private readonly CancellationToken commandCancellationToken; private readonly IDatabaseAdapter databaseAdapter; + private readonly DbCommandDisposer commandDisposer; private readonly DbDataReader dataReader; private Boolean isDisposed; } diff --git a/tests/DbConnectionPlus.IntegrationTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs similarity index 79% rename from tests/DbConnectionPlus.IntegrationTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs rename to tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs index 3890133..27336a1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs @@ -3,27 +3,27 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.Readers; public sealed class - DisposeSignalingDataReaderDecoratorTests_MySql : - DisposeSignalingDataReaderDecoratorTests; + CommandDisposingDataReaderDecoratorTests_MySql : + CommandDisposingDataReaderDecoratorTests; public sealed class - DisposeSignalingDataReaderDecoratorTests_Oracle : - DisposeSignalingDataReaderDecoratorTests; + CommandDisposingDataReaderDecoratorTests_Oracle : + CommandDisposingDataReaderDecoratorTests; public sealed class - DisposeSignalingDataReaderDecoratorTests_PostgreSql : - DisposeSignalingDataReaderDecoratorTests; + CommandDisposingDataReaderDecoratorTests_PostgreSql : + CommandDisposingDataReaderDecoratorTests; public sealed class - DisposeSignalingDataReaderDecoratorTests_Sqlite : - DisposeSignalingDataReaderDecoratorTests; + CommandDisposingDataReaderDecoratorTests_Sqlite : + CommandDisposingDataReaderDecoratorTests; public sealed class - DisposeSignalingDataReaderDecoratorTests_SqlServer : - DisposeSignalingDataReaderDecoratorTests; + CommandDisposingDataReaderDecoratorTests_SqlServer : + CommandDisposingDataReaderDecoratorTests; public abstract class - DisposeSignalingDataReaderDecoratorTests : IntegrationTestsBase + CommandDisposingDataReaderDecoratorTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] @@ -51,11 +51,14 @@ public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanc } ); + var commandDisposer = new DbCommandDisposer(command, [], default); + using var decoratedReader = command.ExecuteReader(); - using var decorator = new DisposeSignalingDataReaderDecorator( + using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, + commandDisposer, cancellationToken ); @@ -95,11 +98,14 @@ public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOp } ); + var commandDisposer = new DbCommandDisposer(command, [], default); + await using var decoratedReader = await command.ExecuteReaderAsync(TestContext.Current.CancellationToken); - await using var decorator = new DisposeSignalingDataReaderDecorator( + await using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, + commandDisposer, cancellationToken ); diff --git a/tests/DbConnectionPlus.UnitTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs similarity index 61% rename from tests/DbConnectionPlus.UnitTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs rename to tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs index 608c92a..0ed80bc 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/DisposeSignalingDataReaderDecoratorTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs @@ -1,39 +1,47 @@ +#pragma warning disable NS1001 + using AutoFixture; using AutoFixture.AutoNSubstitute; +using RentADeveloper.DbConnectionPlus.DatabaseAdapters; +using RentADeveloper.DbConnectionPlus.DbCommands; using RentADeveloper.DbConnectionPlus.Readers; using RentADeveloper.DbConnectionPlus.UnitTests.Assertions; namespace RentADeveloper.DbConnectionPlus.UnitTests.Readers; -public class DisposeSignalingDataReaderDecoratorTests : UnitTestsBase +public class CommandDisposingDataReaderDecoratorTests : UnitTestsBase { /// - public DisposeSignalingDataReaderDecoratorTests() + public CommandDisposingDataReaderDecoratorTests() { this.decoratedReader = Substitute.For(); - this.decorator = new(this.decoratedReader, this.MockDatabaseAdapter, CancellationToken.None); + this.commandDisposer = Substitute.For( + Substitute.For(), + Array.Empty(), + default(CancellationTokenRegistration) + ); + this.decorator = new( + this.decoratedReader, + this.MockDatabaseAdapter, + this.commandDisposer, + CancellationToken.None + ); } [Fact] - public void Dispose_ShouldInvokeOnDisposingFunction() + public void Dispose_ShouldDisposeCommandDisposer() { - var onDisposingFunction = Substitute.For(); - this.decorator.OnDisposing = onDisposingFunction; - this.decorator.Dispose(); - onDisposingFunction.Received()(); + this.commandDisposer.Received().Dispose(); } [Fact] - public async Task DisposeAsync_ShouldInvokeOnDisposingAsyncFunction() + public async Task DisposeAsync_ShouldDisposeCommandDisposer() { - var onDisposingAsyncFunction = Substitute.For>(); - this.decorator.OnDisposingAsync = onDisposingAsyncFunction; - await this.decorator.DisposeAsync(); - await onDisposingAsyncFunction.Received()(); + await this.commandDisposer.Received().DisposeAsync(); } [Fact] @@ -70,11 +78,11 @@ public void ShouldForwardAllMethodCallsToDecoratedReader() { var exceptions = new HashSet { - nameof(DisposeSignalingDataReaderDecorator.Dispose), - nameof(DisposeSignalingDataReaderDecorator.DisposeAsync), - nameof(DisposeSignalingDataReaderDecorator.GetData), - nameof(DisposeSignalingDataReaderDecorator.GetFieldValue), - nameof(DisposeSignalingDataReaderDecorator.GetFieldValueAsync) + nameof(CommandDisposingDataReaderDecorator.Dispose), + nameof(CommandDisposingDataReaderDecorator.DisposeAsync), + nameof(CommandDisposingDataReaderDecorator.GetData), + nameof(CommandDisposingDataReaderDecorator.GetFieldValue), + nameof(CommandDisposingDataReaderDecorator.GetFieldValueAsync) }; var fixture = new Fixture(); @@ -92,13 +100,16 @@ public void ShouldForwardAllMethodCallsToDecoratedReader() [Fact] public void ShouldGuardAgainstNullArguments() => ArgumentNullGuardVerifier.Verify(() => - new DisposeSignalingDataReaderDecorator( + new CommandDisposingDataReaderDecorator( this.decoratedReader, this.MockDatabaseAdapter, + this.commandDisposer, CancellationToken.None ) ); + private readonly DbCommandDisposer commandDisposer; + private readonly DbDataReader decoratedReader; - private readonly DisposeSignalingDataReaderDecorator decorator; + private readonly CommandDisposingDataReaderDecorator decorator; } From 5d7befcd7f2d0be9df19a03cb429918fd8191dce Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Tue, 10 Feb 2026 21:58:31 +0100 Subject: [PATCH 15/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- README.md | 4 +- .../Benchmarks.DeleteEntities.cs | 50 ++- .../Benchmarks.DeleteEntity.cs | 32 +- .../Benchmarks.ExecuteNonQuery.cs | 24 +- .../Benchmarks.ExecuteReader.cs | 47 +-- .../Benchmarks.ExecuteScalar.cs | 32 +- .../Benchmarks.Exists.cs | 36 +- .../Benchmarks.InsertEntities.cs | 162 ++------- .../Benchmarks.InsertEntity.cs | 68 ++-- .../Benchmarks.Parameter.cs | 49 ++- .../Benchmarks.Query_Dynamic.cs | 61 ++-- .../Benchmarks.Query_Entities.cs | 23 +- .../Benchmarks.Query_Scalars.cs | 31 +- .../Benchmarks.Query_ValueTuples.cs | 35 +- ...enchmarks.TemporaryTable_ComplexObjects.cs | 327 +++--------------- .../Benchmarks.TemporaryTable_ScalarValues.cs | 88 ++--- .../Benchmarks.UpdateEntities.cs | 181 +++------- .../Benchmarks.UpdateEntity.cs | 99 +++--- .../DbConnectionPlus.Benchmarks/Benchmarks.cs | 24 ++ .../DbConnectionPlus.Benchmarks/Program.cs | 21 +- .../DbCommands/DbCommandBuilder.cs | 7 +- src/DbConnectionPlus/Helpers/NameHelper.cs | 2 +- .../DbCommands/DbCommandBuilderTests.cs | 8 +- .../Helpers/NameHelperTests.cs | 1 + .../InterpolatedSqlStatementTests.cs | 8 +- 25 files changed, 507 insertions(+), 913 deletions(-) diff --git a/README.md b/README.md index 3be7fb3..6beae60 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ This prevents SQL injection and keeps the SQL readable. > `RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle.OracleDatabaseAdapter.AllowTemporaryTables` to `true`. > [!NOTE] -> **Note for MySQL users** +> **Note for MySQL users** > The temporary tables feature of DbConnectionPlus uses `MySqlBulkCopy` to populate temporary tables. > Therefore, the option `AllowLoadLocalInfile=true` must be set in the connection string and the server side > option `local_infile` must be enabled (e.g. via the statement `SET GLOBAL local_infile=1`). @@ -378,7 +378,7 @@ DbConnectionExtensions.Configure(config => ``` > [!NOTE] -> `DbConnectionExtensions.Configure` can only be called once. +> To prevent multi-threading issues `DbConnectionExtensions.Configure` can only be called once during the application lifetime. > After it has been called the configuration of DbConnectionPlus is frozen and cannot be changed anymore. #### EnumSerializationMode diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs index 2d78458..ecba7a0 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [IterationCleanup( Targets = [ - nameof(DeleteEntities_DbCommand), + nameof(DeleteEntities_Command), nameof(DeleteEntities_Dapper), nameof(DeleteEntities_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void DeleteEntities__Cleanup() => [IterationSetup( Targets = [ - nameof(DeleteEntities_DbCommand), + nameof(DeleteEntities_Command), nameof(DeleteEntities_Dapper), nameof(DeleteEntities_DbConnectionPlus) ] @@ -29,44 +29,43 @@ public void DeleteEntities__Cleanup() => public void DeleteEntities__Setup() => this.SetupDatabase(DeleteEntities_EntitiesPerOperation * DeleteEntities_OperationsPerInvoke); - [Benchmark(Baseline = false)] + [Benchmark(Baseline = true)] [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_Dapper() + public void DeleteEntities_Command() { for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { - var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); + using var command = this.connection.CreateCommand(); + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - SqlMapperExtensions.Delete(this.connection, entities); + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "@Id"; + command.Parameters.Add(idParameter); + + var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); foreach (var entity in entities) { - this.entitiesInDb.Remove(entity); + idParameter.Value = entity.Id; + + command.ExecuteNonQuery(); } + + this.entitiesInDb.RemoveRange(0, DeleteEntities_EntitiesPerOperation); } } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = false)] [BenchmarkCategory(DeleteEntities_Category)] - public void DeleteEntities_DbCommand() + public void DeleteEntities_Dapper() { for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { - using var command = this.connection.CreateCommand(); - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - - var idParameter = command.CreateParameter(); - idParameter.ParameterName = "@Id"; - command.Parameters.Add(idParameter); - - foreach (var entity in this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList()) - { - idParameter.Value = entity.Id; + var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); - command.ExecuteNonQuery(); + SqlMapperExtensions.Delete(this.connection, entities); - this.entitiesInDb.Remove(entity); - } + this.entitiesInDb.RemoveRange(0, DeleteEntities_EntitiesPerOperation); } } @@ -74,16 +73,13 @@ public void DeleteEntities_DbCommand() [BenchmarkCategory(DeleteEntities_Category)] public void DeleteEntities_DbConnectionPlus() { - for (int i = 0; i < DeleteEntities_EntitiesPerOperation; i++) + for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); this.connection.DeleteEntities(entities); - foreach (var entity in entities) - { - this.entitiesInDb.Remove(entity); - } + this.entitiesInDb.RemoveRange(0, DeleteEntities_EntitiesPerOperation); } } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs index 8f3d818..1907490 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [IterationCleanup( Targets = [ - nameof(DeleteEntity_DbCommand), + nameof(DeleteEntity_Command), nameof(DeleteEntity_Dapper), nameof(DeleteEntity_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void DeleteEntity__Cleanup() => [IterationSetup( Targets = [ - nameof(DeleteEntity_DbCommand), + nameof(DeleteEntity_Command), nameof(DeleteEntity_Dapper), nameof(DeleteEntity_DbConnectionPlus) ] @@ -29,34 +29,40 @@ public void DeleteEntity__Cleanup() => public void DeleteEntity__Setup() => this.SetupDatabase(DeleteEntity_OperationsPerInvoke); - [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_Dapper() + public void DeleteEntity_Command() { for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) { var entityToDelete = this.entitiesInDb[0]; - SqlMapperExtensions.Delete(this.connection, entityToDelete); + using var command = this.connection.CreateCommand(); + + command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; + + var idParameter = command.CreateParameter(); + + idParameter.ParameterName = "@Id"; + idParameter.Value = entityToDelete.Id; + + command.Parameters.Add(idParameter); + + command.ExecuteNonQuery(); this.entitiesInDb.Remove(entityToDelete); } } - [Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] + [Benchmark(Baseline = false, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)] [BenchmarkCategory(DeleteEntity_Category)] - public void DeleteEntity_DbCommand() + public void DeleteEntity_Dapper() { for (var i = 0; i < DeleteEntity_OperationsPerInvoke; i++) { var entityToDelete = this.entitiesInDb[0]; - using var command = this.connection.CreateCommand(); - - command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", entityToDelete.Id)); - - command.ExecuteNonQuery(); + SqlMapperExtensions.Delete(this.connection, entityToDelete); this.entitiesInDb.Remove(entityToDelete); } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs index 949966c..9dfba35 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(ExecuteNonQuery_DbCommand), + nameof(ExecuteNonQuery_Command), nameof(ExecuteNonQuery_Dapper), nameof(ExecuteNonQuery_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void ExecuteNonQuery__Cleanup() => [GlobalSetup( Targets = [ - nameof(ExecuteNonQuery_DbCommand), + nameof(ExecuteNonQuery_Command), nameof(ExecuteNonQuery_Dapper), nameof(ExecuteNonQuery_DbConnectionPlus) ] @@ -29,23 +29,29 @@ public void ExecuteNonQuery__Cleanup() => public void ExecuteNonQuery__Setup() => this.SetupDatabase(0); - [Benchmark(Baseline = false)] - [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_Dapper() => - SqlMapper.Execute(this.connection, "DELETE FROM Entity WHERE Id = @Id", new { Id = -1 }); - [Benchmark(Baseline = true)] [BenchmarkCategory(ExecuteNonQuery_Category)] - public void ExecuteNonQuery_DbCommand() + public void ExecuteNonQuery_Command() { using var command = this.connection.CreateCommand(); command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; - command.Parameters.Add(new("@Id", -1)); + + var idParameter = command.CreateParameter(); + + idParameter.ParameterName = "@Id"; + idParameter.Value = -1; + + command.Parameters.Add(idParameter); command.ExecuteNonQuery(); } + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteNonQuery_Category)] + public void ExecuteNonQuery_Dapper() => + SqlMapper.Execute(this.connection, "DELETE FROM Entity WHERE Id = @Id", new { Id = -1 }); + [Benchmark(Baseline = false)] [BenchmarkCategory(ExecuteNonQuery_Category)] public void ExecuteNonQuery_DbConnectionPlus() => diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs index 0cd94dc..2d1db3c 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(ExecuteReader_DbCommand), + nameof(ExecuteReader_Command), nameof(ExecuteReader_Dapper), nameof(ExecuteReader_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void ExecuteReader__Cleanup() => [GlobalSetup( Targets = [ - nameof(ExecuteReader_DbCommand), + nameof(ExecuteReader_Command), nameof(ExecuteReader_Dapper), nameof(ExecuteReader_DbConnectionPlus) ] @@ -29,13 +29,17 @@ public void ExecuteReader__Cleanup() => public void ExecuteReader__Setup() => this.SetupDatabase(100); - [Benchmark(Baseline = false)] + [Benchmark(Baseline = true)] [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_Dapper() + public List ExecuteReader_Command() { var result = new List(); - using var dataReader = SqlMapper.ExecuteReader(this.connection, ExecuteReaderSql); + using var command = this.connection.CreateCommand(); + + command.CommandText = "SELECT * FROM Entity"; + + using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { @@ -45,16 +49,13 @@ public List ExecuteReader_Dapper() return result; } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = false)] [BenchmarkCategory(ExecuteReader_Category)] - public List ExecuteReader_DbCommand() + public List ExecuteReader_Dapper() { var result = new List(); - using var command = this.connection.CreateCommand(); - command.CommandText = ExecuteReaderSql; - - using var dataReader = command.ExecuteReader(); + using var dataReader = SqlMapper.ExecuteReader(this.connection, "SELECT * FROM Entity"); while (dataReader.Read()) { @@ -70,7 +71,7 @@ public List ExecuteReader_DbConnectionPlus() { var result = new List(); - using var dataReader = this.connection.ExecuteReader(ExecuteReaderSql); + using var dataReader = this.connection.ExecuteReader("SELECT * FROM Entity"); while (dataReader.Read()) { @@ -81,26 +82,4 @@ public List ExecuteReader_DbConnectionPlus() } private const String ExecuteReader_Category = "ExecuteReader"; - - private const String ExecuteReaderSql = """ - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - Entity - """; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs index 6a78758..b56cc05 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(ExecuteScalar_DbCommand), + nameof(ExecuteScalar_Command), nameof(ExecuteScalar_Dapper), nameof(ExecuteScalar_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void ExecuteScalar__Cleanup() => [GlobalSetup( Targets = [ - nameof(ExecuteScalar_DbCommand), + nameof(ExecuteScalar_Command), nameof(ExecuteScalar_Dapper), nameof(ExecuteScalar_DbConnectionPlus) ] @@ -29,22 +29,9 @@ public void ExecuteScalar__Cleanup() => public void ExecuteScalar__Setup() => this.SetupDatabase(1); - [Benchmark(Baseline = false)] - [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_Dapper() - { - var entity = this.entitiesInDb[0]; - - return SqlMapper.ExecuteScalar( - this.connection, - "SELECT StringValue FROM Entity WHERE Id = @Id", - new { entity.Id } - )!; - } - [Benchmark(Baseline = true)] [BenchmarkCategory(ExecuteScalar_Category)] - public String ExecuteScalar_DbCommand() + public String ExecuteScalar_Command() { var entity = this.entitiesInDb[0]; @@ -61,6 +48,19 @@ public String ExecuteScalar_DbCommand() return (String)command.ExecuteScalar()!; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(ExecuteScalar_Category)] + public String ExecuteScalar_Dapper() + { + var entity = this.entitiesInDb[0]; + + return SqlMapper.ExecuteScalar( + this.connection, + "SELECT StringValue FROM Entity WHERE Id = @Id", + new { entity.Id } + )!; + } + [Benchmark(Baseline = false)] [BenchmarkCategory(ExecuteScalar_Category)] public String ExecuteScalar_DbConnectionPlus() diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs index 604ff65..65e6233 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Exists_DbCommand), + nameof(Exists_Command), nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void Exists__Cleanup() => [GlobalSetup( Targets = [ - nameof(Exists_DbCommand), + nameof(Exists_Command), nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus) ] @@ -29,24 +29,9 @@ public void Exists__Cleanup() => public void Exists__Setup() => this.SetupDatabase(1); - [Benchmark(Baseline = false)] - [BenchmarkCategory(Exists_Category)] - public Boolean Exists_Dapper() - { - var entityId = this.entitiesInDb[0].Id; - - using var dataReader = SqlMapper.ExecuteReader( - this.connection, - "SELECT 1 FROM Entity WHERE Id = @Id", - new { Id = entityId } - ); - - return dataReader.Read(); - } - [Benchmark(Baseline = true)] [BenchmarkCategory(Exists_Category)] - public Boolean Exists_DbCommand() + public Boolean Exists_Command() { var entityId = this.entitiesInDb[0].Id; @@ -64,6 +49,21 @@ public Boolean Exists_DbCommand() return dataReader.Read(); } + [Benchmark(Baseline = false)] + [BenchmarkCategory(Exists_Category)] + public Boolean Exists_Dapper() + { + var entityId = this.entitiesInDb[0].Id; + + using var dataReader = SqlMapper.ExecuteReader( + this.connection, + "SELECT 1 FROM Entity WHERE Id = @Id", + new { Id = entityId } + ); + + return dataReader.Read(); + } + [Benchmark(Baseline = false)] [BenchmarkCategory(Exists_Category)] public Boolean Exists_DbConnectionPlus() diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs index a12bf62..86d4e9b 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(InsertEntities_DbCommand), + nameof(InsertEntities_Command), nameof(InsertEntities_Dapper), nameof(InsertEntities_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void InsertEntities__Cleanup() => [GlobalSetup( Targets = [ - nameof(InsertEntities_DbCommand), + nameof(InsertEntities_Command), nameof(InsertEntities_Dapper), nameof(InsertEntities_DbConnectionPlus) ] @@ -29,139 +29,39 @@ public void InsertEntities__Cleanup() => public void InsertEntities__Setup() => this.SetupDatabase(0); - [Benchmark(Baseline = false)] - [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_Dapper() - { - var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); - - SqlMapperExtensions.Insert(this.connection, entitiesToInsert); - } - [Benchmark(Baseline = true)] [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_DbCommand() + public void InsertEntities_Command() { - var entities = Generate.Multiple(InsertEntities_EntitiesPerOperation); - using var command = this.connection.CreateCommand(); - command.CommandText = InsertEntitySql; - - var idParameter = new SqliteParameter - { - ParameterName = "@Id" - }; - - var booleanValueParameter = new SqliteParameter - { - ParameterName = "@BooleanValue" - }; - - var bytesValueParameter = new SqliteParameter - { - ParameterName = "@BytesValue" - }; - - var byteValueParameter = new SqliteParameter - { - ParameterName = "@ByteValue" - }; - - var charValueParameter = new SqliteParameter - { - ParameterName = "@CharValue" - }; - - var dateTimeValueParameter = new SqliteParameter - { - ParameterName = "@DateTimeValue" - }; - - var decimalValueParameter = new SqliteParameter - { - ParameterName = "@DecimalValue" - }; - var doubleValueParameter = new SqliteParameter - { - ParameterName = "@DoubleValue" - }; - - var enumValueParameter = new SqliteParameter - { - ParameterName = "@EnumValue" - }; - - var guidValueParameter = new SqliteParameter - { - ParameterName = "@GuidValue" - }; - - var int16ValueParameter = new SqliteParameter - { - ParameterName = "@Int16Value" - }; - - var int32ValueParameter = new SqliteParameter - { - ParameterName = "@Int32Value" - }; - - var int64ValueParameter = new SqliteParameter - { - ParameterName = "@Int64Value" - }; - - var singleValueParameter = new SqliteParameter - { - ParameterName = "@SingleValue" - }; - - var stringValueParameter = new SqliteParameter - { - ParameterName = "@StringValue" - }; + command.CommandText = InsertEntitySql; - var timeSpanValueParameter = new SqliteParameter + var parameters = new Dictionary { - ParameterName = "@TimeSpanValue" + { "Id", new("Id", null) }, + { "BooleanValue", new("BooleanValue", null) }, + { "BytesValue", new("BytesValue", null) }, + { "ByteValue", new("ByteValue", null) }, + { "CharValue", new("CharValue", null) }, + { "DateTimeValue", new("DateTimeValue", null) }, + { "DecimalValue", new("DecimalValue", null) }, + { "DoubleValue", new("DoubleValue", null) }, + { "EnumValue", new("EnumValue", null) }, + { "GuidValue", new("GuidValue", null) }, + { "Int16Value", new("Int16Value", null) }, + { "Int32Value", new("Int32Value", null) }, + { "Int64Value", new("Int64Value", null) }, + { "SingleValue", new("SingleValue", null) }, + { "StringValue", new("StringValue", null) }, + { "TimeSpanValue", new("TimeSpanValue", null) } }; - command.Parameters.Add(idParameter); - command.Parameters.Add(booleanValueParameter); - command.Parameters.Add(bytesValueParameter); - command.Parameters.Add(byteValueParameter); - command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateTimeValueParameter); - command.Parameters.Add(decimalValueParameter); - command.Parameters.Add(doubleValueParameter); - command.Parameters.Add(enumValueParameter); - command.Parameters.Add(guidValueParameter); - command.Parameters.Add(int16ValueParameter); - command.Parameters.Add(int32ValueParameter); - command.Parameters.Add(int64ValueParameter); - command.Parameters.Add(singleValueParameter); - command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeSpanValueParameter); + command.Parameters.AddRange(parameters.Values); - foreach (var entity in entities) + foreach (var entity in this.insertEntities_entitiesToInsert) { - idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = entity.BytesValue; - byteValueParameter.Value = entity.ByteValue; - charValueParameter.Value = entity.CharValue; - dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = entity.DoubleValue; - enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue.ToString(); - int16ValueParameter.Value = entity.Int16Value; - int32ValueParameter.Value = entity.Int32Value; - int64ValueParameter.Value = entity.Int64Value; - singleValueParameter.Value = entity.SingleValue; - stringValueParameter.Value = entity.StringValue; - timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + PopulateEntityParameters(entity, parameters); command.ExecuteNonQuery(); } @@ -169,12 +69,16 @@ public void InsertEntities_DbCommand() [Benchmark(Baseline = false)] [BenchmarkCategory(InsertEntities_Category)] - public void InsertEntities_DbConnectionPlus() - { - var entitiesToInsert = Generate.Multiple(InsertEntities_EntitiesPerOperation); + public void InsertEntities_Dapper() => + SqlMapperExtensions.Insert(this.connection, this.insertEntities_entitiesToInsert); - this.connection.InsertEntities(entitiesToInsert); - } + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntities_Category)] + public void InsertEntities_DbConnectionPlus() => + this.connection.InsertEntities(this.insertEntities_entitiesToInsert); + + private readonly List insertEntities_entitiesToInsert = + Generate.Multiple(InsertEntities_EntitiesPerOperation); private const String InsertEntities_Category = "InsertEntities"; private const Int32 InsertEntities_EntitiesPerOperation = 200; diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs index 31a7d97..3eaee1b 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(InsertEntity_DbCommand), + nameof(InsertEntity_Command), nameof(InsertEntity_Dapper), nameof(InsertEntity_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void InsertEntity__Cleanup() => [GlobalSetup( Targets = [ - nameof(InsertEntity_DbCommand), + nameof(InsertEntity_Command), nameof(InsertEntity_Dapper), nameof(InsertEntity_DbConnectionPlus) ] @@ -29,53 +29,51 @@ public void InsertEntity__Cleanup() => public void InsertEntity__Setup() => this.SetupDatabase(0); - [Benchmark(Baseline = false)] - [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_Dapper() - { - var entity = Generate.Single(); - - SqlMapperExtensions.Insert(this.connection, entity); - } - [Benchmark(Baseline = true)] [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_DbCommand() + public void InsertEntity_Command() { - var entity = Generate.Single(); - using var command = this.connection.CreateCommand(); command.CommandText = InsertEntitySql; - command.Parameters.Add(new("@Id", entity.Id)); - command.Parameters.Add(new("@BooleanValue", entity.BooleanValue ? 1 : 0)); - command.Parameters.Add(new("@BytesValue", entity.BytesValue)); - command.Parameters.Add(new("@ByteValue", entity.ByteValue)); - command.Parameters.Add(new("@CharValue", entity.CharValue)); - command.Parameters.Add(new("@DateTimeValue", entity.DateTimeValue.ToString(CultureInfo.InvariantCulture))); - command.Parameters.Add(new("@DecimalValue", entity.DecimalValue)); - command.Parameters.Add(new("@DoubleValue", entity.DoubleValue)); - command.Parameters.Add(new("@EnumValue", entity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", entity.GuidValue.ToString())); - command.Parameters.Add(new("@Int16Value", entity.Int16Value)); - command.Parameters.Add(new("@Int32Value", entity.Int32Value)); - command.Parameters.Add(new("@Int64Value", entity.Int64Value)); - command.Parameters.Add(new("@SingleValue", entity.SingleValue)); - command.Parameters.Add(new("@StringValue", entity.StringValue)); - command.Parameters.Add(new("@TimeSpanValue", entity.TimeSpanValue.ToString())); + var parameters = new Dictionary + { + { "Id", new("Id", null) }, + { "BooleanValue", new("BooleanValue", null) }, + { "BytesValue", new("BytesValue", null) }, + { "ByteValue", new("ByteValue", null) }, + { "CharValue", new("CharValue", null) }, + { "DateTimeValue", new("DateTimeValue", null) }, + { "DecimalValue", new("DecimalValue", null) }, + { "DoubleValue", new("DoubleValue", null) }, + { "EnumValue", new("EnumValue", null) }, + { "GuidValue", new("GuidValue", null) }, + { "Int16Value", new("Int16Value", null) }, + { "Int32Value", new("Int32Value", null) }, + { "Int64Value", new("Int64Value", null) }, + { "SingleValue", new("SingleValue", null) }, + { "StringValue", new("StringValue", null) }, + { "TimeSpanValue", new("TimeSpanValue", null) } + }; + + command.Parameters.AddRange(parameters.Values); + + PopulateEntityParameters(this.insertEntity_entityToInsert, parameters); command.ExecuteNonQuery(); } [Benchmark(Baseline = false)] [BenchmarkCategory(InsertEntity_Category)] - public void InsertEntity_DbConnectionPlus() - { - var entity = Generate.Single(); + public void InsertEntity_Dapper() => + SqlMapperExtensions.Insert(this.connection, this.insertEntity_entityToInsert); - this.connection.InsertEntity(entity); - } + [Benchmark(Baseline = false)] + [BenchmarkCategory(InsertEntity_Category)] + public void InsertEntity_DbConnectionPlus() => + this.connection.InsertEntity(this.insertEntity_entityToInsert); + private readonly BenchmarkEntity insertEntity_entityToInsert = Generate.Single(); private const String InsertEntity_Category = "InsertEntity"; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs index 19852e0..2b89dc9 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Parameter_DbCommand), + nameof(Parameter_Command), nameof(Parameter_Dapper), nameof(Parameter_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void Parameter__Cleanup() => [GlobalSetup( Targets = [ - nameof(Parameter_DbCommand), + nameof(Parameter_Command), nameof(Parameter_Dapper), nameof(Parameter_DbConnectionPlus) ] @@ -29,24 +29,23 @@ public void Parameter__Cleanup() => public void Parameter__Setup() => this.SetupDatabase(0); - [Benchmark(Baseline = false)] + [Benchmark(Baseline = true)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_Dapper() + public Object Parameter_Command() { var result = new Int32[5]; - using var dataReader = SqlMapper.ExecuteReader( - this.connection, - "SELECT @P1, @P2, @P3, @P4, @P5", - new - { - P1 = 1, - P2 = 2, - P3 = 3, - P4 = 4, - P5 = 5 - } - ); + using var command = this.connection.CreateCommand(); + + command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; + + command.Parameters.Add(new("@P1", 1)); + command.Parameters.Add(new("@P2", 2)); + command.Parameters.Add(new("@P3", 3)); + command.Parameters.Add(new("@P4", 4)); + command.Parameters.Add(new("@P5", 5)); + + using var dataReader = command.ExecuteReader(); dataReader.Read(); @@ -59,21 +58,17 @@ public Object Parameter_Dapper() return result; } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = false)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_DbCommand() + public Object Parameter_Dapper() { var result = new Int32[5]; - using var command = this.connection.CreateCommand(); - command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; - command.Parameters.Add(new("@P1", 1)); - command.Parameters.Add(new("@P2", 2)); - command.Parameters.Add(new("@P3", 3)); - command.Parameters.Add(new("@P4", 4)); - command.Parameters.Add(new("@P5", 5)); - - using var dataReader = command.ExecuteReader(); + using var dataReader = SqlMapper.ExecuteReader( + this.connection, + "SELECT @P1, @P2, @P3, @P4, @P5", + new { P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5 } + ); dataReader.Read(); diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs index 7f56f23..d0c17e6 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs @@ -3,7 +3,7 @@ #pragma warning disable RCS1196 -using System.Dynamic; +using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow; namespace RentADeveloper.DbConnectionPlus.Benchmarks; @@ -12,7 +12,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Query_Dynamic_DbCommand), + nameof(Query_Dynamic_Command), nameof(Query_Dynamic_Dapper), nameof(Query_Dynamic_DbConnectionPlus) ] @@ -23,7 +23,7 @@ public void Query_Dynamic__Cleanup() => [GlobalSetup( Targets = [ - nameof(Query_Dynamic_DbCommand), + nameof(Query_Dynamic_Command), nameof(Query_Dynamic_Dapper), nameof(Query_Dynamic_DbConnectionPlus) ] @@ -31,14 +31,9 @@ public void Query_Dynamic__Cleanup() => public void Query_Dynamic__Setup() => this.SetupDatabase(Query_Dynamic_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_Dapper() => - SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); - [Benchmark(Baseline = true)] [BenchmarkCategory(Query_Dynamic_Category)] - public List Query_Dynamic_DbCommand() + public List Query_Dynamic_Command() { var entities = new List(); @@ -49,34 +44,40 @@ public List Query_Dynamic_DbCommand() var charBuffer = new Char[1]; var ordinal = 0; - var entity = new ExpandoObject(); - IDictionary dictionary = entity; - dictionary["Id"] = dataReader.GetInt64(ordinal++); - dictionary["BooleanValue"] = dataReader.GetInt64(ordinal++) == 1; - dictionary["BytesValue"] = (Byte[])dataReader.GetValue(ordinal++); - dictionary["ByteValue"] = dataReader.GetByte(ordinal++); - dictionary["CharValue"] = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 - ? charBuffer[0] - : throw new(); - dictionary["DateTimeValue"] = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - dictionary["DecimalValue"] = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture); - dictionary["DoubleValue"] = dataReader.GetDouble(ordinal++); - dictionary["EnumValue"] = Enum.Parse(dataReader.GetString(ordinal++)); - dictionary["GuidValue"] = Guid.Parse(dataReader.GetString(ordinal++)); - dictionary["Int16Value"] = (Int16)dataReader.GetInt64(ordinal++); - dictionary["Int32Value"] = (Int32)dataReader.GetInt64(ordinal++); - dictionary["Int64Value"] = dataReader.GetInt64(ordinal++); - dictionary["SingleValue"] = dataReader.GetFloat(ordinal++); - dictionary["StringValue"] = dataReader.GetString(ordinal++); - dictionary["TimeSpanValue"] = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture); + var dictionary = new Dictionary + { + ["Id"] = dataReader.GetInt64(ordinal++), + ["BooleanValue"] = dataReader.GetInt64(ordinal++) == 1, + ["BytesValue"] = (Byte[])dataReader.GetValue(ordinal++), + ["ByteValue"] = dataReader.GetByte(ordinal++), + ["CharValue"] = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 + ? charBuffer[0] + : throw new(), + ["DateTimeValue"] = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture), + ["DecimalValue"] = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture), + ["DoubleValue"] = dataReader.GetDouble(ordinal++), + ["EnumValue"] = Enum.Parse(dataReader.GetString(ordinal++)), + ["GuidValue"] = Guid.Parse(dataReader.GetString(ordinal++)), + ["Int16Value"] = (Int16)dataReader.GetInt64(ordinal++), + ["Int32Value"] = (Int32)dataReader.GetInt64(ordinal++), + ["Int64Value"] = dataReader.GetInt64(ordinal++), + ["SingleValue"] = dataReader.GetFloat(ordinal++), + ["StringValue"] = dataReader.GetString(ordinal++), + ["TimeSpanValue"] = TimeSpan.Parse(dataReader.GetString(ordinal), CultureInfo.InvariantCulture) + }; - entities.Add(entity); + entities.Add(new DataRow(dictionary)); } return entities; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Dynamic_Category)] + public List Query_Dynamic_Dapper() => + SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); + [Benchmark(Baseline = false)] [BenchmarkCategory(Query_Dynamic_Category)] public List Query_Dynamic_DbConnectionPlus() => diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs index a94eb1d..3909b29 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Query_Entities_DbCommand), + nameof(Query_Entities_Command), nameof(Query_Entities_Dapper), nameof(Query_Entities_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void Query_Entities__Cleanup() => [GlobalSetup( Targets = [ - nameof(Query_Entities_DbCommand), + nameof(Query_Entities_Command), nameof(Query_Entities_Dapper), nameof(Query_Entities_DbConnectionPlus) ] @@ -29,30 +29,31 @@ public void Query_Entities__Cleanup() => public void Query_Entities__Setup() => this.SetupDatabase(Query_Entities_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_Dapper() => - SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); - [Benchmark(Baseline = true)] [BenchmarkCategory(Query_Entities_Category)] - public List Query_Entities_DbCommand() + public List Query_Entities_Command() { - var entities = new List(); + var result = new List(); using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT * FROM Entity"; using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { - entities.Add(ReadEntity(dataReader)); + result.Add(ReadEntity(dataReader)); } - return entities; + return result; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Entities_Category)] + public List Query_Entities_Dapper() => + SqlMapper.Query(this.connection, "SELECT * FROM Entity").ToList(); + [Benchmark(Baseline = false)] [BenchmarkCategory(Query_Entities_Category)] public List Query_Entities_DbConnectionPlus() => diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs index 9a65a89..1a880cf 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Query_Scalars_DbCommand), + nameof(Query_Scalars_Command), nameof(Query_Scalars_Dapper), nameof(Query_Scalars_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void Query_Scalars__Cleanup() => [GlobalSetup( Targets = [ - nameof(Query_Scalars_DbCommand), + nameof(Query_Scalars_Command), nameof(Query_Scalars_Dapper), nameof(Query_Scalars_DbConnectionPlus) ] @@ -29,38 +29,35 @@ public void Query_Scalars__Cleanup() => public void Query_Scalars__Setup() => this.SetupDatabase(Query_Scalars_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_Dapper() => - SqlMapper.Query(this.connection, $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}") - .ToList(); - [Benchmark(Baseline = true)] [BenchmarkCategory(Query_Scalars_Category)] - public List Query_Scalars_DbCommand() + public List Query_Scalars_Command() { - var data = new List(); + var result = new List(); using var command = this.connection.CreateCommand(); - command.CommandText = $"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}"; + + command.CommandText = "SELECT Id FROM Entity"; using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { - var id = dataReader.GetInt64(0); - - data.Add(id); + result.Add(dataReader.GetInt64(0)); } - return data; + return result; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_Scalars_Category)] + public List Query_Scalars_Dapper() => + SqlMapper.Query(this.connection, "SELECT Id FROM Entity").ToList(); + [Benchmark(Baseline = false)] [BenchmarkCategory(Query_Scalars_Category)] public List Query_Scalars_DbConnectionPlus() => - this.connection - .Query($"SELECT Id FROM Entity LIMIT {Query_Scalars_EntitiesPerOperation}").ToList(); + this.connection.Query("SELECT Id FROM Entity").ToList(); private const String Query_Scalars_Category = "Query_Scalars"; private const Int32 Query_Scalars_EntitiesPerOperation = 600; diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs index 25efc04..a1874f2 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(Query_ValueTuples_DbCommand), + nameof(Query_ValueTuples_Command), nameof(Query_ValueTuples_Dapper), nameof(Query_ValueTuples_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void Query_ValueTuples__Cleanup() => [GlobalSetup( Targets = [ - nameof(Query_ValueTuples_DbCommand), + nameof(Query_ValueTuples_Command), nameof(Query_ValueTuples_Dapper), nameof(Query_ValueTuples_DbConnectionPlus) ] @@ -29,32 +29,22 @@ public void Query_ValueTuples__Cleanup() => public void Query_ValueTuples__Setup() => this.SetupDatabase(Query_ValueTuples_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(Query_ValueTuples_Category)] - public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> - Query_ValueTuples_Dapper() => - SqlMapper - .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( - this.connection, - "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity" - ) - .ToList(); - [Benchmark(Baseline = true)] [BenchmarkCategory(Query_ValueTuples_Category)] public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> - Query_ValueTuples_DbCommand() + Query_ValueTuples_Command() { - var tuples = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); + var result = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); using var command = this.connection.CreateCommand(); + command.CommandText = "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity"; using var dataReader = command.ExecuteReader(); while (dataReader.Read()) { - tuples.Add( + result.Add( ( dataReader.GetInt64(0), DateTime.Parse(dataReader.GetString(1), CultureInfo.InvariantCulture), @@ -64,9 +54,20 @@ public void Query_ValueTuples__Setup() => ); } - return tuples; + return result; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(Query_ValueTuples_Category)] + public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> + Query_ValueTuples_Dapper() => + SqlMapper + .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>( + this.connection, + "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity" + ) + .ToList(); + [Benchmark(Baseline = false)] [BenchmarkCategory(Query_ValueTuples_Category)] public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)> diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs index c47796d..1d30eb2 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_Command), nameof(TemporaryTable_ComplexObjects_Dapper), nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void TemporaryTable_ComplexObjects__Cleanup() => [GlobalSetup( Targets = [ - nameof(TemporaryTable_ComplexObjects_DbCommand), + nameof(TemporaryTable_ComplexObjects_Command), nameof(TemporaryTable_ComplexObjects_Dapper), nameof(TemporaryTable_ComplexObjects_DbConnectionPlus) ] @@ -29,149 +29,10 @@ public void TemporaryTable_ComplexObjects__Cleanup() => public void TemporaryTable_ComplexObjects__Setup() => this.SetupDatabase(TemporaryTable_ComplexObjects_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_Dapper() - { - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - - SqlMapper.Execute(this.connection, CreateTempEntitiesTableSql); - - using var insertCommand = this.connection.CreateCommand(); - insertCommand.CommandText = InsertIntoTempEntities; - - var idParameter = new SqliteParameter - { - ParameterName = "@Id" - }; - - var booleanValueParameter = new SqliteParameter - { - ParameterName = "@BooleanValue" - }; - - var bytesValueParameter = new SqliteParameter - { - ParameterName = "@BytesValue" - }; - - var byteValueParameter = new SqliteParameter - { - ParameterName = "@ByteValue" - }; - - var charValueParameter = new SqliteParameter - { - ParameterName = "@CharValue" - }; - - var dateTimeValueParameter = new SqliteParameter - { - ParameterName = "@DateTimeValue" - }; - - var decimalValueParameter = new SqliteParameter - { - ParameterName = "@DecimalValue" - }; - - var doubleValueParameter = new SqliteParameter - { - ParameterName = "@DoubleValue" - }; - - var enumValueParameter = new SqliteParameter - { - ParameterName = "@EnumValue" - }; - - var guidValueParameter = new SqliteParameter - { - ParameterName = "@GuidValue" - }; - - var int16ValueParameter = new SqliteParameter - { - ParameterName = "@Int16Value" - }; - - var int32ValueParameter = new SqliteParameter - { - ParameterName = "@Int32Value" - }; - - var int64ValueParameter = new SqliteParameter - { - ParameterName = "@Int64Value" - }; - - var singleValueParameter = new SqliteParameter - { - ParameterName = "@SingleValue" - }; - - var stringValueParameter = new SqliteParameter - { - ParameterName = "@StringValue" - }; - - var timeSpanValueParameter = new SqliteParameter - { - ParameterName = "@TimeSpanValue" - }; - - insertCommand.Parameters.Add(idParameter); - insertCommand.Parameters.Add(booleanValueParameter); - insertCommand.Parameters.Add(bytesValueParameter); - insertCommand.Parameters.Add(byteValueParameter); - insertCommand.Parameters.Add(charValueParameter); - insertCommand.Parameters.Add(dateTimeValueParameter); - insertCommand.Parameters.Add(decimalValueParameter); - insertCommand.Parameters.Add(doubleValueParameter); - insertCommand.Parameters.Add(enumValueParameter); - insertCommand.Parameters.Add(guidValueParameter); - insertCommand.Parameters.Add(int16ValueParameter); - insertCommand.Parameters.Add(int32ValueParameter); - insertCommand.Parameters.Add(int64ValueParameter); - insertCommand.Parameters.Add(singleValueParameter); - insertCommand.Parameters.Add(stringValueParameter); - insertCommand.Parameters.Add(timeSpanValueParameter); - - foreach (var entity in entities) - { - idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = entity.BytesValue; - byteValueParameter.Value = entity.ByteValue; - charValueParameter.Value = entity.CharValue; - dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = entity.DoubleValue; - enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue.ToString(); - int16ValueParameter.Value = entity.Int16Value; - int32ValueParameter.Value = entity.Int32Value; - int64ValueParameter.Value = entity.Int64Value; - singleValueParameter.Value = entity.SingleValue; - stringValueParameter.Value = entity.StringValue; - timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); - - insertCommand.ExecuteNonQuery(); - } - - var result = SqlMapper.Query(this.connection, SelectTempEntitiesSql).ToList(); - - SqlMapper.Execute(this.connection, "DROP TABLE temp.Entities"); - - return result; - } - [Benchmark(Baseline = true)] [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_DbCommand() + public List TemporaryTable_ComplexObjects_Command() { - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); - var result = new List(); using var createTableCommand = this.connection.CreateCommand(); @@ -179,129 +40,41 @@ public List TemporaryTable_ComplexObjects_DbCommand() createTableCommand.ExecuteNonQuery(); using var insertCommand = this.connection.CreateCommand(); - insertCommand.CommandText = InsertIntoTempEntities; - - var idParameter = new SqliteParameter - { - ParameterName = "@Id" - }; - - var booleanValueParameter = new SqliteParameter - { - ParameterName = "@BooleanValue" - }; - - var bytesValueParameter = new SqliteParameter - { - ParameterName = "@BytesValue" - }; - - var byteValueParameter = new SqliteParameter - { - ParameterName = "@ByteValue" - }; - - var charValueParameter = new SqliteParameter - { - ParameterName = "@CharValue" - }; - - var dateTimeValueParameter = new SqliteParameter - { - ParameterName = "@DateTimeValue" - }; - - var decimalValueParameter = new SqliteParameter - { - ParameterName = "@DecimalValue" - }; - - var doubleValueParameter = new SqliteParameter - { - ParameterName = "@DoubleValue" - }; - - var enumValueParameter = new SqliteParameter - { - ParameterName = "@EnumValue" - }; - - var guidValueParameter = new SqliteParameter - { - ParameterName = "@GuidValue" - }; - - var int16ValueParameter = new SqliteParameter - { - ParameterName = "@Int16Value" - }; - var int32ValueParameter = new SqliteParameter - { - ParameterName = "@Int32Value" - }; - - var int64ValueParameter = new SqliteParameter - { - ParameterName = "@Int64Value" - }; - - var singleValueParameter = new SqliteParameter - { - ParameterName = "@SingleValue" - }; - - var stringValueParameter = new SqliteParameter - { - ParameterName = "@StringValue" - }; + insertCommand.CommandText = InsertIntoTempEntities; - var timeSpanValueParameter = new SqliteParameter + var parameters = new Dictionary { - ParameterName = "@TimeSpanValue" + { "Id", new("Id", null) }, + { "BooleanValue", new("BooleanValue", null) }, + { "BytesValue", new("BytesValue", null) }, + { "ByteValue", new("ByteValue", null) }, + { "CharValue", new("CharValue", null) }, + { "DateTimeValue", new("DateTimeValue", null) }, + { "DecimalValue", new("DecimalValue", null) }, + { "DoubleValue", new("DoubleValue", null) }, + { "EnumValue", new("EnumValue", null) }, + { "GuidValue", new("GuidValue", null) }, + { "Int16Value", new("Int16Value", null) }, + { "Int32Value", new("Int32Value", null) }, + { "Int64Value", new("Int64Value", null) }, + { "SingleValue", new("SingleValue", null) }, + { "StringValue", new("StringValue", null) }, + { "TimeSpanValue", new("TimeSpanValue", null) } }; - insertCommand.Parameters.Add(idParameter); - insertCommand.Parameters.Add(booleanValueParameter); - insertCommand.Parameters.Add(bytesValueParameter); - insertCommand.Parameters.Add(byteValueParameter); - insertCommand.Parameters.Add(charValueParameter); - insertCommand.Parameters.Add(dateTimeValueParameter); - insertCommand.Parameters.Add(decimalValueParameter); - insertCommand.Parameters.Add(doubleValueParameter); - insertCommand.Parameters.Add(enumValueParameter); - insertCommand.Parameters.Add(guidValueParameter); - insertCommand.Parameters.Add(int16ValueParameter); - insertCommand.Parameters.Add(int32ValueParameter); - insertCommand.Parameters.Add(int64ValueParameter); - insertCommand.Parameters.Add(singleValueParameter); - insertCommand.Parameters.Add(stringValueParameter); - insertCommand.Parameters.Add(timeSpanValueParameter); + insertCommand.Parameters.AddRange(parameters.Values); - foreach (var entity in entities) + foreach (var entity in this.temporaryTable_ComplexObjects_Entities) { - idParameter.Value = entity.Id; - booleanValueParameter.Value = entity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = entity.BytesValue; - byteValueParameter.Value = entity.ByteValue; - charValueParameter.Value = entity.CharValue; - dateTimeValueParameter.Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = entity.DoubleValue; - enumValueParameter.Value = entity.EnumValue.ToString(); - guidValueParameter.Value = entity.GuidValue.ToString(); - int16ValueParameter.Value = entity.Int16Value; - int32ValueParameter.Value = entity.Int32Value; - int64ValueParameter.Value = entity.Int64Value; - singleValueParameter.Value = entity.SingleValue; - stringValueParameter.Value = entity.StringValue; - timeSpanValueParameter.Value = entity.TimeSpanValue.ToString(); + PopulateEntityParameters(entity, parameters); insertCommand.ExecuteNonQuery(); } using var selectCommand = this.connection.CreateCommand(); - selectCommand.CommandText = SelectTempEntitiesSql; + + selectCommand.CommandText = "SELECT * FROM temp.Entities"; using var dataReader = selectCommand.ExecuteReader(); @@ -319,18 +92,36 @@ public List TemporaryTable_ComplexObjects_DbCommand() [Benchmark(Baseline = false)] [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] - public List TemporaryTable_ComplexObjects_DbConnectionPlus() + public List TemporaryTable_ComplexObjects_Dapper() { - var entities = Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + SqlMapper.Execute(this.connection, CreateTempEntitiesTableSql); + + SqlMapperExtensions.TableNameMapper = _ => "temp.Entities"; + + SqlMapperExtensions.Insert(this.connection, this.temporaryTable_ComplexObjects_Entities); + + var result = SqlMapper.Query(this.connection, "SELECT * FROM temp.Entities").ToList(); + + SqlMapper.Execute(this.connection, "DROP TABLE temp.Entities"); - return this.connection.Query($"SELECT * FROM {TemporaryTable(entities)}").ToList(); + return result; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ComplexObjects_Category)] + public List TemporaryTable_ComplexObjects_DbConnectionPlus() => + this.connection + .Query($"SELECT * FROM {TemporaryTable(this.temporaryTable_ComplexObjects_Entities)}") + .ToList(); + + private readonly List temporaryTable_ComplexObjects_Entities = + Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation); + private const String CreateTempEntitiesTableSql = """ CREATE TEMP TABLE Entities ( Id INTEGER, - BytesValue BLOB, BooleanValue INTEGER, + BytesValue BLOB, ByteValue INTEGER, CharValue TEXT, DateTimeValue TEXT, @@ -386,28 +177,6 @@ INSERT INTO temp.Entities ( ) """; - private const String SelectTempEntitiesSql = """ - SELECT - Id, - BooleanValue, - BytesValue, - ByteValue, - CharValue, - DateTimeValue, - DecimalValue, - DoubleValue, - EnumValue, - GuidValue, - Int16Value, - Int32Value, - Int64Value, - SingleValue, - StringValue, - TimeSpanValue - FROM - temp.Entities - """; - private const String TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects"; private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 250; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs index eaa011f..7f10528 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_Command), nameof(TemporaryTable_ScalarValues_Dapper), nameof(TemporaryTable_ScalarValues_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void TemporaryTable_ScalarValues__Cleanup() => [GlobalSetup( Targets = [ - nameof(TemporaryTable_ScalarValues_DbCommand), + nameof(TemporaryTable_ScalarValues_Command), nameof(TemporaryTable_ScalarValues_Dapper), nameof(TemporaryTable_ScalarValues_DbConnectionPlus) ] @@ -29,54 +29,12 @@ public void TemporaryTable_ScalarValues__Cleanup() => public void TemporaryTable_ScalarValues__Setup() => this.SetupDatabase(0); - [Benchmark(Baseline = false)] - [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_Dapper() - { - var scalarValues = Enumerable - .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString(CultureInfo.InvariantCulture)) - .ToList(); - - SqlMapper.Execute(this.connection, "CREATE TEMP TABLE \"Values\" (Value TEXT)"); - - using var insertCommand = this.connection.CreateCommand(); - insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)"; - - var valueParameter = new SqliteParameter - { - ParameterName = "@Value" - }; - - insertCommand.Parameters.Add(valueParameter); - - foreach (var value in scalarValues) - { - valueParameter.Value = value; - - insertCommand.ExecuteNonQuery(); - } - - var result = SqlMapper.Query(this.connection, "SELECT Value FROM temp.\"Values\"").ToList(); - - SqlMapper.Execute(this.connection, "DROP TABLE temp.\"Values\""); - - return result; - } - [Benchmark(Baseline = true)] [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_DbCommand() + public List TemporaryTable_ScalarValues_Command() { - var scalarValues = Enumerable - .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString(CultureInfo.InvariantCulture)) - .ToList(); - - var result = new List(); - using var createTableCommand = this.connection.CreateCommand(); - createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value TEXT)"; + createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value INTEGER)"; createTableCommand.ExecuteNonQuery(); using var insertCommand = this.connection.CreateCommand(); @@ -89,7 +47,7 @@ public List TemporaryTable_ScalarValues_DbCommand() insertCommand.Parameters.Add(valueParameter); - foreach (var value in scalarValues) + foreach (var value in this.temporaryTable_ScalarValues_Values) { valueParameter.Value = value; @@ -97,13 +55,16 @@ public List TemporaryTable_ScalarValues_DbCommand() } using var selectCommand = this.connection.CreateCommand(); + selectCommand.CommandText = "SELECT Value FROM temp.\"Values\""; using var dataReader = selectCommand.ExecuteReader(); + var result = new List(); + while (dataReader.Read()) { - result.Add(dataReader.GetString(0)); + result.Add(dataReader.GetInt64(0)); } using var dropTableCommand = this.connection.CreateCommand(); @@ -115,16 +76,35 @@ public List TemporaryTable_ScalarValues_DbCommand() [Benchmark(Baseline = false)] [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] - public List TemporaryTable_ScalarValues_DbConnectionPlus() + public List TemporaryTable_ScalarValues_Dapper() { - var scalarValues = Enumerable - .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) - .Select(a => a.ToString(CultureInfo.InvariantCulture)) - .ToList(); + SqlMapper.Execute(this.connection, "CREATE TEMP TABLE \"Values\" (Value INTEGER)"); - return this.connection.Query($"SELECT Value FROM {TemporaryTable(scalarValues)}").ToList(); + SqlMapperExtensions.TableNameMapper = _ => "temp.\"Values\""; + + SqlMapperExtensions.Insert( + this.connection, + this.temporaryTable_ScalarValues_Values.Select(a => new { Value = a }) + ); + + var result = SqlMapper.Query(this.connection, "SELECT Value FROM temp.\"Values\"").ToList(); + + SqlMapper.Execute(this.connection, "DROP TABLE temp.\"Values\""); + + return result; } + [Benchmark(Baseline = false)] + [BenchmarkCategory(TemporaryTable_ScalarValues_Category)] + public List TemporaryTable_ScalarValues_DbConnectionPlus() => + this.connection.Query($"SELECT Value FROM {TemporaryTable(this.temporaryTable_ScalarValues_Values)}") + .ToList(); + + private readonly List temporaryTable_ScalarValues_Values = Enumerable + .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation) + .Select(a => (Int64)a) + .ToList(); + private const String TemporaryTable_ScalarValues_Category = "TemporaryTable_ScalarValues"; private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs index d8c1cee..a261885 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(UpdateEntities_DbCommand), + nameof(UpdateEntities_Command), nameof(UpdateEntities_Dapper), nameof(UpdateEntities_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void UpdateEntities__Cleanup() => [GlobalSetup( Targets = [ - nameof(UpdateEntities_DbCommand), + nameof(UpdateEntities_Command), nameof(UpdateEntities_Dapper), nameof(UpdateEntities_DbConnectionPlus) ] @@ -29,162 +29,73 @@ public void UpdateEntities__Cleanup() => public void UpdateEntities__Setup() => this.SetupDatabase(UpdateEntities_EntitiesPerOperation); - [Benchmark(Baseline = false)] - [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_Dapper() - { - var updatesEntities = Generate.UpdateFor(this.entitiesInDb); - - SqlMapperExtensions.Update(this.connection, updatesEntities); - } - [Benchmark(Baseline = true)] [BenchmarkCategory(UpdateEntities_Category)] - public void UpdateEntities_DbCommand() + public void UpdateEntities_Command() { var updatedEntities = Generate.UpdateFor(this.entitiesInDb); using var command = this.connection.CreateCommand(); + command.CommandText = """ UPDATE Entity SET BooleanValue = @BooleanValue, - BytesValue = @BytesValue, - ByteValue = @ByteValue, - CharValue = @CharValue, - DateTimeValue = @DateTimeValue, - DecimalValue = @DecimalValue, - DoubleValue = @DoubleValue, - EnumValue = @EnumValue, - GuidValue = @GuidValue, - Int16Value = @Int16Value, - Int32Value = @Int32Value, - Int64Value = @Int64Value, - SingleValue = @SingleValue, - StringValue = @StringValue, - TimeSpanValue = @TimeSpanValue + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue WHERE Id = @Id """; - var idParameter = new SqliteParameter - { - ParameterName = "@Id" - }; - - var booleanValueParameter = new SqliteParameter - { - ParameterName = "@BooleanValue" - }; - - var bytesValueParameter = new SqliteParameter - { - ParameterName = "@BytesValue" - }; - - var byteValueParameter = new SqliteParameter - { - ParameterName = "@ByteValue" - }; - - var charValueParameter = new SqliteParameter - { - ParameterName = "@CharValue" - }; - - var dateTimeValueParameter = new SqliteParameter - { - ParameterName = "@DateTimeValue" - }; - - var decimalValueParameter = new SqliteParameter - { - ParameterName = "@DecimalValue" - }; - - var doubleValueParameter = new SqliteParameter - { - ParameterName = "@DoubleValue" - }; - - var enumValueParameter = new SqliteParameter - { - ParameterName = "@EnumValue" - }; - - var guidValueParameter = new SqliteParameter - { - ParameterName = "@GuidValue" - }; - - var int16ValueParameter = new SqliteParameter - { - ParameterName = "@Int16Value" - }; - - var int32ValueParameter = new SqliteParameter - { - ParameterName = "@Int32Value" - }; - - var int64ValueParameter = new SqliteParameter - { - ParameterName = "@Int64Value" - }; - - var singleValueParameter = new SqliteParameter - { - ParameterName = "@SingleValue" - }; - - var stringValueParameter = new SqliteParameter + var parameters = new Dictionary { - ParameterName = "@StringValue" + { "Id", new("Id", null) }, + { "BooleanValue", new("BooleanValue", null) }, + { "BytesValue", new("BytesValue", null) }, + { "ByteValue", new("ByteValue", null) }, + { "CharValue", new("CharValue", null) }, + { "DateTimeValue", new("DateTimeValue", null) }, + { "DecimalValue", new("DecimalValue", null) }, + { "DoubleValue", new("DoubleValue", null) }, + { "EnumValue", new("EnumValue", null) }, + { "GuidValue", new("GuidValue", null) }, + { "Int16Value", new("Int16Value", null) }, + { "Int32Value", new("Int32Value", null) }, + { "Int64Value", new("Int64Value", null) }, + { "SingleValue", new("SingleValue", null) }, + { "StringValue", new("StringValue", null) }, + { "TimeSpanValue", new("TimeSpanValue", null) } }; - var timeSpanValueParameter = new SqliteParameter - { - ParameterName = "@TimeSpanValue" - }; - - command.Parameters.Add(idParameter); - command.Parameters.Add(booleanValueParameter); - command.Parameters.Add(bytesValueParameter); - command.Parameters.Add(byteValueParameter); - command.Parameters.Add(charValueParameter); - command.Parameters.Add(dateTimeValueParameter); - command.Parameters.Add(decimalValueParameter); - command.Parameters.Add(doubleValueParameter); - command.Parameters.Add(enumValueParameter); - command.Parameters.Add(guidValueParameter); - command.Parameters.Add(int16ValueParameter); - command.Parameters.Add(int32ValueParameter); - command.Parameters.Add(int64ValueParameter); - command.Parameters.Add(singleValueParameter); - command.Parameters.Add(stringValueParameter); - command.Parameters.Add(timeSpanValueParameter); + command.Parameters.AddRange(parameters.Values); foreach (var updatedEntity in updatedEntities) { - idParameter.Value = updatedEntity.Id; - booleanValueParameter.Value = updatedEntity.BooleanValue ? 1 : 0; - bytesValueParameter.Value = updatedEntity.BytesValue; - byteValueParameter.Value = updatedEntity.ByteValue; - charValueParameter.Value = updatedEntity.CharValue; - dateTimeValueParameter.Value = updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture); - decimalValueParameter.Value = updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture); - doubleValueParameter.Value = updatedEntity.DoubleValue; - enumValueParameter.Value = updatedEntity.EnumValue.ToString(); - guidValueParameter.Value = updatedEntity.GuidValue.ToString(); - int16ValueParameter.Value = updatedEntity.Int16Value; - int32ValueParameter.Value = updatedEntity.Int32Value; - int64ValueParameter.Value = updatedEntity.Int64Value; - singleValueParameter.Value = updatedEntity.SingleValue; - stringValueParameter.Value = updatedEntity.StringValue; - timeSpanValueParameter.Value = updatedEntity.TimeSpanValue.ToString(); + PopulateEntityParameters(updatedEntity, parameters); command.ExecuteNonQuery(); } } + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntities_Category)] + public void UpdateEntities_Dapper() + { + var updatesEntities = Generate.UpdateFor(this.entitiesInDb); + + SqlMapperExtensions.Update(this.connection, updatesEntities); + } + [Benchmark(Baseline = false)] [BenchmarkCategory(UpdateEntities_Category)] public void UpdateEntities_DbConnectionPlus() diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs index 28ab986..d84c98d 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs @@ -10,7 +10,7 @@ public partial class Benchmarks [GlobalCleanup( Targets = [ - nameof(UpdateEntity_DbCommand), + nameof(UpdateEntity_Command), nameof(UpdateEntity_Dapper), nameof(UpdateEntity_DbConnectionPlus) ] @@ -21,7 +21,7 @@ public void UpdateEntity__Cleanup() => [GlobalSetup( Targets = [ - nameof(UpdateEntity_DbCommand), + nameof(UpdateEntity_Command), nameof(UpdateEntity_Dapper), nameof(UpdateEntity_DbConnectionPlus) ] @@ -29,67 +29,74 @@ public void UpdateEntity__Cleanup() => public void UpdateEntity__Setup() => this.SetupDatabase(1); - [Benchmark(Baseline = false)] - [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_Dapper() - { - var entity = this.entitiesInDb[0]; - - var updatedEntity = Generate.UpdateFor(entity); - - SqlMapperExtensions.Update(this.connection, updatedEntity); - } - [Benchmark(Baseline = true)] [BenchmarkCategory(UpdateEntity_Category)] - public void UpdateEntity_DbCommand() + public void UpdateEntity_Command() { var entity = this.entitiesInDb[0]; var updatedEntity = Generate.UpdateFor(entity); using var command = this.connection.CreateCommand(); + command.CommandText = """ UPDATE Entity SET BooleanValue = @BooleanValue, - BytesValue = @BytesValue, - ByteValue = @ByteValue, - CharValue = @CharValue, - DateTimeValue = @DateTimeValue, - DecimalValue = @DecimalValue, - DoubleValue = @DoubleValue, - EnumValue = @EnumValue, - GuidValue = @GuidValue, - Int16Value = @Int16Value, - Int32Value = @Int32Value, - Int64Value = @Int64Value, - SingleValue = @SingleValue, - StringValue = @StringValue, - TimeSpanValue = @TimeSpanValue + BytesValue = @BytesValue, + ByteValue = @ByteValue, + CharValue = @CharValue, + DateTimeValue = @DateTimeValue, + DecimalValue = @DecimalValue, + DoubleValue = @DoubleValue, + EnumValue = @EnumValue, + GuidValue = @GuidValue, + Int16Value = @Int16Value, + Int32Value = @Int32Value, + Int64Value = @Int64Value, + SingleValue = @SingleValue, + StringValue = @StringValue, + TimeSpanValue = @TimeSpanValue WHERE Id = @Id """; - command.Parameters.Add(new("@Id", updatedEntity.Id)); - command.Parameters.Add(new("@BooleanValue", updatedEntity.BooleanValue ? 1 : 0)); - command.Parameters.Add(new("@BytesValue", updatedEntity.BytesValue)); - command.Parameters.Add(new("@ByteValue", updatedEntity.ByteValue)); - command.Parameters.Add(new("@CharValue", updatedEntity.CharValue)); - command.Parameters.Add( - new("@DateTimeValue", updatedEntity.DateTimeValue.ToString(CultureInfo.InvariantCulture)) - ); - command.Parameters.Add(new("@DecimalValue", updatedEntity.DecimalValue.ToString(CultureInfo.InvariantCulture))); - command.Parameters.Add(new("@DoubleValue", updatedEntity.DoubleValue)); - command.Parameters.Add(new("@EnumValue", updatedEntity.EnumValue.ToString())); - command.Parameters.Add(new("@GuidValue", updatedEntity.GuidValue.ToString())); - command.Parameters.Add(new("@Int16Value", updatedEntity.Int16Value)); - command.Parameters.Add(new("@Int32Value", updatedEntity.Int32Value)); - command.Parameters.Add(new("@Int64Value", updatedEntity.Int64Value)); - command.Parameters.Add(new("@SingleValue", updatedEntity.SingleValue)); - command.Parameters.Add(new("@StringValue", updatedEntity.StringValue)); - command.Parameters.Add(new("@TimeSpanValue", updatedEntity.TimeSpanValue.ToString())); + + var parameters = new Dictionary + { + { "Id", new("Id", null) }, + { "BooleanValue", new("BooleanValue", null) }, + { "BytesValue", new("BytesValue", null) }, + { "ByteValue", new("ByteValue", null) }, + { "CharValue", new("CharValue", null) }, + { "DateTimeValue", new("DateTimeValue", null) }, + { "DecimalValue", new("DecimalValue", null) }, + { "DoubleValue", new("DoubleValue", null) }, + { "EnumValue", new("EnumValue", null) }, + { "GuidValue", new("GuidValue", null) }, + { "Int16Value", new("Int16Value", null) }, + { "Int32Value", new("Int32Value", null) }, + { "Int64Value", new("Int64Value", null) }, + { "SingleValue", new("SingleValue", null) }, + { "StringValue", new("StringValue", null) }, + { "TimeSpanValue", new("TimeSpanValue", null) } + }; + + command.Parameters.AddRange(parameters.Values); + + PopulateEntityParameters(updatedEntity, parameters); command.ExecuteNonQuery(); } + [Benchmark(Baseline = false)] + [BenchmarkCategory(UpdateEntity_Category)] + public void UpdateEntity_Dapper() + { + var entity = this.entitiesInDb[0]; + + var updatedEntity = Generate.UpdateFor(entity); + + SqlMapperExtensions.Update(this.connection, updatedEntity); + } + [Benchmark(Baseline = false)] [BenchmarkCategory(UpdateEntity_Category)] public void UpdateEntity_DbConnectionPlus() diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs index 52ef7e5..b135666 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs @@ -13,6 +13,9 @@ static Benchmarks() SqlMapper.AddTypeHandler(new TimeSpanTypeHandler()); } + public Benchmarks() => + SqlMapperExtensions.TableNameMapper = null; + private void SetupDatabase(Int32 numberOfEntities) { this.connection = new("Data Source=:memory:"); @@ -30,11 +33,32 @@ private void SetupDatabase(Int32 numberOfEntities) transaction.Commit(); } + private static void PopulateEntityParameters(BenchmarkEntity entity, Dictionary parameters) + { + parameters["Id"].Value = entity.Id; + parameters["BooleanValue"].Value = entity.BooleanValue ? 1 : 0; + parameters["BytesValue"].Value = entity.BytesValue; + parameters["ByteValue"].Value = entity.ByteValue; + parameters["CharValue"].Value = entity.CharValue; + parameters["DateTimeValue"].Value = entity.DateTimeValue.ToString(CultureInfo.InvariantCulture); + parameters["DecimalValue"].Value = entity.DecimalValue.ToString(CultureInfo.InvariantCulture); + parameters["DoubleValue"].Value = entity.DoubleValue; + parameters["EnumValue"].Value = entity.EnumValue.ToString(); + parameters["GuidValue"].Value = entity.GuidValue.ToString(); + parameters["Int16Value"].Value = entity.Int16Value; + parameters["Int32Value"].Value = entity.Int32Value; + parameters["Int64Value"].Value = entity.Int64Value; + parameters["SingleValue"].Value = entity.SingleValue; + parameters["StringValue"].Value = entity.StringValue; + parameters["TimeSpanValue"].Value = entity.TimeSpanValue.ToString(); + } + private static BenchmarkEntity ReadEntity(IDataReader dataReader) { var charBuffer = new Char[1]; var ordinal = 0; + return new() { Id = dataReader.GetInt64(ordinal++), diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index c34a972..88fd153 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -8,8 +8,23 @@ public static class Program { public static void Main(String[] args) { - BenchmarkSwitcher - .FromAssembly(typeof(Program).Assembly) - .Run(args); + if (args is not ["test"]) + { + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); + } + else + { + var benchmarks = new Benchmarks(); + benchmarks.Exists__Setup(); + + for (int i = 0; i < 50000; i++) + { + benchmarks.Exists_Command(); + benchmarks.Exists_Dapper(); + benchmarks.Exists_DbConnectionPlus(); + } + } } } diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index d976abc..a7389bf 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -2,7 +2,6 @@ // 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.SqlStatements; namespace RentADeveloper.DbConnectionPlus.DbCommands; @@ -188,6 +187,7 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( { using var codeBuilder = new ValueStringBuilder(stackalloc char[2048]); var parameterNameOccurrences = new Dictionary(StringComparer.OrdinalIgnoreCase); + var parameterCount = 0; var command = connection.CreateCommand(); @@ -212,7 +212,7 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( case InterpolatedParameter interpolatedParameter: { var parameterName = interpolatedParameter.InferredName ?? - "Parameter_" + (parameterNameOccurrences.Count + 1); + "Parameter_" + (parameterCount + 1); if (parameterNameOccurrences.TryGetValue(parameterName, out var count)) { @@ -232,6 +232,8 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( dbParameters.Add(dbParameter); codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName)); + + parameterCount++; break; } @@ -243,6 +245,7 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( dbParameters.Add(dbParameter); parameterNameOccurrences[parameter.Name] = 1; + parameterCount++; break; } diff --git a/src/DbConnectionPlus/Helpers/NameHelper.cs b/src/DbConnectionPlus/Helpers/NameHelper.cs index ce28b3c..9bc17f8 100644 --- a/src/DbConnectionPlus/Helpers/NameHelper.cs +++ b/src/DbConnectionPlus/Helpers/NameHelper.cs @@ -73,6 +73,6 @@ internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan buffer[0] = (Char)(buffer[0] - 32); } - return new(buffer.Slice(0, count)); + return new(buffer[..count]); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs index 9b84e94..d1483c9 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -319,7 +319,7 @@ public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpre useAsyncApi, $""" SELECT {Parameter(baseDiscount * 5 / 3)}, - {Parameter(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0])} + {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} """, this.MockDatabaseAdapter, this.MockDbConnection @@ -329,7 +329,7 @@ public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpre .Should().Be( """ SELECT @BaseDiscount53, - @EntityIdsWhereaa5SelectaaToStringToArray0 + @EntityIdsWhereaa5ToArray0 """ ); @@ -343,10 +343,10 @@ public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpre .Should().Be(baseDiscount * 5 / 3); command.Parameters[1].ParameterName - .Should().Be("EntityIdsWhereaa5SelectaaToStringToArray0"); + .Should().Be("EntityIdsWhereaa5ToArray0"); command.Parameters[1].Value - .Should().Be(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0]); + .Should().Be(entityIds.Where(a => a > 5).ToArray()[0]); } [Theory] diff --git a/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs index ab709eb..4985951 100644 --- a/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs @@ -14,6 +14,7 @@ public class NameHelperTests : UnitTestsBase [InlineData("abc[]-=/<>", 5, "Abc")] [InlineData("this.GetId()", 2, "Id")] [InlineData("[productId]", 10, "ProductId")] + [InlineData("entityIds.Where(a => a > 5).ToArray()[0]", 60, "EntityIdsWhereaa5ToArray0")] [InlineData("", 10, "")] public void CreateNameFromCallerArgumentExpression_ShouldCreateName( String expression, diff --git a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs index 1477025..967d9fe 100644 --- a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs +++ b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs @@ -30,7 +30,7 @@ public void AppendFormatted_InterpolatedParameter_ShouldSupportComplexExpression InterpolatedSqlStatement statement = $""" SELECT {Parameter(baseDiscount * 5 / 3)}, - {Parameter(entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0])} + {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} """; statement.Fragments @@ -46,10 +46,10 @@ public void AppendFormatted_InterpolatedParameter_ShouldSupportComplexExpression .Should().Be(new Literal($",{Environment.NewLine} ")); statement.Fragments[3] - .Should().Be( + .Should().BeEquivalentTo( new InterpolatedParameter( - "EntityIdsWhereaa5SelectaaToStringToArray0", - entityIds.Where(a => a > 5).Select(a => a.ToString()).ToArray()[0] + "EntityIdsWhereaa5ToArray0", + entityIds.Where(a => a > 5).ToArray()[0] ) ); } From 6cc3f4575d270cce14d59d2ac9dd9e9c355e645e Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 11 Feb 2026 00:01:54 +0100 Subject: [PATCH 16/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .../Benchmarks.Parameter.cs | 36 +++++++++---------- .../DbCommands/DbCommandBuilder.cs | 16 ++++----- .../SqlStatements/InterpolatedSqlStatement.cs | 10 +++--- .../DbCommands/DbCommandBuilderTests.cs | 21 +++++++++++ 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs index 2b89dc9..7b6acb0 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -33,7 +33,7 @@ public void Parameter__Setup() => [BenchmarkCategory(Parameter_Category)] public Object Parameter_Command() { - var result = new Int32[5]; + var result = new Int64[5]; using var command = this.connection.CreateCommand(); @@ -49,11 +49,11 @@ public Object Parameter_Command() dataReader.Read(); - result[0] = dataReader.GetInt32(0); - result[1] = dataReader.GetInt32(1); - result[2] = dataReader.GetInt32(2); - result[3] = dataReader.GetInt32(3); - result[4] = dataReader.GetInt32(4); + result[0] = dataReader.GetInt64(0); + result[1] = dataReader.GetInt64(1); + result[2] = dataReader.GetInt64(2); + result[3] = dataReader.GetInt64(3); + result[4] = dataReader.GetInt64(4); return result; } @@ -62,7 +62,7 @@ public Object Parameter_Command() [BenchmarkCategory(Parameter_Category)] public Object Parameter_Dapper() { - var result = new Int32[5]; + var result = new Int64[5]; using var dataReader = SqlMapper.ExecuteReader( this.connection, @@ -72,11 +72,11 @@ public Object Parameter_Dapper() dataReader.Read(); - result[0] = dataReader.GetInt32(0); - result[1] = dataReader.GetInt32(1); - result[2] = dataReader.GetInt32(2); - result[3] = dataReader.GetInt32(3); - result[4] = dataReader.GetInt32(4); + result[0] = dataReader.GetInt64(0); + result[1] = dataReader.GetInt64(1); + result[2] = dataReader.GetInt64(2); + result[3] = dataReader.GetInt64(3); + result[4] = dataReader.GetInt64(4); return result; } @@ -85,7 +85,7 @@ public Object Parameter_Dapper() [BenchmarkCategory(Parameter_Category)] public Object Parameter_DbConnectionPlus() { - var result = new Int32[5]; + var result = new Int64[5]; using var dataReader = this.connection.ExecuteReader( $"SELECT {Parameter(1)}, {Parameter(2)}, {Parameter(3)}, {Parameter(4)}, {Parameter(5)}" @@ -93,11 +93,11 @@ public Object Parameter_DbConnectionPlus() dataReader.Read(); - result[0] = dataReader.GetInt32(0); - result[1] = dataReader.GetInt32(1); - result[2] = dataReader.GetInt32(2); - result[3] = dataReader.GetInt32(3); - result[4] = dataReader.GetInt32(4); + result[0] = dataReader.GetInt64(0); + result[1] = dataReader.GetInt64(1); + result[2] = dataReader.GetInt64(2); + result[3] = dataReader.GetInt64(3); + result[4] = dataReader.GetInt64(4); return result; } diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index a7389bf..cafa2a3 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -185,8 +185,11 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( CancellationToken cancellationToken = default ) { - using var codeBuilder = new ValueStringBuilder(stackalloc char[2048]); - var parameterNameOccurrences = new Dictionary(StringComparer.OrdinalIgnoreCase); + using var codeBuilder = new ValueStringBuilder(stackalloc char[512]); + var parameterNameOccurrences = new Dictionary( + statement.Fragments.Count, + StringComparer.OrdinalIgnoreCase + ); var parameterCount = 0; var command = connection.CreateCommand(); @@ -214,17 +217,12 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( var parameterName = interpolatedParameter.InferredName ?? "Parameter_" + (parameterCount + 1); - if (parameterNameOccurrences.TryGetValue(parameterName, out var count)) + if (!parameterNameOccurrences.TryAdd(parameterName, 1)) { // Parameter name is already used, so we append a suffix to make it unique. - count++; - parameterNameOccurrences[parameterName] = count; + var count = ++parameterNameOccurrences[parameterName]; parameterName += count; } - else - { - parameterNameOccurrences[parameterName] = 1; - } var dbParameter = command.CreateParameter(); dbParameter.ParameterName = parameterName; diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs index a64d33b..0b0c416 100644 --- a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs +++ b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs @@ -182,15 +182,15 @@ public void AppendLiteral(String? value) } /// - public Boolean Equals(InterpolatedSqlStatement other) => + public readonly Boolean Equals(InterpolatedSqlStatement other) => this.fragments.SequenceEqual(other.Fragments); /// - public override Boolean Equals(Object? obj) => + public readonly override Boolean Equals(Object? obj) => obj is InterpolatedSqlStatement other && this.Equals(other); /// - public override Int32 GetHashCode() + public readonly override Int32 GetHashCode() { var hashCode = new HashCode(); @@ -203,7 +203,7 @@ public override Int32 GetHashCode() } /// - public override String ToString() + public readonly override String ToString() { using var stringBuilder = new ValueStringBuilder(stackalloc Char[500]); @@ -355,7 +355,7 @@ public static implicit operator InterpolatedSqlStatement(String value) /// /// The temporary tables used in this SQL statement. /// - internal IReadOnlyList TemporaryTables => this.temporaryTables; + internal readonly IReadOnlyList TemporaryTables => this.temporaryTables; private readonly List fragments; private readonly List temporaryTables; diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs index d1483c9..cc30b3e 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -110,6 +110,27 @@ public async Task BuildDbCommand_CommandType_ShouldUseCommandType(Boolean useAsy .Should().Be(CommandType.StoredProcedure); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildDbCommand_InterpolatedParameter_DuplicateName_ShouldAppendSuffix(Boolean useAsyncApi) + { + var value = Generate.ScalarValue(); + + var (command, _) = await CallApi( + useAsyncApi, + $"SELECT {Parameter(value)}, {Parameter(value)}, {Parameter(value)}, {Parameter(value)}, {Parameter(value)}", + this.MockDatabaseAdapter, + this.MockDbConnection + ); + + command.CommandText + .Should().Be("SELECT @Value, @Value2, @Value3, @Value4, @Value5"); + + command.Parameters.OfType().Select(a => a.ParameterName) + .Should().BeEquivalentTo("Value", "Value2", "Value3", "Value4", "Value5"); + } + [Theory] [InlineData(false)] [InlineData(true)] From c5030b9967798ba4f10e7862ea476c825639af6c Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Wed, 11 Feb 2026 20:05:28 +0100 Subject: [PATCH 17/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- .editorconfig | 3 + .../Benchmarks.Parameter.cs | 68 +++++-------------- .../DbConnectionPlus.Benchmarks/Program.cs | 9 ++- .../SqlStatements/InterpolatedParameter.cs | 2 +- .../InterpolatedTemporaryTable.cs | 2 +- src/DbConnectionPlus/SqlStatements/Literal.cs | 2 +- .../SqlStatements/Parameter.cs | 2 +- 7 files changed, 29 insertions(+), 59 deletions(-) diff --git a/.editorconfig b/.editorconfig index fcf36f6..d8982c2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -53,6 +53,9 @@ dotnet_diagnostic.IDE0290.severity = none # CA2100: Review SQL queries for security vulnerabilities dotnet_diagnostic.CA2100.severity = none +# RCS1222: Merge preprocessor directives +dotnet_diagnostic.RCS1222.severity = none + [*.{cs,vb}] #### Naming styles #### diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs index 7b6acb0..86a3d74 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -31,76 +31,44 @@ public void Parameter__Setup() => [Benchmark(Baseline = true)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_Command() + public Object? Parameter_Command() { - var result = new Int64[5]; - using var command = this.connection.CreateCommand(); - command.CommandText = "SELECT @P1, @P2, @P3, @P4, @P5"; + command.CommandText = "SELECT @P1 + @P2 + @P3 + @P4 + @P5 + @P6 + @P7 + @P8 + @P9 + @P10"; command.Parameters.Add(new("@P1", 1)); command.Parameters.Add(new("@P2", 2)); command.Parameters.Add(new("@P3", 3)); command.Parameters.Add(new("@P4", 4)); command.Parameters.Add(new("@P5", 5)); + command.Parameters.Add(new("@P6", 6)); + command.Parameters.Add(new("@P7", 7)); + command.Parameters.Add(new("@P8", 8)); + command.Parameters.Add(new("@P9", 9)); + command.Parameters.Add(new("@P10", 10)); - using var dataReader = command.ExecuteReader(); - - dataReader.Read(); - - result[0] = dataReader.GetInt64(0); - result[1] = dataReader.GetInt64(1); - result[2] = dataReader.GetInt64(2); - result[3] = dataReader.GetInt64(3); - result[4] = dataReader.GetInt64(4); - - return result; + return command.ExecuteScalar(); } [Benchmark(Baseline = false)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_Dapper() - { - var result = new Int64[5]; - - using var dataReader = SqlMapper.ExecuteReader( + public Object? Parameter_Dapper() => + SqlMapper.ExecuteScalar( this.connection, - "SELECT @P1, @P2, @P3, @P4, @P5", - new { P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5 } + "SELECT @P1 + @P2 + @P3 + @P4 + @P5 + @P6 + @P7 + @P8 + @P9 + @P10", + new { P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5, P6 = 6, P7 = 7, P8 = 8, P9 = 9, P10 = 10 } ); - dataReader.Read(); - - result[0] = dataReader.GetInt64(0); - result[1] = dataReader.GetInt64(1); - result[2] = dataReader.GetInt64(2); - result[3] = dataReader.GetInt64(3); - result[4] = dataReader.GetInt64(4); - - return result; - } - [Benchmark(Baseline = false)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_DbConnectionPlus() - { - var result = new Int64[5]; - - using var dataReader = this.connection.ExecuteReader( - $"SELECT {Parameter(1)}, {Parameter(2)}, {Parameter(3)}, {Parameter(4)}, {Parameter(5)}" + public Object Parameter_DbConnectionPlus() => + this.connection.ExecuteScalar( + $""" + SELECT {Parameter(1)} + {Parameter(2)} + {Parameter(3)} + {Parameter(4)} + {Parameter(5)} + + {Parameter(6)} + {Parameter(7)} + {Parameter(8)} + {Parameter(9)} + {Parameter(10)} + """ ); - dataReader.Read(); - - result[0] = dataReader.GetInt64(0); - result[1] = dataReader.GetInt64(1); - result[2] = dataReader.GetInt64(2); - result[3] = dataReader.GetInt64(3); - result[4] = dataReader.GetInt64(4); - - return result; - } - private const String Parameter_Category = "Parameter"; } diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index 88fd153..320b741 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -17,13 +17,12 @@ public static void Main(String[] args) else { var benchmarks = new Benchmarks(); - benchmarks.Exists__Setup(); + benchmarks.Parameter__Setup(); - for (int i = 0; i < 50000; i++) + for (int i = 0; i < 5000; i++) { - benchmarks.Exists_Command(); - benchmarks.Exists_Dapper(); - benchmarks.Exists_DbConnectionPlus(); + benchmarks.Parameter_Command(); + benchmarks.Parameter_DbConnectionPlus(); } } } diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs index 48a1107..3728bb8 100644 --- a/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs +++ b/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs @@ -11,5 +11,5 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// This is if no name could be inferred. /// /// The value of the parameter. -public readonly record struct InterpolatedParameter(String? InferredName, Object? Value) +public record InterpolatedParameter(String? InferredName, Object? Value) : IInterpolatedSqlStatementFragment; diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs index b35556c..4f51fe7 100644 --- a/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs +++ b/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs @@ -10,5 +10,5 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// The name for the table. /// The values with which to populate the table. /// The type of values in . -public readonly record struct InterpolatedTemporaryTable(String Name, IEnumerable Values, Type ValuesType) +public record InterpolatedTemporaryTable(String Name, IEnumerable Values, Type ValuesType) : IInterpolatedSqlStatementFragment; diff --git a/src/DbConnectionPlus/SqlStatements/Literal.cs b/src/DbConnectionPlus/SqlStatements/Literal.cs index e266da0..20ce284 100644 --- a/src/DbConnectionPlus/SqlStatements/Literal.cs +++ b/src/DbConnectionPlus/SqlStatements/Literal.cs @@ -7,5 +7,5 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// A fragment of an interpolated SQL statement that represents a literal string. /// /// The literal string. -internal readonly record struct Literal(String Value) : IInterpolatedSqlStatementFragment; +internal record Literal(String Value) : IInterpolatedSqlStatementFragment; diff --git a/src/DbConnectionPlus/SqlStatements/Parameter.cs b/src/DbConnectionPlus/SqlStatements/Parameter.cs index 445e8c0..fe932d5 100644 --- a/src/DbConnectionPlus/SqlStatements/Parameter.cs +++ b/src/DbConnectionPlus/SqlStatements/Parameter.cs @@ -8,5 +8,5 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// /// The name of the parameter. /// The value of the parameter. -internal readonly record struct Parameter(String Name, Object? Value) : IInterpolatedSqlStatementFragment; +internal record Parameter(String Name, Object? Value) : IInterpolatedSqlStatementFragment; From 1a65b9f261e6d78d400a70ab276fd0164ab1f278 Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Thu, 12 Feb 2026 00:00:57 +0100 Subject: [PATCH 18/19] WIP: Implement feature Optimistic Concurrency Support via Concurrency Tokens --- README.md | 8 ++++-- .../Benchmarks.DeleteEntities.cs | 6 ++--- .../Benchmarks.Parameter.cs | 10 +++---- .../Benchmarks.Query_ValueTuples.cs | 2 +- .../DbConnectionPlus.Benchmarks/Program.cs | 26 +++---------------- .../DbConnectionPlusConfiguration.cs | 2 +- .../MySql/MySqlEntityManipulator.cs | 6 ++--- .../MySql/MySqlTemporaryTableBuilder.cs | 18 ++++++------- .../Oracle/OracleTemporaryTableBuilder.cs | 4 +-- .../PostgreSqlTemporaryTableBuilder.cs | 2 +- .../SqlServerTemporaryTableBuilder.cs | 4 +-- .../Sqlite/SqliteTemporaryTableBuilder.cs | 4 +-- .../DbCommands/DbCommandBuilder.cs | 4 ++- .../DbConnectionExtensions.Configuration.cs | 5 ++++ .../DbConnectionExtensions.ExecuteReader.cs | 4 +-- src/DbConnectionPlus/Helpers/NameHelper.cs | 6 ++--- .../CommandDisposingDataReaderDecorator.cs | 2 +- .../IInterpolatedSqlStatementFragment.cs | 2 +- src/DbConnectionPlus/SqlStatements/Literal.cs | 1 - .../SqlStatements/Parameter.cs | 1 - ...ommandDisposingDataReaderDecoratorTests.cs | 4 +-- .../Mocks/MockDbParameterCollection.cs | 10 +++---- ...piTest.PublicApiHasNotChanged.verified.txt | 4 +-- .../StatementMethodTestsBase.cs | 4 +-- .../TestData/Entity.cs | 2 +- 25 files changed, 66 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 6beae60..7403be2 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,15 @@ A lightweight .NET ORM and extension library for the type that adds high-performance, type-safe helpers to reduce boilerplate code, boost productivity, and make working with SQL databases in C# more enjoyable. +If you frequently write SQL queries in your C# code and want to avoid boilerplate code, you have come to the right +place! + Highlights: -- Parameterized interpolated-string support -- On-the-fly temporary tables from in-memory collections +- [Parameterized interpolated-string support](#parameters-via-interpolated-strings) +- [On-the-fly temporary tables](#on-the-fly-temporary-tables-via-interpolated-strings) from in-memory collections - Entity mapping helpers (insert, update, delete, query) - Designed to be used in synchronous and asynchronous code paths +- Minimal performance and allocation overhead The following database systems are supported out of the box: - MySQL (via [MySqlConnector](https://www.nuget.org/packages/MySqlConnector/)) diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs index ecba7a0..437accf 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs @@ -33,7 +33,7 @@ public void DeleteEntities__Setup() => [BenchmarkCategory(DeleteEntities_Category)] public void DeleteEntities_Command() { - for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { using var command = this.connection.CreateCommand(); command.CommandText = "DELETE FROM Entity WHERE Id = @Id"; @@ -59,7 +59,7 @@ public void DeleteEntities_Command() [BenchmarkCategory(DeleteEntities_Category)] public void DeleteEntities_Dapper() { - for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); @@ -73,7 +73,7 @@ public void DeleteEntities_Dapper() [BenchmarkCategory(DeleteEntities_Category)] public void DeleteEntities_DbConnectionPlus() { - for (int i = 0; i < DeleteEntities_OperationsPerInvoke; i++) + for (var i = 0; i < DeleteEntities_OperationsPerInvoke; i++) { var entities = this.entitiesInDb.Take(DeleteEntities_EntitiesPerOperation).ToList(); diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs index 86a3d74..8daba2e 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs @@ -31,7 +31,7 @@ public void Parameter__Setup() => [Benchmark(Baseline = true)] [BenchmarkCategory(Parameter_Category)] - public Object? Parameter_Command() + public Int64 Parameter_Command() { using var command = this.connection.CreateCommand(); @@ -48,13 +48,13 @@ public void Parameter__Setup() => command.Parameters.Add(new("@P9", 9)); command.Parameters.Add(new("@P10", 10)); - return command.ExecuteScalar(); + return (Int64)command.ExecuteScalar()!; } [Benchmark(Baseline = false)] [BenchmarkCategory(Parameter_Category)] - public Object? Parameter_Dapper() => - SqlMapper.ExecuteScalar( + public Int64 Parameter_Dapper() => + SqlMapper.ExecuteScalar( this.connection, "SELECT @P1 + @P2 + @P3 + @P4 + @P5 + @P6 + @P7 + @P8 + @P9 + @P10", new { P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5, P6 = 6, P7 = 7, P8 = 8, P9 = 9, P10 = 10 } @@ -62,7 +62,7 @@ public void Parameter__Setup() => [Benchmark(Baseline = false)] [BenchmarkCategory(Parameter_Category)] - public Object Parameter_DbConnectionPlus() => + public Int64 Parameter_DbConnectionPlus() => this.connection.ExecuteScalar( $""" SELECT {Parameter(1)} + {Parameter(2)} + {Parameter(3)} + {Parameter(4)} + {Parameter(5)} + diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs index a1874f2..d6187b8 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs @@ -37,7 +37,7 @@ public void Query_ValueTuples__Setup() => var result = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(); using var command = this.connection.CreateCommand(); - + command.CommandText = "SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity"; using var dataReader = command.ExecuteReader(); diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs index 320b741..3d2df03 100644 --- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs +++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs @@ -1,29 +1,11 @@ -#pragma warning disable RCS1163, IDE0022 - using BenchmarkDotNet.Running; namespace RentADeveloper.DbConnectionPlus.Benchmarks; public static class Program { - public static void Main(String[] args) - { - if (args is not ["test"]) - { - BenchmarkSwitcher - .FromAssembly(typeof(Program).Assembly) - .Run(args); - } - else - { - var benchmarks = new Benchmarks(); - benchmarks.Parameter__Setup(); - - for (int i = 0; i < 5000; i++) - { - benchmarks.Parameter_Command(); - benchmarks.Parameter_DbConnectionPlus(); - } - } - } + public static void Main(String[] args) => + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); } diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs index fa62c32..8693884 100644 --- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs +++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs @@ -107,7 +107,7 @@ public EntityTypeBuilder Entity() if (!this.entityTypeBuilders.TryGetValue(typeof(TEntity), out var builder)) { builder = new EntityTypeBuilder(); - + this.entityTypeBuilders.Add(typeof(TEntity), builder); } diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs index 8a94a35..ef2336e 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlEntityManipulator.cs @@ -742,7 +742,7 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(entityTypeMetadata); var command = connection.CreateCommand(); - + command.CommandText = this.GetDeleteEntitySqlCode(entityTypeMetadata); command.Transaction = transaction; @@ -782,7 +782,7 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(entityTypeMetadata); var command = connection.CreateCommand(); - + command.CommandText = this.GetInsertEntitySqlCode(entityTypeMetadata); command.Transaction = transaction; @@ -818,7 +818,7 @@ EntityTypeMetadata entityTypeMetadata ArgumentNullException.ThrowIfNull(entityTypeMetadata); var command = connection.CreateCommand(); - + command.CommandText = this.GetUpdateEntitySqlCode(entityTypeMetadata); command.Transaction = transaction; diff --git a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs index 644b94d..6dc072b 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/MySql/MySqlTemporaryTableBuilder.cs @@ -81,10 +81,10 @@ public TemporaryTableDisposer BuildTemporaryTable( using var createCommand = connection.CreateCommand(); createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ); + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); createCommand.Transaction = transaction; using var cancellationTokenRegistration = @@ -186,10 +186,10 @@ public async Task BuildTemporaryTableAsync( #pragma warning restore CA2007 createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( - name, - valuesType, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ); + name, + valuesType, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); createCommand.Transaction = transaction; @@ -401,7 +401,7 @@ private static void DropTemporaryTable(String name, MySqlConnection connection, command.CommandText = $"DROP TEMPORARY TABLE IF EXISTS `{name}`"; command.Transaction = transaction; - + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); command.ExecuteNonQuery(); diff --git a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs index 0adc5a1..9b509f7 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Oracle/OracleTemporaryTableBuilder.cs @@ -90,7 +90,7 @@ public TemporaryTableDisposer BuildTemporaryTable( else { using var createCommand = connection.CreateCommand(); - + createCommand.CommandText = this.BuildCreateMultiColumnTemporaryTableSqlCode( quotedTableName, valuesType, @@ -553,7 +553,7 @@ private static void DropTemporaryTable( ) { using var command = connection.CreateCommand(); - + command.CommandText = $"DROP TABLE {quotedTableName}"; command.Transaction = transaction; diff --git a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs index 6a643a7..97a5bda 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilder.cs @@ -416,7 +416,7 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu private static void DropTemporaryTable(String name, NpgsqlConnection connection, NpgsqlTransaction? transaction) { using var command = connection.CreateCommand(); - + command.CommandText = $"DROP TABLE IF EXISTS \"{name}\""; command.Transaction = transaction; diff --git a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs index e6a4035..c63d73a 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilder.cs @@ -439,7 +439,7 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu private static void DropTemporaryTable(String name, SqlConnection connection, SqlTransaction? transaction) { using var command = connection.CreateCommand(); - + command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]"; command.Transaction = transaction; @@ -488,7 +488,7 @@ private static String GetCurrentDatabaseCollation( static (_, args) => { using var command = args.connection.CreateCommand(); - + command.CommandText = GetCurrentDatabaseCollationQuery; command.Transaction = args.transaction; diff --git a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs index d7ce45d..fcdb91d 100644 --- a/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilder.cs @@ -386,7 +386,7 @@ private static DbDataReader CreateValuesDataReader(IEnumerable values, Type valu private static void DropTemporaryTable(String name, SqliteConnection connection, SqliteTransaction? transaction) { using var command = connection.CreateCommand(); - + command.CommandText = $"DROP TABLE IF EXISTS temp.\"{name}\""; command.Transaction = transaction; @@ -411,7 +411,7 @@ private static async ValueTask DropTemporaryTableAsync( #pragma warning disable CA2007 await using var command = connection.CreateCommand(); #pragma warning restore CA2007 - + command.CommandText = $"DROP TABLE IF EXISTS temp.\"{name}\""; command.Transaction = transaction; diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs index cafa2a3..9e7fad8 100644 --- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs +++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs @@ -185,11 +185,13 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore( CancellationToken cancellationToken = default ) { - using var codeBuilder = new ValueStringBuilder(stackalloc char[512]); + using var codeBuilder = new ValueStringBuilder(stackalloc Char[512]); + var parameterNameOccurrences = new Dictionary( statement.Fragments.Count, StringComparer.OrdinalIgnoreCase ); + var parameterCount = 0; var command = connection.CreateCommand(); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs index e35a0a6..ceaa39c 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs @@ -15,6 +15,11 @@ public static partial class DbConnectionExtensions /// Configures DbConnectionPlus. /// /// The action that configures DbConnectionPlus. + /// + /// This method should only be called once during the application's lifetime. + /// This is because the configuration is frozen after it is set for the first time to ensure thread safety and to + /// prevent changes to the configuration after it has been used. + /// public static void Configure(Action configureAction) { ArgumentNullException.ThrowIfNull(configureAction); diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs index 9a3af8e..62489eb 100644 --- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs +++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs @@ -82,7 +82,7 @@ public static DbDataReader ExecuteReader( return new CommandDisposingDataReaderDecorator( dataReader, databaseAdapter, - commandDisposer, + commandDisposer, cancellationToken ); } @@ -171,7 +171,7 @@ public static async Task ExecuteReaderAsync( return new CommandDisposingDataReaderDecorator( dataReader, databaseAdapter, - commandDisposer, + commandDisposer, cancellationToken ); } diff --git a/src/DbConnectionPlus/Helpers/NameHelper.cs b/src/DbConnectionPlus/Helpers/NameHelper.cs index 9bc17f8..c58e7d8 100644 --- a/src/DbConnectionPlus/Helpers/NameHelper.cs +++ b/src/DbConnectionPlus/Helpers/NameHelper.cs @@ -44,18 +44,18 @@ internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan } var scanLength = Math.Min(expression.Length, maximumLength); - + var buffer = scanLength <= 512 ? stackalloc Char[scanLength] : new Char[scanLength]; ref var src = ref MemoryMarshal.GetReference(expression); ref var dst = ref MemoryMarshal.GetReference(buffer); var count = 0; - + for (var i = 0; i < scanLength; i++) { var character = Unsafe.Add(ref src, i); - + if ( (UInt32)(character - '0') <= 9 || // Digits (UInt32)(character - 'A') <= 25 || // Uppercase letters diff --git a/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs index 7186210..2c599e7 100644 --- a/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs +++ b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs @@ -337,8 +337,8 @@ protected override void Dispose(Boolean disposing) } private readonly CancellationToken commandCancellationToken; - private readonly IDatabaseAdapter databaseAdapter; private readonly DbCommandDisposer commandDisposer; + private readonly IDatabaseAdapter databaseAdapter; private readonly DbDataReader dataReader; private Boolean isDisposed; } diff --git a/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs b/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs index 5700758..89c0497 100644 --- a/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs +++ b/src/DbConnectionPlus/SqlStatements/IInterpolatedSqlStatementFragment.cs @@ -6,4 +6,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// /// Represents a fragment of an SQL statement created from an interpolated string. /// -internal interface IInterpolatedSqlStatementFragment; \ No newline at end of file +internal interface IInterpolatedSqlStatementFragment; diff --git a/src/DbConnectionPlus/SqlStatements/Literal.cs b/src/DbConnectionPlus/SqlStatements/Literal.cs index 20ce284..e3705ea 100644 --- a/src/DbConnectionPlus/SqlStatements/Literal.cs +++ b/src/DbConnectionPlus/SqlStatements/Literal.cs @@ -8,4 +8,3 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// /// The literal string. internal record Literal(String Value) : IInterpolatedSqlStatementFragment; - diff --git a/src/DbConnectionPlus/SqlStatements/Parameter.cs b/src/DbConnectionPlus/SqlStatements/Parameter.cs index fe932d5..783be96 100644 --- a/src/DbConnectionPlus/SqlStatements/Parameter.cs +++ b/src/DbConnectionPlus/SqlStatements/Parameter.cs @@ -9,4 +9,3 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements; /// The name of the parameter. /// The value of the parameter. internal record Parameter(String Name, Object? Value) : IInterpolatedSqlStatementFragment; - diff --git a/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs index 27336a1..b48aa5a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs @@ -58,7 +58,7 @@ public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanc using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, - commandDisposer, + commandDisposer, cancellationToken ); @@ -105,7 +105,7 @@ public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOp await using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, - commandDisposer, + commandDisposer, cancellationToken ); diff --git a/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs index e53b006..a0f5c0f 100644 --- a/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs +++ b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs @@ -25,10 +25,10 @@ public override Int32 Add(Object value) public override void Clear() => this.parameters.Clear(); /// - public override bool Contains(Object value) => this.parameters.Contains(value); + public override Boolean Contains(Object value) => this.parameters.Contains(value); /// - public override bool Contains(String value) => this.IndexOf(value) != -1; + public override Boolean Contains(String value) => this.IndexOf(value) != -1; /// public override void CopyTo(Array array, Int32 index) => @@ -43,7 +43,7 @@ public override void CopyTo(Array array, Int32 index) => /// public override Int32 IndexOf(String parameterName) { - for (Int32 index = 0; index < this.parameters.Count; ++index) + for (var index = 0; index < this.parameters.Count; ++index) { if (this.parameters[index].ParameterName == parameterName) return index; @@ -83,8 +83,8 @@ protected override void SetParameter(String parameterName, DbParameter value) => private Int32 IndexOfChecked(String parameterName) { - Int32 num = this.IndexOf(parameterName); - return num != -1 ? num : throw new IndexOutOfRangeException(); + var index = this.IndexOf(parameterName); + return index != -1 ? index : throw new IndexOutOfRangeException(); } private readonly List parameters = []; diff --git a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt index 04dcd52..f551f46 100644 --- a/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt +++ b/tests/DbConnectionPlus.UnitTests/PublicApiTest.PublicApiHasNotChanged.verified.txt @@ -251,7 +251,7 @@ namespace RentADeveloper.DbConnectionPlus.Exceptions } namespace RentADeveloper.DbConnectionPlus.SqlStatements { - public readonly struct InterpolatedParameter : System.IEquatable + public record InterpolatedParameter : System.IEquatable { public InterpolatedParameter(string? InferredName, object? Value) { } public string? InferredName { get; init; } @@ -276,7 +276,7 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements public static bool operator !=(RentADeveloper.DbConnectionPlus.SqlStatements.InterpolatedSqlStatement left, RentADeveloper.DbConnectionPlus.SqlStatements.InterpolatedSqlStatement right) { } public static bool operator ==(RentADeveloper.DbConnectionPlus.SqlStatements.InterpolatedSqlStatement left, RentADeveloper.DbConnectionPlus.SqlStatements.InterpolatedSqlStatement right) { } } - public readonly struct InterpolatedTemporaryTable : System.IEquatable + public record InterpolatedTemporaryTable : System.IEquatable { public InterpolatedTemporaryTable(string Name, System.Collections.IEnumerable Values, System.Type ValuesType) { } public string Name { get; init; } diff --git a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs index dfe46b2..d83d1a0 100644 --- a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs @@ -40,7 +40,7 @@ await this.asyncTestMethod( ); this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandTimeout == (Int32) timeout.TotalSeconds), + Arg.Is(cmd => cmd.CommandTimeout == (Int32)timeout.TotalSeconds), Arg.Any>() ); } @@ -98,7 +98,7 @@ public void SyncMethod_ShouldUseCommandTimeout() ); this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandTimeout == (Int32) timeout.TotalSeconds), + Arg.Is(cmd => cmd.CommandTimeout == (Int32)timeout.TotalSeconds), Arg.Any>() ); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 81824d0..212eff0 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -25,4 +25,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 +} From b8a501c75e078573b541d61bdf4ac64e075086ff Mon Sep 17 00:00:00 2001 From: David Liebeherr Date: Sat, 14 Feb 2026 18:40:52 +0100 Subject: [PATCH 19/19] feat: Implement feature Optimistic Concurrency Support via Concurrency Tokens Fixes #5 --- CHANGELOG.md | 3 +-- README.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3437d31..e056b49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ 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/). -TODO: Update date -## [1.2.0] - 2026-02-XX +## [1.2.0] - 2026-02-14 ### Added - Optimistic Concurrency Support via Concurrency Tokens (Fixes [issue #5](https://github.com/rent-a-developer/DbConnectionPlus/issues/5)) diff --git a/README.md b/README.md index 7403be2..01d051c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ A lightweight .NET ORM and extension library for the type that adds high-performance, type-safe helpers to reduce boilerplate code, boost productivity, and make working with SQL databases in C# more enjoyable. -If you frequently write SQL queries in your C# code and want to avoid boilerplate code, you have come to the right -place! +If you frequently write SQL queries in your C# code and want to avoid boilerplate code, you will love DbConnectionPlus! Highlights: - [Parameterized interpolated-string support](#parameters-via-interpolated-strings)