DataFuse Framework

Like GraphQL, but for your heterogeneous backend systems
A declarative .NET framework that aggregates data from SQL databases, REST APIs, and Entity Framework into unified, strongly-typed objects — replacing hundreds of lines of manual orchestration code.
.NET 9.0 NuGet MIT License Multi-Source Type-Safe

Real-World Use Cases

DataFuse shines when a single response needs data from multiple backend systems. Here are scenarios where it eliminates boilerplate and adds automatic parallelism.

1. E-Commerce Product Page

SQL Server — Inventory REST API — Pricing External Service — Reviews

A product page combines inventory data from your SQL database, live pricing from a pricing microservice, and customer reviews from a third-party platform. DataFuse fetches the product from SQL first, then runs pricing and reviews API calls in parallel.

public class ProductPageConfig : EntityConfiguration<ProductPage>
{
    public override IEnumerable<Mapping<ProductPage, IQueryResult>> GetSchema()
    {
        return CreateSchema.For<ProductPage>()
            .Map<ProductQuery, ProductTransform>(For.Paths("product"),
                p => p.Dependents
                    .Map<PricingApiQuery, PricingTransform>(For.Paths("product/pricing"))
                    .Map<ReviewsApiQuery, ReviewsTransform>(For.Paths("product/reviews")))
            .End();
    }
}

2. Customer 360 Dashboard

CRM Database Billing REST API Support Ticket API Orders Database

Build a unified customer view from CRM data, billing history, support tickets, and order details — each from a different system. Use selective loading to fetch only what the current view needs.

// Full customer 360
var customer = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });

// Just profile + billing (skip tickets and orders)
var billing = dataProvider.GetData(new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer", "customer/billing" }
});

3. Multi-Service Reporting

Sales Database Inventory API Shipping API Revenue Database

Aggregate metrics from multiple microservices into a single report. Each data source is encapsulated in its own query, testable independently, and executed with automatic dependency management.


Why DataFuse?

The Problem

In modern architectures, a single page or API response often needs data from multiple backend systems. The standard approach is manual orchestration code:

// Without DataFuse: 50+ lines per data combination, repeated everywhere
var customer = await GetCustomerFromDatabase(customerId);
var orders = await GetOrdersFromAPI(customerId);           // Sequential!
var billing = await GetBillingFromService(customerId);     // Sequential!
var tickets = await GetTicketsFromAPI(customerId);          // Sequential!

customer.Orders = orders;
customer.Billing = billing;
customer.Tickets = tickets;
Problems with manual orchestration:
  • Boilerplate explosion — every new data combination means 50+ lines of fetch-assemble code
  • No parallelism — developers write sequential calls unless they manually add Task.WhenAll
  • Tight coupling — changing a data source means rewriting orchestration
  • No selective loading — you fetch everything even when consumers only need a subset
  • Inconsistent patterns — every developer solves the same problem differently
VS
// With DataFuse: declare once, use everywhere
var customer = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });
DataFuse gives you:
  • Declarative schema — define data relationships once, reuse everywhere
  • Automatic parallelism — sibling queries run in parallel, no Task.WhenAll needed
  • Selective loading — fetch only the data paths the consumer needs
  • Dependency management — parent results flow to child queries automatically
  • Type safety — strongly-typed queries, results, and transformers
  • Extensible adapters — add any data source by implementing IQueryEngine

Comparison with Alternatives

Capability Manual Code GraphQL MediatR DataFuse
Multi-source aggregation Manual wiring Resolver-based Manual wiring Declarative schema
Parallel execution Manual Task.WhenAll Per-resolver Manual Automatic
Selective loading Manual if/else Built-in Manual Schema path filtering
Dependency management Manual ordering Implicit Manual Parent-child hierarchy
Type safety Varies Schema-based Yes Strongly typed
Learning curve None High (new language) Low Low (C# only)
Backend-only (no client changes) Yes No Yes Yes
When to use DataFuse: Your data lives across different systems (SQL + APIs + external services) and you want declarative orchestration with automatic parallelism. When NOT to use it: All your data is in a single database and simple SQL joins suffice.

Quick Start (5 Minutes)

1. Install Packages

dotnet add package DataFuse.Integration
dotnet add package DataFuse.Adapters.SQL — For SQL with Dapper
dotnet add package DataFuse.Adapters.WebAPI — For REST APIs
dotnet add package DataFuse.Adapters.EntityFramework — For EF Core

2. Define Your Entity

public class Product : IEntity
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public Category Category { get; set; }
    public Review[] Reviews { get; set; }
}

