---
title: "The Evolution of Tuxedo: Dapper Done Right - George Rios"
description: "How Noundry.Tuxedo grew from a simple Dapper wrapper to a full-featured data access toolkit with QueryBuilder, migrations, auditing, and fake data generation."
url: https://georgerios.com/articles/tuxedo-evolution-dapper-done-right
author: George Rios
---

[ All articles](https://georgerios.com/articles)

Noundry

# The Evolution of Tuxedo: Dapper Done Right

How Noundry.Tuxedo grew from a simple Dapper wrapper to a full-featured data access toolkit with QueryBuilder, code-first migrations, auditing, and fake data generation.

Published

2026-01-15

Reading time

5 min

Topic

Noundry

Author

George Rios

Dapper is one of the best things to happen to C# data access. It's fast, it's simple, and it doesn't try to be an ORM when you just want to run SQL. But after using it across dozens of projects, I kept writing the same wrapper code over and over.

That's why Tuxedo exists. It's not a replacement for Dapper. It's everything I wished Dapper included out of the box.

## The Ecosystem Today

Tuxedo has grown into a suite of packages that work together:

- **Noundry.Tuxedo** \- Core ORM with SQL-aligned methods and full Dapper compatibility
- **Noundry.Tuxedo.Exceptions** \- Strongly-typed database exceptions
- **Noundry.Tuxedo.Auditor** \- Automatic CRUD audit logging
- **Noundry.Tuxedo.Bowtie** \- Code-first database migrations
- **Noundry.Tuxedo.Cufflink** \- Native fake data generation

Each targets .NET 8.0, 9.0, and 10.0 with support for SQL Server, PostgreSQL, MySQL, and SQLite.

## QueryBuilder: The Heart of Tuxedo

The fluent QueryBuilder is what makes Tuxedo shine for dynamic queries. Instead of string concatenation or complex conditional SQL building, you get type-safe, composable queries:

```
var products = await new QueryBuilder<Product>(TuxedoDialect.SqlServer)
    .SelectAll()
    .Where(p => p.Price > 10 && p.IsActive)
    .WhereIn(p => p.CategoryId, new[] { 1, 2, 3 })
    .WhereBetween(p => p.CreatedAt, startDate, endDate)
    .OrderBy(p => p.Name)
    .ThenByDescending(p => p.Price)
    .Page(pageIndex: 2, pageSize: 20)
    .ToListAsync(connection);
```

The builder supports everything you'd expect: joins, grouping, aggregations, raw SQL escape hatches, and dialect-specific pagination. But what I'm most proud of is how readable the code stays even for complex queries:

```
var query = new QueryBuilder<Order>()
    .SelectAll()
    .InnerJoin<Customer>((o, c) => o.CustomerId == c.Id)
    .LeftJoin<OrderItem>((o, oi) => o.Id == oi.OrderId)
    .Where(o => o.Status != "Cancelled")
    .WhereNotNull(o => o.ShippedAt)
    .GroupBy(o => o.CustomerId)
    .Having(o => o.TotalAmount > 1000);
```

One important note: QueryBuilder instances are not thread-safe and not reusable. Create a new instance for each query. This is by design to keep the API simple and avoid hidden state bugs.

## Code-First Migrations with Bowtie

Bowtie takes a fundamentally different approach to migrations than most tools. Instead of SQL files or C# migration classes, your entity definitions _are_ the schema. You define everything through attributes on your models:

```
[Table("orders")]
[TableOrder(2)]  // Create after users table
public class Order
{
    [Key]
    public int Id { get; set; }

    [Column("customer_id")]
    [ForeignKey("users", ReferencedColumn = "id",
        OnDelete = ReferentialAction.Cascade)]
    public int CustomerId { get; set; }

    [Index("IX_Orders_Date", IndexType = IndexType.BTree)]
    public DateTime OrderDate { get; set; }

    [Unique("UQ_Orders_Number")]
    public string OrderNumber { get; set; } = string.Empty;

    [CheckConstraint("quantity > 0", Name = "CK_Orders_Quantity")]
    public int Quantity { get; set; }

    [DefaultValue("GETDATE()", IsRawSql = true)]
    public DateTime CreatedAt { get; set; }
}
```

Bowtie uses reflection to analyze your entities, compare them to the current database schema, and generate the necessary DDL. The `[TableOrder]` attribute solves the foreign key ordering problem by ensuring tables are created in dependency order.

For CI/CD pipelines, there's a CLI that can generate migration scripts, run dry-runs, and detect potential data loss:

```
# Preview changes without executing
bowtie sync --dry-run

# Generate DDL script for review
bowtie sync --output schema.sql

# Force mode for automated pipelines
bowtie sync --force
```

The data loss analyzer will warn you about risky operations like column drops, type changes, or length reductions before they happen.

## Fake Data Generation with Cufflink

Every project needs test data. Cufflink is a native fake data generator that understands your Tuxedo models:

```
var engine = new CufflinkEngine();

// Generate 1000 records for all entities in assembly
await engine.GenerateFakeAsync(
    assembly: typeof(Product).Assembly,
    recordCount: 1000,
    connectionString: "your-connection-string",
    provider: DatabaseProvider.SqlServer,
    batchSize: 500);

// Or seed specific data
var seedData = new Dictionary<Type, List<object>>
{
    [typeof(Category)] = new()
    {
        new Category { Id = Guid.NewGuid(), Name = "Electronics" },
        new Category { Id = Guid.NewGuid(), Name = "Books" }
    }
};

await engine.SeedAsync(seedData, connectionString, provider);
```

Cufflink respects foreign key relationships and generates realistic data based on column names and types. It's especially useful for load testing and development environments.

## Exception Translation

Database exceptions are notoriously cryptic. Tuxedo.Exceptions translates them into strongly-typed exceptions you can actually work with:

```
var translatingConnection = connection.WithExceptionTranslation();

try
{
    await translatingConnection.InsertAsync(user);
}
catch (UniqueConstraintException ex)
{
    // ex.ConstraintName, ex.ConstraintProperties
    return BadRequest($"Duplicate value: {ex.ConstraintName}");
}
catch (ReferenceConstraintException ex)
{
    // ex.ConstraintName - FK violation
    return BadRequest("Invalid reference");
}
catch (MaxLengthExceededException ex)
{
    // ex.ColumnName, ex.MaxLength
    return BadRequest($"{ex.ColumnName} too long (max {ex.MaxLength})");
}
```

Works across SQL Server, PostgreSQL, MySQL, and SQLite with consistent exception types regardless of the underlying database.

## Audit Logging

For regulated industries or simply wanting to know who changed what, Tuxedo.Auditor wraps your connection and logs every CRUD operation:

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

// All operations now logged with old/new values, user, timestamp
await auditedConnection.UpdateAsync(product);
```

Audit events capture the operation type, table name, entity ID, old values, new values, changed columns, username, and timestamp. Everything you need for compliance or debugging.

## Dapper Compatibility

If you have existing Dapper code, migrating is trivial. Most APIs are identical:

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

// After (Tuxedo)
using Noundry.Tuxedo;
using Noundry.Tuxedo.Contrib;

// These work identically:
connection.Query<T>(sql, parameters)
connection.QueryAsync<T>(sql, parameters)
connection.Get<T>(id)
connection.Insert(entity)
connection.Update(entity)
connection.Delete(entity)
```

You can migrate incrementally, adding Tuxedo features like QueryBuilder and Bowtie to existing codebases without rewriting working code.

## Thread Safety in v0.8.0

The latest release focused on thread safety and security:

- Property caches now use atomic `ConcurrentDictionary.GetOrAdd()`
- All async methods use `ConfigureAwait(false)` to prevent deadlocks
- SQLite introspector validates identifiers against injection attacks
- Audit table creation uses `Interlocked.CompareExchange` for thread safety

These are the kinds of details that only matter in production under load. We've found them so you don't have to.

## The Bottom Line

Tuxedo isn't trying to be Entity Framework. It's for developers who like Dapper's philosophy of staying close to SQL but want more batteries included:

- Type-safe query building without string concatenation
- Schema management from your C# models
- Meaningful exceptions instead of cryptic database errors
- Built-in audit logging for compliance
- Test data generation that understands your schema

If you're using Dapper today and feeling the pain of writing the same boilerplate, give Tuxedo a look.

_Tuxedo is available on NuGet as part of the Noundry platform. Check the [documentation](https://noundry.com/docs/tuxedo) or reach out with questions at [george@georgerios.com](mailto:george@georgerios.com)._

## References

[ Back to articles](https://georgerios.com/articles) [Book a call ](https://cal.com/georgerios/30min)

---

Contact: george@georgerios.com · Book a call: https://cal.com/georgerios/30min · Services summary for agents: https://georgerios.com/llms.txt
