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
| Feature | Dapper | EF Core | Tuxedo |
|---|---|---|---|
| Performance | Excellent | Good | Excellent |
| Learning Curve | Low | High | Low |
| CRUD Helpers | Contrib (separate) | Built-in | Built-in |
| Migrations | None | Built-in | Bowtie |
| Query Builder | None | LINQ | Fluent API |
| Bulk Operations | None | EF.BulkExtensions | Built-in |
| Audit Logging | None | Interceptors | Auditor |
| Test Data Gen | None | None | Cufflink |
Drop-in replacement: Query, Execute, QueryMultiple, and all Contrib CRUD methods work identically.
Build type-safe queries with Where, Join, OrderBy, GroupBy, and pagination support.
High-performance BulkInsert, BulkUpdate, BulkDelete, and BulkMerge with batching.
Built-in retry policies and circuit breakers with automatic transient error detection.
Strongly-typed exceptions: UniqueConstraint, ReferenceConstraint, CannotInsertNull, and more.
SQL Server, PostgreSQL, MySQL, and SQLite with dialect-aware SQL generation.
dotnet add package Noundry.Tuxedo
dotnet add package Microsoft.Data.SqlClient // or Npgsql, MySqlConnector, Microsoft.Data.Sqlite
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; }
}
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");
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);
}
[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.
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 });
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();
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>();
}
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);
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;
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
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;
}
}
}
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
}
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.
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");
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");
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)
| Package | Version | Purpose |
|---|---|---|
| Noundry.Tuxedo | 0.8.0 | Core ORM (Dapper replacement) |
| Noundry.Tuxedo.Exceptions | 0.5.0 | Strongly-typed DB exceptions |
| Noundry.Tuxedo.Auditor | 0.5.0 | Automatic CRUD audit logging |
| Noundry.Tuxedo.Bowtie | 0.5.3 | Database migrations/DDL |
| Noundry.Tuxedo.Cufflink | 0.6.0 | Fake data generation |
Attribute-based database migrations and schema synchronization.
Fake data generation with Bogus for testing and seeding.
HTTP API client library with authentication, caching, and rate limiting.
High-performance CSV-to-database ingestion tool.