Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions build/common.props
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project>
<PropertyGroup>
<TargetFrameworks>netcoreapp1.0;netcoreapp1.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net46;netstandard2.0</TargetFrameworks>
<VersionPrefix>0.19.2</VersionPrefix>
<VersionPrefix>0.19.3</VersionPrefix>
<AssemblyVersion>0.11.0.0</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>dataaction</Authors>
<PackageTags>Sybase ASE Adaptive SAP AseClient DbProvider</PackageTags>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/DataAction/AdoNetCore.AseClient</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReleaseNotes>Refer to GitHub - https://github.com/DataAction/AdoNetCore.AseClient/releases/tag/0.19.2</PackageReleaseNotes>
<PackageReleaseNotes>Refer to GitHub - https://github.com/DataAction/AdoNetCore.AseClient/releases</PackageReleaseNotes>
<RepositoryUrl>https://github.com/DataAction/AdoNetCore.AseClient</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
Expand All @@ -23,7 +23,7 @@
<DefineConstants>$(DefineConstants);ENABLE_DB_DATAPERMISSION</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'net46' Or '$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>$(DefineConstants);ENABLE_SYSTEM_DATA_COMMON_EXTENSIONS;ENABLE_CLONEABLE_INTERFACE;ENABLE_SYSTEMEXCEPTION</DefineConstants>
<DefineConstants>$(DefineConstants);ENABLE_SYSTEM_DATA_COMMON_EXTENSIONS;ENABLE_CLONEABLE_INTERFACE;ENABLE_SYSTEMEXCEPTION;ENABLE_SYSTEM_TRANSACTIONS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'netcoreapp1.0' Or '$(TargetFramework)' == 'netcoreapp1.1'">
<DefineConstants>$(DefineConstants);ENABLE_ARRAY_POOL</DefineConstants>
Expand All @@ -38,9 +38,12 @@
<PackageReference Include="System.Data.Common" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0'">
<PackageReference Include="System.Security.Permissions">
<Version>4.5.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Permissions" Version="4.5.0" />
</ItemGroup>

<!-- System.Transactions reference for .NET Framework -->
<ItemGroup Condition="'$(TargetFramework)' == 'net46'">
<Reference Include="System.Transactions" />
</ItemGroup>
<ItemGroup>
<None Include="../../icon.png" Pack="true" Visible="false" PackagePath="" />
Expand Down
69 changes: 68 additions & 1 deletion src/AdoNetCore.AseClient/AseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
using System.Data;
using System.Data.Common;
using System.Net.Security;
#if ENABLE_SYSTEM_TRANSACTIONS
using System.Transactions;
using IsolationLevel = System.Data.IsolationLevel;
#endif
using AdoNetCore.AseClient.Interface;
using AdoNetCore.AseClient.Internal;

Expand All @@ -26,6 +30,9 @@ public sealed class AseConnection : DbConnection
private AseTransaction _transaction;
private readonly IEventNotifier _eventNotifier;
private bool? _namedParameters;
#if ENABLE_SYSTEM_TRANSACTIONS
private AseEnlistedTransaction _enlistedTransaction;
#endif

/// <summary>
/// Initializes a new instance of the <see cref="AseConnection" /> class.
Expand Down Expand Up @@ -271,9 +278,10 @@ public override void Open()

InternalState = ConnectionState.Connecting;

IConnectionParameters parameters = null;
try
{
var parameters = ConnectionParameters.Parse(_connectionString);
parameters = ConnectionParameters.Parse(_connectionString);

_internal = _connectionPoolManager.Reserve(_connectionString, parameters, _eventNotifier, UserCertificateValidationCallback);

Expand All @@ -286,6 +294,14 @@ public override void Open()
}

InternalState = ConnectionState.Open;

#if ENABLE_SYSTEM_TRANSACTIONS
// Auto-enlist in ambient transaction if Enlist=true (default)
if (parameters.Enlist && System.Transactions.Transaction.Current != null)
{
EnlistTransaction(System.Transactions.Transaction.Current);
}
#endif
}

/// <summary>
Expand Down Expand Up @@ -631,6 +647,57 @@ public AseTransaction Transaction
/// </summary>
public RemoteCertificateValidationCallback UserCertificateValidationCallback { get; set; }


#if ENABLE_SYSTEM_TRANSACTIONS
/// <summary>
/// Enlists in the specified transaction as a volatile resource manager.
/// </summary>
/// <param name="transaction">The transaction to enlist in, or null to unenlist.</param>
/// <remarks>
/// This method allows the connection to participate in a System.Transactions transaction.
/// If the connection is already enlisted in a different transaction, an exception is thrown.
/// </remarks>
public override void EnlistTransaction(System.Transactions.Transaction transaction)
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(AseConnection));
}

if (transaction == null)
{
// Unenlist from current transaction
_enlistedTransaction = null;
return;
}

if (_enlistedTransaction != null)
{
if (_enlistedTransaction.SystemTransaction == transaction)
{
// Already enlisted in this transaction
return;
}
throw new InvalidOperationException("Connection is already enlisted in a transaction. Complete the current transaction before enlisting in another.");
}

if (State != ConnectionState.Open)
{
throw new InvalidOperationException("Cannot enlist in a transaction when the connection is not open.");
}

_enlistedTransaction = new AseEnlistedTransaction(this, transaction);
_enlistedTransaction.Begin();
}

