Rmp.GraphOrm.Dapper 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Rmp.GraphOrm.Dapper --version 0.2.0
                    
NuGet\Install-Package Rmp.GraphOrm.Dapper -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Rmp.GraphOrm.Dapper" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rmp.GraphOrm.Dapper" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Rmp.GraphOrm.Dapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rmp.GraphOrm.Dapper --version 0.2.0
                    
#r "nuget: Rmp.GraphOrm.Dapper, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Rmp.GraphOrm.Dapper@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rmp.GraphOrm.Dapper&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Rmp.GraphOrm.Dapper&version=0.2.0
                    
Install as a Cake Tool

Rmp.GraphOrm

Eliminates the fundamental ORM row-duplication problem by pushing JSON aggregation into SQL.

NuGet License: MIT

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 Targets Dependency Extension Point
Rmp.GraphOrm.Core net462, netstandard2.0, net8.0, net10.0 — Attributes, mapping, SQL builders, deserializer
Rmp.GraphOrm.Dapper net462, netstandard2.0, net8.0, net10.0 Dapper IDbConnection.GraphQuery<T>()
Rmp.GraphOrm.EFCore net6.0, net8.0, net10.0 EF Core 6 / 8 / 10 DbContext.GraphQuery<T>() / DbSet<T>.AsGraphQuery()
Rmp.GraphOrm.EF6 net462, netstandard2.1 EF 6 DbContext.GraphQuery<T>() / DbSet<T>.AsGraphQuery()
Rmp.GraphOrm.NHibernate net462, netstandard2.0, net8.0, net10.0 NHibernate ISession.GraphQuery<T>()
Rmp.GraphOrm.Standalone net462, netstandard2.0, net8.0, net10.0 None (ADO.NET) DbConnection.GraphQuery<T>()

Rmp.GraphOrm.Core.Legacy is deprecated: it now only depends on Rmp.GraphOrm.Core, which supports .NET Framework directly. Reference Rmp.GraphOrm.Core instead.

Installation

Works on:

  • .NET Framework 4.6.2 – 4.8.1 (all providers except EF Core; use Rmp.GraphOrm.EF6 for Entity Framework)
  • .NET Standard 2.0 platforms: .NET Core 2.0 – 3.1, .NET 5 – 7, Mono, Xamarin, Unity
  • .NET 6 / 7 for the EF Core provider (EF Core 6)
  • .NET 8 and .NET 10 (EF Core 8 / EF Core 10)

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.6.2+ or .NET Standard 2.1):

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
├── Rmp.GraphOrm.Dapper        ← Dapper IDbConnection extensions
├── Rmp.GraphOrm.EFCore        ← EF Core DbContext/DbSet extensions
├── Rmp.GraphOrm.EF6           ← EF6 DbContext/DbSet extensions
├── 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

Rmp.GraphOrm.Tests runs on .NET 6, 8 and 10; Rmp.GraphOrm.Tests.NetFramework runs on .NET Framework 4.8 (Windows only) and covers Standalone, Dapper and EF6 there.

CI (.github/workflows/ci.yml) runs the modern suite against SQL Server 2022 and PostgreSQL 16 on Linux, runs the .NET Framework suite against SQL Server LocalDB on Windows, 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.6.2+ and .NET Standard 2.1 (EF6 on .NET Core 3.1 and later) and depends on Rmp.GraphOrm.Core. 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 Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 57 11/8/2026
0.2.0 60 11/7/2026
0.1.1 61 11/6/2026
0.1.0 58 11/6/2026