Dot.Conductor 2.0.1

dotnet add package Dot.Conductor --version 2.0.1
                    
NuGet\Install-Package Dot.Conductor -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dot.Conductor" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dot.Conductor" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Dot.Conductor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dot.Conductor --version 2.0.1
                    
#r "nuget: Dot.Conductor, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dot.Conductor@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dot.Conductor&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Dot.Conductor&version=2.0.1
                    
Install as a Cake Tool

Unit of Work and Repository Pattern with Entity Framework Core

This library provides an implementation of the Unit of Work (UoW) and Repository pattern using Entity Framework (EF) Core, with support for multi-tenancy and temporal tables.

Installation

Install the package from NuGet:

dotnet add package Dot.Conductor

Features

  • Unit of Work pattern implementation
    • Generic Repository pattern
    • Multi-tenancy support
    • Temporal table querying support
    • Customizable database context configuration
    • Paged results for efficient data retrieval
    • Transaction management
    • Stored procedures support

Setting Up

Basic Setup

In your Startup.cs or Program.cs (for .NET 6+), register the UnitOfWork and repositories using the provided extension method. You can now configure EF Core using a fluent API:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddUnitOfWorkAndRepositories<MyDbContext>(options =>
    {
        options
            .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
            .UseDevelopment(Environment.IsDevelopment());
    });

    // ...
}

Multiple database providers

This library can work with different EF Core providers. Out of the box, helper extensions target SQL Server. A fluent options builder also supports SQLite. Below are examples.

  • SQL Server (default)
services.AddUnitOfWorkAndRepositories<MyDbContext>(options =>
{
    options
        .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
        .UseDevelopment(Environment.IsDevelopment());
});
  • SQLite (via fluent options)
services.AddUnitOfWorkAndRepositories<MyDbContext>(options =>
{
    options
        .UseSqlite("Data Source=app.db")
        .UseDevelopment(Environment.IsDevelopment());
});

Notes:

  • SQL Server has dedicated helpers for both single-tenant and multi-tenant scenarios (see next section).
  • SQLite is supported via the fluent options builder. Ensure your DbContext has a constructor that accepts DbContextOptions<MyDbContext>.
  • Provider-specific options delegates are available:
    • SQL Server: options.UseSqlServer(conn, sql ⇒ { sql.MigrationsAssembly("Your.Assembly"); /* etc. */ });
    • SQLite: options.UseSqlite(conn, sqlite ⇒ { /* sqlite-specific options */ });
  • Other providers (e.g., PostgreSQL/Npgsql, MySQL/MariaDB) are planned. Contributions are welcome.
  • For tests, you can use the EF Core InMemory provider by constructing your DbContext in the test and using UnitOfWork directly.

Multi-tenant Setup

For multi-tenant applications, use the following setup:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddMultiTenantUnitOfWorkAndRepositories<MyDbContext>(
        sqlOptions => {
            // Configure SQL options if needed
        },
        isDevelopment: Environment.IsDevelopment()
    );

    // Register your tenant strategy
    services.AddScoped<ITenantStrategy, YourTenantStrategyImplementation>();

    // ...
}

Make sure to implement the ITenantStrategy interface to provide tenant-specific connection strings:

public class YourTenantStrategyImplementation : ITenantStrategy
{
    public Task<string> GetTenantIdentifierAsync()
    {
        // Implement logic to get the current tenant identifier
    }

    public Task<string> GetConnectionStringAsync(string tenantIdentifier)
    {
        // Implement logic to get the connection string for the given tenant
    }
}

DbContext Logging

To log executed SQL commands, implement the IDbContextLogger interface and register it with the service collection.

public class ConsoleDbContextLogger : IDbContextLogger
{
    public void OnCommandExecuting(DbCommand command, DbContext context)
        => Console.WriteLine($"Executing: {command.CommandText}");

    public void OnCommandExecuted(DbCommand command, DbContext context, TimeSpan duration)
        => Console.WriteLine($"Executed in {duration.TotalMilliseconds} ms");

    public void OnCommandError(DbCommand command, DbContext context, Exception exception)
        => Console.WriteLine($"Error: {exception.Message}");
}

services.AddSingleton<IDbContextLogger, ConsoleDbContextLogger>();

The logger is automatically attached when calling AddUnitOfWorkAndRepositories or AddMultiTenantUnitOfWorkAndRepositories.

Using Unit of Work

In Controllers or Services

Inject IUnitOfWork<MyDbContext> in your controller or service constructor:

public class UserController : ControllerBase
{
    private readonly IUnitOfWork<MyDbContext> _unitOfWork;

    public UserController(IUnitOfWork<MyDbContext> unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    // ...
}

Querying Data

public async Task<IActionResult> GetUser(int id)
{
    var userRepository = _unitOfWork.GetRepository<User>();
    var user = await userRepository.GetByIdAsync(id);

    if (user == null)
    {
        return NotFound();
    }

    return Ok(user);
}

Modifying Data

public async Task<IActionResult> UpdateUser(User user)
{
    var userRepository = _unitOfWork.GetRepository<User>();
    await userRepository.UpdateAsync(user);
    await _unitOfWork.CommitAsync();
    // or use _unitOfWork.Commit() for synchronous commits

    return Ok(user);
}

Using Transactions

public async Task<IActionResult> TransferFunds(int fromUserId, int toUserId, decimal amount)
{
    try
    {
        await _unitOfWork.BeginTransactionAsync();

        var userRepository = _unitOfWork.GetRepository<User>();
        
        var fromUser = await userRepository.GetByIdAsync(fromUserId);
        var toUser = await userRepository.GetByIdAsync(toUserId);

        fromUser.Balance -= amount;
        toUser.Balance += amount;

        await userRepository.UpdateAsync(fromUser);
        await userRepository.UpdateAsync(toUser);

        await _unitOfWork.CommitTransactionAsync();

        return Ok("Transfer successful");
    }
    catch (Exception)
    {
        await _unitOfWork.RollbackTransactionAsync();
        return BadRequest("Transfer failed");
    }
}

Paged Results

public async Task<IActionResult> GetUsers(int pageNumber, int pageSize)
{
    var userRepository = _unitOfWork.GetRepository<User>();
    var pagedUsers = await userRepository.GetPagedDataAsync(
        pageNumber,
        pageSize,
        u => u.LastName
    );

    return Ok(pagedUsers);
}

Temporal Queries

Entity Framework Core supports querying temporal tables, allowing you to retrieve data as it existed at a specific point in time or over a range of time. This library extends the repository pattern to include methods for temporal querying.

Enabling Temporal Tables

First, ensure that your entities are configured to use temporal tables in your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<User>(entity =>
    {
        entity.ToTable("Users", b => b.IsTemporal());
    });