/// <summary>
/// Called by AseEnlistedTransaction when the transaction completes.
/// </summary>
internal void OnEnlistedTransactionCompleted()
{
_enlistedTransaction = null;
}
#endif
}

/// <summary>
Expand Down
205 changes: 205 additions & 0 deletions src/AdoNetCore.AseClient/AseEnlistedTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#if ENABLE_SYSTEM_TRANSACTIONS
using System;
using System.Data;
using System.Transactions;
using DataIsolationLevel = System.Data.IsolationLevel;

namespace AdoNetCore.AseClient
{
/// <summary>
/// Handles enlistment in a System.Transactions transaction for AseConnection.
/// This class implements IEnlistmentNotification to participate in the two-phase commit protocol.
/// </summary>
internal sealed class AseEnlistedTransaction : IEnlistmentNotification
{
private readonly AseConnection _connection;
private readonly Transaction _systemTransaction;
private AseTransaction _localTransaction;
private bool _isCompleted;

/// <summary>
/// Gets the System.Transactions.Transaction that this enlistment is associated with.
/// </summary>
internal Transaction SystemTransaction => _systemTransaction;

/// <summary>
/// Creates a new enlisted transaction wrapper.
/// </summary>
/// <param name="connection">The AseConnection to enlist.</param>
/// <param name="transaction">The System.Transactions.Transaction to enlist in.</param>
internal AseEnlistedTransaction(AseConnection connection, Transaction transaction)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
_systemTransaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
_isCompleted = false;
}

/// <summary>
/// Begins a local transaction on the connection to participate in the distributed transaction.
/// </summary>
internal void Begin()
{
if (_connection.State != ConnectionState.Open)
{
throw new InvalidOperationException("Connection must be open to enlist in a transaction.");
}

// Map System.Transactions isolation level to ADO.NET isolation level
var isolationLevel = MapIsolationLevel(_systemTransaction.IsolationLevel);

// Start a local transaction that will be committed/rolled back based on the distributed transaction outcome
_localTransaction = new AseTransaction(_connection, isolationLevel);
_localTransaction.Begin();

// Enlist in the System.Transactions transaction
_systemTransaction.EnlistVolatile(this, EnlistmentOptions.None);
}

/// <summary>
/// Maps System.Transactions.IsolationLevel to System.Data.IsolationLevel.
/// </summary>
private static DataIsolationLevel MapIsolationLevel(System.Transactions.IsolationLevel isolationLevel)
{
switch (isolationLevel)
{
case System.Transactions.IsolationLevel.Serializable:
return DataIsolationLevel.Serializable;
case System.Transactions.IsolationLevel.RepeatableRead:
return DataIsolationLevel.RepeatableRead;
case System.Transactions.IsolationLevel.ReadCommitted:
return DataIsolationLevel.ReadCommitted;
case System.Transactions.IsolationLevel.ReadUncommitted:
return DataIsolationLevel.ReadUncommitted;
case System.Transactions.IsolationLevel.Snapshot:
return DataIsolationLevel.Serializable;
case System.Transactions.IsolationLevel.Chaos:
return DataIsolationLevel.ReadUncommitted;
case System.Transactions.IsolationLevel.Unspecified:
default:
return DataIsolationLevel.ReadCommitted;
}
}

/// <summary>
/// Called by the transaction manager during phase 1 of the two-phase commit protocol.
/// </summary>
public void Prepare(PreparingEnlistment preparingEnlistment)
{
if (_isCompleted)
{
preparingEnlistment.Prepared();
return;
}

try
{
preparingEnlistment.Prepared();
}
catch (Exception ex)
{
preparingEnlistment.ForceRollback(ex);
}
}

/// <summary>
/// Called by the transaction manager during phase 2 when the transaction should be committed.
/// </summary>
public void Commit(Enlistment enlistment)
{
if (_isCompleted)
{
enlistment.Done();
return;
}

try
{
if (_localTransaction != null && !_localTransaction.IsDisposed)
{
_localTransaction.Commit();
}
_isCompleted = true;
enlistment.Done();
}
catch (Exception)
{
_isCompleted = true;
enlistment.Done();
throw;
}
finally
{
Cleanup();
}
}

/// <summary>
/// Called by the transaction manager when the transaction should be rolled back.
/// </summary>
public void Rollback(Enlistment enlistment)
{
if (_isCompleted)
{
enlistment.Done();
return;
}

try
{
if (_localTransaction != null && !_localTransaction.IsDisposed)
{
_localTransaction.Rollback();
}
}
catch (Exception)
{
// Swallow rollback exceptions
}
finally
{
_isCompleted = true;
Cleanup();
enlistment.Done();
}
}

/// <summary>
/// Called by the transaction manager when the transaction outcome is in doubt.
/// </summary>
public void InDoubt(Enlistment enlistment)
{
try
{
if (_localTransaction != null && !_localTransaction.IsDisposed)
{
_localTransaction.Rollback();
}
}
catch (Exception)
{
// Swallow exceptions during in-doubt handling
}
finally
{
_isCompleted = true;
Cleanup();
enlistment.Done();
}
}

/// <summary>
/// Cleans up resources after transaction completion.
/// </summary>
private void Cleanup()
{
if (_localTransaction != null && !_localTransaction.IsDisposed)
{
_localTransaction.Dispose();
}
_localTransaction = null;

_connection.OnEnlistedTransactionCompleted();
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ internal interface IConnectionParameters
bool AnsiNull { get; }
bool EnableServerPacketSize { get; }
bool NamedParameters { get; }
bool Enlist { get; }
}
}
Loading