▲ 0 r/dotnet

I really like Dapper but...

I've used Dapper a lot across different projects and I really like the core idea. Write SQL, pass params, ask for a type, get the type back. It gets rid of most of the annoying ADO.NET stuff without trying to hide SQL from you.

The part that always annoyed me is multi mapping.

A pretty common case for me is values used in combo boxes, so I end up with models like:

```csharp

class Employee

{

public int Id { get; set; }

public string Name { get; set; }

public KeyValuePair<int, string>? Department { get; set; }

public KeyValuePair<int, string>? JobTitle { get; set; }

}

```

And SQL like:

```sql

SELECT E.Id, E.Name,

D.Id AS DepartmentId, D.Name AS DepartmentName,

J.Id AS JobTitleId, J.Name AS JobTitleName

FROM Employee E

LEFT JOIN Department D ON D.Id = E.DepartmentId

LEFT JOIN JobTitle J ON J.Id = E.JobTitleId

```

Dapper can do this, but then I end up doing something like:

```csharp

var employees = cnn.Query<EmployeeRow, DepartmentRow, JobTitleRow, Employee>(

sql,

(e, d, j) => new Employee {

Id = e.Id,

Name = e.Name,

Department = d.DepartmentId is null ? null : new(d.DepartmentId.Value, d.DepartmentName),

JobTitle = j.JobTitleId is null ? null : new(j.JobTitleId.Value, j.JobTitleName)

},

splitOn: "DepartmentId,JobTitleId");

```

Which works. It also lets me handle the `LEFT JOIN` and say "if the id is null, this whole thing is null".

But this is where it starts feeling a bit weird to me. The type already says what I want, and now I'm manually rebuilding it anyway.

Dapper is already such a thin layer over ADO.NET that once I start writing a bunch of mapping code, I start wondering why I'm not just doing the ADO.NET part myself too.

What I really want is basically:

```csharp

var employees = cnn.Query<Employee>(sql);

```

and let the mapper figure out the structure from there.

That kind of thing is what eventually pushed me to make Rinku. The idea was basically to keep that same simplicity, but have the library adapt better when either the SQL or the C# side gets more complicated.

https://rinkulib.github.io/RinkuLib

Curious what other Dapper users do here. Just multi map everything, or is there another pattern I missed?

reddit.com
u/Bobamoss — 5 days ago
▲ 0 r/csharp

I really like the concept of Dapper but...

I've used Dapper a lot across different projects and I really like the core idea. Write SQL, pass params, ask for a type, get the type back. It gets rid of most of the annoying ADO.NET stuff without trying to hide SQL from you.

The part that always annoyed me is multi mapping.

A pretty common case for me is values used in combo boxes, so I end up with models like:

```csharp

class Employee

{

public int Id { get; set; }

public string Name { get; set; }

public KeyValuePair<int, string>? Department { get; set; }

public KeyValuePair<int, string>? JobTitle { get; set; }

}

```

And SQL like:

```sql

SELECT E.Id, E.Name,

D.Id AS DepartmentId, D.Name AS DepartmentName,

J.Id AS JobTitleId, J.Name AS JobTitleName

FROM Employee E

LEFT JOIN Department D ON D.Id = E.DepartmentId

LEFT JOIN JobTitle J ON J.Id = E.JobTitleId

```

Dapper can do this, but then I end up doing something like:

```csharp

var employees = cnn.Query<EmployeeRow, DepartmentRow, JobTitleRow, Employee>(

sql,

(e, d, j) => new Employee {

Id = e.Id,

Name = e.Name,

Department = d.DepartmentId is null ? null : new(d.DepartmentId.Value, d.DepartmentName),

JobTitle = j.JobTitleId is null ? null : new(j.JobTitleId.Value, j.JobTitleName)

},

splitOn: "DepartmentId,JobTitleId");

```

Which works. It also lets me handle the `LEFT JOIN` and say "if the id is null, this whole thing is null".

But this is where it starts feeling a bit weird to me. The type already says what I want, and now I'm manually rebuilding it anyway.

Dapper is already such a thin layer over ADO.NET that once I start writing a bunch of mapping code, I start wondering why I'm not just doing the ADO.NET part myself too.

What I really want is basically:

```csharp

var employees = cnn.Query<Employee>(sql);

```

and let the mapper figure out the structure from there.

That kind of thing is what eventually pushed me to make Rinku. The idea was basically to keep that same simplicity, but have the library adapt better when either the SQL or the C# side gets more complicated.

https://rinkulib.github.io/RinkuLib

Curious what other Dapper users do here. Just multi map everything, or is there another pattern I missed?

reddit.com
u/Bobamoss — 5 days ago
▲ 7 r/moderndotnet+2 crossposts

I built Rinku, a micro-ORM focused on clean mapping and dynamic queries

The Rinku NuGet is a micro-ORM built directly on top of ADO.NET. The core philosophy is strictly SQL-first. The goal is giving you deterministic execution and total control over your queries while keeping the mapping and execution pipeline fully customizable and extensible.

Nested Object Mapping

C#

public record Artist(int Id, string Name) : IDbReadable;
public record Album(int Id, string Title, Artist Artist);

static readonly QueryCommand GetAlbums = new(
    "SELECT AlbumId AS Id, Title, ArtistId, ArtistName FROM albums");

// Automatically matches ArtistId -&gt; Artist.Id and ArtistName -&gt; Artist.Name
List&lt;Album&gt; albums = GetAlbums.Query&lt;List&lt;Album&gt;&gt;(cnn);

The engine resolves the hierarchy automatically without requiring attributes, configuration or manual mapping code.

Conditional SQL

C#

static readonly QueryCommand Search = new(
    "SELECT TrackId AS Id, Name FROM tracks WHERE AlbumId = ?@albumId AND GenreId IN (?@genreIds_X)");

// If albumId is passed alone, the engine cleanly drops "AND GenreId IN(...)" from the executed SQL
Search.Query&lt;List&lt;Track&gt;&gt;(cnn, new { albumId = 1 });

// If a collection is passed, ?@genreIds_X automatically expands into (@genreIds_1, u/genreIds_2...)
Search.Query&lt;List&lt;Track&gt;&gt;(cnn, new { genreIds = new[] { 1, 2, 3 } });

The command adapts to the parameters provided at execution time, expanding collections when needed and removing unused sections cleanly.

Beyond these examples, Rinku provides an extensible mapping and execution pipeline. The default behavior covers common cases, while custom parsers, dynamic objects, result handling and other parts of the pipeline can be adapted when needed. Conditional SQL also extends beyond optional filters, allowing dynamic sections throughout your queries.

Documentation and architecture
https://rinkulib.github.io/RinkuLib

GitHub repo
https://github.com/RinkuLib/RinkuLib

NuGet
https://www.nuget.org/packages/Rinku

Open to any feedback, critique or edge cases I might have missed.

reddit.com
u/Bobamoss — 5 days ago
▲ 0 r/csharp

The main difference is that now, instead of having
QueryOne<T>(...) and QueryAll<T>(...)
everything is regrouped and dispatched via
Query<T>(...)
Meaning that when you want a list you call Query<List<T>>(...), if you want a stream Query<IEnumerable<T>>(...)...

I made the change since, eventually, i plan to support constructors with collection as parameters like
User(int ID, string Name, int[] Roles)
The goal is to support to have multiple rows related to the user (one per role) and to be able to generate the user once with the associated roles in a way that could simply be called Query<User>(...)

GitHub https://github.com/RinkuLib/RinkuLib

u/Bobamoss — 4 months ago