Skip to content

gentoorax/Promethix.Framework.Ado

Repository files navigation

AdoScope (Official Release)

Build and Test 0.1.x-alpha Published to nuget.org 0.1.x-x-alpha

Recently promoted to v1.0.0 official stable release.

Now incldues .NET 8.0 support

AdoScope offers a simple and flexible solution for managing your ADO.NET connections and transactions. It draws inspiration from the remarkable work in DbContextScope by Mehdime El Gueddari, whose DbContextScope library has been a source of great inspiration for the creation of AdoScope.

While AdoScope is compatible with any ADO.NET provider, it was specifically designed with Dapper in mind. Having extensive experience with Entity Framework and DbContextScope, the goal was to provide a similar solution tailored to the requirements of Dapper.

Unlike Entity Framework, Dapper lacks a DbContext, which can lead to challenges in managing DbConnection and DbTransaction. To address this, AdoScope introduces the concept of an AdoContext—a wrapper around DbConnection and DbTransaction, simplifying their management.

If you are seeking a Unit of Work pattern for Dapper with minimal coding overhead, AdoScope provides an elegant solution.

Supported Features

  • Simple and flexible configuration
  • Database provider agnostic
  • Support for nested transactions
  • Support for multiple database connections
  • Support for explicit database transactions
  • Context specific execution options (transactional, non-transactional)
  • Support for specific isolation levels per context and per (explicit) transaction
  • Support for multiple databases in a single distributed transaction
  • Support for explicit distributed transactions
  • Support for asynchronous operations
  • Support for default distributed transactions

Future Features

  • Support for read only transactions

Release Notes

v1.0.0

Promoted from rc4. Official stable release.

v1.0.0-rc4

Allow for null transaction when "NonTransactional" execution option is used. Prevents exception when using Dapper Query methods.

v1.0.0-rc3

Expose context transaction for compatibility with some Dapper queries. All major features implemented.

v1.0.0-rc2

As a result of targeting for .NET 8 and the latest compiler with existing multi-targeting support for .NET 4.8 - .NET 7, some analyser rule adjustments have been made. This is to avoid littering the code with lots of #if statements to enable new syntax.

Please be aware

That when your AdoContext is configured in transactional mode, it will hold a transaction open until you call Complete() or Dispose() on the AdoScope, this is by design. If you do not want this behavior, configure your AdoContext to be non-transactional.

CreateWithTransaction() forces the creation of a new ambient AdoContext (i.e. does not join the ambient scope if there is one) and wraps all AdoContext instances created within that scope in an explicit database transaction with the provided isolation level.

CreateWithDistributedTransaction() forces the creation of a new ambient AdoContext (i.e. does not join the ambient scope if there is one) and wraps all AdoContext instances created within that scope in an distributed transaction.

Distributed Transactions will not work at all on .NET 5 or 6. They are supported on .NET 7 or better and only on Windows AFAIK this is a limitation of .NET and it's requirement on OS DTC support e.g. MSDTC. MSDTC will of course need to be enabled and running on Windows as well.

Usage

Install the NuGet package

Install-Package Promethix.Framework.Ado -Version 1.0.0-rc4

Create an ADO Context

public class SqliteContextExample1 : AdoContext
    {
        public SqliteContextExample1()
        {
            // No Implementation
        }
    }

Create a Repository making use of this context

public class SimpleTestRepository : ISimpleTestRepository
{
    private readonly IAmbientAdoContextLocator ambientAdoContextLocator;

    public SimpleTestRepository(IAmbientAdoContextLocator ambientAdoContextLocator)
    {
        this.ambientAdoContextLocator = ambientAdoContextLocator;
    }

    private IDbConnection SqliteConnection => ambientAdoContextLocator.GetContext<SqliteContextExample1>().Connection;

    public void Add(TestEntity entity)
    {
        const string query = "INSERT INTO TestEntity (Name, Description, Quantity) VALUES (@Name, @Description, @Quantity)";
        SqliteConnection.Execute(query, entity);
    }

    public TestEntity GetEntityByName(string name)
    {
        const string query = "SELECT * FROM TestEntity WHERE Name = @Name";
        return SqliteConnection.QuerySingleOrDefault<TestEntity>(query, new { Name = name });
    }
}

For Dapper transactions Dapper requires access to the DbTransaction, if you need access to the transaction you will need to use the following syntax, the transaction is null when configured as non-transactional and Dapper will accept that. This will allow seamless configuration of transactional and non-transactional in the AdoScope configuration.

public class SimpleTestRepository : ISimpleTestRepository
{
	private readonly IAmbientAdoContextLocator ambientAdoContextLocator;

	public SimpleTestRepository(IAmbientAdoContextLocator ambientAdoContextLocator)
	{
		this.ambientAdoContextLocator = ambientAdoContextLocator;
	}

	private IDbConnection SqliteConnection => ambientAdoContextLocator.GetContext<SqliteContextExample1>().Connection;

