Rmp.GraphOrm.Dapper
0.1.1
See the version list below for details.
dotnet add package Rmp.GraphOrm.Dapper --version 0.1.1
NuGet\Install-Package Rmp.GraphOrm.Dapper -Version 0.1.1
<PackageReference Include="Rmp.GraphOrm.Dapper" Version="0.1.1" />
<PackageVersion Include="Rmp.GraphOrm.Dapper" Version="0.1.1" />
<PackageReference Include="Rmp.GraphOrm.Dapper" />
paket add Rmp.GraphOrm.Dapper --version 0.1.1
#r "nuget: Rmp.GraphOrm.Dapper, 0.1.1"
#:package Rmp.GraphOrm.Dapper@0.1.1
#addin nuget:?package=Rmp.GraphOrm.Dapper&version=0.1.1
#tool nuget:?package=Rmp.GraphOrm.Dapper&version=0.1.1
Rmp.GraphOrm
Eliminates the fundamental ORM row-duplication problem by pushing JSON aggregation into SQL.
The Problem
When querying object graphs (parent → children → grandchildren), traditional ORMs join tables and return flat result sets. A parent's fields are duplicated for every child row:
-- Traditional JOIN: Order with 100 items, each with a Product
SELECT o.*, oi.*, p.*
FROM Orders o
JOIN OrderItems oi ON oi.OrderId = o.Id
JOIN Products p ON p.Id = oi.ProductId
-- Result: 100 rows where Order fields are repeated 100 times
-- On a 4-level graph: rows = level1 × level2 × level3 × level4 (cartesian explosion!)
The Solution
GraphORM generates SQL that uses database-native JSON aggregation to return the object graph as a single JSON document. Each entity appears exactly once:
| Database | Technique |
|---|---|
| SQL Server | FOR JSON PATH / WITHOUT_ARRAY_WRAPPER |
| PostgreSQL | json_agg() / json_build_object() |
Providers
Rmp.GraphOrm offers 6 packages — use whichever ORM you already have, or go dependency-free:
| Package | Target | Dependency | Extension Point |
|---|---|---|---|
| Rmp.GraphOrm.Core | net8.0 | — | Attributes, mapping, SQL builders, deserializer |
| Rmp.GraphOrm.Core.Legacy | net48 | — | Same API as Core, for .NET Framework 4.8 |
| Rmp.GraphOrm.Dapper | net8.0 | Dapper | IDbConnection.GraphQuery<T>() |
| Rmp.GraphOrm.EFCore | net8.0 | EF Core 8 | DbContext.GraphQuery<T>() / DbSet<T>.AsGraphQuery() |
| Rmp.GraphOrm.EF6 | net48 | EF 6 | DbContext.GraphQuery<T>() / DbSet<T>.AsGraphQuery() |
| Rmp.GraphOrm.NHibernate | net8.0 | NHibernate | ISession.GraphQuery<T>() |
| Rmp.GraphOrm.Standalone | net8.0 | None (ADO.NET) | DbConnection.GraphQuery<T>() |
Installation
Targets .NET 8 and .NET 10 (EF Core 8 / EF Core 10 respectively for the EF Core provider),
plus .NET Framework 4.8 for Rmp.GraphOrm.EF6 / Rmp.GraphOrm.Core.Legacy.
Install the provider you need (each pulls in Rmp.GraphOrm.Core automatically):
dotnet add package Rmp.GraphOrm.Dapper # Dapper users
dotnet add package Rmp.GraphOrm.EFCore # EF Core users
dotnet add package Rmp.GraphOrm.EF6 # EF6 (.NET Framework) users
dotnet add package Rmp.GraphOrm.NHibernate # NHibernate users
dotnet add package Rmp.GraphOrm.Standalone # Pure ADO.NET
Quick Start
1. Define entities — plain POCOs work out of the box
GraphORM uses conventions so you don't need any attributes at all:
| Convention | Rule |
|---|---|
| Table name | Class name (override with [GraphTable("name")]) |
| Primary key | Property named Id or {ClassName}Id (override with [GraphKey]) |
| Column name | Property name (override with [GraphColumn("col")]) |
| Children | List<T> where T is an entity → FK = {ParentClass}Id (override with [GraphChildren("fk")]) |
| Parent ref | Reference to another entity → FK = {PropertyName}Id (override with [GraphParent("fk")]) |
Zero-attribute example:
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; } = new(); // → FK "OrderId" on OrderItem
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public int Quantity { get; set; }
public int ProductId { get; set; }
public Product? Product { get; set; } // → FK "ProductId" on this table
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Sku { get; set; } = "";
}
With attributes (only needed when conventions don't match):
[GraphTable("Orders")] // table name differs from class name
public class Order
{
[GraphKey] public int Id { get; set; }
public string CustomerName { get; set; } = "";
[GraphChildren("OrderId")] // explicit FK (same as convention here, but shown for clarity)
public List<OrderItem> Items { get; set; } = new();
}
2. Query with any provider
The database dialect is auto-detected from the connection type — no need to specify it manually. All providers support the same fluent API with lambda-based terminal methods.
EF Core (recommended — zero ceremony):
// Lambda terminal — no AsGraphQuery() needed
var orders = await dbContext.Orders.ToListAsGraphAsync(q => q
.Include(o => o.Items)
.Include("Items.Product")
.Where(o => o.CustomerName == "Acme Corp"));
// Or via DbSet.AsGraphQuery() for more control
var orders = await dbContext.Orders.AsGraphQuery()
.Include(o => o.Items)
.IncludeAll(maxDepth: 3)
.Top(10)
.OrderBy("t0.[OrderDate] DESC")
.ToListAsync();
// Projection
var slim = await dbContext.Orders.SelectAsGraphAsync(
q => q.Include(o => o.Items).Where(o => o.TotalAmount > 100),
o => new { o.CustomerName, ItemCount = o.Items.Count });
Dapper:
using var conn = new SqlConnection(connectionString);
// Dialect is auto-detected from connection type
var orders = await conn.GraphQuery<Order>()
.Include(o => o.Items)
.Include("Items.Product")
.Where("t0.[CustomerId] = @cid", ("@cid", 5))
.OrderBy("t0.[OrderDate] DESC")
.Top(10)
.ToListAsync();
// Lambda terminal
var acme = await conn.ToListAsGraphAsync<Order>(q => q
.Include(o => o.Items)
.Where(o => o.CustomerName == "Acme Corp"));
NHibernate:
var orders = await session.GraphQuery<Order>()
.Include(o => o.Items)
.IncludeAll(maxDepth: 3)
.ToListAsync();
Standalone (zero ORM dependencies):
using var conn = new SqlConnection(connectionString);
var orders = await conn.GraphQuery<Order>()
.Include(o => o.Items)
.ToListAsync();
EF6 (.NET Framework 4.8):
using (var context = new MyDbContext())
{
var orders = await context.Set<Order>().ToListAsGraphAsync(q => q
.Include(o => o.Items)
.Where(o => o.CustomerName == "Acme Corp"));
}
3. Or build the query and execute manually
var query = GraphQueryBuilder.For<Order>(DatabaseDialect.SqlServer)
.Include(o => o.Items)
.Include("Items.Product")
.Where("t0.[Id] = @id", ("@id", 42))
.Build();
// query.Sql contains the full JSON-returning SQL
// query.Parameters contains the bound parameters
// Execute with any ADO.NET mechanism you prefer
Generated SQL Examples
SQL Server (FOR JSON PATH)
SELECT t0.[Id] AS [Id], t0.[customer_name] AS [CustomerName], t0.[OrderDate] AS [OrderDate],
JSON_QUERY(ISNULL((SELECT t1.[Id] AS [Id], t1.[Quantity] AS [Quantity], t1.[ProductId] AS [ProductId],
JSON_QUERY((SELECT TOP 1 t2.[Id] AS [Id], t2.[Name] AS [Name], t2.[Sku] AS [Sku]
FROM [Products] t2
WHERE t2.[Id] = t1.[ProductId]
FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)) AS [Product]
FROM [OrderItems] t1
WHERE t1.[OrderId] = t0.[Id]
FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')) AS [Items]
FROM [Orders] t0
WHERE (t0.[Id] = @__p0)
FOR JSON PATH, INCLUDE_NULL_VALUES
JSON keys are always property names (columns are aliased), NULL columns are included,
and an empty child collection comes back as [].
PostgreSQL (json_agg / json_build_object)
SELECT json_agg(
json_build_object(
'Id', t0."Id",
'CustomerName', t0."CustomerName",
'Items', (
SELECT json_agg(
json_build_object(
'Id', t1."Id",
'Quantity', t1."Quantity",
'Product', (
SELECT json_build_object('Id', t2."Id", 'Name', t2."Name")
FROM "Products" t2 WHERE t2."Id" = t1."ProductId"
)
)
) FROM "OrderItems" t1 WHERE t1."OrderId" = t0."Id"
)
)
)::text
FROM "Orders" t0
WHERE t0."Id" = @id
Child collections are wrapped in COALESCE(..., '[]'::json). With Top/OrderBy, the root rows
are filtered, sorted and limited in a subquery first, so the limit applies to rows rather than
to the single aggregated JSON value:
SELECT json_agg(json_build_object(...) ORDER BY t0."OrderDate")::text
FROM (SELECT t0.* FROM "Orders" t0 WHERE ... ORDER BY t0."OrderDate" LIMIT 10) t0
Attributes (all optional — conventions handle the common cases)
| Attribute | Target | Purpose | Convention fallback |
|---|---|---|---|
[GraphTable("name", "schema")] |
Class | Map class to table | Class name |
[GraphColumn("col")] |
Property | Map property to column | Property name |
[GraphKey] |
Property | Mark as primary key | Id or {ClassName}Id |
[GraphChildren("fk")] |
List<T> property |
One-to-many: FK column on child table | {ParentClass}Id |
[GraphParent("fk")] |
Navigation property | Many-to-one: FK column on this table | {PropertyName}Id |
[GraphIgnore] |
Property | Exclude from mapping ([NotMapped] works too) |
Computed getter-only properties are skipped automatically |
Fluent API
GraphQueryBuilder.For<T>(dialect)
.Include(x => x.Nav) // include navigation by expression
.Include("Nav.SubNav") // include by dot-path (deep)
.IncludeAll(maxDepth: 3) // include everything up to depth
.Where(x => x.Id == 42) // LINQ expression (parameterized)
.Where(x => x.Name.Contains("a")) // string methods → LIKE (wildcards escaped)
.Where("t0.[Col] = @p", ("@p", v)) // raw WHERE with parameters
.OrderBy("t0.[Col] DESC") // ORDER BY
.Top(n) // SQL Server TOP / PG LIMIT
.IgnoreQueryFilters() // skip EF Core global query filters
.Build(); // → GraphQuery { Sql, Parameters }
LINQ Where Support
The LINQ Where translates C# expressions to parameterized SQL:
| C# Expression | SQL |
|---|---|
x => x.Id == 42 |
t0.[Id] = @__p0 |
x => x.Name != null |
t0.[Name] IS NOT NULL |
x => x.Name == someNullVariable |
t0.[Name] IS NULL |
x => x.Name.Contains("50%") |
t0.[Name] LIKE @__p0 ESCAPE '\' (%50\%%, wildcards escaped) |
x => x.Name.StartsWith("abc") |
t0.[Name] LIKE @__p0 ESCAPE '\' (abc%) |
x => ids.Contains(x.Id) |
t0.[Id] IN (@__p0, @__p1, ...) |
x => x.IsActive / !x.IsActive |
t0.[IsActive] = 1 / NOT (t0.[IsActive] = 1) (= TRUE on PostgreSQL) |
x => x.Score.HasValue |
t0.[Score] IS NOT NULL |
x => string.IsNullOrEmpty(x.Name) |
t0.[Name] IS NULL OR t0.[Name] = '' |
Only columns of the root entity can be used. Navigation access such as x => x.Customer.Name
throws NotSupportedException; filter on the foreign key or use a raw SQL Where.
EF Core specifics
- Table/column names, keys, one-to-one, many-to-many (join tables) and composite keys come from the EF model.
- Global query filters (soft delete, multi-tenancy; on EF Core 10 all named filters, combined with AND) are applied to the root and every included level,
using the current context's values. Call
.IgnoreQueryFilters()to skip them. A filter that can't be translated throws instead of being silently dropped. .ToGraphQuery()returns the generated SQL and parameters without running it.
Terminal Methods
Each provider supports familiar LINQ-style terminal methods. Dialect is auto-detected — no need to pass DatabaseDialect.SqlServer manually.
// All results (auto-detect dialect from connection type)
var orders = await conn.GraphQuery<Order>().ToListAsync();
// First or default
var order = await conn.GraphQuery<Order>()
.Where(o => o.Id == 42)
.FirstOrDefaultAsync();
// Single (throws if not exactly one)
var order = await conn.GraphQuery<Order>()
.Where(o => o.Id == 42)
.SingleAsync();
// Lambda-based terminal — skip AsGraphQuery() entirely
var orders = await dbContext.Orders.ToListAsGraphAsync(q => q
.Include(o => o.Items)
.Where(o => o.CustomerName == "Acme Corp"));
// Projection with lambda
var slim = await dbContext.Orders.SelectAsGraphAsync(
q => q.Include(o => o.Items),
o => new { o.CustomerName, ItemCount = o.Items.Count });
// Select projection on fluent builder
var names = await conn.GraphQuery<Product>()
.Select(p => new { p.Name, p.Price })
.ToListAsync();
Architecture
Rmp.GraphOrm.Core ← Attributes, mapping metadata, SQL builders, deserializer (net8.0)
Rmp.GraphOrm.Core.Legacy ← Same API for .NET Framework 4.8
├── Rmp.GraphOrm.Dapper ← Dapper IDbConnection extensions
├── Rmp.GraphOrm.EFCore ← EF Core DbContext/DbSet extensions
├── Rmp.GraphOrm.EF6 ← EF6 DbContext/DbSet extensions (net48)
├── Rmp.GraphOrm.NHibernate ← NHibernate ISession extensions
└── Rmp.GraphOrm.Standalone ← Pure ADO.NET DbConnection extensions
Build & Test
dotnet build Rmp.GraphOrm.sln -c Release
dotnet test Rmp.GraphOrm.Tests -c Release
Integration tests are skipped (and reported as skipped) unless a database is configured:
# SQL Server (the database is created if missing)
export GRAPHORM_TEST_CONNSTR="Server=(localdb)\MSSQLLocalDB;Database=GraphOrmTests;Integrated Security=True;TrustServerCertificate=True"
# PostgreSQL (creates graphorm_tests and graphorm_regression on that server)
docker run -d --name graphorm-pg -e POSTGRES_USER=graphorm -e POSTGRES_PASSWORD=graphorm -p 55439:5432 postgres:16
export GRAPHORM_TEST_PG_CONNSTR="Host=localhost;Port=55439;Username=graphorm;Password=graphorm"
dotnet test Rmp.GraphOrm.Tests -c Release
CI (.github/workflows/ci.yml) runs the full suite against SQL Server 2022 and PostgreSQL 16 and uploads the packages as a build artifact.
Publishing to NuGet
All seven packages share one version (Directory.Build.props) and are released together.
Pack from a clean checkout of this repository so the packages record the right commit:
dotnet pack Rmp.GraphOrm.sln -c Release -o artifacts
dotnet nuget push "artifacts/*.nupkg" --api-key <key> --source https://api.nuget.org/v3/index.json
dotnet nuget push uploads the matching .snupkg symbol packages automatically.
Support
Report bugs and feature requests at https://github.com/rubiconmp/public-issues. More from Rubicon Microproducts: https://rubiconmp.com/
Bandwidth Comparison
| Scenario | Traditional JOIN | GraphORM JSON |
|---|---|---|
| Order + 100 items + 100 products | 100 rows × 30 cols = 3000 values | 201 objects, each once |
| Company → 5 depts → 20 emps → 20 addrs | 5×20×20 = 2000 rows | 5+20+20 = 45 objects |
| 4-level graph, 10 per level | 10⁴ = 10,000 rows | 10+10+10+10 = 40 objects |
Entity Framework 6 Support
The Rmp.GraphOrm.EF6 package targets .NET Framework 4.8 and depends on Rmp.GraphOrm.Core.Legacy. It provides the same fluent API as the other providers:
using (var context = new MyDbContext())
{
// Lambda terminal — auto-detects dialect from connection
var orders = await context.Set<Order>().ToListAsGraphAsync(q => q
.Include(o => o.Items)
.Where(o => o.Status == "Active"));
// Fluent builder
var results = await context.GraphQuery<Order>()
.Include(o => o.Items)
.IncludeAll(maxDepth: 3)
.ToListAsync();
// Projection
var slim = await context.Set<Order>().SelectAsGraphAsync(
q => q.Include(o => o.Items),
o => new { o.CustomerName, ItemCount = o.Items.Count });
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Dapper (>= 2.1.35)
- Rmp.GraphOrm.Core (>= 0.1.1)
-
net8.0
- Dapper (>= 2.1.35)
- Rmp.GraphOrm.Core (>= 0.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.