5rem 0; position: relative; padding-left: 2rem; } .checklist li::before { content: '✅'; position: absolute; left: 0; top: 0.5rem; } .flow-diagram { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 2rem; text-align: center; margin: 2rem 0; box-shadow: var(--shadow); } .flow-diagram pre { background: transparent; border: none; box-shadow: none; color: var(--text-primary); font-weight: 600; } ul, ol { margin-bottom: 1.5rem; } li { margin-bottom: 0.5rem; color: var(--text-secondary); } hr { border: none; height: 1px; background: linear-gradient(to right, transparent, var(--border-color), transparent); margin: 3rem 0; } footer { background: var(--dark-color); color: white; text-align: center; padding: 2rem; margin-top: 3rem; } footer p { color: #d1d5db; margin: 0.5rem 0; } .badge { display: inline-block; padding: 0.25rem 0.75rem; background: var(--primary-color); color: white; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0.25rem; } @media (max-width: 768px) { .header h1 { font-size: 2.5rem; } .content { padding: 1rem; } .container { margin: 1rem; border-radius: 8px; } h2 { font-size: 1.875rem; } pre { font-size: 0.75rem; } }

Schemio Framework

A powerful .NET library for schema-driven data aggregation
.NET 9.0 Schema-Driven Multi-Source Type-Safe

Introduction

Schemio is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. It enables developers to hydrate complex object graphs by fetching data from multiple sources (SQL databases, Web APIs, NoSQL stores) using XPath and JSONPath schema mappings.

Key Benefits

  • Unified Data Access: Aggregate data from SQL databases, REST APIs, and custom data sources
  • Schema-Driven: Use XPath or JSONPath to define object graph mappings
  • Performance Optimized: Execute queries in parallel with dependency management
  • Extensible: Easily add support for new data sources
  • Type-Safe: Strongly-typed entities and query results
  • Flexible: Support for nested queries up to 5 levels deep

The Need for Data Aggregation

Modern Application Challenges

In today's microservices and distributed system architectures, applications often need to:

1. Combine Data from Multiple Sources

2. Handle Different Data Formats

3. Manage Complex Dependencies

Traditional Approaches and Their Limitations

Manual Data Assembly

// Traditional approach - brittle and hard to maintain
var customer = GetCustomerFromDatabase(customerId);
var orders = GetOrdersFromAPI(customerId);
var communication = GetCommunicationFromService(customerId);

// Manual assembly - error-prone
customer.Orders = orders;
customer.Communication = communication;
Problems:
  • Tight coupling between data sources
  • Difficult to maintain and extend
  • No standard approach for error handling
  • Limited reusability
  • Performance issues with sequential calls

Schemio's Solution

Schemio provides a declarative, schema-driven approach:

// Schemio approach - declarative and maintainable
public class CustomerConfiguration : EntityConfiguration<Customer>
{
    public override IEnumerable<Mapping<Customer, IQueryResult>> GetSchema()
    {
        return CreateSchema.For<Customer>()
            .Map<CustomerQuery, CustomerTransform>(For.Paths("customer"),
                customer => customer.Dependents
                    .Map<CommunicationQuery, CommunicationTransform>(For.Paths("customer/communication"))
                    .Map<OrdersQuery, OrdersTransform>(For.Paths("customer/orders")))
            .End();
    }
}
Benefits:
  • Declarative configuration
  • Automatic dependency management
  • Parallel query execution
  • Type-safe transformations
  • Extensible to new data sources
  • Built-in caching support

Core Concepts

Entities

Entities represent the final aggregated data structure implementing IEntity. They define the complete object graph that will be hydrated with data from multiple sources:

public class Customer : IEntity
{
    // Level 1: Root properties
    public int CustomerId { get; set; }
    public string CustomerCode { get; set; }
    public string CustomerName { get; set; }
    
    // Level 2: Nested objects
    public Communication Communication { get; set; }
    public Address Address { get; set; }
    public Order[] Orders { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public string OrderNumber { get; set; }
    public DateTime OrderDate { get; set; }
    
    // Level 3: Deep nesting
    public OrderItem[] Items { get; set; }
    public Payment Payment { get; set; }
}

public class OrderItem
{
    public int ItemId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Queries and Parent-Child Relationships

Queries form a hierarchical structure where child queries depend on data from their parent queries. This creates a powerful data flow where each query can use the results of its parent to customize its own execution.

Parent Query (Root Level)

Parent queries execute first and don't depend on other query results:

public class CustomerQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        // parentQueryResult is null for root queries
        var request = (CustomerRequest)context.Request;
        
        return connection => connection.QueryFirstOrDefaultAsync<CustomerResult>(
            "SELECT CustomerId as Id, CustomerName as Name, CustomerCode as Code FROM Customers WHERE CustomerId = @Id",
            new { Id = request.CustomerId });
    }
}

Child Query (Dependent Level)

Child queries receive their parent's result and use it to determine what data to fetch:

public class OrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<OrderResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        // parentQueryResult contains the CustomerResult from the parent query
        var customer = (CustomerResult)parentQueryResult;
        
        return async connection =>
        {
            var orders = await connection.QueryAsync<OrderResult>(
                "SELECT OrderId, OrderNumber, OrderDate FROM Orders WHERE CustomerId = @CustomerId",
                new { CustomerId = customer.Id });
                
            return new CollectionResult<OrderResult>(orders);
        };
    }
}

