Noundry.Tuxedo

Modern .NET ORM - A performant drop-in replacement for Dapper

Noundry.Tuxedo unifies Dapper and Dapper.Contrib into a single, modern package for .NET 8, 9, and 10. It provides high-performance ADO.NET mapping, CRUD helpers, a fluent query builder, bulk operations, audit logging, resiliency, and more -- all working seamlessly with SQL Server, PostgreSQL, MySQL, and SQLite.

dotnet add package Noundry.Tuxedo

Why Tuxedo?

Feature Dapper EF Core Tuxedo
PerformanceExcellentGoodExcellent
Learning CurveLowHighLow
CRUD HelpersContrib (separate)Built-inBuilt-in
MigrationsNoneBuilt-inBowtie
Query BuilderNoneLINQFluent API
Bulk OperationsNoneEF.BulkExtensionsBuilt-in
Audit LoggingNoneInterceptorsAuditor
Test Data GenNoneNoneCufflink

Key Features

Dapper-Compatible API

Drop-in replacement: Query, Execute, QueryMultiple, and all Contrib CRUD methods work identically.

Fluent Query Builder

Build type-safe queries with Where, Join, OrderBy, GroupBy, and pagination support.

Bulk Operations

High-performance BulkInsert, BulkUpdate, BulkDelete, and BulkMerge with batching.

Resiliency (Polly)

Built-in retry policies and circuit breakers with automatic transient error detection.

Exception Translation

Strongly-typed exceptions: UniqueConstraint, ReferenceConstraint, CannotInsertNull, and more.

Multi-Database Support

SQL Server, PostgreSQL, MySQL, and SQLite with dialect-aware SQL generation.

Quick Start

1. Install Packages

dotnet add package Noundry.Tuxedo
dotnet add package Microsoft.Data.SqlClient // or Npgsql, MySqlConnector, Microsoft.Data.Sqlite

2. Define Your Model

using Noundry.Tuxedo.Contrib;

[Table("Products")]
public class Product
{
 [Key] // Auto-increment primary key
 public int Id { get; set; }

 public string Name { get; set; } = string.Empty;

 [Column("unit_price")] // Map to different column name
 public decimal Price { get; set; }

 [Computed] // Excluded from INSERT/UPDATE
 public DateTime CreatedAt { get; set; }
}

3. Register with DI

using Noundry.Tuxedo.DependencyInjection;

// SQL Server
services.AddTuxedoSqlServer("Server=.;Database=MyDb;Integrated Security=true;");

// PostgreSQL
services.AddTuxedoPostgres("Host=localhost;Database=mydb;Username=user;Password=pass;");

// MySQL
services.AddTuxedoMySql("Server=localhost;Database=mydb;Uid=user;Pwd=pass;");

// SQLite
services.AddTuxedoSqlite("mydb.sqlite");

// SQLite in-memory (for tests)
services.AddTuxedoSqliteInMemory("TestDb");

4. Use CRUD Operations

using Noundry.Tuxedo.Contrib;

public class ProductService
{
 private readonly IDbConnection _db;
 public ProductService(IDbConnection db) => _db = db;

 public async Task<Product?> GetByIdAsync(int id)
 => await _db.GetAsync<Product>(id);

 public async Task<IEnumerable<Product>> GetAllAsync()
 => await _db.GetAllAsync<Product>();

 public async Task<int> CreateAsync(Product product)
 => await _db.InsertAsync(product);

 public async Task<bool> UpdateAsync(Product product)
 => await _db.UpdateAsync(product);

 public async Task<bool> DeleteAsync(Product product)
 => await _db.DeleteAsync(product);
}

Model Attributes

[Table("name")]

Maps a class to a specific database table name.

[Key]

Auto-increment primary key. The database generates the value on INSERT.

[ExplicitKey]

Non-auto-increment primary key. You provide the value (GUIDs, strings, etc.).

[Column("name")]

Maps a property to a different column name. Supports MaxLength, Precision, Scale, IsNullable, and TypeName options. Works consistently across SELECT, INSERT, and UPDATE (v0.7.0+).

[Computed]

Excluded from INSERT and UPDATE operations. Use for database-generated columns like timestamps or calculated fields.

[Write(false)]

Never written to the database. Use for display-only computed properties.

Raw Queries (Dapper-Compatible)

