From ec8ff04db76da3c8d220b7d2acf2fb5fceb1a996 Mon Sep 17 00:00:00 2001 From: Vedran B Date: Tue, 28 Nov 2023 20:02:47 +0100 Subject: [PATCH] Initial 5.4.0 --- Norm/Execute/NormExecute.cs | 4 +- Norm/Execute/NormExecuteAsync.cs | 8 +- Norm/Extensions/Execute.cs | 10 +-- Norm/Extensions/NormExtensions.cs | 22 +++++- Norm/Norm.cs | 75 +++++++++++++++++-- Norm/Norm.csproj | 8 +- .../FormattableUnitTests.cs | 9 ++- .../RowsAffectedUnitTests.cs | 38 ++++++++++ .../WithTransactionUnitTests.cs | 8 +- changelog.md | 65 ++++++++++++++++ 10 files changed, 217 insertions(+), 30 deletions(-) create mode 100644 Tests/PostgreSqlUnitTests/RowsAffectedUnitTests.cs diff --git a/Norm/Execute/NormExecute.cs b/Norm/Execute/NormExecute.cs index faeead7..9713946 100644 --- a/Norm/Execute/NormExecute.cs +++ b/Norm/Execute/NormExecute.cs @@ -28,7 +28,7 @@ public Norm Execute(string command, this.sourceFilePath = sourceFilePath; this.sourceLineNumber = sourceLineNumber; using var cmd = CreateCommand(command); - cmd.ExecuteNonQuery(); + this.recordsAffected = cmd.ExecuteNonQuery(); return this; } @@ -54,7 +54,7 @@ public Norm ExecuteFormat(FormattableString command, this.sourceFilePath = sourceFilePath; this.sourceLineNumber = sourceLineNumber; using var cmd = CreateCommand(command); - cmd.ExecuteNonQuery(); + this.recordsAffected = cmd.ExecuteNonQuery(); return this; } } diff --git a/Norm/Execute/NormExecuteAsync.cs b/Norm/Execute/NormExecuteAsync.cs index 9b24619..0c0d71b 100644 --- a/Norm/Execute/NormExecuteAsync.cs +++ b/Norm/Execute/NormExecuteAsync.cs @@ -32,11 +32,11 @@ public async ValueTask ExecuteAsync(string command, using var cmd = await CreateCommandAsync(command); if (cancellationToken.HasValue) { - await cmd.ExecuteNonQueryAsync(cancellationToken.Value); + this.recordsAffected = await cmd.ExecuteNonQueryAsync(cancellationToken.Value); } else { - await cmd.ExecuteNonQueryAsync(); + this.recordsAffected = await cmd.ExecuteNonQueryAsync(); } return this; } @@ -65,11 +65,11 @@ public async ValueTask ExecuteFormatAsync(FormattableString command, using var cmd = await CreateCommandAsync(command); if (cancellationToken.HasValue) { - await cmd.ExecuteNonQueryAsync(cancellationToken.Value); + this.recordsAffected = await cmd.ExecuteNonQueryAsync(cancellationToken.Value); } else { - await cmd.ExecuteNonQueryAsync(); + this.recordsAffected = await cmd.ExecuteNonQueryAsync(); } return this; } diff --git a/Norm/Extensions/Execute.cs b/Norm/Extensions/Execute.cs index 5357740..309a54f 100644 --- a/Norm/Extensions/Execute.cs +++ b/Norm/Extensions/Execute.cs @@ -14,7 +14,7 @@ public static partial class NormExtensions ///SQL command text. ///Database parameters object (anonymous object or SqlParameter array). ///Same DbConnection instance. - public static DbConnection Execute(this DbConnection connection, string command, + public static Norm Execute(this DbConnection connection, string command, object parameters = null, #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) [CallerMemberName] string memberName = "", @@ -22,8 +22,7 @@ public static DbConnection Execute(this DbConnection connection, string command, [CallerLineNumber] int sourceLineNumber = 0) #pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) { - connection.Instance().Execute(command, parameters, memberName, sourceFilePath, sourceLineNumber); - return connection; + return connection.Instance().Execute(command, parameters, memberName, sourceFilePath, sourceLineNumber); } /// /// Parse interpolated (formattable) command as database parameters and execute resulting SQL. @@ -32,7 +31,7 @@ public static DbConnection Execute(this DbConnection connection, string command, ///SQL command text. ///Database parameters object (anonymous object or SqlParameter array). ///Same DbConnection instance. - public static DbConnection ExecuteFormat(this DbConnection connection, FormattableString command, + public static Norm ExecuteFormat(this DbConnection connection, FormattableString command, object parameters = null, #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) [CallerMemberName] string memberName = "", @@ -40,8 +39,7 @@ public static DbConnection ExecuteFormat(this DbConnection connection, Formattab [CallerLineNumber] int sourceLineNumber = 0) #pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) { - connection.Instance().ExecuteFormat(command, parameters, memberName, sourceFilePath, sourceLineNumber); - return connection; + return connection.Instance().ExecuteFormat(command, parameters, memberName, sourceFilePath, sourceLineNumber); } } } diff --git a/Norm/Extensions/NormExtensions.cs b/Norm/Extensions/NormExtensions.cs index 26499c1..9732b01 100644 --- a/Norm/Extensions/NormExtensions.cs +++ b/Norm/Extensions/NormExtensions.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading; namespace Norm { - public static partial class NormExtensions { private static T Instance(this DbConnection connection) where T : Norm @@ -218,5 +218,25 @@ public static Norm WithUnknownResultType(this DbConnection connection, params bo { return connection.Instance().WithUnknownResultType(list); } + + /// + ///Returns number of rows affected for this instance (rows changed, inserted, or deleted by execution of the SQL statement). + ///This is a number returned by the command ExecuteNonQuery() call for the last executed command. + ///Or, value of RecordsAffected reader property of the last read command. + /// + public static int? GetRecordsAffected(this DbConnection connection) + { + return connection.Instance().GetRecordsAffected(); + } + + /// + /// Creates a new Norm Instance + /// + /// + /// + public static Norm Norm(this DbConnection connection) + { + return connection.Instance(); + } } } \ No newline at end of file diff --git a/Norm/Norm.cs b/Norm/Norm.cs index 80c19a7..4938629 100644 --- a/Norm/Norm.cs +++ b/Norm/Norm.cs @@ -1,13 +1,15 @@ using System; using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; namespace Norm { - public partial class Norm + public partial class Norm : IDisposable, IAsyncDisposable { protected string commandText; protected string commentHeader; @@ -36,8 +38,9 @@ public partial class Norm protected string memberName = null; protected string sourceFilePath = null; protected int sourceLineNumber = 0; + protected int? recordsAffected = null; private string[] names = null; - + public DatabaseType DbType => dbType; public Norm(DbConnection connection) @@ -60,10 +63,52 @@ public Norm(DbConnection connection) } } + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + + Dispose(disposing: false); +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + GC.SuppressFinalize(this); +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Connection?.Dispose(); + Connection = null; + transaction?.Dispose(); + transaction = null; + } + } + + protected virtual async ValueTask DisposeAsyncCore() + { + if (Connection != null) + { + await Connection.DisposeAsync().ConfigureAwait(false); + Connection = null; + } + + if (transaction != null) + { + await transaction.DisposeAsync().ConfigureAwait(false); + transaction = null; + } + } + /// ///returns DbConnection for this instance /// - public DbConnection Connection { get; } + public DbConnection Connection { get; private set; } /// ///returns CancellationToken for this instance or null if CancellationToken is not set @@ -75,6 +120,16 @@ public Norm(DbConnection connection) /// public Func<(string Name, int Ordinal, DbDataReader Reader), object> ReaderCallback { get => readerCallback; } + /// + ///Returns number of rows affected for this instance (rows changed, inserted, or deleted by execution of the SQL statement). + ///This is a number returned by the command ExecuteNonQuery() call for the last executed command. + ///Or, value of RecordsAffected reader property of the last read command. + /// + public int? GetRecordsAffected() + { + return this.recordsAffected; + } + /// /// Set command type for the connection commands and return Norm instance. /// Default command type for new instance is Text. @@ -422,7 +477,9 @@ public async ValueTask CreateCommandAsync(FormattableString command) /// DbDataReader public DbDataReader ExecuteReader(DbCommand cmd) { - return cmd.ExecuteReader(this.behavior); + var reader = cmd.ExecuteReader(this.behavior); + this.recordsAffected = reader.RecordsAffected; + return reader; } /// @@ -432,11 +489,17 @@ public DbDataReader ExecuteReader(DbCommand cmd) /// DbDataReader public async ValueTask ExecuteReaderAsync(DbCommand cmd) { + DbDataReader reader; if (this.cancellationToken.HasValue) { - return await cmd.ExecuteReaderAsync(this.behavior, this.cancellationToken.Value); + reader = await cmd.ExecuteReaderAsync(this.behavior, this.cancellationToken.Value); + } + else + { + reader = await cmd.ExecuteReaderAsync(this.behavior); } - return await cmd.ExecuteReaderAsync(this.behavior); + this.recordsAffected = reader.RecordsAffected; + return reader; } } } diff --git a/Norm/Norm.csproj b/Norm/Norm.csproj index 724e853..927a307 100644 --- a/Norm/Norm.csproj +++ b/Norm/Norm.csproj @@ -13,12 +13,12 @@ High performance micro-ORM modern Dapper replacement for .NET Standard 2.1 and higher VB-Consulting Norm;norm;data acceess;orm;no-orm;micro-orm;sql - 5.3.9 - 5.3.9 + 5.4.0 + 5.4.0 https://github.com/vb-consulting/Norm.net/blob/master/changelog.md Norm.net true - 5.3.9 + 5.4.0 snupkg true true @@ -26,7 +26,7 @@ true $(NoWarn);1591 bin\$(Configuration)\$(AssemblyName).xml - 5.3.9 + 5.4.0 diff --git a/Tests/PostgreSqlUnitTests/FormattableUnitTests.cs b/Tests/PostgreSqlUnitTests/FormattableUnitTests.cs index ab799b7..7ac02ca 100644 --- a/Tests/PostgreSqlUnitTests/FormattableUnitTests.cs +++ b/Tests/PostgreSqlUnitTests/FormattableUnitTests.cs @@ -79,14 +79,17 @@ public void Execute_Format_With_Sql_Params_Test() public void Execute_Format_Mixed_Params_Test() { using var connection = new NpgsqlConnection(fixture.ConnectionString); - var result = connection + + connection .Execute("begin") .Execute("create table test (i int, t text, d date)") - .ExecuteFormat($"insert into test values ({1}, {new NpgsqlParameter("", "foo")}, {new DateTime(1977, 5, 19)})") + .ExecuteFormat($"insert into test values ({1}, {new NpgsqlParameter("", "foo")}, {new DateTime(1977, 5, 19)})"); + + var result = connection .Read("select * from test") .Single() .ToDictionary(t => t.name, t => t.value); - + Assert.Equal(1, result["i"]); Assert.Equal("foo", result["t"]); Assert.Equal(new DateTime(1977, 5, 19), result["d"]); diff --git a/Tests/PostgreSqlUnitTests/RowsAffectedUnitTests.cs b/Tests/PostgreSqlUnitTests/RowsAffectedUnitTests.cs new file mode 100644 index 0000000..1c16978 --- /dev/null +++ b/Tests/PostgreSqlUnitTests/RowsAffectedUnitTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Norm; +using Npgsql; +using Xunit; + +namespace PostgreSqlUnitTests; + +[Collection("PostgreSqlDatabase")] +public class RowsAffectedUnitTests +{ + private readonly PostgreSqlFixture fixture; + + public RowsAffectedUnitTests(PostgreSqlFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void Execute_Format_Test() + { + using var connection = new NpgsqlConnection(fixture.ConnectionString); + connection.Execute("create temp table rows_affected_test (t text)"); + + var rowsAffected = connection + .Execute("insert into rows_affected_test values ('foo')") + .GetRecordsAffected(); + + Assert.Equal(1, rowsAffected); + + var inst = connection.Norm(); + inst.Read("select * from rows_affected_test").ToList(); + rowsAffected = inst.GetRecordsAffected(); + + Assert.Equal(1, rowsAffected); + } +} diff --git a/Tests/PostgreSqlUnitTests/WithTransactionUnitTests.cs b/Tests/PostgreSqlUnitTests/WithTransactionUnitTests.cs index ae09b15..bcce540 100644 --- a/Tests/PostgreSqlUnitTests/WithTransactionUnitTests.cs +++ b/Tests/PostgreSqlUnitTests/WithTransactionUnitTests.cs @@ -20,8 +20,8 @@ public WithTransactionUnitTests(PostgreSqlFixture fixture) [Fact] public void WithTransaction_Rollback_Test() { - using var connection = new NpgsqlConnection(fixture.ConnectionString) - .Execute("create temp table transaction_test1 (i int);"); + using var connection = new NpgsqlConnection(fixture.ConnectionString); + connection.Execute("create temp table transaction_test1 (i int);"); using var transaction = connection.BeginTransaction(); @@ -41,8 +41,8 @@ public void WithTransaction_Rollback_Test() [Fact] public async Task WithTransaction_Rollback_Test_Async() { - await using var connection = new NpgsqlConnection(fixture.ConnectionString) - .Execute("create temp table transaction_test2 (i int);"); + await using var connection = new NpgsqlConnection(fixture.ConnectionString); + connection.Execute("create temp table transaction_test2 (i int);"); await using var transaction = await connection.BeginTransactionAsync(); diff --git a/changelog.md b/changelog.md index 357f6b9..63308cd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,70 @@ # Changelog +## [5.4.0](https://github.com/vb-consulting/Norm.net/tree/5.3.9) (2023-11-27) + +[Full Changelog](https://github.com/vb-consulting/Norm.net/compare/5.3.9...5.4.0) + +### New feature: GetRecordsAffected method + +Signature: + +- Extension: `public static int? GetRecordsAffected(this DbConnection connection)` +- Instance: `public int? GetRecordsAffected()` + +Returns a number of records affected by the last query. + +This is the value that [`ExecuteNonQuery()`](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.executenonquery) method returns if one of the `Execute` versions is executed. + +Example: + +```csharp +var rowsAffected = connection + .Execute("insert into rows_affected_test values ('foo')") + .GetRecordsAffected(); +``` + +If one of the `Read` methods is executed, this method will contain a value of [`RecordsAffected` reader property](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqldatareader.recordsaffected) + +However, `Read` methods will always return a value of the read operation (enumerator), so access to the instance is hidden. + +That is why the `Norm` extension method is also introduced. + +### New feature: Norm extension method + +Signature: + +- Extension: `public static Norm Norm(this DbConnection connection)` + +Creates and returns a new `Norm` instance from the connection. + +Example: + +```csharp +var instance = connection.Norm(); +instance.Read("select * from rows_affected_test").ToList(); +rowsAffected = instance.GetRecordsAffected(); +``` + +### Breaking changes: + +- Extensions `Execute` and `ExecuteFormat` were returning the connection instance in previous versions. +- This is changed to return the current `Norm` instance instead. + +This may break certain codebases. + +For example, before this version, it was possible to chain `Execute` method with connection creation like this: + +```csharp +using var connection = new NpgsqlConnection(connectionString).Execute("create temp table test (i int);"); +``` + +Now, this code won't work because `Execute` method returns `Norm` instance. Instead, it should look like this: + +```csharp +using var connection = new NpgsqlConnection(connectionString); +connection.Execute("create temp table test (i int);"); +``` + ## [5.3.9](https://github.com/vb-consulting/Norm.net/tree/5.3.9) (2023-10-07) [Full Changelog](https://github.com/vb-consulting/Norm.net/compare/5.3.8...5.3.9)