    // Repeat for other entities as needed...
}
Temporal Query Methods

The repository interface includes the following temporal query methods:

  • TemporalAll(): Retrieves all historical versions of the entity.
  • TemporalAsOf(DateTime dateTime): Retrieves the data as it existed at a specific point in time.
  • TemporalBetween(DateTime from, DateTime to): Retrieves the data that changed between two points in time.
  • TemporalFromTo(DateTime from, DateTime to): Retrieves the data from a specific start time to an end time (exclusive).
Examples
public async Task<IActionResult> GetUserHistory(int userId)
{
    var userRepository = _unitOfWork.GetRepository<User>();

    // Get all historical data for the user
    var userHistory = await userRepository
        .TemporalAll()
        .Where(u => u.Id == userId)
        .ToListAsync();

    return Ok(userHistory);
}

public async Task<IActionResult> GetUserAsOf(int userId, DateTime dateTime)
{
    var userRepository = _unitOfWork.GetRepository<User>();

    // Get user data as of a specific date
    var userAsOf = await userRepository
        .TemporalAsOf(dateTime)
        .FirstOrDefaultAsync(u => u.Id == userId);

    if (userAsOf == null)
    {
        return NotFound();
    }

    return Ok(userAsOf);
}

public async Task<IActionResult> GetUserChanges(int userId, DateTime from, DateTime to)
{
    var userRepository = _unitOfWork.GetRepository<User>();

    // Get user changes between two dates
    var userChanges = await userRepository
        .TemporalBetween(from, to)
        .Where(u => u.Id == userId)
        .ToListAsync();

    return Ok(userChanges);
}

Using Unit of Work Outside of Scope

To use UnitOfWork outside of the default scope, use the extension method provided for IServiceScopeFactory:

public async Task<bool> PerformUnitOfWorkAsync(IServiceScopeFactory scopeFactory)
{
    var result = await scopeFactory.UseUnitOfWork<MyDbContext, bool>("YourConnectionString", async uow =>
    {
        var userRepository = uow.GetRepository<User>();
        var user = new User { Name = "John Doe", Email = "john@example.com" };
        await userRepository.AddAsync(user);
        await uow.CommitAsync();
        // or use uow.Commit() for synchronous commits
        return true;
    });

    return result;
}

Notes

  • Lifecycle Management: The implementation handles proper disposal of resources and manages the lifecycle of the DbContext.
  • Multi-tenancy: For multi-tenant scenarios, ensure that your ITenantStrategy implementation correctly identifies tenants and provides the appropriate connection strings.
  • Stored Procedures: The library includes support for stored procedures through the GetStoredProcedures<TProcedures>() method, but you need to implement the procedures class yourself.
  • Development Mode: In development mode, the library enables sensitive data logging, detailed errors, and console logging for the DbContext.
  • Temporal Tables: When using temporal tables, make sure your database supports them and that they are properly configured in your entity mappings.

Disposal Patterns

UnitOfWork implements both IDisposable and IAsyncDisposable. This allows resources to be released using either pattern:

await using var asyncUow = new UnitOfWork<MyDbContext>(context);
// work with asyncUow
using var uow = new UnitOfWork<MyDbContext>(context);
// work with uow

Licensing

Before using this software, you must purchase a license. Please contact: license@n8.lu

Notes:

  • A licensing hook is present and is wired into the default DI registrations.

Contributing

Contributions to improve the library are welcome. Please submit issues and pull requests on the project's repository.

License

This library is not free to use. A paid license is required for any use (including development and testing). To purchase a license, contact: license@n8.lu

See the repository LICENSE file for the full terms.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 54 8/15/2025
1.5.0 277 11/21/2024
1.4.0 1,108 10/17/2024
1.3.0 1,071 6/23/2024
1.2.23 390 3/11/2024
1.2.22 398 12/29/2023
1.2.21 171 12/29/2023
1.2.20 169 12/28/2023
1.2.19 179 12/19/2023
1.2.18 145 12/19/2023
1.2.17 143 12/19/2023
1.2.16 278 11/15/2023
1.2.15 182 11/5/2023
1.2.14 165 11/4/2023
1.2.13 142 11/4/2023
1.2.12 134 11/4/2023
1.2.11 140 11/4/2023
1.2.10 141 11/4/2023
1.2.9 146 11/4/2023
1.2.8 149 11/4/2023
1.2.7 151 11/2/2023
1.2.6 149 11/2/2023
1.2.5 166 11/2/2023
1.2.4 159 11/2/2023
1.2.3 152 11/1/2023
1.2.2 147 11/1/2023
1.2.1 162 10/27/2023
1.2.0 161 10/27/2023
1.1.1 152 10/25/2023
1.1.0 162 10/19/2023