Rmp.GraphOrm.EF6
0.2.1
dotnet add package Rmp.GraphOrm.EF6 --version 0.2.1
NuGet\Install-Package Rmp.GraphOrm.EF6 -Version 0.2.1
<PackageReference Include="Rmp.GraphOrm.EF6" Version="0.2.1" />
<PackageVersion Include="Rmp.GraphOrm.EF6" Version="0.2.1" />
<PackageReference Include="Rmp.GraphOrm.EF6" />
paket add Rmp.GraphOrm.EF6 --version 0.2.1
#r "nuget: Rmp.GraphOrm.EF6, 0.2.1"
#:package Rmp.GraphOrm.EF6@0.2.1
#addin nuget:?package=Rmp.GraphOrm.EF6&version=0.2.1
#tool nuget:?package=Rmp.GraphOrm.EF6&version=0.2.1
Rmp.GraphOrm
Fast object-graph loading for legacy and modern .NET apps — without rewriting your data access.
Rmp.GraphOrm loads an entity with its children and grandchildren in one SQL query that returns each row once,
using the database's own JSON aggregation. It is built in large part for heavy legacy applications: big
.NET Framework 4.x codebases on EF6, NHibernate, Dapper or plain ADO.NET, where a few slow screens are caused by
Include chains, N+1 queries or cartesian explosion, and a rewrite is not on the table.
Built for legacy .NET apps
If you maintain an ASP.NET MVC 5 / Web Forms / WCF / Windows Service application on .NET Framework, this is for you.
- Runs where your app runs. .NET Framework 4.6.2 – 4.8.1, .NET Standard 2.0 (.NET Core 2.0 – 3.1, .NET 5 – 7, Mono), and .NET 6 – 10. Tested on .NET Framework 4.8 and .NET 6, 8 and 10.
- Works with the ORM you already have. EF6, NHibernate, Dapper or raw
DbConnection— no new ORM, no migration, no schema changes. The EF6 provider reads your existing EF6 model (tables, columns, keys, relationships), so there is nothing new to map. - Adopt one screen at a time. Keep your existing queries. Swap only the slow ones for a graph query; the rest of the app is untouched. Queries run on your existing connection and inside your existing transaction.
- Works on old databases. SQL Server 2016 or later (and Azure SQL) at any database compatibility level — tested on a database left at level 100 (SQL Server 2008 behaviour). PostgreSQL 9.6 or later.
- Read-only and side-effect free. Results are plain objects: no change tracking, no lazy-loading proxies, no context state to leak between requests.
- Same API on old and new .NET. The fluent API is identical on .NET Framework and modern .NET, so graph queries
move unchanged when you migrate (for EF, switch the
Rmp.GraphOrm.EF6package forRmp.GraphOrm.EFCore).
Typical result: a page that loads an order with 100 lines and their products goes from one huge duplicated result set (or 101 round trips) to a single query that returns exactly one copy of each row.
// EF6 on .NET Framework 4.8 — inside your existing DbContext
var orders = await db.GraphQuery<Order>()
.Include(o => o.Items)
.Include("Items.Product")
.Where(o => o.CustomerId == customerId)
.ToListAsync();
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 | Minimum version |
|---|---|---|
| SQL Server / Azure SQL | FOR JSON PATH / WITHOUT_ARRAY_WRAPPER |
SQL Server 2016, any compatibility level |
| PostgreSQL | json_agg() / json_build_object() |
9.6 |
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 — legacy apps are a primary target (all providers except EF Core; use
Rmp.GraphOrm.EF6for 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 | Versions 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 was computed. 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 was computed. 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 | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| .NET Framework | 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 | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- EntityFramework (>= 6.5.1)
- Rmp.GraphOrm.Core (>= 0.2.1)
-
.NETStandard 2.1
- EntityFramework (>= 6.5.1)
- Rmp.GraphOrm.Core (>= 0.2.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.