Query Execution Flow

The parent-child relationship creates a specific execution order:

1. Parent Query (CustomerQuery) executes first
   ↓ (CustomerResult passed to children)
   
2. Child Queries execute in parallel:
   - OrdersQuery (uses CustomerId from CustomerResult)
   - CommunicationQuery (uses CustomerId from CustomerResult)
   - AddressQuery (uses CustomerId from CustomerResult)
   ↓ (OrderResult passed to grandchildren)
   
3. Grandchild Queries execute:
   - OrderItemsQuery (uses OrderId from OrderResult)
   - PaymentQuery (uses OrderId from OrderResult)

Package Overview

Core Packages

1. Schemio.Core

Purpose: Foundation package providing core interfaces and implementations.

Key Components:

Installation:
Install-Package Schemio.Core

2. Schemio.SQL

Purpose: SQL database support using Dapper for query execution.

Key Components:

Installation:
Install-Package Schemio.SQL

Supported Databases:

3. Schemio.EntityFramework

Purpose: Entity Framework Core integration for advanced ORM scenarios.

Key Components:

Installation:
Install-Package Schemio.EntityFramework

4. Schemio.API

Purpose: HTTP/REST API data source support using HttpClient.

Key Components:

Installation:
Install-Package Schemio.API

Package Compatibility Matrix

Package .NET Framework .NET Standard .NET Core/.NET
Schemio.Core 4.6.2+ 2.0, 2.1 9.0+
Schemio.SQL 4.6.2+ 2.1 9.0+
Schemio.EntityFramework - - 9.0+
Schemio.API 4.6.2+ 2.0, 2.1 9.0+

Getting Started

1. Basic Setup

First, install the required packages:

Install-Package Schemio.Core
Install-Package Schemio.SQL  # For SQL database support
Install-Package Schemio.API  # For REST API support
Install-Package Schemio.EntityFramework # For Entity Framework support

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; }
}

public class Category
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
}

public class Review
{
    public int ReviewId { get; set; }
    public string Comment { get; set; }
    public int Rating { get; set; }
}

3. Create Query Results

public class ProductResult : IQueryResult
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

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

public class ReviewResult : IQueryResult
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public int Rating { get; set; }
    public int ProductId { get; set; }
}

4. Implement Queries

SQL Query Example:

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, CategoryId FROM Products WHERE ProductId = @Id",
            new { Id = request.ProductId });
    }
}

API Query Example:

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);
    }
}

5. Create Transformers

public class ProductTransformer : 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 CategoryTransformer : BaseTransformer<CategoryResult, Product>
{
    public override void Transform(CategoryResult queryResult, Product entity)
    {
        if (entity.Category == null)
            entity.Category = new Category();
            
        entity.Category.CategoryId = queryResult.Id;
        entity.Category.Name = queryResult.Name;
    }
}

6. Configure Entity Schema

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

7. Register Dependencies

// Using fluent interface
services.UseSchemio()
    .WithEngine(c => new QueryEngine(sqlConfiguration))  // SQL support
    .WithEngine<Schemio.API.QueryEngine>()              // API support
    .WithPathMatcher(c => new XPathMatcher())
    .WithEntityConfiguration<Product>(c => new ProductConfiguration());