using Noundry.Tuxedo;

// Query multiple rows
var products = await _db.QueryAsync<Product>(
 "SELECT * FROM Products WHERE Price > @MinPrice",
 new { MinPrice = 10.00m });

// Query single or default
var product = await _db.QuerySingleOrDefaultAsync<Product>(
 "SELECT * FROM Products WHERE Id = @Id",
 new { Id = 1 });

// Execute non-query
int rows = await _db.ExecuteAsync(
 "UPDATE Products SET Price = @Price WHERE Id = @Id",
 new { Price = 19.99m, Id = 1 });

// Execute scalar
int count = await _db.ExecuteScalarAsync<int>(
 "SELECT COUNT(*) FROM Products WHERE Price > @MinPrice",
 new { MinPrice = 10.00m });

Multi-Mapping (Joins)

var order = _db.Query<Order, Customer, Order>(
 @"SELECT o.*, c.* FROM Orders o
 INNER JOIN Customers c ON o.CustomerId = c.Id
 WHERE o.Id = @OrderId",
 (order, customer) => { order.Customer = customer; return order; },
 new { OrderId = 1 },
 splitOn: "Id").FirstOrDefault();

Multiple Result Sets

using (var multi = await _db.QueryMultipleAsync(@"
 SELECT * FROM Products WHERE CategoryId = @CatId;
 SELECT * FROM Categories WHERE Id = @CatId;",
 new { CatId = 1 }))
{
 var products = await multi.ReadAsync<Product>();
 var category = await multi.ReadSingleAsync<Category>();
}

Fluent Query Builder

Build type-safe queries with expressions. Each QueryBuilder instance is single-use and not thread-safe -- create a new one per query.

using Noundry.Tuxedo.QueryBuilder;

// Basic query with filtering and ordering
var products = await new QueryBuilder<Product>(TuxedoDialect.SqlServer)
 .SelectAll()
 .Where(p => p.Price > 10 && p.IsActive)
 .OrderBy(p => p.Name)
 .ToListAsync(connection);

// Advanced filtering
var query = new QueryBuilder<Product>()
 .SelectAll()
 .Where(p => p.Price > 10)
 .And(p => p.IsActive == true)
 .WhereIn(p => p.CategoryId, new[] { 1, 2, 3 })
 .WhereBetween(p => p.Price, 10.00m, 50.00m)
 .WhereNull(p => p.DeletedAt);

// Joins
var orders = new QueryBuilder<Order>()
 .SelectAll()
 .InnerJoin<Customer>((o, c) => o.CustomerId == c.Id)
 .LeftJoin<OrderItem>((o, oi) => o.Id == oi.OrderId);

// Pagination
var page = new QueryBuilder<Product>()
 .SelectAll()
 .OrderBy(p => p.Name)
 .Page(pageIndex: 2, pageSize: 10);

// Aggregations
int total = await new QueryBuilder<Product>()
 .Count()
 .Where(p => p.IsActive)
 .CountAsync(connection);

Pagination

using Noundry.Tuxedo.Pagination;

PagedResult<Product> result = await _db.QueryPagedAsync<Product>(
 "SELECT * FROM Products WHERE IsActive = 1 ORDER BY Name",
 pageIndex: 0, // Zero-based page index
 pageSize: 20, // Max 1,000 per page
 new { IsActive = true });

// Access results
List<Product> items = result.Items;
int totalCount = result.TotalCount;
int totalPages = result.TotalPages;
bool hasNext = result.HasNextPage;
bool hasPrev = result.HasPreviousPage;

Bulk Operations

using Noundry.Tuxedo.BulkOperations;

var bulkOps = new BulkOperations(TuxedoDialect.SqlServer);

// Bulk Insert
int inserted = await bulkOps.BulkInsertAsync(connection, products, batchSize: 1000);

// Bulk Update
int updated = await bulkOps.BulkUpdateAsync(connection, products);

// Bulk Merge (Upsert)
int merged = await bulkOps.BulkMergeAsync(connection, products);
// SQL Server: MERGE, PostgreSQL: ON CONFLICT DO UPDATE, MySQL: ON DUPLICATE KEY UPDATE

Repository & Unit of Work Patterns

using Noundry.Tuxedo.Patterns;

public class OrderService
{
 private readonly IDbConnection _connection;