        private IDbTransaction SqliteTransaction => ambientAdoContextLocator.GetContext<SqliteContextExample1>().Transaction;

	public void Add(TestEntity entity)
	{
		const string query = "INSERT INTO TestEntity (Name, Description, Quantity) VALUES (@Name, @Description, @Quantity)";
		SqliteConnection.Execute(query, entity, SqliteTransaction);
	}

	public TestEntity GetEntityByName(string name)
	{
		const string query = "SELECT * FROM TestEntity WHERE Name = @Name";
		return SqliteConnection.QuerySingleOrDefault<TestEntity>(query, new { Name = name }, SqliteTransaction);
	}
}

Create a Service making use of this repository and AdoScope.

When the ADO Context is configured with a transactional execution option, this will behave as a Unit of Work. It will commit when Complete() is called.

You can also configure the ADO Context to be non-transactional, in which case it will behave as a simple connection manager, executing queries as they are called.

public void ServiceLayerAddTestEntity()
{
    using IAdoScope adoScope = adoScopeFactory.Create();

    // Create a test entity
    var newTestEntity = new TestEntity { Name = "CreateTest", Description = "Test Description", Quantity = 1 };

    // Call our repository to add the entity
    simpleTestRepository.Add(newTestEntity);

    // Commit the unit of work / transaction (if using ExecutionOption.Transactional)
    adoScope.Complete();
}

Configure your DI Container. Just one of many examples of configuration. This example is by hand to show the available options. Recommend you use appsettings.json for configuration. See example below this one for that.

// Still need to register ADO providers you will be using. This is a .NET ADO requirement.
DbProviderFactories.RegisterFactory("Microsoft.Data.Sqlite", SqliteFactory.Instance);

// Register your repositories et al
_ = services.AddSingleton<IAmbientAdoContextLocator, AmbientAdoContextLocator>();
_ = services.AddSingleton<IAdoScopeFactory, AdoScopeFactory>();
_ = services.AddSingleton<IAdoContextGroupFactory, AdoContextGroupFactory>();
_ = services.AddScoped<ISimpleTestRepository, SimpleTestRepository>();
_ = services.AddScoped<IMultiTestRepository, MultiTestRepository>();

// Register your ADO Contexts
var adoContextConfiguration = new AdoContextConfigurationBuilder()
.AddAdoContext<SqliteContextExample1>(options =>
{
    _ = options.WithNamedConnection("SqliteContextExample");
    _ = options.WithConnectionString("Data Source=mydatabase.db");
    _ = options.WithProviderName("Microsoft.Data.Sqlite");
    _ = options.WithExecutionOption(AdoContextExecutionOption.Transactional);
    _ = options.WithDefaultIsolationLevel(IsolationLevel.ReadCommitted);
})
.Build();

_ = services.AddScoped(provider => adoContextConfiguration);  

Recommended Approach

Use appsettings.json for configuration, you can use the following code in your DI composition:

// AdoScope Configuration
var adoScopeConfiguration = new AdoScopeConfigurationBuilder()
.ConfigureScope(options =>
{
    _ = options.WithScopeConfiguration(configuration);
})
.Build();

// AdoContexts Configuration
var adoContextConfiguration = new AdoContextConfigurationBuilder()
    .AddAdoContext<SqliteContextExample1>(options =>
    {
        // JSON AdoContext Configuration File Example 1
        _ = options.WithNamedContext("SqliteContextExample1", configuration);
    })
    .AddAdoContext<SqliteContextExample3>(options =>
    {
        // JSON AdoContext Configuration File Example 3
        _ = options.WithNamedContext("SqliteContextExample3", configuration);
    })
    .Build();

// Register entire AdoScope configuration in DI
_ = services.AddScoped(provider => adoScopeConfiguration);
_ = services.AddScoped(provider => adoContextConfiguration); 

For most use cases you want the following configuration:

  • Distributed Transactions Off.
  • AdoContext Transactions On - provides Unit of Work behaviour.

appsettings.json as follows (see unit test project for more examples):

{
  "AdoScopeOptions": {
    "ScopeExecutionOption": "Standard",
  },
  "AdoContextOptions": {
    "SqliteContextExample1": {
      "ProviderName": "Microsoft.Data.Sqlite",
      "ConnectionString": "Data Source=mydatabase.db",
      "ExecutionOption": "Transactional"
    },
    "SqliteContextExample3": {
      "ProviderName": "Microsoft.Data.Sqlite",
      "ConnectionString": "Data Source=mydatabase3.db",
      "ExecutionOption": "Transactional"
    }
  }
}

For MS SQL Server the Provider Name should be Microsoft.Data.SqlClient e.g. DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance);

About

AdoScope: A simple and flexible way to manage your ADO.NET connections and transactions.

Resources

License

Stars

Watchers

Forks

Packages

No packages published