// Enable logging
services.AddLogging();

// For API queries
services.AddHttpClient();

// For SQL queries  
DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance);

8. Use the Data Provider

public class ProductService
{
    private readonly IDataProvider<Product> dataProvider;
    
    public ProductService(IDataProvider<Product> dataProvider)
    {
        this.dataProvider = dataProvider;
    }
    
    public Product GetProduct(int productId)
    {
        var request = new ProductRequest { ProductId = productId };
        return dataProvider.GetData(request);
    }
    
    public Product GetProductWithReviews(int productId)
    {
        var request = new ProductRequest 
        { 
            ProductId = productId,
            SchemaPaths = new[] { "product", "product/reviews" }
        };
        return dataProvider.GetData(request);
    }
}

Architecture Deep Dive

Execution Flow

Client Request
    ↓
DataProvider
    ↓
QueryBuilder
    ↓
Generate Query Plan
    ↓
QueryExecutor
    ↓
Execute Level 1 Queries
    ↓
Resolve Dependencies
    ↓
Execute Level 2 Queries
    ↓
Continue Until All Levels Complete
    ↓
EntityBuilder
    ↓
Apply Transformers
    ↓
Return Aggregated Entity

Component Interactions

1. DataProvider Orchestration

The DataProvider<T> serves as the main orchestrator:

public TEntity GetData(IEntityRequest request)
{
    var context = new DataContext(request);
    
    // Build execution plan
    var queries = queryBuilder.Build(context);
    
    // Execute queries
    var results = queryExecutor.Execute(context, queries);
    
    // Build final entity
    var entity = entityBuilder.Build(context, results);
    
    return entity;
}

2. Query Building Process

The QueryBuilder<T> creates an optimized execution plan:

  1. Filter by Schema Paths: Only include queries matching requested paths
  2. Resolve Dependencies: Build parent-child query relationships
  3. Optimize Execution: Determine optimal query execution order

3. Query Execution Strategy

The QueryExecutor manages parallel execution with dependency resolution:

  1. Level-by-Level Execution: Execute queries level by level to handle dependencies
  2. Parallel Processing: Run independent queries at the same level in parallel
  3. Result Propagation: Pass parent results to child queries
  4. Caching: Cache results marked with [CacheResult] attribute

4. Entity Building Process

The EntityBuilder<T> assembles the final entity:

  1. Transformer Resolution: Match query results to appropriate transformers
  2. Sequential Application: Apply transformers in dependency order
  3. Type Safety: Ensure type compatibility between results and transformers

Memory Management


Configuration Guide

Entity Configuration Patterns

1. Simple Linear Configuration

return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"))
    .Map<AddressQuery, AddressTransformer>(For.Paths("customer/address"))
    .End();

2. Branching Configuration

return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"),
        customer => customer.Dependents
            .Map<ContactQuery, ContactTransformer>(For.Paths("customer/contact"))
            .Map<PreferencesQuery, PreferencesTransformer>(For.Paths("customer/preferences")))
    .End();

3. Deep Nesting Configuration

return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"),
        customer => customer.Dependents
            .Map<OrdersQuery, OrdersTransformer>(For.Paths("customer/orders"),
                orders => orders.Dependents
                    .Map<OrderItemsQuery, OrderItemsTransformer>(For.Paths("customer/orders/order/items"))))
    .End();

Path Matching Strategies

XPath Patterns

JSONPath Patterns

Schema Path Filtering

Control which parts of the object graph to load:

// Load only customer basic info
var request = new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer" }
};

// Load customer with orders but no items
var request = new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer", "customer/orders" }
};

// Load everything
var request = new CustomerRequest
{
    CustomerId = 123
    // SchemaPaths = null loads all configured paths
};

Query Implementation Guide

SQL Queries with Dapper

Basic Query

public class CustomerQuery : SQLQuery<CustomerResult>
{
    protected override Func<IDbConnection, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (CustomerRequest)context.Request;
        
        return connection => connection.QueryFirstOrDefaultAsync<CustomerResult>(
            @"SELECT CustomerId as Id, 
                     CustomerName as Name, 
                     CustomerCode as Code 
              FROM Customers 
              WHERE CustomerId = @CustomerId",
            new { CustomerId = request.CustomerId });
    }
}