3. Create a Query

public class ProductQuery : SQLQuery<ProductResult>
{
    protected override Func<IDbConnection, Task<ProductResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (ProductRequest)context.Request;
        return connection => connection.QueryFirstOrDefaultAsync<ProductResult>(
            "SELECT ProductId as Id, Name, Price FROM Products WHERE ProductId = @Id",
            new { Id = request.ProductId });
    }
}

public class ReviewsApiQuery : WebQuery<CollectionResult<ReviewResult>>
{
    public ReviewsApiQuery() : base("https://api.reviews.com/") { }

    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var product = (ProductResult)parentQueryResult;
        return () => new Uri($"products/{product.Id}/reviews", UriKind.Relative);
    }
}

4. Create Transformers

public class ProductTransform : BaseTransformer<ProductResult, Product>
{
    public override void Transform(ProductResult queryResult, Product entity)
    {
        entity.ProductId = queryResult.Id;
        entity.Name = queryResult.Name;
        entity.Price = queryResult.Price;
    }
}

public class ReviewsTransform : BaseTransformer<CollectionResult<ReviewResult>, Product>
{
    public override void Transform(CollectionResult<ReviewResult> queryResult, Product entity)
    {
        entity.Reviews = queryResult?.Select(r => new Review
        {
            ReviewId = r.Id, Comment = r.Comment, Rating = r.Rating
        }).ToArray() ?? Array.Empty<Review>();
    }
}

5. Configure Schema

public class ProductConfiguration : EntityConfiguration<Product>
{
    public override IEnumerable<Mapping<Product, IQueryResult>> GetSchema()
    {
        return CreateSchema.For<Product>()
            .Map<ProductQuery, ProductTransform>(For.Paths("product"),
                product => product.Dependents
                    .Map<CategoryQuery, CategoryTransform>(For.Paths("product/category"))
                    .Map<ReviewsApiQuery, ReviewsTransform>(For.Paths("product/reviews")))
            .End();
    }
}

6. Register & Use

// Startup / DI registration
services.UseDataFuse()
    .WithEngine(c => new QueryEngine(sqlConfiguration))
    .WithEngine<DataFuse.Adapters.WebAPI.QueryEngine>()
    .WithPathMatcher(c => new XPathMatcher())
    .WithEntityConfiguration<Product>(c => new ProductConfiguration());

services.AddHttpClient();

// Usage
public class ProductService
{
    private readonly IDataProvider<Product> _dataProvider;

    public ProductService(IDataProvider<Product> dataProvider)
        => _dataProvider = dataProvider;

    public Product GetProduct(int productId)
        => _dataProvider.GetData(new ProductRequest { ProductId = productId });

    public Product GetProductWithReviews(int productId)
        => _dataProvider.GetData(new ProductRequest
        {
            ProductId = productId,
            SchemaPaths = new[] { "product", "product/reviews" }
        });
}

Core Concepts

Entities

Entities implement IEntity and define the complete object graph. Each property can be hydrated from a different data source:

public class Customer : IEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public Communication Communication { get; set; }  // From REST API
    public Address Address { get; set; }                // From SQL
    public Order[] Orders { get; set; }                 // From EF Core
}

Queries & Parent-Child Dependencies

Child queries receive their parent's result, enabling dependent fetching across data sources:

// Root: fetches from SQL
public class CustomerQuery : SQLQuery<CustomerResult> { /* uses request context */ }

