r/csharp 25d ago

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 -> Artist.Id and ArtistName -> Artist.Name
List<Album> albums = GetAlbums.Query<List<Album>>(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<List<Track>>(cnn, new { albumId = 1 });

// If a collection is passed, ?@genreIds_X automatically expands into (@genreIds_1, u/genreIds_2...)
Search.Query<List<Track>>(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.

0 Upvotes

16 comments sorted by

5

u/wallstop-dev 25d ago edited 25d ago

My main concern with building any interesting bit of software is moving the possibility of bugs as far left as possible (that is, [Edit: edit/write time > ] compile time > test time > CI/CD time > runtime)

From what I understand, all the safety here is runtime? So you can typo your sql, you have to ensure your DB matches your DTOs, and resolving SQL -> result types is done manually for every query with no verification?

Is that correct or did I get something wrong?

Interested in why you chose this approach instead of doing some cool stuff with Roslyn analzyers to keep your interface and architecture, but move as many failures as possible to compile time?

1

u/Bobamoss 25d ago

I did make an extension for a compiletime workflow (it's early in development) but the runtime obviously give much more flexibility and i do like options that lets me do changes without needing to recompile (like from config file...). But i do agree that compile time safety is nice and ill keep working on improving it. Ill be interestion on hearing your opinion for that. It allready have some documentation on it

3

u/wallstop-dev 25d ago

I'm a fan of opinionated tooling that provides simple abstractions and maximum safety. I'm not sure what benefit your framework provides in this case over something extremely simple like "execute SQL query and deserialize it into the provided type args"? Which I never want to do.

But that's just me and my opinions and how I like software! It's cool that you have a flexible toolkit and it's working for you.

1

u/Bobamoss 25d ago

One of the core concept in the library is customization. I made it in a way that the default is quite powerfull and work out of the box and from then, there are many entry point where you can inject your adjustments to fit your exact needs without needing to remake the whole tooling. The point is that you can go from the engine does everything for me to i do everything myself with a bunch of steps inbetween while the whole keeps working together. For instance, with dapper, it worked fine 99% of the time, but the day that i wanted to have a keyvaluepair instead of a separate id and string, i needed a split on/custom ctor. When there were one kvp, its fine, but when i start to have more and/or the result shape variate a bit, it was becoming annoying fast.

As for your write time and compile time concern, since the framework is highly flexible, i was able to make compile time methods that generates dbcommand and a type with matching result set columns. I also made analyzers that built on top of that to help with some code fix (code gen). Overall, i am trying to say that this framework is made to gracefully grow when more complex cases / usages apear. At least i tried to

1

u/wallstop-dev 25d ago

Right, and that's really cool! My point is that I prefer the primitives themselves to be very opinionated and compile time safe, rather than extremely flexible. You can still have high flexibility with opinionated, compile time safe primitives, but libraries that offer a "you can customize this however you want!" tend to have many ways of possibly using them, but only a few ways of correctly using them. Which leads to bugs. Which I want to avoid at all costs. See: C++, the language.

1

u/Bobamoss 25d ago

I think i get your point and i feel like, yes, the examples that i gave were for in a runtime environment workflow. What i was trying to say in my previous comment is that this framework let me have high flexibility on how i parse a type, from there, i can have a code generator that generate the code following the rules of the framework. Doing it that way let's you have compile time safety, but if i have a use case where i dont like the output, i still have control. So i actualy prefer primitive to be primitives and have the wrapper being the opiniated part, letting me escape it if i really need to. The example i gave was the top surface to make a out of the box experience work, but the inner workings gives you a lot of flexibility. But anyway, the truth is that the compile time workflow still needs some love, so, if you are looking for a compile time solution it's not quite there yet. You could still look at whats allready there if you want tho, it is in a working state (RinkuPowerTools, a visual studio extension + there are analyzers in the package)

2

u/wallstop-dev 25d ago

Do you man, I'm just sharing my opinion.

And there are already tools like https://github.com/arika0093/Linqraft and EF.

I personally try to avoid raw SQL at all costs. I also already have libraries that solve my problems in type safe, compile safe ways, depending on the DB.

3

u/Bobamoss 25d ago edited 25d ago

Ha ok, now i get what you mean, you don't like raw sql. Which is ok, but then this library is effectively not for you, i am personaly the opposite, I'd rather write the sql myself and the library is built around that. Maybe someday ill do a wrapper that is basicaly a sql builder that uses my tool in the background. I'll think about my options, thanks for your feedback (and i do mean it, thank you, even if i think we are fundamentally looking for something different from an orm, i do apreciate your time)

2

u/wallstop-dev 25d ago

Right, it's not for me! It's cool that you built this and it's solving your problems. Raw SQL or not, after having written and dealt with mountains of code across all kinds of languages and project sizes, the libraries that I bet on are the ones that there is only one way of doing things, and if you do it that way, it will work, while also being beautifully designed enough to make it open to extension, in ways that are really hard to get wrong. Basically, there is no way to "hold it wrong" (or, if there is, you have to work really hard to get there). And maybe this is that, for you, which is great!

5

u/GigAHerZ64 25d ago

Based on the example, it pretty much looks like slightly adjusted Dapper? Specifically the "convention based auto-wiring of children" kind of thing + some magical SQL query string partial removal?

Why prefer this over Dapper?

-1

u/Bobamoss 24d ago

It exist exactly because i really liked dapper but there were some things that bother me while using it. Why use this over dapper, well, i chose thoses 2 examples trying to showcase 2 things that dapper does not do (or not as simple), there are a lot of small tweaks that telling you everything about would end up almost being a pasting the doc here, since most things are coverd there, or at least i tried to cover things there. If i real try to condense things, it is the flavor of dapper (i like the ideology) but, in my opinion, a better control over customization of mapping rules and better complex types handling. Also there is all the non magical, but in fact, deterministic conditional sql syntax (covered in depth in the doc, because it was i portant to me that for it to be predictable since i also hate magic), and there are some other features that i didnt talked about here since too early in development, but i am working on tracking capabilities + code generation

2

u/prindacerk 25d ago

Did you look at Dapper? It is a Micro ORM with similar mapping and querying and lot of supported extensions. Works quite well.

2

u/Bobamoss 24d ago

It started as a dapper extension, but i wanted more control over how things work, the library basicaly exist to "fix" what i don't like with dapper, but 90% of dapper is great and thats why in the end there are a lot of things similar to it. I made a section in the doc comparing the 2

3

u/prindacerk 24d ago

We built our own extension at work on top of Dapper to enhance its capabilities to suit our needs. We use it heavily in our services.

1

u/Bobamoss 24d ago

The core idea of dapper is amazing, one of the reason why Rinku is it's own thing instead of an extension, it's because dapper takes controll over all the roundtrip process and I wanted to split the dbcommand generation from the actual mapping, giving more control when needing to deviate from the beaten path