Collection Query

public class OrdersQuery : SQLQuery<CollectionResult<OrderResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<OrderResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var customer = (CustomerResult)parentQueryResult;
        
        return async connection =>
        {
            var orders = await connection.QueryAsync<OrderResult>(
                @"SELECT OrderId, OrderNumber, OrderDate, TotalAmount
                  FROM Orders 
                  WHERE CustomerId = @CustomerId",
                new { CustomerId = customer.Id });
                
            return new CollectionResult<OrderResult>(orders);
        };
    }
}

Complex Query with Parameters

public class ProductSearchQuery : SQLQuery<CollectionResult<ProductResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<ProductResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (ProductSearchRequest)context.Request;
        
        return async connection =>
        {
            var sql = @"
                SELECT p.ProductId as Id, p.Name, p.Price, p.CategoryId
                FROM Products p
                WHERE (@CategoryId IS NULL OR p.CategoryId = @CategoryId)
                  AND (@MinPrice IS NULL OR p.Price >= @MinPrice)
                  AND (@MaxPrice IS NULL OR p.Price <= @MaxPrice)
                  AND (@SearchTerm IS NULL OR p.Name LIKE @SearchPattern)
                ORDER BY p.Name";
            
            var products = await connection.QueryAsync<ProductResult>(sql, new
            {
                CategoryId = request.CategoryId,
                MinPrice = request.MinPrice,
                MaxPrice = request.MaxPrice,
                SearchTerm = request.SearchTerm,
                SearchPattern = $"%{request.SearchTerm}%"
            });
            
            return new CollectionResult<ProductResult>(products);
        };
    }
}

Entity Framework Queries

Basic EF Query

public class CustomerQuery : SQLQuery<CustomerResult>
{
    protected override Func<DbContext, Task<CustomerResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (CustomerRequest)context.Request;
        
        return async dbContext =>
        {
            var result = await dbContext.Set<CustomerEntity>()
                .Where(c => c.CustomerId == request.CustomerId)
                .Select(c => new CustomerResult
                {
                    Id = c.CustomerId,
                    Name = c.Name,
                    Code = c.Code,
                    Email = c.Email
                })
                .FirstOrDefaultAsync();
                
            return result;
        };
    }
}

Web API Queries

Basic API Query

public class UserProfileQuery : WebQuery<UserProfileResult>
{
    public UserProfileQuery() : base("https://api.userservice.com/") { }

    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (UserRequest)context.Request;
        return () => new Uri($"users/{request.UserId}", UriKind.Relative);
    }
}

API Query with Headers

public class AuthenticatedApiQuery : WebQuery<UserDataResult>
{
    public AuthenticatedApiQuery() : base("https://api.secure.com/") { }

    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (SecureRequest)context.Request;
        return () => new Uri($"secure/data/{request.Id}", UriKind.Relative);
    }

    protected override IDictionary<string, string> GetRequestHeaders()
    {
        return new Dictionary<string, string>
        {
            { "Authorization", "Bearer " + GetAccessToken() },
            { "X-Client-Version", "1.0" },
            { "Accept", "application/json" }
        };
    }

    protected override IEnumerable<string> GetResponseHeaders()
    {
        return new[] { "X-Rate-Limit-Remaining", "X-Request-Id" };
    }

    private string GetAccessToken()
    {
        // Implement token retrieval logic
        return "your-access-token";
    }
}

Query Result Types

Simple Result

public class CustomerResult : IQueryResult
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Code { get; set; }
    public string Email { get; set; }
}

Collection Result

public class CollectionResult<T> : List<T>, IQueryResult
{
    public CollectionResult(IEnumerable<T> items) : base(items) { }
    public CollectionResult() { }
}

Cached Result

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

Transformer Implementation Guide

Basic Transformers

Simple Property Mapping

public class CustomerTransformer : BaseTransformer<CustomerResult, Customer>
{
    public override void Transform(CustomerResult queryResult, Customer entity)
    {
        entity.CustomerId = queryResult.Id;
        entity.CustomerName = queryResult.Name;
        entity.CustomerCode = queryResult.Code;
        entity.Email = queryResult.Email;
    }
}

Collection Transformation