// Child: uses parent CustomerResult to call API
public class OrdersApiQuery : WebQuery<CollectionResult<OrderResult>>
{
    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var customer = (CustomerResult)parentQueryResult;
        return () => new Uri($"orders?customerId={customer.Id}", UriKind.Relative);
    }
}

Execution Flow

1. Root Query executes first
   |
   v  Result passed to children

2. Child Queries execute IN PARALLEL
   - OrdersQuery, CommunicationQuery, AddressQuery
   |
   v  Results passed to grandchildren

3. Grandchild Queries execute
   - OrderItemsQuery (uses OrderId from parent)

Transformers

Transformers map query results onto the entity. Each query-transformer pair handles one piece of the object graph:

public class OrdersTransform : BaseTransformer<CollectionResult<OrderResult>, Customer>
{
    public override void Transform(CollectionResult<OrderResult> queryResult, Customer entity)
    {
        entity.Orders = queryResult?.Select(o => new Order
        {
            OrderId = o.OrderId,
            OrderNumber = o.OrderNumber,
            OrderDate = o.OrderDate
        }).ToArray() ?? Array.Empty<Order>();
    }
}

Schema Configuration

The schema declares the full hierarchy of queries, transformers, and path mappings:

return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransform>(For.Paths("customer"),
        c => c.Dependents
            .Map<CommunicationQuery, CommTransform>(For.Paths("customer/communication"))
            .Map<OrdersQuery, OrdersTransform>(For.Paths("customer/orders"),
                o => o.Dependents
                    .Map<ItemsQuery, ItemsTransform>(For.Paths("customer/orders/order/items"))))
    .End();

Packages

Package Purpose .NET .NET Standard .NET Framework
DataFuse.Integration Core orchestration, DI, helpers, path matchers 9.0+ 2.0, 2.1 4.6.2+
DataFuse.Adapters.Abstraction Interfaces & base classes (IEntity, IQuery, BaseTransformer) 9.0+ 2.0, 2.1 4.6.2+
DataFuse.Adapters.SQL SQL via Dapper (SQL Server, SQLite, MySQL, PostgreSQL) 9.0+ 2.1 4.6.2+
DataFuse.Adapters.EntityFramework Entity Framework Core with full LINQ support 9.0+ - -
DataFuse.Adapters.WebAPI REST APIs via HttpClient with header management 9.0+ 2.0, 2.1 4.6.2+

Advanced Features

Selective Loading

Fetch only the parts of the object graph you need via SchemaPaths:

// Load everything
var full = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });

// Load only customer + orders
var partial = dataProvider.GetData(new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer", "customer/orders" }
});

Caching

Mark query results with [CacheResult] to make them available across queries and transformers:

[CacheResult]
public class CategoryResult : IQueryResult
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// Access in a transformer
if (Context.Cache.TryGetValue("CategoryResult", out var cached))
{
    entity.CategoryName = ((CategoryResult)cached).Name;
}

Custom Data Source Adapters

Add support for any data source by implementing IQueryEngine:

public class MongoQueryEngine : IQueryEngine
{
    private readonly IMongoDatabase _db;
    public MongoQueryEngine(IMongoDatabase db) => _db = db;

    public bool CanExecute(IQuery query) => query is IMongoQuery;

    public async Task<IQueryResult> Execute(IQuery query)
    {
        var mongoQuery = (IMongoQuery)query;
        return await mongoQuery.Execute(_db);
    }
}

// Register alongside other engines
services.UseDataFuse()
    .WithEngine(c => new MongoQueryEngine(mongoDb))
    .WithEngine(c => new QueryEngine(sqlConfig));

Conditional Query Execution

Skip queries based on parent data by returning null:

protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
{
    var customer = (CustomerResult)parentQueryResult;
    if (customer.CustomerType != "Premium")
        return null; // Query is skipped

    return () => new Uri($"premium/{customer.Id}", UriKind.Relative);
}

Path Matching

DataFuse supports XPath and JSONPath patterns for schema paths:

XPathJSONPathMatches
customer$.customerRoot entity
customer/orders$.customer.ordersNested property
customer/orders/order/items$.customer.orders[*].itemsDeep nesting