diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index b546e1f..7326f19 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ on: jobs: build: name: Build - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 @@ -20,30 +20,13 @@ jobs: with: dotnet-version: '3.1.x' - - name: Restore - run: | - dotnet restore TxCommand/TxCommand.csproj - dotnet restore TxCommand.Tests/TxCommand.Tests.csproj - - - name: Build TxCommand - run: dotnet build -c Release --no-restore TxCommand/TxCommand.csproj - - - name: Build TxCommand.Abstractions - run: dotnet build -c Release --no-restore TxCommand.Abstractions/TxCommand.Abstractions.csproj - - - name: Unit Test - run: dotnet test --no-restore -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura TxCommand.Tests/TxCommand.Tests.csproj - - - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v1 with: - file: TxCommand.Tests/coverage.cobertura.xml + dotnet-version: '5.x' - - name: Pack TxCommand - run: dotnet pack -c Release --no-restore --no-build -o packages TxCommand/TxCommand.csproj - - - name: Pack TxCommand.Abstractions - run: dotnet pack -c Release --no-restore --no-build -o packages TxCommand.Abstractions/TxCommand.Abstractions.csproj + - name: Build + run: ./scripts/build.sh - name: Upload Artifacts if: github.ref == 'refs/heads/master' && github.event_name == 'push' @@ -60,17 +43,29 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Pull Database Image - run: docker pull mcr.microsoft.com/mssql/server:2017-CU17-ubuntu + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' - - name: Restore - run: dotnet restore Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '5.x' - - name: Build - run: dotnet build --no-restore Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj + - name: Pull Database Images + run: | + docker pull mcr.microsoft.com/mssql/server:2019-latest + docker pull mysql:8.0.23 - name: Test - run: dotnet test Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj + run: ./scripts/test.sh + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v1 + with: + files: > + ./Coverage/TxCommand.Net5.Tests/coverage.cobertura.xml,./Coverage/TxCommand.NetCore3_1.Tests/coverage.cobertura.xml,./Sql/Coverage/TxCommand.Sql.Net5.Tests/coverage.cobertura.xml,./Sql/Coverage/TxCommand.Sql.NetCore3_1.Tests/coverage.cobertura.xml publish: name: Publish @@ -94,3 +89,9 @@ jobs: - name: Publish TxCommand.Abstractions run: dotnet nuget push packages/TxCommand.Abstractions.*.*.*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://www.nuget.org/api/v2/package --skip-duplicate + + - name: Publish TxCommand + run: dotnet nuget push packages/TxCommand.Sql.*.*.*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://www.nuget.org/api/v2/package --skip-duplicate + + - name: Publish TxCommand.Abstractions + run: dotnet nuget push packages/TxCommand.Sql.Abstractions.*.*.*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://www.nuget.org/api/v2/package --skip-duplicate diff --git a/.gitignore b/.gitignore index 3ace256..f2d6be0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ **/obj/* *.user -TxCommand.Tests/coverage.json -TxCommand.Tests/coverage.cobertura.xml -TxCommand.Tests/TestResults \ No newline at end of file +*coverage.json +*coverage.cobertura.xml +TestResults +Coverage \ No newline at end of file diff --git a/Example/TxCommand.Example.IntegrationTests/AddPetTests.cs b/Example/TxCommand.Example.IntegrationTests/AddPetTests.cs deleted file mode 100644 index 1eeb5d0..0000000 --- a/Example/TxCommand.Example.IntegrationTests/AddPetTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Dapper; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Data; -using System.Data.SqlClient; -using System.Threading.Tasks; -using TxCommand.Example.IntegrationTests.Setup; -using Xunit; - -namespace TxCommand.Example.IntegrationTests -{ - [Collection("IntegrationTests")] - public class AddPetTests : IAsyncLifetime - { - private readonly ServiceProvider _services; - private readonly IDbConnection _connection; - - private Exception _exception; - private int _personId; - - public AddPetTests(DatabaseSetup database) - { - _services = new ServiceCollection() - .AddScoped(_ => new SqlConnection( - $"Server=localhost,12937;Database=Test;User Id=sa;Password={DatabaseSetup.SaPassword};")) - .AddTxCommand() - .AddTransient() - .BuildServiceProvider(); - - _connection = database.Connection; - } - - public async Task InitializeAsync() - { - _personId = await _connection.ExecuteScalarAsync("INSERT INTO [People] ([Name]) VALUES ('John'); SELECT SCOPE_IDENTITY()"); - - try - { - var service = _services.GetRequiredService(); - await service.AddPet(_personId, "Dog"); - } - catch (Exception e) - { - _exception = e; - } - } - - public async Task DisposeAsync() - { - await _connection.ExecuteAsync("DELETE FROM [Pets];"); - await _connection.ExecuteAsync("DELETE FROM [People];"); - - await _services.DisposeAsync(); - } - - [Fact] - public void ThenTheExceptionIsNull() - { - Assert.Null(_exception); - } - - [Fact] - public async Task ThenTheNumberOfPetsCreatedIs1() - { - var count = await _connection.QuerySingleAsync( - $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal(1, count); - } - - [Fact] - public async Task ThenThePetDataIsCorrect() - { - var pet = await _connection.QuerySingleAsync( - $"SELECT TOP (1) * FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal("Dog", pet.Name); - } - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/AddPetsTests.cs b/Example/TxCommand.Example.IntegrationTests/AddPetsTests.cs deleted file mode 100644 index 3c61ed4..0000000 --- a/Example/TxCommand.Example.IntegrationTests/AddPetsTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Dapper; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Threading.Tasks; -using TxCommand.Example.IntegrationTests.Setup; -using Xunit; - -namespace TxCommand.Example.IntegrationTests -{ - [Collection("IntegrationTests")] - public class AddPetsTests : IAsyncLifetime - { - private readonly ServiceProvider _services; - private readonly IDbConnection _connection; - - private Exception _exception; - private int _personId; - - public AddPetsTests(DatabaseSetup database) - { - _services = new ServiceCollection() - .AddScoped(_ => new SqlConnection( - $"Server=localhost,12937;Database=Test;User Id=sa;Password={DatabaseSetup.SaPassword};")) - .AddTxCommand() - .AddTransient() - .BuildServiceProvider(); - - _connection = database.Connection; - } - - public async Task InitializeAsync() - { - _personId = await _connection.ExecuteScalarAsync("INSERT INTO [People] ([Name]) VALUES ('John'); SELECT SCOPE_IDENTITY()"); - - try - { - var pets = new [] {"Dog", "Cat", "Snake"}; - var service = _services.GetRequiredService(); - await service.AddPets(_personId, pets); - } - catch (Exception e) - { - _exception = e; - } - } - - public async Task DisposeAsync() - { - await _connection.ExecuteAsync("DELETE FROM [Pets];"); - await _connection.ExecuteAsync("DELETE FROM [People];"); - - await _services.DisposeAsync(); - } - - [Fact] - public void ThenTheExceptionIsNull() - { - Assert.Null(_exception); - } - - [Fact] - public async Task ThenTheNumberOfPetsCreatedIs3() - { - var count = await _connection.QuerySingleAsync( - $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal(3, count); - } - - [Fact] - public async Task ThenThePetDataIsCorrect() - { - var pets = (await _connection.QueryAsync( - $"SELECT [Name] FROM [Pets] WHERE [PersonId] = {_personId}")) - .ToList(); - - Assert.Contains(pets, p => p == "Dog"); - Assert.Contains(pets, p => p == "Cat"); - Assert.Contains(pets, p => p == "Snake"); - } - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/CreatePersonTests.cs b/Example/TxCommand.Example.IntegrationTests/CreatePersonTests.cs deleted file mode 100644 index 5ce468c..0000000 --- a/Example/TxCommand.Example.IntegrationTests/CreatePersonTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Dapper; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Data; -using System.Data.SqlClient; -using System.Threading.Tasks; -using TxCommand.Example.IntegrationTests.Setup; -using Xunit; - -namespace TxCommand.Example.IntegrationTests -{ - [Collection("IntegrationTests")] - public class CreatePersonTests : IAsyncLifetime - { - private readonly ServiceProvider _services; - private readonly IDbConnection _connection; - - private Exception _exception; - private int _personId; - - public CreatePersonTests(DatabaseSetup database) - { - _services = new ServiceCollection() - .AddScoped(_ => new SqlConnection( - $"Server=localhost,12937;Database=Test;User Id=sa;Password={DatabaseSetup.SaPassword};")) - .AddTxCommand() - .AddTransient() - .BuildServiceProvider(); - _connection = database.Connection; - } - - public async Task InitializeAsync() - { - try - { - using (var service = _services.GetRequiredService()) - { - _personId = await service.Create("John", "Doe"); - } - } - catch (Exception e) - { - _exception = e; - } - } - - public async Task DisposeAsync() - { - await _connection.ExecuteAsync("DELETE FROM [Pets];"); - await _connection.ExecuteAsync("DELETE FROM [People];"); - - await _services.DisposeAsync(); - } - - [Fact] - public void ThenTheExceptionIsNull() - { - Assert.Null(_exception); - } - - [Fact] - public async Task ThenThePersonIsCreated() - { - var person = await _connection.QuerySingleAsync($"SELECT * FROM [People] WHERE [Id] = {_personId}"); - - Assert.Equal("John", person.Name); - } - - [Fact] - public async Task ThenTheNumberOfPetsCreatedIs1() - { - var count = await _connection.ExecuteScalarAsync( - $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal(1, count); - } - - [Fact] - public async Task ThenThePetDataIsCorrect() - { - var pet = await _connection.QuerySingleAsync( - $"SELECT TOP (1) * FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal("Doe", pet.Name); - } - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/FailingCommandRollsBackTest.cs b/Example/TxCommand.Example.IntegrationTests/FailingCommandRollsBackTest.cs deleted file mode 100644 index 9e9f447..0000000 --- a/Example/TxCommand.Example.IntegrationTests/FailingCommandRollsBackTest.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Dapper; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Data; -using System.Data.SqlClient; -using System.Threading.Tasks; -using TxCommand.Example.IntegrationTests.Setup; -using Xunit; - -namespace TxCommand.Example.IntegrationTests -{ - [Collection("IntegrationTests")] - public class FailingCommandRollsBackTest : IAsyncLifetime - { - private readonly ServiceProvider _services; - private readonly IDbConnection _connection; - - private Exception _exception; - private int _personId; - - public FailingCommandRollsBackTest(DatabaseSetup database) - { - _services = new ServiceCollection() - .AddScoped(_ => new SqlConnection( - $"Server=localhost,12937;Database=Test;User Id=sa;Password={DatabaseSetup.SaPassword};")) - .AddTxCommand() - .AddTransient() - .BuildServiceProvider(); - - _connection = database.Connection; - } - - public async Task InitializeAsync() - { - _personId = await _connection.ExecuteScalarAsync("INSERT INTO [People] ([Name]) VALUES ('John'); SELECT SCOPE_IDENTITY()"); - - try - { - // Attempt to create two pets with the same name - var pets = new[] { "Dog", "Dog" }; - var service = _services.GetRequiredService(); - await service.AddPets(_personId, pets); - } - catch (Exception e) - { - _exception = e; - } - } - - public async Task DisposeAsync() - { - await _connection.ExecuteAsync("DELETE FROM [Pets];"); - await _connection.ExecuteAsync("DELETE FROM [People];"); - - await _services.DisposeAsync(); - } - - [Fact] - public void ThenTheExceptionIsNotNull() - { - Assert.NotNull(_exception); - } - - [Fact] - public async Task ThenTheNumberOfPetsCreatedIs0() - { - var count = await _connection.QuerySingleAsync( - $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); - - Assert.Equal(0, count); - } - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/Setup/DatabaseSetup.cs b/Example/TxCommand.Example.IntegrationTests/Setup/DatabaseSetup.cs deleted file mode 100644 index cf8dd0f..0000000 --- a/Example/TxCommand.Example.IntegrationTests/Setup/DatabaseSetup.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Dapper; -using Docker.DotNet; -using Docker.DotNet.Models; -using FluentAssertions; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SqlClient; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace TxCommand.Example.IntegrationTests.Setup -{ - public class DatabaseSetup : IDisposable - { - private const string WindowsDockerPath = "npipe://./pipe/docker_engine"; - private const string UnixDockerPath = "unix:///var/run/docker.sock"; - public const string SaPassword = "MySuperSecur3Password!"; - - private readonly IDockerClient _docker; - private string _containerId; - - public readonly IDbConnection Connection; - - public DatabaseSetup() - { - var dockerSocketPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? WindowsDockerPath - : UnixDockerPath; - - _docker = new DockerClientConfiguration(new Uri(dockerSocketPath)).CreateClient(); - - StartContainer().Wait(); - SetupDatabase().Wait(); - - Connection = new SqlConnection($"Server=localhost,12937;Database=Test;User Id=sa;Password={SaPassword};"); - Connection.Open(); - } - - private async Task SetupDatabase() - { - using (var connection = new SqlConnection($"Server=localhost,12937;Database=master;User Id=sa;Password={SaPassword};")) - { - await connection.OpenAsync(); - - await connection.ExecuteAsync("CREATE DATABASE [Test];"); - } - - using (var connection = new SqlConnection($"Server=localhost,12937;Database=Test;User Id=sa;Password={SaPassword};")) - { - await connection.OpenAsync(); - - await connection.ExecuteAsync(@"CREATE TABLE [dbo].[People] ( - [Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, - [Name] VARCHAR(255) NOT NULL - )"); - - await connection.ExecuteAsync(@"CREATE TABLE [dbo].[Pets] ( - [Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, - [PersonId] INT NOT NULL, - [Name] VARCHAR(255) NOT NULL UNIQUE, - CONSTRAINT FK_People_Pets FOREIGN KEY ([PersonId]) REFERENCES [People] ([Id]) - )"); - } - } - - private async Task StartContainer() - { - var resp = await _docker.Containers.CreateContainerAsync(new CreateContainerParameters - { - Image = "mcr.microsoft.com/mssql/server:2017-CU17-ubuntu", - Name = "tx-command-database-integration", - Env = new List - { - $"SA_PASSWORD={SaPassword}", - "ACCEPT_EULA=y" - }, - ExposedPorts = new Dictionary - { - {"1433", new EmptyStruct()} - }, - HostConfig = new HostConfig - { - AutoRemove = true, - PortBindings = new Dictionary> - { - {"1433", new List{new PortBinding{HostPort = "12937"}}} - } - } - }); - - _containerId = resp.ID; - - await _docker.Containers.StartContainerAsync(_containerId, new ContainerStartParameters()); - - Action checkSqlServer = () => - { - using var connection = new SqlConnection($"Server=localhost,12937;Database=master;User Id=sa;Password={SaPassword};"); - connection.Open(); - }; - - checkSqlServer.Should().NotThrowAfter(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(1)); - } - - private async Task StopContainer() - { - await _docker.Containers.StopContainerAsync(_containerId, new ContainerStopParameters()); - } - - public void Dispose() - { - StopContainer().Wait(); - } - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/Setup/TestCollection.cs b/Example/TxCommand.Example.IntegrationTests/Setup/TestCollection.cs deleted file mode 100644 index 846dc51..0000000 --- a/Example/TxCommand.Example.IntegrationTests/Setup/TestCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace TxCommand.Example.IntegrationTests.Setup -{ - [CollectionDefinition("IntegrationTests")] - public class TestCollection : ICollectionFixture - { - } -} diff --git a/Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj b/Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj deleted file mode 100644 index d22e863..0000000 --- a/Example/TxCommand.Example.IntegrationTests/TxCommand.Example.IntegrationTests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - netcoreapp3.1 - - false - - - - - - - - - - - - - - - - - - - diff --git a/Example/TxCommand.Example/CreatePersonService.cs b/Example/TxCommand.Example/CreatePersonService.cs deleted file mode 100644 index aa08144..0000000 --- a/Example/TxCommand.Example/CreatePersonService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading.Tasks; -using TxCommand.Abstractions; -using TxCommand.Example.Commands; - -namespace TxCommand.Example -{ - public interface ICreatePersonService : IDisposable - { - Task Create(string personName, string petName); - } - - public class CreatePersonService : ICreatePersonService - { - private readonly ITxCommandExecutor _executor; - - public CreatePersonService(ITxCommandExecutor executor) - { - _executor = executor; - } - - public async Task Create(string personName, string petName) - { - var createPersonCommand = new CreatePersonCommand(personName); - var personId = await _executor.ExecuteAsync(createPersonCommand); - - var addPetCommand = new AddPetCommand(personId, petName); - await _executor.ExecuteAsync(addPetCommand); - - return personId; - } - - public void Dispose() - { - _executor?.Dispose(); - } - } -} diff --git a/Example/TxCommand.Example/PetService.cs b/Example/TxCommand.Example/PetService.cs deleted file mode 100644 index 42608c3..0000000 --- a/Example/TxCommand.Example/PetService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using TxCommand.Abstractions; -using TxCommand.Example.Commands; - -namespace TxCommand.Example -{ - public interface IPetService - { - Task AddPet(int personId, string pet); - Task AddPets(int personId, string[] pets); - } - - public class PetService : IPetService - { - private readonly ITxCommandExecutorFactory _commandExecutorFactory; - - public PetService(ITxCommandExecutorFactory commandExecutorFactory) - { - _commandExecutorFactory = commandExecutorFactory; - } - - public async Task AddPet(int personId, string pet) - { - using (var executor = _commandExecutorFactory.Create()) - { - var command = new AddPetCommand(personId, pet); - - await executor.ExecuteAsync(command); - } - } - - public async Task AddPets(int personId, string[] pets) - { - using (var executor = _commandExecutorFactory.Create()) - { - foreach (var pet in pets) - { - var command = new AddPetCommand(personId, pet); - - await executor.ExecuteAsync(command); - } - } - } - } -} diff --git a/Example/TxCommand.Example/TxCommand.Example.csproj b/Example/TxCommand.Example/TxCommand.Example.csproj deleted file mode 100644 index d187b65..0000000 --- a/Example/TxCommand.Example/TxCommand.Example.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - - - diff --git a/README.md b/README.md index 902a48a..6af875b 100644 --- a/README.md +++ b/README.md @@ -4,92 +4,113 @@ ![Nuget](https://img.shields.io/nuget/v/TxCommand) [![Nuget](https://img.shields.io/nuget/dt/TxCommand)](https://www.nuget.org/packages/TxCommand/) - # TxCommand -A simple commanding library with support for executing commands within a database transaction. +TxCommand is a simple commanding package which provides commanding interfaces that can be executed within a transaction. TxCommand is built in a way where it can be extended to support multiple platforms and drivers. + +| Package | Version | Downloads | +| ---------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| TxCommand | ![Nuget](https://img.shields.io/nuget/v/TxCommand) | [![Nuget](https://img.shields.io/nuget/dt/TxCommand)](https://www.nuget.org/packages/TxCommand/) | +| TxCommand.Abstractions | ![Nuget](https://img.shields.io/nuget/v/TxCommand.Abstractions) | [![Nuget](https://img.shields.io/nuget/dt/TxCommand.Abstractions)](https://www.nuget.org/packages/TxCommand.Abstractions/) | -|Package | Version| Downloads| -|--------|--------|---| -|TxCommand|![Nuget](https://img.shields.io/nuget/v/TxCommand)|[![Nuget](https://img.shields.io/nuget/dt/TxCommand)](https://www.nuget.org/packages/TxCommand/) -|TxCommand.Abstractions|![Nuget](https://img.shields.io/nuget/v/TxCommand.Abstractions)|[![Nuget](https://img.shields.io/nuget/dt/TxCommand.Abstractions)](https://www.nuget.org/packages/TxCommand.Abstractions/)| +## Get Started -## Usage +For this example, we'll be using the Sql variant of TxCommand, `TxCommand.Sql`. To get started, install the `TxCommand.Sql` package to your project - this can be done either with the NuGet Package Manager or the NuGet CLI. -Commands are executed with a CommandExecutor, which provides a database transaction. CommandExecutors are a single use object and should be used for a specific set of operations. +``` +> Install-Package TxCommand.Sql +``` -Below is an example of how to use the command executors in a service. +After installing the TxCommand, the package can be easily configured in your DI setup. For example: ```csharp -public class PetService +public void ConfigureServices(IServiceCollection services) { - private readonly ITxCommandExecutorFactory _commandExecutorFactory; + // TxCommand.Sql depends on an IDbConnection, so here we configure + // an instance of MySqlConnection. + services.AddTransient(_ => new MySqlConnection("")); + + // Configure TxCommand and the Sql package. + services.AddTxCommand() + .AddSql(); +} +``` + +Once the DI is configured, you're good to go. The next step is to setup a command. Below is an example of a command that is used to insert a `Car` record into a database. - public PetService(ITxCommandExecutorFactory commandExecutorFactory) +```csharp +using System; +using System.Data; +using TxCommand.Abstractions; + +... + +// This is the command which is used to create a car record. It implemented the +// ITxCommand interface, which has an optional type parameter used as a result. +public class CreateCarCommand : ITxCommand +{ + public string Reg { get; set; } + + public CreateCarCommand(string reg) { - _commandExecutorFactory = commandExecutorFactory; + Reg = reg; } - // Adds a collection of pets, if any of them fail, the - // transaction will be rolled back. - public async Task AddPets(int personId, string[] pets) + // This is the main entrypoint to the command. + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) { - // The executor commits the transaction on disposal. - using (var executor = _commandExecutorFactory.Create()) - { - foreach (var pet in pets) - { - var command = new AddPetCommand(personId, pet); + const string query = "INSERT INTO `Cars` (`Reg`) VALUES (@Reg); SELECT LAST_INSERT_ID();"; + + return await connection.ExecuteScalarAsync(query, new {Reg}, transaction); + } - await executor.ExecuteAsync(command); - } + // Validate is used to validate that the data passed to the command + // is valid, before execution. + public void Validate() + { + if (string.IsNullOrEmpty(Reg)) + { + throw new ArgumentException("Reg cannot be empty", nameof(Reg)); } } } ``` -Here is an example of a transient service, `CreatePersonService`. This should be initialised on a per-use basis, as it has a command executor as a dependency. +Now we got the command sorted, the final step is to execute it. To execute the command we use the `ISession` interface. A `Session` is used to execute a set of command within a single transaction. An instance of `ISession` can be instantiated using the `ISessionFactory` dependency. ```csharp -public class CreatePersonService : IDisposable +using TxCommand.Abstractions; + +... + +public class CarFactory { - private readonly ITxCommandExecutor _executor; + private readonly ISessionFactory _sessionFactory; - public CreatePersonService(ITxCommandExecutor executor) + // ISessionFactory can be injected into another service, using the DI container. + public CarFactory(ISessionFactory sessionFactory) { - _executor = executor; + _sessionFactory = sessionFactory; } - // Creates a new Person, then adds a Pet. If either command throws - // an exception, the transaction will be rolled back. - public async Task Create(string personName, string petName) + public async Task CreateAsync(string reg) { - var createPersonCommand = new CreatePersonCommand(personName); - var personId = await _executor.ExecuteAsync(createPersonCommand); - - var addPetCommand = new AddPetCommand(personId, petName); - await _executor.ExecuteAsync(addPetCommand); - - return personId; - } + // A session should be disposed to commit the transaction. Alternatively, + // session.CommitAsync() can be called - or even session.RollbackAsync(); + using (var session = _sessionFactory.Create()) + { + // Create a new instance of the command. + var command = new CreateCarCommand(reg); - public void Dispose() - { - // On disposal, the executor is disposed, which commits the transaction. - _executor?.Dispose(); + // Then call execute! The session will first call command.Validate(), + // then it will be executed and return the result of the command. + return await session.ExecuteAsync(command); + } } } -``` -## Dependency Injection - -If you're using `Microsoft.Extensions.DependencyInjection` for dependency injection, `AddTxCommand()` can be called on a `IServiceCollection`. +``` -```csharp -var services = new ServiceCollection() - .AddTxCommand() - .BuildServiceProvider(); +## Contributing -var factory = services.GetRequiredService(); -var executor = services.GetRequiredService(); -``` \ No newline at end of file +Not much here, but feel free to raise and issues or open a Pull Request if you think of an enhancement or spot a bug! diff --git a/Sql/.mssql/Dockerfile b/Sql/.mssql/Dockerfile new file mode 100644 index 0000000..f950b9d --- /dev/null +++ b/Sql/.mssql/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/mssql/server:2019-latest + +USER root + +COPY setup.sql setup.sql +COPY import_data.sh import_data.sh +COPY entrypoint.sh entrypoint.sh + +RUN chmod +x import_data.sh +RUN chmod +x entrypoint.sh + +ENTRYPOINT [ "/bin/bash", "-c", "./entrypoint.sh" ] \ No newline at end of file diff --git a/Sql/.mssql/entrypoint.sh b/Sql/.mssql/entrypoint.sh new file mode 100644 index 0000000..7e2cf39 --- /dev/null +++ b/Sql/.mssql/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./import_data.sh & /opt/mssql/bin/sqlservr \ No newline at end of file diff --git a/Sql/.mssql/import_data.sh b/Sql/.mssql/import_data.sh new file mode 100644 index 0000000..ffd7e1f --- /dev/null +++ b/Sql/.mssql/import_data.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +for i in {1..50}; +do + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "MySuperSecur3Password!" -d master -i setup.sql + if [ $? -eq 0 ] + then + echo "setup.sql completed" + break + else + echo "not ready yet..." + sleep 1 + fi +done \ No newline at end of file diff --git a/Sql/.mssql/setup.sql b/Sql/.mssql/setup.sql new file mode 100644 index 0000000..1446577 --- /dev/null +++ b/Sql/.mssql/setup.sql @@ -0,0 +1,17 @@ +CREATE DATABASE [Test]; +GO + +USE [Test]; +GO + +CREATE TABLE [dbo].[People] ( + [Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, + [Name] VARCHAR(255) NOT NULL +); + +CREATE TABLE [dbo].[Pets] ( + [Id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, + [PersonId] INT NOT NULL, + [Name] VARCHAR(255) NOT NULL UNIQUE, + CONSTRAINT FK_People_Pets FOREIGN KEY ([PersonId]) REFERENCES [People] ([Id]) +); \ No newline at end of file diff --git a/Sql/.mysql/init_mysql.sql b/Sql/.mysql/init_mysql.sql new file mode 100644 index 0000000..407315e --- /dev/null +++ b/Sql/.mysql/init_mysql.sql @@ -0,0 +1,11 @@ +CREATE TABLE `People` ( + `Id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `Name` VARCHAR(255) NOT NULL +); + +CREATE TABLE `Pets` ( + `Id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `PersonId` INT NOT NULL, + `Name` VARCHAR(255) NOT NULL UNIQUE, + CONSTRAINT FK_People_Pets FOREIGN KEY (`PersonId`) REFERENCES `People`(`Id`) +); \ No newline at end of file diff --git a/Sql/TxCommand.Sql.Abstractions/ISession.cs b/Sql/TxCommand.Sql.Abstractions/ISession.cs new file mode 100644 index 0000000..4832a1e --- /dev/null +++ b/Sql/TxCommand.Sql.Abstractions/ISession.cs @@ -0,0 +1,9 @@ +using System.Data; + +namespace TxCommand.Abstractions +{ + /// + public interface ISession : ISession + { + } +} diff --git a/Sql/TxCommand.Sql.Abstractions/ISessionFactory.cs b/Sql/TxCommand.Sql.Abstractions/ISessionFactory.cs new file mode 100644 index 0000000..d373911 --- /dev/null +++ b/Sql/TxCommand.Sql.Abstractions/ISessionFactory.cs @@ -0,0 +1,9 @@ +namespace TxCommand.Abstractions +{ + /// + /// Used to create new instance of for Sql. + /// + public interface ISessionFactory : ISessionFactory + { + } +} diff --git a/Sql/TxCommand.Sql.Abstractions/ITxCommand.cs b/Sql/TxCommand.Sql.Abstractions/ITxCommand.cs new file mode 100644 index 0000000..7a99b5b --- /dev/null +++ b/Sql/TxCommand.Sql.Abstractions/ITxCommand.cs @@ -0,0 +1,14 @@ +using System.Data; + +namespace TxCommand.Abstractions +{ + /// + public interface ITxCommand : ITxCommand + { + } + + /// + public interface ITxCommand : ITxCommand + { + } +} diff --git a/Sql/TxCommand.Sql.Abstractions/TxCommand.Sql.Abstractions.csproj b/Sql/TxCommand.Sql.Abstractions/TxCommand.Sql.Abstractions.csproj new file mode 100644 index 0000000..b393475 --- /dev/null +++ b/Sql/TxCommand.Sql.Abstractions/TxCommand.Sql.Abstractions.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0;net5.0 + TxCommand.Abstractions + Reece Russell + Provides abstractions for TxCommand.Sql, including ITxCommand, ISession and ISessionFactory. + LICENSE + https://github.com/reecerussell/tx-command + https://github.com/reecerussell/tx-command + git + cqrs, sql, mysql, tsql, commanding, query + + + + + + + + + True + + + + + diff --git a/Sql/TxCommand.Sql.Net5.Tests/MySql/AddPetCommand.cs b/Sql/TxCommand.Sql.Net5.Tests/MySql/AddPetCommand.cs new file mode 100644 index 0000000..a57ffe1 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/MySql/AddPetCommand.cs @@ -0,0 +1,40 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.MySql +{ + public class AddPetCommand : ITxCommand + { + public int PersonId { get; set; } + public string PetName { get; set; } + + public AddPetCommand(int personId, string petName) + { + PersonId = personId; + PetName = petName; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO `Pets` (`PersonId`,`Name`) VALUES (@PersonId,@PetName)"; + + await connection.ExecuteAsync(query, new {PersonId, PetName}, transaction); + } + + public void Validate() + { + if (PersonId < 1) + { + throw new ArgumentException("PersonId must be at least 1", nameof(PersonId)); + } + + if (string.IsNullOrEmpty(PetName)) + { + throw new ArgumentException("Pet name can not be empty", nameof(PetName)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/MySql/CreatePersonCommand.cs b/Sql/TxCommand.Sql.Net5.Tests/MySql/CreatePersonCommand.cs new file mode 100644 index 0000000..a9e1596 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/MySql/CreatePersonCommand.cs @@ -0,0 +1,33 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.MySql +{ + public class CreatePersonCommand : ITxCommand + { + public string Name { get; set; } + + public CreatePersonCommand(string name) + { + Name = name; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO `People` (`Name`) VALUES (@Name); SELECT LAST_INSERT_ID();"; + + return await connection.ExecuteScalarAsync(query, new {Name}, transaction); + } + + public void Validate() + { + if (string.IsNullOrEmpty(Name)) + { + throw new ArgumentException("Name cannot be empty", nameof(Name)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereExecutesSuccessfullyTests.cs b/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereExecutesSuccessfullyTests.cs new file mode 100644 index 0000000..84958b9 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereExecutesSuccessfullyTests.cs @@ -0,0 +1,73 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Threading.Tasks; +using MySql.Data.MySqlClient; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.MySql +{ + public class WhereExecutesSuccessfullyTests : IAsyncLifetime + { + private IDbConnection _connection; + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new MySqlConnection("server=localhost;database=Test;user=Test;password=Test"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + await using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, "Dog"); + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM `Pets` WHERE `PersonId` = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM `People` WHERE `Id` = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldBeNull() + { + _exception.Should().BeNull(); + } + + [Fact] + public async Task ThePersonAndPetIsCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `People` WHERE `Id` = {_personId}"); + count.Should().Be(1); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `Pets` WHERE `PersonId` = {_personId}"); + count.Should().Be(1); + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereSessionRollsBackTests.cs b/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereSessionRollsBackTests.cs new file mode 100644 index 0000000..2041447 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/MySql/WhereSessionRollsBackTests.cs @@ -0,0 +1,74 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MySql.Data.MySqlClient; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.MySql +{ + public class WhereSessionRollsBackTests : IAsyncLifetime + { + private IDbConnection _connection; + + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new MySqlConnection("server=localhost;database=Test;user=Test;password=Test"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + await using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, null); // name cannot be null + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM `Pets` WHERE `PersonId` = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM `People` WHERE `Id` = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldNotBeNull() + { + _exception.Should().NotBeNull(); + } + + [Fact] + public async Task ThePersonAndPetAreNotCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `People` WHERE `Id` = {_personId}"); + count.Should().Be(0); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `Pets` WHERE `PersonId` = {_personId}"); + count.Should().Be(0); + } + } +} diff --git a/Example/TxCommand.Example/Commands/AddPetCommand.cs b/Sql/TxCommand.Sql.Net5.Tests/Sql/AddPetCommand.cs similarity index 96% rename from Example/TxCommand.Example/Commands/AddPetCommand.cs rename to Sql/TxCommand.Sql.Net5.Tests/Sql/AddPetCommand.cs index d71d5b8..04a506e 100644 --- a/Example/TxCommand.Example/Commands/AddPetCommand.cs +++ b/Sql/TxCommand.Sql.Net5.Tests/Sql/AddPetCommand.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using TxCommand.Abstractions; -namespace TxCommand.Example.Commands +namespace TxCommand.Sql.Tests.Sql { public class AddPetCommand : ITxCommand { diff --git a/Example/TxCommand.Example/Commands/CreatePersonCommand.cs b/Sql/TxCommand.Sql.Net5.Tests/Sql/CreatePersonCommand.cs similarity index 95% rename from Example/TxCommand.Example/Commands/CreatePersonCommand.cs rename to Sql/TxCommand.Sql.Net5.Tests/Sql/CreatePersonCommand.cs index 905bfc1..00e71cd 100644 --- a/Example/TxCommand.Example/Commands/CreatePersonCommand.cs +++ b/Sql/TxCommand.Sql.Net5.Tests/Sql/CreatePersonCommand.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using TxCommand.Abstractions; -namespace TxCommand.Example.Commands +namespace TxCommand.Sql.Tests.Sql { public class CreatePersonCommand : ITxCommand { diff --git a/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereExecutesSuccessfullyTests.cs b/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereExecutesSuccessfullyTests.cs new file mode 100644 index 0000000..c1b62e2 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereExecutesSuccessfullyTests.cs @@ -0,0 +1,73 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.Sql +{ + public class WhereExecutesSuccessfullyTests : IAsyncLifetime + { + private IDbConnection _connection; + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new SqlConnection($"Server=localhost;Database=Test;User Id=sa;Password=MySuperSecur3Password!;"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + await using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, "Dog"); + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM [Pets] WHERE [PersonId] = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM [People] WHERE [Id] = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldBeNull() + { + _exception.Should().BeNull(); + } + + [Fact] + public async Task ThePersonAndPetIsCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [People] WHERE [Id] = {_personId}"); + count.Should().Be(1); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); + count.Should().Be(1); + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereSessionRollsBackTests.cs b/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereSessionRollsBackTests.cs new file mode 100644 index 0000000..da9c85c --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/Sql/WhereSessionRollsBackTests.cs @@ -0,0 +1,74 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.Sql +{ + public class WhereSessionRollsBackTests : IAsyncLifetime + { + private IDbConnection _connection; + + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new SqlConnection($"Server=localhost;Database=Test;User Id=sa;Password=MySuperSecur3Password!;"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + await using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, null); // name cannot be null + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM [Pets] WHERE [PersonId] = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM [People] WHERE [Id] = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldNotBeNull() + { + _exception.Should().NotBeNull(); + } + + [Fact] + public async Task ThePersonAndPetAreNotCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [People] WHERE [Id] = {_personId}"); + count.Should().Be(0); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); + count.Should().Be(0); + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/TransactionProviderTests.cs b/Sql/TxCommand.Sql.Net5.Tests/TransactionProviderTests.cs new file mode 100644 index 0000000..7f04750 --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/TransactionProviderTests.cs @@ -0,0 +1,199 @@ +using FluentAssertions; +using Moq; +using System; +using System.Data; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace TxCommand.Sql.Tests +{ + public class TransactionProviderTests + { + [Fact] + public void Ctor_GivenNullConnection_ThrowsArgumentNullException() + { + var ex = Assert.Throws(() => new TransactionProvider(null, new SqlOptions())); + ex.ParamName.Should().Be("connection"); + } + + [Fact] + public void Ctor_GivenNullOptions_ThrowsArgumentNullException() + { + var ex = Assert.Throws(() => new TransactionProvider(Mock.Of(), null)); + ex.ParamName.Should().Be("options"); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereNoTransactionIsActive_BeginsNewTransaction() + { + var database = new Mock(); + database.SetupGet(x => x.State).Returns(ConnectionState.Closed); + database.Setup(x => x.Open()).Verifiable(); + + var transaction = Mock.Of(); + database.Setup(x => x.BeginTransaction(IsolationLevel.ReadUncommitted)).Returns(transaction).Verifiable(); + + var provider = new TransactionProvider(database.Object, new SqlOptions{IsolationLevel = IsolationLevel.ReadUncommitted}); + await provider.EnsureTransactionAsync(CancellationToken.None); + + database.VerifyAll(); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereTransactionIsActive_DoesNotBeginNewTransaction() + { + var database = new Mock(); + database.SetupGet(x => x.State).Returns(ConnectionState.Open); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, Mock.Of()); + + await provider.EnsureTransactionAsync(CancellationToken.None); + + database.VerifyAll(); + database.Verify(x => x.BeginTransaction(), Times.Never); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.EnsureTransactionAsync(CancellationToken.None)); + + database.Verify(x => x.BeginTransaction(), Times.Never); + } + + [Fact] + public async Task CommitAsync_WhereTransactionIsActive_Commits() + { + var transaction = new Mock(); + transaction.Setup(x => x.Commit()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + await provider.CommitAsync(CancellationToken.None); + + transaction.VerifyAll(); + transaction.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public async Task CommitAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.CommitAsync(CancellationToken.None)); + } + + [Fact] + public void Commit_WhereTransactionIsActive_Commits() + { + var transaction = new Mock(); + transaction.Setup(x => x.Commit()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + provider.Commit(); + + transaction.VerifyAll(); + transaction.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Commit_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + Assert.Throws(() => provider.Commit()); + } + + [Fact] + public async Task RollbackAsync_WhereTransactionIsActive_RollsBack() + { + var transaction = new Mock(); + transaction.Setup(x => x.Rollback()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + await provider.RollbackAsync(CancellationToken.None); + + transaction.VerifyAll(); + transaction.Verify(x => x.Rollback(), Times.Once); + } + + [Fact] + public async Task RollbackAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.RollbackAsync(CancellationToken.None)); + } + + [Fact] + public void GetExecutionArguments_WhereTransactionIsNotActive_ReturnsDatabase() + { + var database = Mock.Of(); + + var provider = new TransactionProvider(database, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, null); + + var (db, tx) = provider.GetExecutionArguments(); + db.Should().Be(database); + tx.Should().BeNull(); + } + + [Fact] + public void GetExecutionArguments_WhereTransactionIsNotActive_ReturnsDatabaseAndTransaction() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new TransactionProvider(database, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction); + + var (db, tx) = provider.GetExecutionArguments(); + db.Should().Be(database); + tx.Should().Be(transaction); + } + + [Fact] + public void GetExecutionArguments_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + Assert.Throws(() => provider.GetExecutionArguments()); + } + } +} diff --git a/Sql/TxCommand.Sql.Net5.Tests/TxCommand.Sql.Net5.Tests.csproj b/Sql/TxCommand.Sql.Net5.Tests/TxCommand.Sql.Net5.Tests.csproj new file mode 100644 index 0000000..c7ce88b --- /dev/null +++ b/Sql/TxCommand.Sql.Net5.Tests/TxCommand.Sql.Net5.Tests.csproj @@ -0,0 +1,39 @@ + + + + net5.0 + + false + + TxCommand.Sql.Tests + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/AddPetCommand.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/AddPetCommand.cs new file mode 100644 index 0000000..a57ffe1 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/AddPetCommand.cs @@ -0,0 +1,40 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.MySql +{ + public class AddPetCommand : ITxCommand + { + public int PersonId { get; set; } + public string PetName { get; set; } + + public AddPetCommand(int personId, string petName) + { + PersonId = personId; + PetName = petName; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO `Pets` (`PersonId`,`Name`) VALUES (@PersonId,@PetName)"; + + await connection.ExecuteAsync(query, new {PersonId, PetName}, transaction); + } + + public void Validate() + { + if (PersonId < 1) + { + throw new ArgumentException("PersonId must be at least 1", nameof(PersonId)); + } + + if (string.IsNullOrEmpty(PetName)) + { + throw new ArgumentException("Pet name can not be empty", nameof(PetName)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/CreatePersonCommand.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/CreatePersonCommand.cs new file mode 100644 index 0000000..a9e1596 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/CreatePersonCommand.cs @@ -0,0 +1,33 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.MySql +{ + public class CreatePersonCommand : ITxCommand + { + public string Name { get; set; } + + public CreatePersonCommand(string name) + { + Name = name; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO `People` (`Name`) VALUES (@Name); SELECT LAST_INSERT_ID();"; + + return await connection.ExecuteScalarAsync(query, new {Name}, transaction); + } + + public void Validate() + { + if (string.IsNullOrEmpty(Name)) + { + throw new ArgumentException("Name cannot be empty", nameof(Name)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereExecutesSuccessfullyTests.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereExecutesSuccessfullyTests.cs new file mode 100644 index 0000000..fabb865 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereExecutesSuccessfullyTests.cs @@ -0,0 +1,73 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Threading.Tasks; +using MySql.Data.MySqlClient; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.MySql +{ + public class WhereExecutesSuccessfullyTests : IAsyncLifetime + { + private IDbConnection _connection; + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new MySqlConnection("server=localhost;database=Test;user=Test;password=Test"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, "Dog"); + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM `Pets` WHERE `PersonId` = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM `People` WHERE `Id` = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldBeNull() + { + _exception.Should().BeNull(); + } + + [Fact] + public async Task ThePersonAndPetIsCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `People` WHERE `Id` = {_personId}"); + count.Should().Be(1); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `Pets` WHERE `PersonId` = {_personId}"); + count.Should().Be(1); + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereSessionRollsBackTests.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereSessionRollsBackTests.cs new file mode 100644 index 0000000..bc5a161 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/MySql/WhereSessionRollsBackTests.cs @@ -0,0 +1,74 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MySql.Data.MySqlClient; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.MySql +{ + public class WhereSessionRollsBackTests : IAsyncLifetime + { + private IDbConnection _connection; + + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new MySqlConnection("server=localhost;database=Test;user=Test;password=Test"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, null); // name cannot be null + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM `Pets` WHERE `PersonId` = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM `People` WHERE `Id` = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldNotBeNull() + { + _exception.Should().NotBeNull(); + } + + [Fact] + public async Task ThePersonAndPetAreNotCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `People` WHERE `Id` = {_personId}"); + count.Should().Be(0); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM `Pets` WHERE `PersonId` = {_personId}"); + count.Should().Be(0); + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/AddPetCommand.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/AddPetCommand.cs new file mode 100644 index 0000000..04a506e --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/AddPetCommand.cs @@ -0,0 +1,40 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.Sql +{ + public class AddPetCommand : ITxCommand + { + public int PersonId { get; set; } + public string PetName { get; set; } + + public AddPetCommand(int personId, string petName) + { + PersonId = personId; + PetName = petName; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO [Pets] ([PersonId],[Name]) VALUES (@PersonId,@PetName)"; + + await connection.ExecuteAsync(query, new {PersonId, PetName}, transaction); + } + + public void Validate() + { + if (PersonId < 1) + { + throw new ArgumentException("PersonId must be at least 1", nameof(PersonId)); + } + + if (string.IsNullOrEmpty(PetName)) + { + throw new ArgumentException("Pet name can not be empty", nameof(PetName)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/CreatePersonCommand.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/CreatePersonCommand.cs new file mode 100644 index 0000000..00e71cd --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/CreatePersonCommand.cs @@ -0,0 +1,33 @@ +using Dapper; +using System; +using System.Data; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand.Sql.Tests.Sql +{ + public class CreatePersonCommand : ITxCommand + { + public string Name { get; set; } + + public CreatePersonCommand(string name) + { + Name = name; + } + + public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction) + { + const string query = "INSERT INTO [People] ([Name]) VALUES (@Name); SELECT SCOPE_IDENTITY();"; + + return await connection.ExecuteScalarAsync(query, new {Name}, transaction); + } + + public void Validate() + { + if (string.IsNullOrEmpty(Name)) + { + throw new ArgumentException("Name cannot be empty", nameof(Name)); + } + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereExecutesSuccessfullyTests.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereExecutesSuccessfullyTests.cs new file mode 100644 index 0000000..c5fe888 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereExecutesSuccessfullyTests.cs @@ -0,0 +1,73 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.Sql +{ + public class WhereExecutesSuccessfullyTests : IAsyncLifetime + { + private IDbConnection _connection; + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new SqlConnection($"Server=localhost;Database=Test;User Id=sa;Password=MySuperSecur3Password!;"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, "Dog"); + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM [Pets] WHERE [PersonId] = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM [People] WHERE [Id] = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldBeNull() + { + _exception.Should().BeNull(); + } + + [Fact] + public async Task ThePersonAndPetIsCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [People] WHERE [Id] = {_personId}"); + count.Should().Be(1); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); + count.Should().Be(1); + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereSessionRollsBackTests.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereSessionRollsBackTests.cs new file mode 100644 index 0000000..8552285 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/Sql/WhereSessionRollsBackTests.cs @@ -0,0 +1,74 @@ +using Dapper; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Sql.Tests.Sql +{ + public class WhereSessionRollsBackTests : IAsyncLifetime + { + private IDbConnection _connection; + + private int _personId; + private Exception _exception; + + public async Task InitializeAsync() + { + _connection = new SqlConnection($"Server=localhost;Database=Test;User Id=sa;Password=MySuperSecur3Password!;"); + _connection.Open(); + + await using var services = new ServiceCollection() + .AddSingleton(_connection) + .AddTxCommand(b => b.AddSql()) + .BuildServiceProvider(); + + var factory = services.GetRequiredService(); + + try + { + using (var session = factory.Create()) + { + var createPerson = new CreatePersonCommand("John"); + _personId = await session.ExecuteAsync(createPerson); + + var createPet = new AddPetCommand(_personId, null); // name cannot be null + await session.ExecuteAsync(createPet); + } + } + catch (Exception e) + { + _exception = e; + } + } + + public async Task DisposeAsync() + { + await _connection.ExecuteAsync($"DELETE FROM [Pets] WHERE [PersonId] = {_personId}"); + await _connection.ExecuteAsync($"DELETE FROM [People] WHERE [Id] = {_personId}"); + _connection?.Dispose(); + } + + [Fact] + public void TheExceptionShouldNotBeNull() + { + _exception.Should().NotBeNull(); + } + + [Fact] + public async Task ThePersonAndPetAreNotCreated() + { + var count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [People] WHERE [Id] = {_personId}"); + count.Should().Be(0); + + count = await _connection.ExecuteScalarAsync( + $"SELECT COUNT(*) FROM [Pets] WHERE [PersonId] = {_personId}"); + count.Should().Be(0); + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/TransactionProviderTests.cs b/Sql/TxCommand.Sql.NetCore3_1.Tests/TransactionProviderTests.cs new file mode 100644 index 0000000..21cb615 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/TransactionProviderTests.cs @@ -0,0 +1,199 @@ +using FluentAssertions; +using Moq; +using System; +using System.Data; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace TxCommand.Sql.Tests +{ + public class TransactionProviderTests + { + [Fact] + public void Ctor_GivenNullConnection_ThrowsArgumentNullException() + { + var ex = Assert.Throws(() => new TransactionProvider(null, new SqlOptions())); + ex.ParamName.Should().Be("connection"); + } + + [Fact] + public void Ctor_GivenNullOptions_ThrowsArgumentNullException() + { + var ex = Assert.Throws(() => new TransactionProvider(Mock.Of(), null)); + ex.ParamName.Should().Be("options"); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereNoTransactionIsActive_BeginsNewTransaction() + { + var database = new Mock(); + database.SetupGet(x => x.State).Returns(ConnectionState.Closed); + database.Setup(x => x.Open()).Verifiable(); + + var transaction = Mock.Of(); + database.Setup(x => x.BeginTransaction(IsolationLevel.ReadUncommitted)).Returns(transaction).Verifiable(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + await provider.EnsureTransactionAsync(CancellationToken.None); + + database.VerifyAll(); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereTransactionIsActive_DoesNotBeginNewTransaction() + { + var database = new Mock(); + database.SetupGet(x => x.State).Returns(ConnectionState.Open); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, Mock.Of()); + + await provider.EnsureTransactionAsync(CancellationToken.None); + + database.VerifyAll(); + database.Verify(x => x.BeginTransaction(), Times.Never); + } + + [Fact] + public async Task EnsureTransactionAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.EnsureTransactionAsync(CancellationToken.None)); + + database.Verify(x => x.BeginTransaction(), Times.Never); + } + + [Fact] + public async Task CommitAsync_WhereTransactionIsActive_Commits() + { + var transaction = new Mock(); + transaction.Setup(x => x.Commit()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + await provider.CommitAsync(CancellationToken.None); + + transaction.VerifyAll(); + transaction.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public async Task CommitAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.CommitAsync(CancellationToken.None)); + } + + [Fact] + public void Commit_WhereTransactionIsActive_Commits() + { + var transaction = new Mock(); + transaction.Setup(x => x.Commit()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + provider.Commit(); + + transaction.VerifyAll(); + transaction.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Commit_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + Assert.Throws(() => provider.Commit()); + } + + [Fact] + public async Task RollbackAsync_WhereTransactionIsActive_RollsBack() + { + var transaction = new Mock(); + transaction.Setup(x => x.Rollback()).Verifiable(); + + var provider = new TransactionProvider(Mock.Of(), new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction.Object); + + await provider.RollbackAsync(CancellationToken.None); + + transaction.VerifyAll(); + transaction.Verify(x => x.Rollback(), Times.Once); + } + + [Fact] + public async Task RollbackAsync_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + await Assert.ThrowsAsync(() => provider.RollbackAsync(CancellationToken.None)); + } + + [Fact] + public void GetExecutionArguments_WhereTransactionIsNotActive_ReturnsDatabase() + { + var database = Mock.Of(); + + var provider = new TransactionProvider(database, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, null); + + var (db, tx) = provider.GetExecutionArguments(); + db.Should().Be(database); + tx.Should().BeNull(); + } + + [Fact] + public void GetExecutionArguments_WhereTransactionIsNotActive_ReturnsDatabaseAndTransaction() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new TransactionProvider(database, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_transaction", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, transaction); + + var (db, tx) = provider.GetExecutionArguments(); + db.Should().Be(database); + tx.Should().Be(transaction); + } + + [Fact] + public void GetExecutionArguments_WhereProviderIsDisposed_ThrowsObjectDisposedException() + { + var database = new Mock(); + + var provider = new TransactionProvider(database.Object, new SqlOptions { IsolationLevel = IsolationLevel.ReadUncommitted }); + provider.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(provider, true); + + Assert.Throws(() => provider.GetExecutionArguments()); + } + } +} diff --git a/Sql/TxCommand.Sql.NetCore3_1.Tests/TxCommand.Sql.NetCore3_1.Tests.csproj b/Sql/TxCommand.Sql.NetCore3_1.Tests/TxCommand.Sql.NetCore3_1.Tests.csproj new file mode 100644 index 0000000..7375230 --- /dev/null +++ b/Sql/TxCommand.Sql.NetCore3_1.Tests/TxCommand.Sql.NetCore3_1.Tests.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp3.1 + + false + + TxCommand.Sql.Tests + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Sql/TxCommand.Sql/BuilderExtensions.cs b/Sql/TxCommand.Sql/BuilderExtensions.cs new file mode 100644 index 0000000..af6297f --- /dev/null +++ b/Sql/TxCommand.Sql/BuilderExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; +using System.Data; +using TxCommand.Abstractions; + +namespace TxCommand +{ + public static class BuilderExtensions + { + public static ITxCommandBuilder AddSql(this ITxCommandBuilder builder) + => AddSql(builder, sqlOptions => + { + sqlOptions.IsolationLevel = IsolationLevel.ReadUncommitted; + }); + + public static ITxCommandBuilder AddSql(this ITxCommandBuilder builder, Action options) + { + var sqlOptions = new SqlOptions(); + options?.Invoke(sqlOptions); + + builder.Services.AddSingleton(sqlOptions); + builder.Services.TryAddTransient, TransactionProvider>(); + builder.Services.TryAddTransient(); + builder.Services.TryAddSingleton(); + + return builder; + } + } +} diff --git a/Sql/TxCommand.Sql/Session.cs b/Sql/TxCommand.Sql/Session.cs new file mode 100644 index 0000000..b698d7d --- /dev/null +++ b/Sql/TxCommand.Sql/Session.cs @@ -0,0 +1,14 @@ +using System.Data; +using TxCommand.Abstractions; + +namespace TxCommand +{ + /// + public class Session : Session, ISession + { + public Session(ITransactionProvider provider) + : base(provider) + { + } + } +} diff --git a/Sql/TxCommand.Sql/SessionFactory.cs b/Sql/TxCommand.Sql/SessionFactory.cs new file mode 100644 index 0000000..e7b243a --- /dev/null +++ b/Sql/TxCommand.Sql/SessionFactory.cs @@ -0,0 +1,15 @@ +using System; +using TxCommand.Abstractions; + +namespace TxCommand +{ + /// + /// Implements + /// + public class SessionFactory : SessionFactory, ISessionFactory + { + public SessionFactory(IServiceProvider services) : base(services) + { + } + } +} diff --git a/Sql/TxCommand.Sql/SqlOptions.cs b/Sql/TxCommand.Sql/SqlOptions.cs new file mode 100644 index 0000000..f1af118 --- /dev/null +++ b/Sql/TxCommand.Sql/SqlOptions.cs @@ -0,0 +1,12 @@ +using System.Data; + +namespace TxCommand +{ + /// + /// A set of options used to configure the Session. + /// + public class SqlOptions + { + public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadUncommitted; + } +} diff --git a/Sql/TxCommand.Sql/TransactionProvider.cs b/Sql/TxCommand.Sql/TransactionProvider.cs new file mode 100644 index 0000000..0fb6a2e --- /dev/null +++ b/Sql/TxCommand.Sql/TransactionProvider.cs @@ -0,0 +1,94 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using TxCommand.Abstractions; + +namespace TxCommand +{ + /// + public class TransactionProvider : ITransactionProvider + { + private readonly IDbConnection _connection; + private readonly SqlOptions _options; + private IDbTransaction _transaction; + + private bool _disposed = false; + + public TransactionProvider(IDbConnection connection, SqlOptions options) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Task EnsureTransactionAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TransactionProvider)); + } + + if (_connection.State == ConnectionState.Closed) + { + _connection.Open(); + } + + if (_transaction == null) + { + _transaction = _connection.BeginTransaction(_options.IsolationLevel); + } + + return Task.CompletedTask; + } + + public Task CommitAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TransactionProvider)); + } + + _transaction.Commit(); + + return Task.CompletedTask; + } + + public void Commit() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TransactionProvider)); + } + + _transaction.Commit(); + } + + public Task RollbackAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TransactionProvider)); + } + + _transaction.Rollback(); + + return Task.CompletedTask; + } + + public (IDbConnection database, IDbTransaction transaction) GetExecutionArguments() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TransactionProvider)); + } + + return (_connection, _transaction); + } + + public void Dispose() + { + _transaction?.Dispose(); + _disposed = true; + } + } +} diff --git a/Sql/TxCommand.Sql/TxCommand.Sql.csproj b/Sql/TxCommand.Sql/TxCommand.Sql.csproj new file mode 100644 index 0000000..bce5036 --- /dev/null +++ b/Sql/TxCommand.Sql/TxCommand.Sql.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0;net5.0 + TxCommand + Reece Russell + Provides SQL implementations for TxCommand, supporting MySQL and SQL Server. + https://github.com/reecerussell/tx-command + git + cqrs, msql, sql, mssql, commanding, query + https://github.com/reecerussell/tx-command + LICENSE + + + + + + + + + + True + + + + + diff --git a/Sql/docker-compose.yaml b/Sql/docker-compose.yaml new file mode 100644 index 0000000..4a861fe --- /dev/null +++ b/Sql/docker-compose.yaml @@ -0,0 +1,30 @@ +version: "3.8" + +services: + mysql: + image: mysql:8.0.23 + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + volumes: + - "./.mysql/:/docker-entrypoint-initdb.d/" + environment: + MYSQL_ROOT_PASSWORD: Test + MYSQL_DATABASE: Test + MYSQL_USER: Test + MYSQL_PASSWORD: Test + ports: + - 3306:3306 + healthcheck: + test: '/usr/bin/mysql --user=root --password=test --execute "SHOW DATABASES;"' + interval: 2s + timeout: 20s + retries: 10 + + mssql: + build: + context: .mssql + dockerfile: Dockerfile + environment: + SA_PASSWORD: "MySuperSecur3Password!" + ACCEPT_EULA: "y" + ports: + - 1433:1433 diff --git a/TxCommand.Abstractions/Exceptions/TransactionNotStartedException.cs b/TxCommand.Abstractions/Exceptions/TransactionNotStartedException.cs new file mode 100644 index 0000000..8b72f39 --- /dev/null +++ b/TxCommand.Abstractions/Exceptions/TransactionNotStartedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace TxCommand.Abstractions.Exceptions +{ + public class TransactionNotStartedException : InvalidOperationException + { + public TransactionNotStartedException(string methodName) + : base($"Cannot call {methodName} as a transaction has either not started, or been completed.") + { + } + } +} diff --git a/TxCommand.Abstractions/ISession.cs b/TxCommand.Abstractions/ISession.cs new file mode 100644 index 0000000..04589c7 --- /dev/null +++ b/TxCommand.Abstractions/ISession.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; + +namespace TxCommand.Abstractions +{ +#if NETSTANDARD2_0_OR_GREATER + + public interface ISession : IDisposable + where TDatabase : class + where TTransaction : class + { + Task ExecuteAsync(ITxCommand command); + + Task ExecuteAsync(ITxCommand command); + + Task CommitAsync(); + + void Commit(); + + Task RollbackAsync(); + } + +#endif + +#if NET5_0 + + public interface ISession : IDisposable, IAsyncDisposable + where TDatabase : class + where TTransaction : class + { + Task ExecuteAsync(ITxCommand command); + + Task ExecuteAsync(ITxCommand command); + + Task CommitAsync(); + + void Commit(); + + Task RollbackAsync(); + } + +#endif +} diff --git a/TxCommand.Abstractions/ISessionFactory.cs b/TxCommand.Abstractions/ISessionFactory.cs new file mode 100644 index 0000000..2645bed --- /dev/null +++ b/TxCommand.Abstractions/ISessionFactory.cs @@ -0,0 +1,14 @@ +namespace TxCommand.Abstractions +{ + /// + /// Used to create new instances of . + /// + public interface ISessionFactory + where TSession : class + { + /// + /// Creates and returns a new instance of . + /// + TSession Create(); + } +} diff --git a/TxCommand.Abstractions/ITransactionProvider.cs b/TxCommand.Abstractions/ITransactionProvider.cs new file mode 100644 index 0000000..5638c3a --- /dev/null +++ b/TxCommand.Abstractions/ITransactionProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace TxCommand.Abstractions +{ + /// + /// Used in a Session to interface with database providers. + /// + /// + /// + public interface ITransactionProvider : IDisposable + where TDatabase : class + where TTransaction : class + { + /// + /// Ensures that a transaction has started. + /// + Task EnsureTransactionAsync(CancellationToken cancellationToken); + + /// + /// Commits the underlying database transaction. + /// + Task CommitAsync(CancellationToken cancellationToken); + + /// + /// Commits the underlying database transaction. + /// + void Commit(); + + /// + /// Rolls back the underlying database transaction. + /// + Task RollbackAsync(CancellationToken cancellationToken); + + /// + /// Used by Session to provide a command with the arguments required to execute. + /// + /// Returns the an instance of and the current . + (TDatabase database, TTransaction transaction) GetExecutionArguments(); + } +} diff --git a/TxCommand.Abstractions/ITxCommand.cs b/TxCommand.Abstractions/ITxCommand.cs index 969d66f..7cc3e65 100644 --- a/TxCommand.Abstractions/ITxCommand.cs +++ b/TxCommand.Abstractions/ITxCommand.cs @@ -8,53 +8,49 @@ namespace TxCommand.Abstractions /// public interface ICommand { + /// + /// Validates the command before execution, ensuring the command contains valid arguments. + /// + void Validate(); } /// /// A transaction command is an abstraction used to execute a command which requires - /// a database transaction in order to operate correctly. provides a method + /// a database transaction in order to operate correctly. provides a method /// to execute the command, providing it with a . /// - public interface ITxCommand : ICommand + public interface ITxCommand : ICommand + where TDatabase : class + where TTransaction : class { /// - /// Executes the implementing command, providing a , allowing + /// Executes the implementing command, providing a , allowing /// the command to execute database operations within the bounds of a transaction. /// - /// A database connection. + /// A database connection. /// A database transaction for the current scope. /// - Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction); - - /// - /// Validates the command before execution, ensuring the command contains valid arguments. - /// - void Validate(); + Task ExecuteAsync(TDatabase database, TTransaction transaction); } /// /// A transaction command is an abstraction used to execute a command which requires - /// a database transaction in order to operate correctly. provides a method - /// to execute the command, providing it with a . + /// a database transaction in order to operate correctly. provides a method + /// to execute the command, providing it with a . /// /// This command interface behaves the same as the non-generic interface, however, /// this provides a type argument, , allowing the /// command to output data. /// - public interface ITxCommand : ICommand + public interface ITxCommand : ICommand { /// - /// Executes the implementing command, providing a , allowing + /// Executes the implementing command, providing a , allowing /// the command to execute database operations within the bounds of a transaction. /// - /// A database connection. + /// A database connection. /// A database transaction for the current scope. /// - Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction); - - /// - /// Validates the command before execution, ensuring the command contains valid arguments. - /// - void Validate(); + Task ExecuteAsync(TDatabase database, TTransaction transaction); } } diff --git a/TxCommand.Abstractions/ITxCommandBuilder.cs b/TxCommand.Abstractions/ITxCommandBuilder.cs new file mode 100644 index 0000000..d594b82 --- /dev/null +++ b/TxCommand.Abstractions/ITxCommandBuilder.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace TxCommand.Abstractions +{ + /// + /// Used to provide extension methods when setting up TxCommand + /// in a DI container. + /// + public interface ITxCommandBuilder + { + IServiceCollection Services { get; } + } +} diff --git a/TxCommand.Abstractions/ITxCommandExecutor.cs b/TxCommand.Abstractions/ITxCommandExecutor.cs deleted file mode 100644 index e322ec6..0000000 --- a/TxCommand.Abstractions/ITxCommandExecutor.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Data; -using System.Threading.Tasks; - -namespace TxCommand.Abstractions -{ - /// - /// Used to execute s, providing them with an open . - /// acts as a transaction, therefore should only used on a per-scope basis, - /// and only for a specific collection of s with a specific area of concern. - /// - public interface ITxCommandExecutor : IDisposable - { - /// - /// Commits the underlying . - /// - void Commit(); - - /// - /// Rolls back the underlying . - /// - void Rollback(); - - /// - /// Executes the given , , within the current transaction. - /// - /// The command to execute. - /// - Task ExecuteAsync(ITxCommand command); - - /// - /// Executes the given , , within the current transaction. - /// - /// The command to execute. - /// The command's output, . - Task ExecuteAsync(ITxCommand command); - } -} diff --git a/TxCommand.Abstractions/ITxCommandExecutorFactory.cs b/TxCommand.Abstractions/ITxCommandExecutorFactory.cs deleted file mode 100644 index 4e41034..0000000 --- a/TxCommand.Abstractions/ITxCommandExecutorFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace TxCommand.Abstractions -{ - /// - /// Used to initialise new instances of . This is used - /// as s should be used on a per-scope basis, so this - /// gives the ability to create a new instance for a required scope. - /// - public interface ITxCommandExecutorFactory - { - /// - /// Used to return a new instance of . - /// - /// A new instance of . - ITxCommandExecutor Create(); - } -} diff --git a/TxCommand.Abstractions/TxCommand.Abstractions.csproj b/TxCommand.Abstractions/TxCommand.Abstractions.csproj index 1e0463e..c85c29d 100644 --- a/TxCommand.Abstractions/TxCommand.Abstractions.csproj +++ b/TxCommand.Abstractions/TxCommand.Abstractions.csproj @@ -1,18 +1,19 @@ - netstandard2.0 + netstandard2.0;net5.0 true Reece Russell - Provides key interfaces for TxCommand, including ITxCommand, ITxCommandExecutor and ITxCommandExecutorFactory. + Provides core abstractions for the TxCommand package. https://github.com/reecerussell/tx-command git CQRS, commanding, sql, transaction - 0.3.0.0 - 0.3.0.0 - 0.3.0 + 1.0.0.0 + 1.0.0.0 + 1.0.0 LICENSE https://github.com/reecerussell/tx-command + A major rewrite of internals and key interfaces. ITxCommandExecutor has been replaced with ISession. @@ -22,4 +23,8 @@ + + + + diff --git a/TxCommand.Net5.Tests/SessionFactoryTests.cs b/TxCommand.Net5.Tests/SessionFactoryTests.cs new file mode 100644 index 0000000..6989f9e --- /dev/null +++ b/TxCommand.Net5.Tests/SessionFactoryTests.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Tests +{ + public class SessionFactoryTests + { + [Fact] + public void Create_ReturnsNewInstanceOfSession() + { + var services = new ServiceCollection() + .AddTransient(_ => Mock.Of>()) + .BuildServiceProvider(); + + var factory = new SessionFactory>(services); + + factory.Create().Should().NotBeNull(); + } + } +} diff --git a/TxCommand.Net5.Tests/SessionTests.cs b/TxCommand.Net5.Tests/SessionTests.cs new file mode 100644 index 0000000..a87af76 --- /dev/null +++ b/TxCommand.Net5.Tests/SessionTests.cs @@ -0,0 +1,579 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using TxCommand.Abstractions; +using TxCommand.Abstractions.Exceptions; +using Xunit; + +namespace TxCommand.Tests +{ + public interface ITestDatabase {} + + public interface ITestTransaction {} + + public class SessionTests + { + #region TxCommand + + [Fact] + public async Task ExecuteAsync_GivenTxCommand_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.CommitAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var callbackCalled = false; + + await using (var session = new Session(provider.Object)) + { + session.OnExecuted += cmd => callbackCalled = cmd == command.Object; + + var task = session.ExecuteAsync(command.Object); + await task; + + task.IsCompletedSuccessfully.Should().BeTrue(); + } + + callbackCalled.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Once); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereValidateFails_ExecutesSuccessfully() + { + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()) + .Throws(testException) + .Verifiable(); + + await using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereExecuteFails_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Throws(testException) + .Verifiable(); + + await using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenNullTxCommand_ThrowsArgumentNullException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + ITxCommand command = null; + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + ex.ParamName.Should().Be("command"); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereSessionHasBeenDisposed_ThrowsObjectDisposedException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + var command = Mock.Of>(); + await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + } + + #endregion + + #region TxCommandT + + [Fact] + public async Task ExecuteAsync_GivenTxCommandT_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.CommitAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + const string testResult = "Hello World"; + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .ReturnsAsync(testResult) + .Verifiable(); + + var callbackCalled = false; + + await using (var session = new Session(provider.Object)) + { + session.OnExecuted += cmd => callbackCalled = cmd == command.Object; + + var task = session.ExecuteAsync(command.Object); + await task; + + task.IsCompletedSuccessfully.Should().BeTrue(); + task.Result.Should().Be(testResult); + } + + callbackCalled.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Once); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereValidateFails_ExecutesSuccessfully() + { + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()) + .Throws(testException) + .Verifiable(); + + await using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereExecuteFails_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Throws(testException) + .Verifiable(); + + await using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenNullTxCommandT_ThrowsArgumentNullException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + ITxCommand command = null; + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + ex.ParamName.Should().Be("command"); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereSessionHasBeenDisposed_ThrowsObjectDisposedException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + var command = Mock.Of>(); + await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + } + + #endregion + + #region CommitAsync + + [Fact] + public async Task CommitAsync_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.CommitAsync(It.IsAny())) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnCommitted += () => callbackInvoked = true; + + await session.CommitAsync(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task CommitAsync_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.CommitAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task CommitAsync_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.CommitAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + } + + #endregion + + #region Commit + + [Fact] + public void Commit_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.Commit()) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnCommitted += () => callbackInvoked = true; + + session.Commit(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Commit_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + Assert.Throws(() => session.Commit()); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + [Fact] + public void Commit_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + Assert.Throws(() => session.Commit()); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + #endregion + + #region RollbackAsync + + [Fact] + public async Task RollbackAsync_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnRolledBack += () => callbackInvoked = true; + + await session.RollbackAsync(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task RollbackAsync_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.RollbackAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task RollbackAsync_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.RollbackAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + } + + #endregion + + #region Dispose + + [Fact] + public void Dispose_WhereNotAlreadyDisposed_Commits() + { + var provider = new Mock>(); + provider.Setup(x => x.Commit()) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + session.Dispose(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Dispose_WhereAlreadyDisposed_DoesNothing() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + session.Dispose(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + #endregion + + #region DisposeAsync + + [Fact] + public async Task DisposeAsync_WhereNotAlreadyDisposed_Commits() + { + var provider = new Mock>(); + provider.Setup(x => x.CommitAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + await session.DisposeAsync(); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task DisposeAsync_WhereAlreadyDisposed_DoesNothing() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + await session.DisposeAsync(); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + } + + #endregion + } +} diff --git a/TxCommand.Net5.Tests/TxCommand.Net5.Tests.csproj b/TxCommand.Net5.Tests/TxCommand.Net5.Tests.csproj new file mode 100644 index 0000000..979bab2 --- /dev/null +++ b/TxCommand.Net5.Tests/TxCommand.Net5.Tests.csproj @@ -0,0 +1,35 @@ + + + + net5.0 + + false + + TxCommand.Tests + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/TxCommand.NetCore3_1.Tests/SessionFactoryTests.cs b/TxCommand.NetCore3_1.Tests/SessionFactoryTests.cs new file mode 100644 index 0000000..6989f9e --- /dev/null +++ b/TxCommand.NetCore3_1.Tests/SessionFactoryTests.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using TxCommand.Abstractions; +using Xunit; + +namespace TxCommand.Tests +{ + public class SessionFactoryTests + { + [Fact] + public void Create_ReturnsNewInstanceOfSession() + { + var services = new ServiceCollection() + .AddTransient(_ => Mock.Of>()) + .BuildServiceProvider(); + + var factory = new SessionFactory>(services); + + factory.Create().Should().NotBeNull(); + } + } +} diff --git a/TxCommand.NetCore3_1.Tests/SessionTests.cs b/TxCommand.NetCore3_1.Tests/SessionTests.cs new file mode 100644 index 0000000..18cdedc --- /dev/null +++ b/TxCommand.NetCore3_1.Tests/SessionTests.cs @@ -0,0 +1,534 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using TxCommand.Abstractions; +using TxCommand.Abstractions.Exceptions; +using Xunit; + +namespace TxCommand.Tests +{ + public interface ITestDatabase {} + + public interface ITestTransaction {} + + public class SessionTests + { + #region TxCommand + + [Fact] + public async Task ExecuteAsync_GivenTxCommand_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.Commit()) + .Verifiable(); + + var command = new Mock>(); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var callbackCalled = false; + + using (var session = new Session(provider.Object)) + { + session.OnExecuted += cmd => callbackCalled = cmd == command.Object; + + var task = session.ExecuteAsync(command.Object); + await task; + + task.IsCompletedSuccessfully.Should().BeTrue(); + } + + callbackCalled.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereValidateFails_ExecutesSuccessfully() + { + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()) + .Throws(testException) + .Verifiable(); + + using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereExecuteFails_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Throws(testException) + .Verifiable(); + + using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenNullTxCommand_ThrowsArgumentNullException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + ITxCommand command = null; + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + ex.ParamName.Should().Be("command"); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandWhereSessionHasBeenDisposed_ThrowsObjectDisposedException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + var command = Mock.Of>(); + await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + } + + #endregion + + #region TxCommandT + + [Fact] + public async Task ExecuteAsync_GivenTxCommandT_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.Commit()) + .Verifiable(); + + var command = new Mock>(); + const string testResult = "Hello World"; + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .ReturnsAsync(testResult) + .Verifiable(); + + var callbackCalled = false; + + using (var session = new Session(provider.Object)) + { + session.OnExecuted += cmd => callbackCalled = cmd == command.Object; + + var task = session.ExecuteAsync(command.Object); + await task; + + task.IsCompletedSuccessfully.Should().BeTrue(); + task.Result.Should().Be(testResult); + } + + callbackCalled.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereValidateFails_ExecutesSuccessfully() + { + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()) + .Throws(testException) + .Verifiable(); + + using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereExecuteFails_ExecutesSuccessfully() + { + var database = Mock.Of(); + var transaction = Mock.Of(); + + var provider = new Mock>(); + provider.Setup(x => x.EnsureTransactionAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + provider.Setup(x => x.GetExecutionArguments()) + .Returns((database, transaction)); + + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var command = new Mock>(); + var testException = new Exception("test"); + command.Setup(x => x.Validate()).Verifiable(); + command.Setup(x => x.ExecuteAsync(database, transaction)) + .Throws(testException) + .Verifiable(); + + using (var session = new Session(provider.Object)) + { + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command.Object)); + ex.Should().Be(testException); + } + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + + command.VerifyAll(); + command.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_GivenNullTxCommandT_ThrowsArgumentNullException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + ITxCommand command = null; + var ex = await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + ex.ParamName.Should().Be("command"); + } + + [Fact] + public async Task ExecuteAsync_GivenTxCommandTWhereSessionHasBeenDisposed_ThrowsObjectDisposedException() + { + var provider = Mock.Of>(); + var session = new Session(provider); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + var command = Mock.Of>(); + await Assert.ThrowsAsync(() => session.ExecuteAsync(command)); + } + + #endregion + + #region CommitAsync + + [Fact] + public async Task CommitAsync_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.CommitAsync(It.IsAny())) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnCommitted += () => callbackInvoked = true; + + await session.CommitAsync(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task CommitAsync_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.CommitAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task CommitAsync_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.CommitAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.CommitAsync(It.IsAny()), Times.Never); + } + + #endregion + + #region Commit + + [Fact] + public void Commit_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.Commit()) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnCommitted += () => callbackInvoked = true; + + session.Commit(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Commit_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + Assert.Throws(() => session.Commit()); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + [Fact] + public void Commit_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + Assert.Throws(() => session.Commit()); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + #endregion + + #region RollbackAsync + + [Fact] + public async Task RollbackAsync_WhereNotCompleted_CommitsProvider() + { + var provider = new Mock>(); + provider.Setup(x => x.RollbackAsync(It.IsAny())) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + var callbackInvoked = false; + session.OnRolledBack += () => callbackInvoked = true; + + await session.RollbackAsync(); + + callbackInvoked.Should().BeTrue(); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task RollbackAsync_WhereCompleted_ThrowsTransactionNotStartedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.RollbackAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task RollbackAsync_WhereDisposed_ThrowsObjectDisposedException() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + + await Assert.ThrowsAsync(() => session.RollbackAsync()); + + provider.VerifyAll(); + provider.Verify(x => x.RollbackAsync(It.IsAny()), Times.Never); + } + + #endregion + + #region Dispose + + [Fact] + public void Dispose_WhereNotAlreadyDisposed_Commits() + { + var provider = new Mock>(); + provider.Setup(x => x.Commit()) + .Verifiable(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + session.Dispose(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Once); + } + + [Fact] + public void Dispose_WhereAlreadyDisposed_DoesNothing() + { + var provider = new Mock>(); + + var session = new Session(provider.Object); + + session.GetType().GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, true); + session.GetType().GetField("_completed", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(session, false); + + session.Dispose(); + + provider.VerifyAll(); + provider.Verify(x => x.Commit(), Times.Never); + } + + #endregion + } +} diff --git a/TxCommand.Tests/TxCommand.Tests.csproj b/TxCommand.NetCore3_1.Tests/TxCommand.NetCore3_1.Tests.csproj similarity index 88% rename from TxCommand.Tests/TxCommand.Tests.csproj rename to TxCommand.NetCore3_1.Tests/TxCommand.NetCore3_1.Tests.csproj index bca8198..a074a08 100644 --- a/TxCommand.Tests/TxCommand.Tests.csproj +++ b/TxCommand.NetCore3_1.Tests/TxCommand.NetCore3_1.Tests.csproj @@ -4,6 +4,8 @@ netcoreapp3.1 false + + TxCommand.Tests @@ -11,6 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/TxCommand.Tests/ServiceCollectionExtensionsTests.cs b/TxCommand.Tests/ServiceCollectionExtensionsTests.cs deleted file mode 100644 index 6810bd8..0000000 --- a/TxCommand.Tests/ServiceCollectionExtensionsTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moq; -using System; -using System.Data; -using TxCommand.Abstractions; -using Xunit; - -namespace TxCommand.Tests -{ - public class ServiceCollectionExtensionsTests - { - [Fact] - public void AddTxCommand_SetsUpServices_ReturnsInitialCollectionWithServices() - { - var services = new ServiceCollection() - .AddScoped(_ => Mock.Of()); - - services.AddTxCommand(); - - var provider = services.BuildServiceProvider(); - - Assert.IsType(provider.GetRequiredService()); - Assert.IsType(provider.GetRequiredService()); - } - - [Fact] - public void AddTxCommand_RetrievingITxCommandExecutorFactoryInSameScope_ReturnsNewInstances() - { - var services = new ServiceCollection() - .AddScoped(_ => Mock.Of()); - - services.AddTxCommand(); - - var provider = services.BuildServiceProvider() - .CreateScope() - .ServiceProvider; - - var factory1 = provider.GetRequiredService(); - var factory2 = provider.GetRequiredService(); - - // Factories are transient. - Assert.NotEqual(factory1, factory2); - } - - [Fact] - public void AddTxCommand_RetrievingITxCommandExecutorInSameScope_ReturnsNewInstances() - { - var services = new ServiceCollection() - .AddScoped(_ => Mock.Of()); - - services.AddTxCommand(); - - var provider = services.BuildServiceProvider() - .CreateScope() - .ServiceProvider; - - var executor1 = provider.GetRequiredService(); - var executor2 = provider.GetRequiredService(); - - // Executors are transient. - Assert.NotEqual(executor1, executor2); - } - - [Fact] - public void ResolveCommandExecutorFactory_WithNoIDbConnectionService_Throws() - { - var services = new ServiceCollection(); - - services.AddTxCommand(); - - var provider = services.BuildServiceProvider(); - - Assert.Throws(() => provider.GetRequiredService()); - } - } -} diff --git a/TxCommand.Tests/TxCommandExecutorFactoryTests.cs b/TxCommand.Tests/TxCommandExecutorFactoryTests.cs deleted file mode 100644 index f4ae117..0000000 --- a/TxCommand.Tests/TxCommandExecutorFactoryTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Moq; -using System.Data; -using System.Threading.Tasks; -using TxCommand.Abstractions; -using Xunit; - -namespace TxCommand.Tests -{ - public class TxCommandExecutorFactoryTests - { - [Fact] - public async Task Create_WithIDBConnectionDependency_ReturnsNewCommandExecutor() - { - var connection = new Mock(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Open).Verifiable(); - - var factory = new TxCommandExecutorFactory(connection.Object); - - // Proves connection was passed through to the new command executor. - var commandExecutor = factory.Create(); - await commandExecutor.ExecuteAsync(Mock.Of()); - connection.Verify(x => x.State, Times.Once); - } - } -} diff --git a/TxCommand.Tests/TxCommandExecutorTests.cs b/TxCommand.Tests/TxCommandExecutorTests.cs deleted file mode 100644 index 1854ce5..0000000 --- a/TxCommand.Tests/TxCommandExecutorTests.cs +++ /dev/null @@ -1,403 +0,0 @@ -using Moq; -using System; -using System.Data; -using System.Threading.Tasks; -using TxCommand.Abstractions; -using Xunit; - -namespace TxCommand.Tests -{ - public class TxCommandExecutorTests - { - [Fact] - public async Task Commit_CommitsTheUnderlyingTransaction_WithNoError() - { - var transaction = new Mock(); - transaction.Setup(x => x.Commit()).Verifiable(); - - var connection = new Mock(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Open); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var callbackCalled = false; - commandExecutor.OnCommitted += () => - { - callbackCalled = true; - }; - - // Start a transaction. - await commandExecutor.ExecuteAsync(Mock.Of()); - - // Act - commandExecutor.Commit(); - - Assert.True(callbackCalled); - - transaction.Verify(x => x.Commit(), Times.Once); - } - - [Fact] - public void Commit_WithDisposedCommandExecutor_ThrowsObjectDisposedException() - { - var connection = new Mock(); - connection.Setup(x => x.BeginTransaction()).Returns(Mock.Of()); - - var commandExecutor = new TxCommandExecutor(connection.Object); - commandExecutor.Dispose(); - - Assert.Throws(() => commandExecutor.Commit()); - } - - [Fact] - public void Commit_WithUninitializedTransaction_DoesNotThrowNullReference() - { - var commandExecutor = new TxCommandExecutor(Mock.Of()); - commandExecutor.Commit(); - } - - [Fact] - public async Task Rollback_RollsBackTheUnderlyingTransaction_WithNoError() - { - var transaction = new Mock(); - transaction.Setup(x => x.Rollback()).Verifiable(); - - var connection = new Mock(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var callbackCalled = false; - commandExecutor.OnRolledBack += () => - { - callbackCalled = true; - }; - - // Start a transaction. - await commandExecutor.ExecuteAsync(Mock.Of()); - - // Act - commandExecutor.Rollback(); - - Assert.True(callbackCalled); - - transaction.Verify(x => x.Rollback(), Times.Once); - } - - [Fact] - public void Rollback_WithDisposedCommandExecutor_ThrowsObjectDisposedException() - { - var connection = new Mock(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Open); - connection.Setup(x => x.BeginTransaction()).Returns(Mock.Of()); - - var commandExecutor = new TxCommandExecutor(connection.Object); - commandExecutor.Dispose(); - - Assert.Throws(() => commandExecutor.Rollback()); - } - - [Fact] - public void Rollback_WithUninitializedTransaction_DoesNotThrowNullReference() - { - var commandExecutor = new TxCommandExecutor(Mock.Of()); - commandExecutor.Rollback(); - } - - [Fact] - public async Task ExecuteAsync_GivenCommand_ExecutesTheCommand() - { - var connection = new Mock(); - var transaction = Mock.Of(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction).Verifiable(); - - var command = new Mock(); - command.Setup(x => x.Validate()).Verifiable(); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction)).Returns(Task.CompletedTask).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var callbackCalled = false; - commandExecutor.OnExecuted += (c) => - { - callbackCalled = true; - Assert.Equal(command.Object, c); - }; - - await commandExecutor.ExecuteAsync(command.Object); - - Assert.True(callbackCalled); - - command.Verify(x => x.Validate(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction), Times.Once); - connection.Verify(x => x.BeginTransaction()); - } - - [Fact] - public async Task ExecuteAsync_GivenClosedConnection_OpensTheConnection() - { - var connection = new Mock(); - var transaction = Mock.Of(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction).Verifiable(); - connection.Setup(x => x.Open()).Verifiable(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Closed); - - var command = new Mock(); - command.Setup(x => x.Validate()).Verifiable(); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction)).Returns(Task.CompletedTask).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - await commandExecutor.ExecuteAsync(command.Object); - - command.Verify(x => x.Validate(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction), Times.Once); - connection.Verify(x => x.BeginTransaction()); - connection.Verify(x => x.Open()); - } - - [Fact] - public async Task ExecuteAsync_GivenNullCommand_ThrowsArgumentNullException() - { - var connection = new Mock(); - var transaction = Mock.Of(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(null)); - - Assert.Equal("command", ex.ParamName); - } - - [Fact] - public async Task ExecuteAsync_WithDisposedCommandExecutor_ThrowsObjectDisposedException() - { - var transaction = new Mock(); - var connection = new Mock(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock(); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction.Object)).Returns(Task.CompletedTask).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - commandExecutor.Dispose(); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(nameof(TxCommandExecutor), ex.ObjectName); - - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Never); - } - - [Fact] - public async Task ExecuteAsync_WhereCommandThrowsException_RollsBackAndThrows() - { - var connection = new Mock(); - var transaction = new Mock(); - transaction.Setup(x => x.Rollback()).Verifiable(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock(); - var testException = new Exception("Test"); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction.Object)).Throws(testException).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(ex, testException); - - transaction.Verify(x => x.Rollback(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Once); - } - - [Fact] - public async Task ExecuteAsync_WhereValidationFails_RollsBackAndThrows() - { - var connection = new Mock(); - var transaction = new Mock(); - transaction.Setup(x => x.Rollback()).Verifiable(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock(); - var testException = new Exception("Test"); - command.Setup(x => x.Validate()).Throws(testException).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(ex, testException); - - transaction.Verify(x => x.Rollback(), Times.Once); - command.Verify(x => x.Validate(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Never); - } - - [Fact] - public async Task ExecuteAsyncWithResult_GivenCommand_ExecutesTheCommand() - { - const string testResult = "Test"; - - var connection = new Mock(); - var transaction = Mock.Of(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction).Verifiable(); - - var command = new Mock>(); - command.Setup(x => x.Validate()).Verifiable(); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction)).ReturnsAsync(testResult).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var callbackCalled = false; - commandExecutor.OnExecuted += (c) => - { - callbackCalled = true; - Assert.Equal(command.Object, c); - }; - - var result =await commandExecutor.ExecuteAsync(command.Object); - - Assert.Equal(testResult, result); - Assert.True(callbackCalled); - - command.Verify(x => x.Validate(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction), Times.Once); - connection.Verify(x => x.BeginTransaction()); - } - - [Fact] - public async Task ExecuteAsyncWithResult_GivenNullCommand_ThrowsArgumentNullException() - { - var connection = new Mock(); - var transaction = Mock.Of(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(null)); - - Assert.Equal("command", ex.ParamName); - } - - [Fact] - public async Task ExecuteAsyncWithResult_WithDisposedCommandExecutor_ThrowsObjectDisposedException() - { - const string testResult = "Test"; - - var transaction = new Mock(); - var connection = new Mock(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Open); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock>(); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction.Object)).ReturnsAsync(testResult).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - commandExecutor.Dispose(); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(nameof(TxCommandExecutor), ex.ObjectName); - - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Never); - } - - [Fact] - public async Task ExecuteAsyncWithResult_WhereCommandThrowsException_RollsBackAndThrows() - { - var connection = new Mock(); - var transaction = new Mock(); - transaction.Setup(x => x.Rollback()).Verifiable(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock>(); - var testException = new Exception("Test"); - command.Setup(x => x.ExecuteAsync(connection.Object, transaction.Object)).Throws(testException).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(ex, testException); - - transaction.Verify(x => x.Rollback(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Once); - } - - [Fact] - public async Task ExecuteAsyncWithResult_WhereValidationFails_RollsBackAndThrows() - { - var connection = new Mock(); - var transaction = new Mock(); - transaction.Setup(x => x.Rollback()).Verifiable(); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var command = new Mock>(); - var testException = new Exception("Test"); - command.Setup(x => x.Validate()).Throws(testException).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - var ex = await Assert.ThrowsAsync( - async () => await commandExecutor.ExecuteAsync(command.Object)); - Assert.Equal(ex, testException); - - transaction.Verify(x => x.Rollback(), Times.Once); - command.Verify(x => x.Validate(), Times.Once); - command.Verify(x => x.ExecuteAsync(connection.Object, transaction.Object), Times.Never); - } - - [Fact] - public async Task Dispose_GivenInCompleteTransaction_CommitsAndDisposedTransaction() - { - var transaction = new Mock(); - transaction.Setup(x => x.Commit()).Verifiable(); - transaction.Setup(x => x.Dispose()).Verifiable(); - - var connection = new Mock(); - connection.SetupGet(x => x.State).Returns(ConnectionState.Open); - connection.Setup(x => x.BeginTransaction()).Returns(transaction.Object); - - var commandExecutor = new TxCommandExecutor(connection.Object); - - // Start a transaction. - await commandExecutor.ExecuteAsync(Mock.Of()); - - // Act - commandExecutor.Dispose(); - - transaction.Verify(x => x.Commit(), Times.Once); - transaction.Verify(x => x.Dispose(), Times.Once); - } - - [Fact] - public void Dispose_GivenDisposedCommandExecutor_DoesNothing() - { - var connection = new Mock(); - var transaction = new Mock(); - transaction.Setup(x => x.Commit()).Verifiable(); - transaction.Setup(x => x.Dispose()).Verifiable(); - - var commandExecutor = new TxCommandExecutor(connection.Object); - commandExecutor.Dispose(); // call dispose to mark executor as disposed. - - // Act - commandExecutor.Dispose(); - - // Ensure transaction is only called in setup dispose call. - transaction.Verify(x => x.Commit(), Times.Never); - transaction.Verify(x => x.Dispose(), Times.Never); - } - - [Fact] - public void Dispose_WithUninitializedTransaction_DoesNotThrowNullReference() - { - var commandExecutor = new TxCommandExecutor(Mock.Of()); - commandExecutor.Dispose(); - } - } -} diff --git a/TxCommand.sln b/TxCommand.sln index a781013..8765c5f 100644 --- a/TxCommand.sln +++ b/TxCommand.sln @@ -1,55 +1,75 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29905.134 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand", "TxCommand\TxCommand.csproj", "{513B97B7-62E5-40B1-BA9C-C292100FA656}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Abstractions", "TxCommand.Abstractions\TxCommand.Abstractions.csproj", "{B12D5EE5-EFA9-4492-8897-DD39A2889ABF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TxCommand.Tests", "TxCommand.Tests\TxCommand.Tests.csproj", "{9AD7230D-988A-466C-9536-37340078BBA1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Example", "Example", "{8B8F046F-9A36-4C19-91C8-E747300D9FDF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TxCommand.Example", "Example\TxCommand.Example\TxCommand.Example.csproj", "{F33E6CE6-51D3-418E-B216-606CDCF2DCD1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TxCommand.Example.IntegrationTests", "Example\TxCommand.Example.IntegrationTests\TxCommand.Example.IntegrationTests.csproj", "{FAC983FD-8992-41C5-A407-CB6514F110D3}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {513B97B7-62E5-40B1-BA9C-C292100FA656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {513B97B7-62E5-40B1-BA9C-C292100FA656}.Debug|Any CPU.Build.0 = Debug|Any CPU - {513B97B7-62E5-40B1-BA9C-C292100FA656}.Release|Any CPU.ActiveCfg = Release|Any CPU - {513B97B7-62E5-40B1-BA9C-C292100FA656}.Release|Any CPU.Build.0 = Release|Any CPU - {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Release|Any CPU.Build.0 = Release|Any CPU - {9AD7230D-988A-466C-9536-37340078BBA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AD7230D-988A-466C-9536-37340078BBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AD7230D-988A-466C-9536-37340078BBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AD7230D-988A-466C-9536-37340078BBA1}.Release|Any CPU.Build.0 = Release|Any CPU - {F33E6CE6-51D3-418E-B216-606CDCF2DCD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F33E6CE6-51D3-418E-B216-606CDCF2DCD1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F33E6CE6-51D3-418E-B216-606CDCF2DCD1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F33E6CE6-51D3-418E-B216-606CDCF2DCD1}.Release|Any CPU.Build.0 = Release|Any CPU - {FAC983FD-8992-41C5-A407-CB6514F110D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAC983FD-8992-41C5-A407-CB6514F110D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAC983FD-8992-41C5-A407-CB6514F110D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAC983FD-8992-41C5-A407-CB6514F110D3}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F33E6CE6-51D3-418E-B216-606CDCF2DCD1} = {8B8F046F-9A36-4C19-91C8-E747300D9FDF} - {FAC983FD-8992-41C5-A407-CB6514F110D3} = {8B8F046F-9A36-4C19-91C8-E747300D9FDF} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {85A24F20-DC39-4A89-9EF5-71AD5672F3D0} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29905.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand", "TxCommand\TxCommand.csproj", "{513B97B7-62E5-40B1-BA9C-C292100FA656}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Abstractions", "TxCommand.Abstractions\TxCommand.Abstractions.csproj", "{B12D5EE5-EFA9-4492-8897-DD39A2889ABF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.NetCore3_1.Tests", "TxCommand.NetCore3_1.Tests\TxCommand.NetCore3_1.Tests.csproj", "{9AD7230D-988A-466C-9536-37340078BBA1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sql", "Sql", "{A09BDC81-7A8A-46C4-83B9-E7148E5627F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Sql", "Sql\TxCommand.Sql\TxCommand.Sql.csproj", "{BC3755CF-12D8-42F3-ACB5-F7CC11218CD7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Sql.Abstractions", "Sql\TxCommand.Sql.Abstractions\TxCommand.Sql.Abstractions.csproj", "{B55572A6-D9B4-45CA-8648-EE7E87638948}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Net5.Tests", "TxCommand.Net5.Tests\TxCommand.Net5.Tests.csproj", "{77226287-7F3C-47E9-BD8B-9D9B82CAAC9E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Sql.Net5.Tests", "Sql\TxCommand.Sql.Net5.Tests\TxCommand.Sql.Net5.Tests.csproj", "{066B378A-8E28-49F9-8EAB-0D70FF0AE939}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TxCommand.Sql.NetCore3_1.Tests", "Sql\TxCommand.Sql.NetCore3_1.Tests\TxCommand.Sql.NetCore3_1.Tests.csproj", "{D9B675D0-8B64-46CC-8959-6D9426313EA3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {513B97B7-62E5-40B1-BA9C-C292100FA656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {513B97B7-62E5-40B1-BA9C-C292100FA656}.Debug|Any CPU.Build.0 = Debug|Any CPU + {513B97B7-62E5-40B1-BA9C-C292100FA656}.Release|Any CPU.ActiveCfg = Release|Any CPU + {513B97B7-62E5-40B1-BA9C-C292100FA656}.Release|Any CPU.Build.0 = Release|Any CPU + {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B12D5EE5-EFA9-4492-8897-DD39A2889ABF}.Release|Any CPU.Build.0 = Release|Any CPU + {9AD7230D-988A-466C-9536-37340078BBA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AD7230D-988A-466C-9536-37340078BBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AD7230D-988A-466C-9536-37340078BBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AD7230D-988A-466C-9536-37340078BBA1}.Release|Any CPU.Build.0 = Release|Any CPU + {BC3755CF-12D8-42F3-ACB5-F7CC11218CD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC3755CF-12D8-42F3-ACB5-F7CC11218CD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC3755CF-12D8-42F3-ACB5-F7CC11218CD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC3755CF-12D8-42F3-ACB5-F7CC11218CD7}.Release|Any CPU.Build.0 = Release|Any CPU + {B55572A6-D9B4-45CA-8648-EE7E87638948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B55572A6-D9B4-45CA-8648-EE7E87638948}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B55572A6-D9B4-45CA-8648-EE7E87638948}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B55572A6-D9B4-45CA-8648-EE7E87638948}.Release|Any CPU.Build.0 = Release|Any CPU + {77226287-7F3C-47E9-BD8B-9D9B82CAAC9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77226287-7F3C-47E9-BD8B-9D9B82CAAC9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77226287-7F3C-47E9-BD8B-9D9B82CAAC9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77226287-7F3C-47E9-BD8B-9D9B82CAAC9E}.Release|Any CPU.Build.0 = Release|Any CPU + {066B378A-8E28-49F9-8EAB-0D70FF0AE939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {066B378A-8E28-49F9-8EAB-0D70FF0AE939}.Debug|Any CPU.Build.0 = Debug|Any CPU + {066B378A-8E28-49F9-8EAB-0D70FF0AE939}.Release|Any CPU.ActiveCfg = Release|Any CPU + {066B378A-8E28-49F9-8EAB-0D70FF0AE939}.Release|Any CPU.Build.0 = Release|Any CPU + {D9B675D0-8B64-46CC-8959-6D9426313EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9B675D0-8B64-46CC-8959-6D9426313EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9B675D0-8B64-46CC-8959-6D9426313EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9B675D0-8B64-46CC-8959-6D9426313EA3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BC3755CF-12D8-42F3-ACB5-F7CC11218CD7} = {A09BDC81-7A8A-46C4-83B9-E7148E5627F5} + {B55572A6-D9B4-45CA-8648-EE7E87638948} = {A09BDC81-7A8A-46C4-83B9-E7148E5627F5} + {066B378A-8E28-49F9-8EAB-0D70FF0AE939} = {A09BDC81-7A8A-46C4-83B9-E7148E5627F5} + {D9B675D0-8B64-46CC-8959-6D9426313EA3} = {A09BDC81-7A8A-46C4-83B9-E7148E5627F5} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {85A24F20-DC39-4A89-9EF5-71AD5672F3D0} + EndGlobalSection +EndGlobal diff --git a/TxCommand/Delegates.cs b/TxCommand/Delegates.cs index 2ba081f..fa45ac7 100644 --- a/TxCommand/Delegates.cs +++ b/TxCommand/Delegates.cs @@ -3,18 +3,15 @@ namespace TxCommand { /// - /// A delegate used to execute a callback when a command has been executed. + /// Used to provide eventing hooks on . + /// Typically called when a session has been committed, or rolled back. /// - /// The executed command. - public delegate void ExecutedDelegate(ICommand command); + public delegate void SessionEvent(); /// - /// A delegate used to execute a callback when a transaction is committed. + /// Used to provide an eventing hook on , + /// which is called when a command has been successfully executed. /// - public delegate void CommitDelegate(); - - /// - /// A delegate used to execute a callback when a transaction is rolled back. - /// - public delegate void RollbackDelegate(); + /// + public delegate void ExecutedEvent(ICommand command); } diff --git a/TxCommand/ServiceCollectionExtensions.cs b/TxCommand/ServiceCollectionExtensions.cs index afd694f..cb05b5e 100644 --- a/TxCommand/ServiceCollectionExtensions.cs +++ b/TxCommand/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using Microsoft.Extensions.DependencyInjection; using TxCommand.Abstractions; namespace TxCommand @@ -11,17 +12,12 @@ public static class ServiceCollectionExtensions /// /// Registers the required services for using the TxCommand interfaces. /// - /// An instance of . - /// with the TxCommand services. - public static IServiceCollection AddTxCommand(this IServiceCollection services) + public static IServiceCollection AddTxCommand(this IServiceCollection services, Action builder) { - return services.AddTransient() - .AddTransient(provider => - { - var factory = provider.GetRequiredService(); + var builderInstance = new TxCommandBuilder(services); + builder?.Invoke(builderInstance); - return factory.Create(); - }); + return services; } } } diff --git a/TxCommand/Session.cs b/TxCommand/Session.cs new file mode 100644 index 0000000..1d4b378 --- /dev/null +++ b/TxCommand/Session.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TxCommand.Abstractions; +using TxCommand.Abstractions.Exceptions; + +namespace TxCommand +{ + public class Session : ISession + where TDatabase : class + where TTransaction : class + { + private readonly CancellationTokenSource _ctx; + private readonly ITransactionProvider _provider; + + private bool _completed = true; + private bool _disposed = false; + + public SessionEvent OnCommitted { get; set; } + public SessionEvent OnRolledBack { get; set; } + public ExecutedEvent OnExecuted { get; set; } + + public Session(ITransactionProvider provider) + { + _ctx = new CancellationTokenSource(); + _provider = provider; + } + + public async Task ExecuteAsync(ITxCommand command) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Session)); + } + + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + var token = _ctx.Token; + await _provider.EnsureTransactionAsync(token); + _completed = false; + + try + { + command.Validate(); + + var (database, transaction) = _provider.GetExecutionArguments(); + await command.ExecuteAsync(database, transaction); + + OnExecuted?.Invoke(command); + } + catch (Exception) + { + await RollbackAsync(); + + throw; + } + } + + public async Task ExecuteAsync(ITxCommand command) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Session)); + } + + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + var token = _ctx.Token; + await _provider.EnsureTransactionAsync(token); + _completed = false; + + try + { + command.Validate(); + + var (database, transaction) = _provider.GetExecutionArguments(); + var result = await command.ExecuteAsync(database, transaction); + + OnExecuted?.Invoke(command); + + return result; + } + catch (Exception) + { + await RollbackAsync(); + + throw; + } + } + + public async Task CommitAsync() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Session)); + } + + if (_completed) + { + throw new TransactionNotStartedException(nameof(CommitAsync)); + } + + await _provider.CommitAsync(_ctx.Token); + _completed = true; + + OnCommitted?.Invoke(); + } + + public void Commit() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Session)); + } + + if (_completed) + { + throw new TransactionNotStartedException(nameof(Commit)); + } + + _provider.Commit(); + _completed = true; + + OnCommitted?.Invoke(); + } + + public async Task RollbackAsync() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Session)); + } + + if (_completed) + { + throw new TransactionNotStartedException(nameof(CommitAsync)); + } + + await _provider.RollbackAsync(_ctx.Token); + _completed = true; + + OnRolledBack?.Invoke(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + if (!_completed) + { + Commit(); + } + + _ctx.Cancel(); + _ctx.Dispose(); + + _disposed = true; + } + +#if NET5_0 + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + if (!_completed) + { + await CommitAsync(); + } + + _ctx.Cancel(); + _ctx.Dispose(); + + _disposed = true; + } + +#endif + } +} diff --git a/TxCommand/SessionFactory.cs b/TxCommand/SessionFactory.cs new file mode 100644 index 0000000..7029161 --- /dev/null +++ b/TxCommand/SessionFactory.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using TxCommand.Abstractions; + +namespace TxCommand +{ + /// + public class SessionFactory : ISessionFactory where TSession : class + { + private readonly IServiceProvider _services; + + public SessionFactory(IServiceProvider services) + { + _services = services; + } + + public TSession Create() => _services.GetRequiredService(); + } +} diff --git a/TxCommand/TxCommand.csproj b/TxCommand/TxCommand.csproj index c8508be..478ac68 100644 --- a/TxCommand/TxCommand.csproj +++ b/TxCommand/TxCommand.csproj @@ -1,19 +1,20 @@  - netstandard2.0 - 0.5.0 + netstandard2.0;net5.0 + 1.0.0 Reece Russell https://github.com/reecerussell/tx-command git CQRS, commanding, sql, transaction - Provides support for executing CQRS command within a database transaction. + Provides the core implementation for TxCommand, to support driver specific packages. true - 0.5.0.0 - 0.5.0.0 + 1.0.0.0 + 1.0.0.0 https://github.com/reecerussell/tx-command LICENSE + A major rewrite of internals and key interfaces. TxCommandExecutor has been replaced with Session. diff --git a/TxCommand/TxCommandBuilder.cs b/TxCommand/TxCommandBuilder.cs new file mode 100644 index 0000000..31b8039 --- /dev/null +++ b/TxCommand/TxCommandBuilder.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using TxCommand.Abstractions; + +namespace TxCommand +{ + internal class TxCommandBuilder : ITxCommandBuilder + { + public IServiceCollection Services { get; } + + public TxCommandBuilder(IServiceCollection services) + { + Services = services; + } + } +} diff --git a/TxCommand/TxCommandExecutor.cs b/TxCommand/TxCommandExecutor.cs deleted file mode 100644 index 8950051..0000000 --- a/TxCommand/TxCommandExecutor.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Data; -using System.Threading.Tasks; -using TxCommand.Abstractions; - -namespace TxCommand -{ - /// - /// An implementation of . - /// - public class TxCommandExecutor : ITxCommandExecutor - { - private readonly IDbConnection _connection; - private IDbTransaction _transaction; - - private bool _disposed = false; - private bool _completed = true; - - public ExecutedDelegate OnExecuted; - public CommitDelegate OnCommitted; - public RollbackDelegate OnRolledBack; - - /// - /// Initializes a new instance of . - /// - /// The backing connection to the transaction. - public TxCommandExecutor(IDbConnection connection) - { - _connection = connection; - } - - /// - /// Commits the underlying transaction. - /// - /// Throws if the has been disposed. - public virtual void Commit() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(TxCommandExecutor)); - } - - _transaction?.Commit(); - _completed = true; - - OnCommitted?.Invoke(); - } - - /// - /// Rolls back the underlying transaction. - /// - /// Throws if the has been disposed. - public virtual void Rollback() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(TxCommandExecutor)); - } - - _transaction?.Rollback(); - _completed = true; - - OnRolledBack?.Invoke(); - } - - /// - /// Executes the given , , within the bounds of the current transaction. - /// If an exception is thrown while executing , the current transaction will be rolled back. - /// - /// Validates before executing. - /// - /// The command to execute. - /// - /// Throws if the has been disposed. - /// Throws if is null. - public virtual async Task ExecuteAsync(ITxCommand command) - { - if (command == null) - { - throw new ArgumentNullException(nameof(command)); - } - - if (_disposed) - { - throw new ObjectDisposedException(nameof(TxCommandExecutor)); - } - - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } - - if (_transaction == null) - { - _transaction = _connection.BeginTransaction(); - _completed = false; - } - - try - { - command.Validate(); - await command.ExecuteAsync(_connection, _transaction); - - OnExecuted?.Invoke(command); - } - catch - { - Rollback(); - - throw; - } - } - - /// - /// Executes the given , , within the bounds of the current transaction. - /// If an exception is thrown while executing , the current transaction will be rolled back. - /// - /// Validates before executing. - /// - /// The command to execute. - /// Returns the output of . - /// Throws if the has been disposed. - /// Throws if is null. - public virtual async Task ExecuteAsync(ITxCommand command) - { - if (command == null) - { - throw new ArgumentNullException(nameof(command)); - } - - if (_disposed) - { - throw new ObjectDisposedException(nameof(TxCommandExecutor)); - } - - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } - - if (_transaction == null) - { - _transaction = _connection.BeginTransaction(); - _completed = false; - } - - try - { - command.Validate(); - - var result = await command.ExecuteAsync(_connection, _transaction); - - OnExecuted?.Invoke(command); - - return result; - } - catch - { - Rollback(); - - throw; - } - } - - public virtual void Dispose() - { - if (_disposed) - { - return; - } - - if (!_completed) - { - Commit(); - } - - _transaction?.Dispose(); - _disposed = true; - } - } -} diff --git a/TxCommand/TxCommandExecutorFactory.cs b/TxCommand/TxCommandExecutorFactory.cs deleted file mode 100644 index 7224df9..0000000 --- a/TxCommand/TxCommandExecutorFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Data; -using TxCommand.Abstractions; - -namespace TxCommand -{ - /// - /// A simple implementation of , to create new instances - /// of , with a . - /// - public class TxCommandExecutorFactory : ITxCommandExecutorFactory - { - private readonly IDbConnection _connection; - - public TxCommandExecutorFactory(IDbConnection connection) - { - _connection = connection; - } - - /// - /// Returns a new instance of , with the underlying connection. - /// - /// A new instance of . - public ITxCommandExecutor Create() - { - return new TxCommandExecutor(_connection); - } - } -} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..5c960cf --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +run_build() { + dotnet build -c Release --no-restore "$1" +} + +run_pack() { + dotnet pack -c Release --no-restore --no-build -o packages "$1" +} + +echo "Restoring..." + +dotnet restore TxCommand/TxCommand.csproj +dotnet restore Sql/TxCommand.Sql/TxCommand.Sql.csproj + +echo "Building..." + +run_build TxCommand/TxCommand.csproj +run_build TxCommand.Abstractions/TxCommand.Abstractions.csproj + +run_build Sql/TxCommand.Sql/TxCommand.Sql.csproj +run_build Sql/TxCommand.Sql.Abstractions/TxCommand.Sql.Abstractions.csproj + +echo "Packing..." + +run_pack TxCommand/TxCommand.csproj +run_pack TxCommand.Abstractions/TxCommand.Abstractions.csproj + +run_pack Sql/TxCommand.Sql/TxCommand.Sql.csproj +run_pack Sql/TxCommand.Sql.Abstractions/TxCommand.Sql.Abstractions.csproj \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..657cdf1 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +run_test() { + echo "Testing $1..." + dotnet test -c Release -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../Coverage/$2/ "$1" +} + +echo "Running tests..." + +run_test TxCommand.Net5.Tests/TxCommand.Net5.Tests.csproj TxCommand.Net5.Tests +run_test TxCommand.NetCore3_1.Tests/TxCommand.NetCore3_1.Tests.csproj TxCommand.NetCore3_1.Tests + +echo "Running SQL tests..." + +cd Sql + +echo "Starting Docker environment..." +docker-compose up -d & sleep 20 + +echo "Running tests..." +run_test TxCommand.Sql.Net5.Tests/TxCommand.Sql.Net5.Tests.csproj TxCommand.Sql.Net5.Tests +run_test TxCommand.Sql.NetCore3_1.Tests/TxCommand.Sql.NetCore3_1.Tests.csproj TxCommand.Sql.NetCore3_1.Tests + +echo "Cleaning up Docker environment..." +docker-compose down + +cd .. + +echo "Finished!" \ No newline at end of file