public class OrdersTransformer : BaseTransformer<CollectionResult<OrderResult>, Customer>
{
    public override void Transform(CollectionResult<OrderResult> queryResult, Customer entity)
    {
        if (queryResult == null || !queryResult.Any())
        {
            entity.Orders = new Order[0];
            return;
        }

        entity.Orders = queryResult.Select(orderResult => new Order
        {
            OrderId = orderResult.OrderId,
            OrderNumber = orderResult.OrderNumber,
            OrderDate = orderResult.OrderDate,
            TotalAmount = orderResult.TotalAmount
        }).ToArray();
    }
}

Advanced Transformers

Context-Aware Transformation

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

        // Access request context
        var request = Context.Request as ProductRequest;
        if (request?.UserId.HasValue == true)
        {
            // Apply user-specific logic
            entity.IsInWishlist = CheckWishlist(request.UserId.Value, entity.ProductId);
            entity.UserRating = GetUserRating(request.UserId.Value, entity.ProductId);
        }
    }

    private bool CheckWishlist(int userId, int productId)
    {
        // Check if product is in user's wishlist
        return false;
    }

    private int? GetUserRating(int userId, int productId)
    {
        // Get user's rating for this product
        return null;
    }
}

Transformation Best Practices

1. Null Safety

public override void Transform(CustomerResult queryResult, Customer entity)
{
    if (queryResult == null) return;

    entity.CustomerId = queryResult.Id;
    entity.CustomerName = queryResult.Name ?? string.Empty;
    entity.Email = queryResult.Email?.ToLowerInvariant();
    
    // Initialize collections to prevent null reference exceptions
    if (entity.Orders == null)
        entity.Orders = new List<Order>();
}

2. Data Validation

public override void Transform(ProductResult queryResult, Product entity)
{
    entity.ProductId = queryResult.Id;
    entity.Name = ValidateAndCleanName(queryResult.Name);
    entity.Price = Math.Max(0, queryResult.Price); // Ensure non-negative price
    entity.Description = SanitizeHtml(queryResult.Description);
}

private string ValidateAndCleanName(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return "Unknown Product";
        
    return name.Trim().Length > 100 
        ? name.Trim().Substring(0, 100) + "..."
        : name.Trim();
}

Advanced Features

Caching Support

Schemio provides built-in caching for expensive query results:

Enable Caching

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

Access Cached Results

The cached results are available to queries and transformers via context object.

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

        // Access cached category data
        if (Context.Cache.TryGetValue("CategoryResult", out var cachedCategory))
        {
            var category = (CategoryResult)cachedCategory;
            entity.CategoryName = category.Name;
        }
    }
}

Selective Data Loading

Control which parts of the object graph to load based on request parameters:

public class CustomerRequest : IEntityRequest
{
    public int CustomerId { get; set; }
    public string[] SchemaPaths { get; set; }
    public bool IncludeOrders { get; set; }
    public bool IncludeOrderItems { get; set; }
}

// Usage
var fullCustomer = dataProvider.GetData(new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer", "customer/orders", "customer/orders/order/items" }
});

var basicCustomer = dataProvider.GetData(new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer" }
});

Error Handling and Resilience

Query-Level Error Handling

public class ResilientApiQuery : WebQuery<UserResult>
{
    public ResilientApiQuery() : base("https://api.external.com/") { }

    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (UserRequest)context.Request;
        return () => new Uri($"users/{request.UserId}", UriKind.Relative);
    }

    // Override to handle HTTP errors
    protected override async Task<IQueryResult> HandleError(Exception ex)
    {
        if (ex is HttpRequestException httpEx)
        {
            // Return default result or try alternative endpoint
            return new UserResult { Id = -1, Name = "Unknown User" };
        }
        
        throw ex; // Re-throw if not handled
    }
}

Parallel Query Execution

Schemio automatically executes independent queries in parallel:

// These queries will execute in parallel since they're at the same level
return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"),
        customer => customer.Dependents
            .Map<ContactQuery, ContactTransformer>(For.Paths("customer/contact"))        // Parallel
            .Map<PreferencesQuery, PreferencesTransformer>(For.Paths("customer/preferences")) // Parallel
            .Map<AddressQuery, AddressTransformer>(For.Paths("customer/address")))       // Parallel
    .End();

Extension Points

Creating Custom Query Engines

Implement IQueryEngine to support new data sources:

