Skip to content

Commit

Permalink
#45 Properly implemented and improved exception handling and cancella…
Browse files Browse the repository at this point in the history
…tion support.

- Proper exceptions are now throw for errors
- Exceptions that occur in a batch get wrapped in either a SqlProgramExecutionException or a SqlBatchExecutionException depending on the location.
  • Loading branch information
billings7 committed May 24, 2017
1 parent 4fee2b7 commit 3029860
Show file tree
Hide file tree
Showing 23 changed files with 2,288 additions and 317 deletions.
76 changes: 63 additions & 13 deletions Database/DbBatchDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
using System.Threading.Tasks;
using System.Xml;
using WebApplications.Utilities.Annotations;
using WebApplications.Utilities.Database.Exceptions;
using WebApplications.Utilities.Logging;

namespace WebApplications.Utilities.Database
{
Expand All @@ -47,6 +49,9 @@ namespace WebApplications.Utilities.Database
[PublicAPI]
public abstract class DbBatchDataReader : DbDataReader
{
[NotNull]
private readonly SqlBatch _batch;

[NotNull]
private readonly DbDataReader _baseReader;

Expand All @@ -71,6 +76,7 @@ protected DbDataReader BaseReaderOpen([CallerMemberName] string name = null)
{
if (IsOpen) return _baseReader;
if (IsFinished) throw FinishedError();
// ReSharper disable once ExplicitCallerInfoArgument
throw ClosedError(name);
}

Expand Down Expand Up @@ -138,6 +144,12 @@ internal BatchReaderState State
/// </summary>
protected readonly CommandBehavior CommandBehavior;

/// <summary>
/// The reference to the flag indicating if there are any more result sets.
/// </summary>
[NotNull]
private readonly ValueReference<bool> _hasResultSet;

/// <summary>
/// Whether to skip the remaining rows
/// </summary>
Expand All @@ -156,13 +168,21 @@ protected bool IsCommandBehavior(CommandBehavior condition)
/// <summary>
/// Initializes a new instance of the <see cref="DbBatchDataReader" /> class.
/// </summary>
/// <param name="batch">The batch being read.</param>
/// <param name="baseReader">The base reader.</param>
/// <param name="commandBehavior">The command behavior.</param>
/// <param name="hasResultSet">The reference to the flag indicating if there are any more result sets.</param>
/// <exception cref="System.ArgumentNullException">baseReader</exception>
protected internal DbBatchDataReader([NotNull] DbDataReader baseReader, CommandBehavior commandBehavior)
protected internal DbBatchDataReader(
[NotNull]SqlBatch batch,
[NotNull] DbDataReader baseReader,
CommandBehavior commandBehavior,
[NotNull] ValueReference<bool> hasResultSet)
{
_batch = batch ?? throw new ArgumentNullException(nameof(batch));
_baseReader = baseReader ?? throw new ArgumentNullException(nameof(baseReader));
CommandBehavior = commandBehavior;
_hasResultSet = hasResultSet ?? throw new ArgumentNullException(nameof(hasResultSet));
}

/// <summary>Gets the number of columns in the current row.</summary>
Expand Down Expand Up @@ -264,9 +284,12 @@ public override bool NextResult()
}

// Read the rest of the record set
while (BaseReader.Read()) { }
while (BaseReader.Read())
{
}

bool result = BaseReader.NextResult();
_hasResultSet.Value = result;
if (!result)
return false;
if (IsOpen) return true;
Expand Down Expand Up @@ -298,9 +321,13 @@ public override Task<bool> NextResultAsync(CancellationToken cancellationToken)
async Task<bool> Next()
{
// Read the rest of the record set
while (await BaseReader.ReadAsync().ConfigureAwait(false)) { }
while (await BaseReader.ReadAsync().ConfigureAwait(false))
{
}

// ReSharper disable once PossibleNullReferenceException
bool result = await BaseReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
_hasResultSet.Value = result;
if (!result)
return false;

Expand All @@ -317,17 +344,30 @@ async Task<bool> Next()
/// <returns></returns>
internal async Task StartAsync(CancellationToken cancellationToken)
{
// Expecting a single row with a single column with the string "Start"

if (!await BaseReader.ReadAsync(cancellationToken).ConfigureAwait(false))
throw new InvalidDataException("Missing start record set.");
{
throw new SqlBatchExecutionException(
_batch,
LoggingLevel.Critical,
() => Resources.DbBatchDataReader_StartAsync_MissingStart);
}

if (BaseReader.FieldCount != 1
|| !await BaseReader.IsDBNullAsync(0, cancellationToken).ConfigureAwait(false)
// ReSharper disable once PossibleNullReferenceException
|| await BaseReader.IsDBNullAsync(0, cancellationToken).ConfigureAwait(false)
|| !"Start".Equals(BaseReader.GetValue(0))
|| await BaseReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new InvalidDataException("Start record set in the wrong format.");
throw new SqlBatchExecutionException(
_batch,
LoggingLevel.Critical,
() => Resources.DbBatchDataReader_StartAsync_WrongFormat);
}

await BaseReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
// ReSharper disable once PossibleNullReferenceException
_hasResultSet.Value = await BaseReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -343,8 +383,9 @@ internal Task ReadTillClosedAsync(CancellationToken cancellationToken)
async Task DoAsync()
{
// ReSharper disable once PossibleNullReferenceException
while (IsOpen && await BaseReader.NextResultAsync(cancellationToken).ConfigureAwait(false))
while (IsOpen && _hasResultSet)
{
_hasResultSet.Value = await BaseReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
}
}
}
Expand Down Expand Up @@ -525,7 +566,7 @@ public override long GetChars(int ordinal, long dataOffset, char[] buffer, int b
/// <exception cref="T:System.InvalidOperationException">The connection drops or is closed during the data retrieval.The <see cref="T:System.Data.SqlClient.SqlDataReader" /> is closed during the data retrieval.There is no data ready to be read (for example, the first <see cref="M:System.Data.SqlClient.SqlDataReader.Read" /> hasn't been called, or returned false).Tried to read a previously-read column in sequential mode.There was an asynchronous operation in progress. This applies to all Get* methods when running in sequential mode, as they could be called while reading a stream.</exception>
/// <exception cref="T:System.IndexOutOfRangeException">Trying to read a column that does not exist.</exception>
/// <exception cref="T:System.InvalidCastException">
/// <paramref name="T" /> doesn’t match the type returned by SQL Server or cannot be cast.</exception>
/// <typeparamref name="T" /> doesn’t match the type returned by SQL Server or cannot be cast.</exception>
public override T GetFieldValue<T>(int ordinal) => BaseReaderOpen().GetFieldValue<T>(ordinal);

/// <summary>Asynchronously gets the value of the specified column as a type.</summary>
Expand All @@ -536,7 +577,7 @@ public override long GetChars(int ordinal, long dataOffset, char[] buffer, int b
/// <exception cref="T:System.InvalidOperationException">The connection drops or is closed during the data retrieval.The <see cref="T:System.Data.Common.DbDataReader" /> is closed during the data retrieval.There is no data ready to be read (for example, the first <see cref="M:System.Data.Common.DbDataReader.Read" /> hasn't been called, or returned false).Tried to read a previously-read column in sequential mode.There was an asynchronous operation in progress. This applies to all Get* methods when running in sequential mode, as they could be called while reading a stream.</exception>
/// <exception cref="T:System.IndexOutOfRangeException">Trying to read a column that does not exist.</exception>
/// <exception cref="T:System.InvalidCastException">
/// <paramref name="T" /> doesn’t match the type returned by the data source or cannot be cast.</exception>
/// <typeparamref name="T" /> doesn’t match the type returned by the data source or cannot be cast.</exception>
public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
=> BaseReaderOpen().GetFieldValueAsync<T>(ordinal, cancellationToken);

Expand Down Expand Up @@ -632,23 +673,30 @@ public sealed class SqlBatchDataReader : DbBatchDataReader
[NotNull]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private new SqlDataReader BaseReaderOpen([CallerMemberName] string name = null)
// ReSharper disable once ExplicitCallerInfoArgument
=> (SqlDataReader)base.BaseReaderOpen(name);

/// <summary>
/// Initializes a new instance of the <see cref="DbBatchDataReader" /> class.
/// </summary>
/// <param name="batch">The batch being read.</param>
/// <param name="baseReader">The base reader.</param>
/// <param name="commandBehavior">The command behavior.</param>
internal SqlBatchDataReader([NotNull] SqlDataReader baseReader, CommandBehavior commandBehavior)
: base(baseReader, commandBehavior)
/// <param name="hasResultSet">The reference to the flag indicating if there are any more result sets.</param>
internal SqlBatchDataReader(
[NotNull] SqlBatch batch,
[NotNull] SqlDataReader baseReader,
CommandBehavior commandBehavior,
[NotNull] ValueReference<bool> hasResultSet)
: base(batch, baseReader, commandBehavior, hasResultSet)
{
}

/// <summary>Fills an array of <see cref="T:System.Object" /> that contains the values for all the columns in the record, expressed as SQL Server types.</summary>
/// <returns>An integer indicating the number of columns copied.</returns>
/// <param name="values">An array of <see cref="T:System.Object" /> into which to copy the values. The column values are expressed as SQL Server types.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="values" /> is null. </exception>
public int GetSqlValues(Object[] values) => BaseReaderOpen().GetSqlValues(values);
public int GetSqlValues([NotNull] Object[] values) => BaseReaderOpen().GetSqlValues(values);

/// <summary>Gets the value of the specified column as <see cref="T:System.Data.SqlTypes.SqlBytes" />.</summary>
/// <returns>The value of the specified column.</returns>
Expand Down Expand Up @@ -734,6 +782,7 @@ internal SqlBatchDataReader([NotNull] SqlDataReader baseReader, CommandBehavior
/// <exception cref="T:System.InvalidOperationException">An attempt was made to read or access columns in a closed <see cref="T:System.Data.SqlClient.SqlDataReader" />.</exception>
/// <exception cref="T:System.InvalidCastException">The retrieved data is not compatible with the <see cref="T:System.Data.SqlTypes.SqlXml" /> type.</exception>
[NotNull]
// ReSharper disable once AssignNullToNotNullAttribute
public SqlXml GetSqlXml(int i) => BaseReaderOpen().GetSqlXml(i);

/// <summary>Retrieves data of type XML as an <see cref="T:System.Xml.XmlReader" />.</summary>
Expand All @@ -743,6 +792,7 @@ internal SqlBatchDataReader([NotNull] SqlDataReader baseReader, CommandBehavior
/// <exception cref="T:System.InvalidOperationException">An attempt was made to read or access columns in a closed <see cref="T:System.Data.SqlClient.SqlDataReader" />.</exception>
/// <exception cref="T:System.InvalidCastException">The retrieved data is not compatible with the <see cref="T:System.Data.SqlTypes.SqlXml" /> type.</exception>
[NotNull]
// ReSharper disable once AssignNullToNotNullAttribute
public XmlReader GetXmlReader(int i) => BaseReaderOpen().GetXmlReader(i);

/// <summary>Retrieves the value of the specified column as a <see cref="T:System.DateTimeOffset" /> object.</summary>
Expand Down
58 changes: 58 additions & 0 deletions Database/Exceptions/ContextReservations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#region © Copyright Web Applications (UK) Ltd, 2017. All rights reserved.
// Copyright (c) 2017, Web Applications UK Ltd
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Web Applications UK Ltd nor the
// names of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL WEB APPLICATIONS UK LTD BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion

using System;
using WebApplications.Utilities.Annotations;
using WebApplications.Utilities.Logging;

namespace WebApplications.Utilities.Database.Exceptions
{
/// <summary>
/// <see cref="LogContext"/> key reservations for exceptions.
/// </summary>
internal class ContextReservations
{
/// <summary>
/// The prefix reservation.
/// </summary>
public static readonly Guid PrefixReservation = Guid.NewGuid();

/// <summary>
/// The program name context key.
/// </summary>
[NotNull]
public static readonly string ProgramNameContextKey
= LogContext.ReserveKey("SqlProgram Name", PrefixReservation);

/// <summary>
/// The batch ID context key.
/// </summary>
[NotNull]
public static readonly string BatchIdContextKey
= LogContext.ReserveKey("SqlBatch ID", PrefixReservation);
}
}
Loading

0 comments on commit 3029860

Please sign in to comment.