 public async Task CreateOrderAsync(Order order, List<OrderItem> items)
 {
 using var uow = new UnitOfWork(_connection);
 await uow.BeginTransactionAsync();

 try
 {
 var orderRepo = uow.Repository<Order>();
 var itemRepo = uow.Repository<OrderItem>();

 var created = await orderRepo.AddAsync(order);
 foreach (var item in items)
 {
 item.OrderId = created.Id;
 await itemRepo.AddAsync(item);
 }

 await uow.CommitAsync();
 }
 catch
 {
 await uow.RollbackAsync();
 throw;
 }
 }
}

Exception Translation

Wrap your connection with .WithExceptionTranslation() to get strongly-typed database exceptions.

using Noundry.Tuxedo.Exceptions;

var db = connection.WithExceptionTranslation();

try
{
 await db.InsertAsync(product);
}
catch (UniqueConstraintException ex)
{
 // Duplicate key: ex.ConstraintName, ex.ConstraintProperties
}
catch (ReferenceConstraintException ex)
{
 // Foreign key violation: ex.ConstraintName
}
catch (CannotInsertNullException ex)
{
 // NULL not allowed: ex.ColumnName
}
catch (MaxLengthExceededException ex)
{
 // String too long: ex.ColumnName, ex.MaxLength
}

Audit Logging

using Noundry.Tuxedo.Auditor;

var auditedDb = connection.WithAuditing(config =>
{
 config.UserProvider = new HttpContextAuditUserProvider(httpContextAccessor);
 config.DataProvider = new SqlServerAuditDataProvider(connection);
});

// All CRUD operations are now automatically audited
await auditedDb.InsertAsync(product); // Logs Insert event
await auditedDb.UpdateAsync(product); // Logs Update with old/new values
await auditedDb.DeleteAsync(product); // Logs Delete event

User providers: StaticAuditUserProvider, DelegateAuditUserProvider, HttpContextAuditUserProvider. Data providers available for SQL Server, PostgreSQL, MySQL, and SQLite.

Resiliency (Polly Integration)

using Noundry.Tuxedo.Resiliency;

var options = new ResiliencyOptions
{
 MaxRetryAttempts = 3,
 BaseDelay = TimeSpan.FromMilliseconds(100),
 EnableCircuitBreaker = true,
 CircuitBreakerThreshold = 5,
 CircuitBreakerTimeout = TimeSpan.FromSeconds(30)
};

var resilient = new PollyResiliencyProvider(options)
 .WrapConnection(connection);

// All operations now have automatic retry and circuit breaker
var products = await resilient.QueryAsync<Product>("SELECT * FROM Products");

Query Caching

using Noundry.Tuxedo.Caching;

// Setup
services.AddTuxedoCaching(options =>
{
 options.UseMemoryCache = true;
 options.DefaultExpiration = TimeSpan.FromMinutes(5);
});

// Query with automatic caching
var products = await connection.QueryWithCacheAsync<Product>(
 "SELECT * FROM Products WHERE CategoryId = @CatId",
 new { CatId = 5 },
 cacheExpiration: TimeSpan.FromMinutes(10),
 cacheKey: "products_category_5");

// Invalidate cache
await CachingExtensions.InvalidateCacheAsync("products_category_5");

Migrating from Dapper

Migration is a simple namespace change. All Dapper and Dapper.Contrib APIs are fully compatible:

// Before (Dapper)
using Dapper;
using Dapper.Contrib.Extensions;

// After (Tuxedo) -- same API, one-line change
using Noundry.Tuxedo;
using Noundry.Tuxedo.Contrib;

// All these work identically in both:
connection.Query<T>(sql, parameters)
connection.Execute(sql, parameters)
connection.Get<T>(id)
connection.Insert(entity)
connection.Update(entity)
connection.Delete(entity)

Tuxedo Ecosystem

Package Version Purpose
Noundry.Tuxedo0.8.0Core ORM (Dapper replacement)
Noundry.Tuxedo.Exceptions0.5.0Strongly-typed DB exceptions
Noundry.Tuxedo.Auditor0.5.0Automatic CRUD audit logging
Noundry.Tuxedo.Bowtie0.5.3Database migrations/DDL
Noundry.Tuxedo.Cufflink0.6.0Fake data generation