public class RedisQueryEngine : IQueryEngine
{
    private readonly IConnectionMultiplexer redis;

    public RedisQueryEngine(IConnectionMultiplexer redis)
    {
        this.redis = redis;
    }

    public bool CanExecute(IQuery query) 
        => query != null && query is IRedisQuery;

    public async Task<IQueryResult> Execute(IQuery query)
    {
        var serviceBusQuery = (IServiceBusQuery)query;
        var sender = serviceBusClient.CreateSender(serviceBusQuery.QueueName);
        
        var message = new ServiceBusMessage(serviceBusQuery.GetMessageBody());
        await sender.SendMessageAsync(message);
        
        // Wait for response (implement request-response pattern)
        var response = await WaitForResponse(serviceBusQuery.CorrelationId);
        return serviceBusQuery.ProcessResponse(response);
    }
}

2. AWS Lambda Integration

public class LambdaQueryEngine : IQueryEngine
{
    private readonly IAmazonLambda lambdaClient;

    public LambdaQueryEngine(IAmazonLambda lambdaClient)
    {
        this.lambdaClient = lambdaClient;
    }

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

    public async Task<IQueryResult> Execute(IQuery query)
    {
        var lambdaQuery = (ILambdaQuery)query;
        
        var request = new InvokeRequest
        {
            FunctionName = lambdaQuery.FunctionName,
            Payload = JsonSerializer.Serialize(lambdaQuery.GetPayload())
        };

        var response = await lambdaClient.InvokeAsync(request);
        var responsePayload = Encoding.UTF8.GetString(response.Payload.ToArray());
        
        return lambdaQuery.ProcessLambdaResponse(responsePayload);
    }
}

Performance Monitoring Extensions

1. Application Insights Integration

public class ApplicationInsightsDataProvider<T> : IDataProvider<T> where T : IEntity
{
    private readonly IDataProvider<T> innerProvider;
    private readonly TelemetryClient telemetryClient;

    public T GetData(IEntityRequest request)
    {
        using var operation = telemetryClient.StartOperation<DependencyTelemetry>("Schemio.GetData");
        operation.Telemetry.Type = "Schemio";
        operation.Telemetry.Target = typeof(T).Name;

        try
        {
            var result = innerProvider.GetData(request);
            operation.Telemetry.Success = true;
            
            telemetryClient.TrackMetric($"Schemio.{typeof(T).Name}.Success", 1);
            
            return result;
        }
        catch (Exception ex)
        {
            operation.Telemetry.Success = false;
            telemetryClient.TrackException(ex);
            telemetryClient.TrackMetric($"Schemio.{typeof(T).Name}.Error", 1);
            throw;
        }
    }
}

2. Prometheus Metrics

public class PrometheusDataProvider<T> : IDataProvider<T> where T : IEntity
{
    private readonly IDataProvider<T> innerProvider;
    private static readonly Counter RequestCounter = Metrics
        .CreateCounter("schemio_requests_total", "Total requests", new[] { "entity_type", "status" });
    private static readonly Histogram RequestDuration = Metrics
        .CreateHistogram("schemio_request_duration_seconds", "Request duration", new[] { "entity_type" });

    public T GetData(IEntityRequest request)
    {
        var entityType = typeof(T).Name;
        
        using (RequestDuration.WithLabels(entityType).NewTimer())
        {
            try
            {
                var result = innerProvider.GetData(request);
                RequestCounter.WithLabels(entityType, "success").Inc();
                return result;
            }
            catch
            {
                RequestCounter.WithLabels(entityType, "error").Inc();
                throw;
            }
        }
    }
}

Testing Extensions

1. Test Data Providers

public class TestDataProvider<T> : IDataProvider<T> where T : IEntity, new()
{
    private readonly Dictionary<string, T> testData;

    public TestDataProvider(Dictionary<string, T> testData)
    {
        this.testData = testData;
    }

    public T GetData(IEntityRequest request)
    {
        var key = GenerateKey(request);
        return testData.TryGetValue(key, out var data) ? data : new T();
    }

    private string GenerateKey(IEntityRequest request)
    {
        // Generate key based on request properties
        return request.GetType().Name + "_" + GetRequestId(request);
    }
}

Usage in Tests

[Test]
public void Should_Return_Customer_Data()
{
    var testData = new Dictionary<string, Customer>
    {
        ["CustomerRequest_123"] = new Customer
        {
            CustomerId = 123,
            Name = "Test Customer",
            Email = "test@example.com"
        }
    };

    var testProvider = new TestDataProvider<Customer>(testData);
    var request = new CustomerRequest { CustomerId = 123 };
    
    var result = testProvider.GetData(request);
    
    Assert.AreEqual(123, result.CustomerId);
    Assert.AreEqual("Test Customer", result.Name);
}

2. Mock Query Engines

public class MockQueryEngine : IQueryEngine
{
    private readonly Dictionary<Type, Func<IQuery, IQueryResult>> mockImplementations;

    public MockQueryEngine()
    {
        mockImplementations = new Dictionary<Type, Func<IQuery, IQueryResult>>();
    }

    public MockQueryEngine Setup<TQuery>(Func<TQuery, IQueryResult> implementation) 
        where TQuery : IQuery
    {
        mockImplementations[typeof(TQuery)] = query => implementation((TQuery)query);
        return this;
    }

    public bool CanExecute(IQuery query) 
        => mockImplementations.ContainsKey(query.GetType());

    public Task<IQueryResult> Execute(IQuery query)
    {
        var implementation = mockImplementations[query.GetType()];
        var result = implementation(query);
        return Task.FromResult(result);
    }
}

Conclusion

Schemio provides a powerful, extensible framework for aggregating data from heterogeneous data sources. Its schema-driven approach, combined with flexible query and transformation capabilities, makes it an ideal choice for modern applications that need to combine data from multiple systems.

Key Takeaways

  1. Declarative Configuration: Define your data aggregation logic through configuration rather than imperative code
  2. Extensible Architecture: Easy to add support for new data sources and transformation logic
  3. Performance Optimized: Built-in support for parallel execution and selective loading
  4. Type Safe: Strong typing throughout the pipeline reduces runtime errors
  5. Testable: Clear separation of concerns makes unit testing straightforward

When to Use Schemio

Getting Support

  • Documentation: Visit the GitHub Wiki for detailed documentation
  • Issues: Report bugs and feature requests on GitHub Issues
  • Discussions: Join the community discussions for questions and best practices
  • Samples: Check out the example projects in the repository for real-world usage patterns

The Schemio framework continues to evolve with the community's needs. Its extensible design ensures that as new data sources and patterns emerge, the framework can adapt while maintaining backward compatibility and ease of use.


IQueryResult> Execute(IQuery query) { var redisQuery = (IRedisQuery)query; var database = redis.GetDatabase(); return await redisQuery.Execute(database); } }

Custom Query Interface

public interface IRedisQuery : IQuery
{
    Task<IQueryResult> Execute(IDatabase database);
}

Custom Query Implementation

public abstract class RedisQuery<TResult> : BaseQuery<TResult>, IRedisQuery
    where TResult : IQueryResult
{
    private Func<IDatabase, Task<TResult>> queryDelegate;

    public override bool IsContextResolved() => queryDelegate != null;

    public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult)
    {
        queryDelegate = GetQuery(context, parentQueryResult);
    }

    public async Task<IQueryResult> Execute(IDatabase database)
    {
        return await queryDelegate(database);
    }

    protected abstract Func<IDatabase, Task<TResult>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult);
}

Best Practices

1. Entity Design

Keep Entities Simple

// Good: Simple, focused entity
public class Customer : IEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public Address Address { get; set; }
    public Order[] Orders { get; set; }
}
Avoid: Overly complex entities with too many responsibilities

Use Composition

public class Customer : IEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public ContactInfo Contact { get; set; }
    public ShippingInfo Shipping { get; set; }
    public BillingInfo Billing { get; set; }
}

public class ContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }
}

2. Query Design

Single Responsibility

Good: Focused queries
Avoid: Monolithic queries that try to fetch everything in one query

Parameterization

public class ProductSearchQuery : SQLQuery<CollectionResult<ProductResult>>
{
    protected override Func<IDbConnection, Task<CollectionResult<ProductResult>>> GetQuery(
        IDataContext context, IQueryResult parentQueryResult)
    {
        var request = (ProductSearchRequest)context.Request;
        
        return async connection =>
        {
            // Good: Parameterized query
            var sql = @"
                SELECT * FROM Products 
                WHERE (@CategoryId IS NULL OR CategoryId = @CategoryId)
                  AND (@SearchTerm IS NULL OR Name LIKE @SearchPattern)";
                  
            var results = await connection.QueryAsync<ProductResult>(sql, new
            {
                CategoryId = request.CategoryId,
                SearchTerm = request.SearchTerm,
                SearchPattern = $"%{request.SearchTerm}%"
            });
            
            return new CollectionResult<ProductResult>(results);
        };
    }
}

Performance Considerations

Query Optimization

1. Selective Loading

Only load what you need:

// Load only basic customer info
var basicRequest = new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer" }
};

// Load customer with orders when needed
var detailedRequest = new CustomerRequest
{
    CustomerId = 123,
    SchemaPaths = new[] { "customer", "customer/orders" }
};

2. Parallel Execution

Structure your configuration to maximize parallelism:

// Good: Independent queries can run in parallel
return CreateSchema.For<Customer>()
    .Map<CustomerQuery, CustomerTransformer>(For.Paths("customer"),
        customer => customer.Dependents
            .Map<AddressQuery, AddressTransformer>(For.Paths("customer/address"))
            .Map<ContactQuery, ContactTransformer>(For.Paths("customer/contact"))
            .Map<PreferencesQuery, PreferencesTransformer>(For.Paths("customer/preferences")))
    .End();

3. Caching Strategy

Use caching for static or slow-changing data:

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

Troubleshooting

Common Issues and Solutions

1. Query Not Executing

Problem: Query doesn't execute or returns null results.
Causes & Solutions:

2. Transformation Errors

Problem: Data not mapped correctly to entity.
Solutions:

3. Performance Problems

Problem: Slow query execution or high memory usage.
Solutions:

Debugging Tips

1. Enable Detailed Logging

services.AddLogging(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Debug);
});

2. Validate Configuration

public static class ConfigurationValidator
{
    public static void ValidateConfiguration<T>(IEntityConfiguration<T> configuration) 
        where T : IEntity
    {
        var mappings = configuration.Mappings.ToList();
        
        // Check for duplicate paths
        var duplicates = mappings
            .SelectMany(m => m.SchemaPaths.Paths)
            .GroupBy(p => p)
            .Where(g => g.Count() > 1)
            .Select(g => g.Key);
            
        if (duplicates.Any())
        {
            throw new InvalidOperationException(
                $"Duplicate schema paths found: {string.Join(", ", duplicates)}");
        }
    }
}

Future Extensions

Potential Framework Enhancements

1. GraphQL Integration

public class GraphQLQueryEngine : IQueryEngine
{
    private readonly IGraphQLClient graphQLClient;

    public GraphQLQueryEngine(IGraphQLClient graphQLClient)
    {
        this.graphQLClient = graphQLClient;
    }

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

    public async Task<IQueryResult> Execute(IQuery query)
    {
        var graphQLQuery = (IGraphQLQuery)query;
        var request = graphQLQuery.BuildRequest();
        var response = await graphQLClient.SendQueryAsync<dynamic>(request);
        return graphQLQuery.ProcessResponse(response);
    }
}

2. Message Queue Integration

public class MessageQueueQueryEngine : IQueryEngine
{
    private readonly IMessageBus messageBus;
    
    public MessageQueueQueryEngine(IMessageBus messageBus)
    {
        this.messageBus = messageBus;
    }

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

    public async Task<IQueryResult> Execute(IQuery query)
    {
        var messageQuery = (IMessageQuery)query;
        var message = messageQuery.BuildMessage();
        
        // Send message and wait for response
        var response = await messageBus.SendAsync<MessageQueryResponse>(message);
        return messageQuery.ProcessResponse(response);
    }
}

3. NoSQL Database Support

// MongoDB Integration
public class MongoQueryEngine : IQueryEngine
{
    private readonly IMongoDatabase database;

    public MongoQueryEngine(IMongoDatabase database)
    {
        this.database = database;
    }

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

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

Cloud-Native Extensions

1. Azure Service Bus Integration

public class ServiceBusQueryEngine : IQueryEngine
{
    private readonly ServiceBusClient serviceBusClient;

    public ServiceBusQueryEngine(ServiceBusClient serviceBusClient)
    {
        this.serviceBusClient = serviceBusClient;
    }

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

    public async Task<